@@ -49,7 +49,7 @@ pub(crate) struct Chunk {
4949 pub ( crate ) packages : Vec < String > ,
5050}
5151
52- #[ derive( Debug , Deserialize , Serialize ) ]
52+ #[ derive( Debug , Clone , Deserialize , Serialize ) ]
5353/// Object metadata, but with additional size data
5454pub struct ObjectSourceMetaSized {
5555 /// The original metadata
@@ -276,9 +276,10 @@ impl Chunking {
276276 meta : & ObjectMetaSized ,
277277 max_layers : & Option < NonZeroU32 > ,
278278 prior_build_metadata : Option < & oci_spec:: image:: ImageManifest > ,
279+ specific_contentmeta : Option < & ObjectMetaSized > ,
279280 ) -> Result < Self > {
280281 let mut r = Self :: new ( repo, rev) ?;
281- r. process_mapping ( meta, max_layers, prior_build_metadata) ?;
282+ r. process_mapping ( meta, max_layers, prior_build_metadata, specific_contentmeta ) ?;
282283 Ok ( r)
283284 }
284285
@@ -294,6 +295,7 @@ impl Chunking {
294295 meta : & ObjectMetaSized ,
295296 max_layers : & Option < NonZeroU32 > ,
296297 prior_build_metadata : Option < & oci_spec:: image:: ImageManifest > ,
298+ specific_contentmeta : Option < & ObjectMetaSized > ,
297299 ) -> Result < ( ) > {
298300 self . max = max_layers
299301 . unwrap_or ( NonZeroU32 :: new ( MAX_CHUNKS ) . unwrap ( ) )
@@ -314,6 +316,25 @@ impl Chunking {
314316 rmap. entry ( Rc :: clone ( contentid) ) . or_default ( ) . push ( checksum) ;
315317 }
316318
319+ // Create exclusive chunks first if specified
320+ let mut processed_specific_components = BTreeSet :: new ( ) ;
321+ if let Some ( specific_meta) = specific_contentmeta {
322+ for component in & specific_meta. sizes {
323+ let mut chunk = Chunk :: new ( & component. meta . name ) ;
324+ chunk. packages = vec ! [ component. meta. name. to_string( ) ] ;
325+
326+ // Move all objects belonging to this exclusive component
327+ if let Some ( objects) = rmap. get ( & component. meta . identifier ) {
328+ for & obj in objects {
329+ self . remainder . move_obj ( & mut chunk, obj) ;
330+ }
331+ }
332+
333+ self . chunks . push ( chunk) ;
334+ processed_specific_components. insert ( & * component. meta . identifier ) ;
335+ }
336+ }
337+
317338 // Safety: Let's assume no one has over 4 billion components.
318339 self . n_provided_components = meta. sizes . len ( ) . try_into ( ) . unwrap ( ) ;
319340 self . n_sized_components = sizes
@@ -323,49 +344,68 @@ impl Chunking {
323344 . try_into ( )
324345 . unwrap ( ) ;
325346
326- // TODO: Compute bin packing in a better way
327- let start = Instant :: now ( ) ;
328- let packing = basic_packing (
329- sizes,
330- NonZeroU32 :: new ( self . max ) . unwrap ( ) ,
331- prior_build_metadata,
332- ) ?;
333- let duration = start. elapsed ( ) ;
334- tracing:: debug!( "Time elapsed in packing: {:#?}" , duration) ;
335-
336- for bin in packing. into_iter ( ) {
337- let name = match bin. len ( ) {
338- 0 => Cow :: Borrowed ( "Reserved for new packages" ) ,
339- 1 => {
340- let first = bin[ 0 ] ;
341- let first_name = & * first. meta . identifier ;
342- Cow :: Borrowed ( first_name)
343- }
344- 2 ..=5 => {
345- let first = bin[ 0 ] ;
346- let first_name = & * first. meta . identifier ;
347- let r = bin. iter ( ) . map ( |v| & * v. meta . identifier ) . skip ( 1 ) . fold (
348- String :: from ( first_name) ,
349- |mut acc, v| {
350- write ! ( acc, " and {}" , v) . unwrap ( ) ;
351- acc
352- } ,
353- ) ;
354- Cow :: Owned ( r)
355- }
356- n => Cow :: Owned ( format ! ( "{n} components" ) ) ,
357- } ;
358- let mut chunk = Chunk :: new ( & name) ;
359- chunk. packages = bin. iter ( ) . map ( |v| String :: from ( & * v. meta . name ) ) . collect ( ) ;
360- for szmeta in bin {
361- for & obj in rmap. get ( & szmeta. meta . identifier ) . unwrap ( ) {
362- self . remainder . move_obj ( & mut chunk, obj. as_str ( ) ) ;
347+ // Filter out exclusive components for regular packing
348+ let regular_sizes: Vec < ObjectSourceMetaSized > = sizes
349+ . iter ( )
350+ . filter ( |component| {
351+ !processed_specific_components. contains ( & * component. meta . identifier )
352+ } )
353+ . map ( |component| ObjectSourceMetaSized {
354+ meta : ObjectSourceMeta {
355+ identifier : Rc :: clone ( & component. meta . identifier ) ,
356+ name : Rc :: clone ( & component. meta . name ) ,
357+ srcid : Rc :: clone ( & component. meta . srcid ) ,
358+ change_time_offset : component. meta . change_time_offset ,
359+ change_frequency : component. meta . change_frequency ,
360+ } ,
361+ size : component. size ,
362+ } )
363+ . collect ( ) ;
364+
365+ // Process regular components with bin packing if we have remaining layers
366+ if let Some ( remaining) = NonZeroU32 :: new ( self . remaining ( ) ) {
367+ let start = Instant :: now ( ) ;
368+ let packing = basic_packing ( & regular_sizes, remaining, prior_build_metadata) ?;
369+ let duration = start. elapsed ( ) ;
370+ tracing:: debug!( "Time elapsed in packing: {:#?}" , duration) ;
371+
372+ for bin in packing. into_iter ( ) {
373+ let name = match bin. len ( ) {
374+ 0 => Cow :: Borrowed ( "Reserved for new packages" ) ,
375+ 1 => {
376+ let first = bin[ 0 ] ;
377+ let first_name = & * first. meta . identifier ;
378+ Cow :: Borrowed ( first_name)
379+ }
380+ 2 ..=5 => {
381+ let first = bin[ 0 ] ;
382+ let first_name = & * first. meta . identifier ;
383+ let r = bin. iter ( ) . map ( |v| & * v. meta . identifier ) . skip ( 1 ) . fold (
384+ String :: from ( first_name) ,
385+ |mut acc, v| {
386+ write ! ( acc, " and {}" , v) . unwrap ( ) ;
387+ acc
388+ } ,
389+ ) ;
390+ Cow :: Owned ( r)
391+ }
392+ n => Cow :: Owned ( format ! ( "{n} components" ) ) ,
393+ } ;
394+ let mut chunk = Chunk :: new ( & name) ;
395+ chunk. packages = bin. iter ( ) . map ( |v| String :: from ( & * v. meta . name ) ) . collect ( ) ;
396+ for szmeta in bin {
397+ for & obj in rmap. get ( & szmeta. meta . identifier ) . unwrap ( ) {
398+ self . remainder . move_obj ( & mut chunk, obj. as_str ( ) ) ;
399+ }
363400 }
401+ self . chunks . push ( chunk) ;
364402 }
365- self . chunks . push ( chunk) ;
366403 }
367404
368- assert_eq ! ( self . remainder. content. len( ) , 0 ) ;
405+ // Check that all objects have been processed
406+ if !processed_specific_components. is_empty ( ) || !regular_sizes. is_empty ( ) {
407+ assert_eq ! ( self . remainder. content. len( ) , 0 ) ;
408+ }
369409
370410 Ok ( ( ) )
371411 }
@@ -1003,4 +1043,191 @@ mod test {
10031043 assert_eq ! ( structure_derived, v2_expected_structure) ;
10041044 Ok ( ( ) )
10051045 }
1046+
1047+ fn setup_exclusive_test (
1048+ component_data : & [ ( u32 , u32 , u64 ) ] ,
1049+ max_layers : u32 ,
1050+ num_fake_objects : Option < usize > ,
1051+ ) -> Result < (
1052+ Vec < ObjectSourceMetaSized > ,
1053+ ObjectMetaSized ,
1054+ ObjectMetaSized ,
1055+ Chunking ,
1056+ ) > {
1057+ // Create content metadata from provided data
1058+ let contentmeta: Vec < ObjectSourceMetaSized > = component_data
1059+ . iter ( )
1060+ . map ( |& ( id, freq, size) | ObjectSourceMetaSized {
1061+ meta : ObjectSourceMeta {
1062+ identifier : RcStr :: from ( format ! ( "pkg{}.0" , id) ) ,
1063+ name : RcStr :: from ( format ! ( "pkg{}" , id) ) ,
1064+ srcid : RcStr :: from ( format ! ( "srcpkg{}" , id) ) ,
1065+ change_time_offset : 0 ,
1066+ change_frequency : freq,
1067+ } ,
1068+ size,
1069+ } )
1070+ . collect ( ) ;
1071+
1072+ // Create object maps with fake checksums
1073+ let mut object_map = IndexMap :: new ( ) ;
1074+ let mut regular_map = IndexMap :: new ( ) ;
1075+
1076+ for ( i, component) in contentmeta. iter ( ) . enumerate ( ) {
1077+ let checksum = format ! ( "checksum_{}" , i) ;
1078+ regular_map. insert ( checksum. clone ( ) , component. meta . identifier . clone ( ) ) ;
1079+ object_map. insert ( checksum, component. meta . identifier . clone ( ) ) ;
1080+ }
1081+
1082+ let regular_meta = ObjectMetaSized {
1083+ map : regular_map,
1084+ sizes : contentmeta. clone ( ) ,
1085+ } ;
1086+
1087+ // Create exclusive metadata (initially empty, to be populated by individual tests)
1088+ let exclusive_meta = ObjectMetaSized {
1089+ map : object_map,
1090+ sizes : Vec :: new ( ) ,
1091+ } ;
1092+
1093+ // Set up chunking with remainder chunk
1094+ let mut chunking = Chunking :: default ( ) ;
1095+ chunking. max = max_layers;
1096+ chunking. remainder = Chunk :: new ( "remainder" ) ;
1097+
1098+ // Add fake objects to the remainder chunk if specified
1099+ if let Some ( num_objects) = num_fake_objects {
1100+ for i in 0 ..num_objects {
1101+ let checksum = format ! ( "checksum_{}" , i) ;
1102+ chunking
1103+ . remainder
1104+ . content
1105+ . insert ( RcStr :: from ( checksum) , ( 1000 , vec ! [ ] ) ) ;
1106+ chunking. remainder . size += 1000 ;
1107+ }
1108+ }
1109+
1110+ Ok ( ( contentmeta, regular_meta, exclusive_meta, chunking) )
1111+ }
1112+
1113+ #[ test]
1114+ fn test_exclusive_chunks ( ) -> Result < ( ) > {
1115+ // Test that exclusive chunks are created first and get their own layers
1116+ let component_data = [
1117+ ( 1 , 100 , 50000 ) ,
1118+ ( 2 , 200 , 40000 ) ,
1119+ ( 3 , 300 , 30000 ) ,
1120+ ( 4 , 400 , 20000 ) ,
1121+ ( 5 , 500 , 10000 ) ,
1122+ ] ;
1123+
1124+ let ( contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1125+ setup_exclusive_test ( & component_data, 8 , Some ( 5 ) ) ?;
1126+
1127+ // Create exclusive content metadata for pkg1 and pkg2
1128+ let exclusive_content: Vec < ObjectSourceMetaSized > =
1129+ vec ! [ contentmeta[ 0 ] . clone( ) , contentmeta[ 1 ] . clone( ) ] ;
1130+ exclusive_meta. sizes = exclusive_content;
1131+
1132+ chunking. process_mapping (
1133+ & regular_meta,
1134+ & Some ( NonZeroU32 :: new ( 8 ) . unwrap ( ) ) ,
1135+ None ,
1136+ Some ( & exclusive_meta) ,
1137+ ) ?;
1138+
1139+ // Verify exclusive chunks are created first
1140+ assert ! ( chunking. chunks. len( ) >= 2 ) ;
1141+ assert_eq ! ( chunking. chunks[ 0 ] . name, "pkg1" ) ;
1142+ assert_eq ! ( chunking. chunks[ 1 ] . name, "pkg2" ) ;
1143+ assert_eq ! ( chunking. chunks[ 0 ] . packages, vec![ "pkg1" . to_string( ) ] ) ;
1144+ assert_eq ! ( chunking. chunks[ 1 ] . packages, vec![ "pkg2" . to_string( ) ] ) ;
1145+
1146+ Ok ( ( ) )
1147+ }
1148+
1149+ #[ test]
1150+ fn test_exclusive_chunks_with_regular_packing ( ) -> Result < ( ) > {
1151+ // Test that exclusive chunks are created first, then regular packing continues
1152+ let component_data = [
1153+ ( 1 , 100 , 50000 ) , // exclusive
1154+ ( 2 , 200 , 40000 ) , // exclusive
1155+ ( 3 , 300 , 30000 ) , // regular
1156+ ( 4 , 400 , 20000 ) , // regular
1157+ ( 5 , 500 , 10000 ) , // regular
1158+ ( 6 , 600 , 5000 ) , // regular
1159+ ] ;
1160+
1161+ let ( contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1162+ setup_exclusive_test ( & component_data, 8 , Some ( 6 ) ) ?;
1163+
1164+ // Create exclusive content metadata for pkg1 and pkg2
1165+ let exclusive_content: Vec < ObjectSourceMetaSized > =
1166+ vec ! [ contentmeta[ 0 ] . clone( ) , contentmeta[ 1 ] . clone( ) ] ;
1167+ exclusive_meta. sizes = exclusive_content;
1168+
1169+ chunking. process_mapping (
1170+ & regular_meta,
1171+ & Some ( NonZeroU32 :: new ( 8 ) . unwrap ( ) ) ,
1172+ None ,
1173+ Some ( & exclusive_meta) ,
1174+ ) ?;
1175+
1176+ // Verify exclusive chunks are created first
1177+ assert ! ( chunking. chunks. len( ) >= 2 ) ;
1178+ assert_eq ! ( chunking. chunks[ 0 ] . name, "pkg1" ) ;
1179+ assert_eq ! ( chunking. chunks[ 1 ] . name, "pkg2" ) ;
1180+ assert_eq ! ( chunking. chunks[ 0 ] . packages, vec![ "pkg1" . to_string( ) ] ) ;
1181+ assert_eq ! ( chunking. chunks[ 1 ] . packages, vec![ "pkg2" . to_string( ) ] ) ;
1182+
1183+ // Verify regular components are not in exclusive chunks
1184+ for chunk in & chunking. chunks [ 2 ..] {
1185+ assert ! ( !chunk. packages. contains( & "pkg1" . to_string( ) ) ) ;
1186+ assert ! ( !chunk. packages. contains( & "pkg2" . to_string( ) ) ) ;
1187+ }
1188+
1189+ Ok ( ( ) )
1190+ }
1191+
1192+ #[ test]
1193+ fn test_exclusive_chunks_isolation ( ) -> Result < ( ) > {
1194+ // Test that exclusive chunks properly isolate components
1195+ let component_data = [ ( 1 , 100 , 50000 ) , ( 2 , 200 , 40000 ) , ( 3 , 300 , 30000 ) ] ;
1196+
1197+ let ( contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1198+ setup_exclusive_test ( & component_data, 8 , Some ( 3 ) ) ?;
1199+
1200+ // Create exclusive content metadata for pkg1 only
1201+ let exclusive_content: Vec < ObjectSourceMetaSized > = vec ! [ contentmeta[ 0 ] . clone( ) ] ;
1202+ exclusive_meta. sizes = exclusive_content;
1203+
1204+ chunking. process_mapping (
1205+ & regular_meta,
1206+ & Some ( NonZeroU32 :: new ( 8 ) . unwrap ( ) ) ,
1207+ None ,
1208+ Some ( & exclusive_meta) ,
1209+ ) ?;
1210+
1211+ // Verify pkg1 is in its own exclusive chunk
1212+ assert ! ( chunking. chunks. len( ) >= 1 ) ;
1213+ assert_eq ! ( chunking. chunks[ 0 ] . name, "pkg1" ) ;
1214+ assert_eq ! ( chunking. chunks[ 0 ] . packages, vec![ "pkg1" . to_string( ) ] ) ;
1215+
1216+ // Verify pkg2 and pkg3 are in regular chunks, not mixed with pkg1
1217+ let mut found_pkg2 = false ;
1218+ let mut found_pkg3 = false ;
1219+ for chunk in & chunking. chunks [ 1 ..] {
1220+ if chunk. packages . contains ( & "pkg2" . to_string ( ) ) {
1221+ found_pkg2 = true ;
1222+ assert ! ( !chunk. packages. contains( & "pkg1" . to_string( ) ) ) ;
1223+ }
1224+ if chunk. packages . contains ( & "pkg3" . to_string ( ) ) {
1225+ found_pkg3 = true ;
1226+ assert ! ( !chunk. packages. contains( & "pkg1" . to_string( ) ) ) ;
1227+ }
1228+ }
1229+ assert ! ( found_pkg2 && found_pkg3) ;
1230+
1231+ Ok ( ( ) )
1232+ }
10061233}
0 commit comments