@@ -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,72 @@ 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 self . remaining ( ) > 0 {
367+ let start = Instant :: now ( ) ;
368+ let packing = basic_packing (
369+ & regular_sizes,
370+ NonZeroU32 :: new ( self . remaining ( ) ) . unwrap ( ) ,
371+ prior_build_metadata,
372+ ) ?;
373+ let duration = start. elapsed ( ) ;
374+ tracing:: debug!( "Time elapsed in packing: {:#?}" , duration) ;
375+
376+ for bin in packing. into_iter ( ) {
377+ let name = match bin. len ( ) {
378+ 0 => Cow :: Borrowed ( "Reserved for new packages" ) ,
379+ 1 => {
380+ let first = bin[ 0 ] ;
381+ let first_name = & * first. meta . identifier ;
382+ Cow :: Borrowed ( first_name)
383+ }
384+ 2 ..=5 => {
385+ let first = bin[ 0 ] ;
386+ let first_name = & * first. meta . identifier ;
387+ let r = bin. iter ( ) . map ( |v| & * v. meta . identifier ) . skip ( 1 ) . fold (
388+ String :: from ( first_name) ,
389+ |mut acc, v| {
390+ write ! ( acc, " and {}" , v) . unwrap ( ) ;
391+ acc
392+ } ,
393+ ) ;
394+ Cow :: Owned ( r)
395+ }
396+ n => Cow :: Owned ( format ! ( "{n} components" ) ) ,
397+ } ;
398+ let mut chunk = Chunk :: new ( & name) ;
399+ chunk. packages = bin. iter ( ) . map ( |v| String :: from ( & * v. meta . name ) ) . collect ( ) ;
400+ for szmeta in bin {
401+ for & obj in rmap. get ( & szmeta. meta . identifier ) . unwrap ( ) {
402+ self . remainder . move_obj ( & mut chunk, obj. as_str ( ) ) ;
403+ }
363404 }
405+ self . chunks . push ( chunk) ;
364406 }
365- self . chunks . push ( chunk) ;
366407 }
367408
368- assert_eq ! ( self . remainder. content. len( ) , 0 ) ;
409+ // Check that all objects have been processed
410+ if !processed_specific_components. is_empty ( ) || !regular_sizes. is_empty ( ) {
411+ assert_eq ! ( self . remainder. content. len( ) , 0 ) ;
412+ }
369413
370414 Ok ( ( ) )
371415 }
@@ -1003,4 +1047,191 @@ mod test {
10031047 assert_eq ! ( structure_derived, v2_expected_structure) ;
10041048 Ok ( ( ) )
10051049 }
1050+
1051+ fn setup_exclusive_test (
1052+ component_data : & [ ( u32 , u32 , u64 ) ] ,
1053+ max_layers : u32 ,
1054+ num_fake_objects : Option < usize > ,
1055+ ) -> Result < (
1056+ Vec < ObjectSourceMetaSized > ,
1057+ ObjectMetaSized ,
1058+ ObjectMetaSized ,
1059+ Chunking ,
1060+ ) > {
1061+ // Create content metadata from provided data
1062+ let contentmeta: Vec < ObjectSourceMetaSized > = component_data
1063+ . iter ( )
1064+ . map ( |& ( id, freq, size) | ObjectSourceMetaSized {
1065+ meta : ObjectSourceMeta {
1066+ identifier : RcStr :: from ( format ! ( "pkg{}.0" , id) ) ,
1067+ name : RcStr :: from ( format ! ( "pkg{}" , id) ) ,
1068+ srcid : RcStr :: from ( format ! ( "srcpkg{}" , id) ) ,
1069+ change_time_offset : 0 ,
1070+ change_frequency : freq,
1071+ } ,
1072+ size,
1073+ } )
1074+ . collect ( ) ;
1075+
1076+ // Create object maps with fake checksums
1077+ let mut object_map = IndexMap :: new ( ) ;
1078+ let mut regular_map = IndexMap :: new ( ) ;
1079+
1080+ for ( i, component) in contentmeta. iter ( ) . enumerate ( ) {
1081+ let checksum = format ! ( "checksum_{}" , i) ;
1082+ regular_map. insert ( checksum. clone ( ) , component. meta . identifier . clone ( ) ) ;
1083+ object_map. insert ( checksum, component. meta . identifier . clone ( ) ) ;
1084+ }
1085+
1086+ let regular_meta = ObjectMetaSized {
1087+ map : regular_map,
1088+ sizes : contentmeta. clone ( ) ,
1089+ } ;
1090+
1091+ // Create exclusive metadata (initially empty, to be populated by individual tests)
1092+ let exclusive_meta = ObjectMetaSized {
1093+ map : object_map,
1094+ sizes : Vec :: new ( ) ,
1095+ } ;
1096+
1097+ // Set up chunking with remainder chunk
1098+ let mut chunking = Chunking :: default ( ) ;
1099+ chunking. max = max_layers;
1100+ chunking. remainder = Chunk :: new ( "remainder" ) ;
1101+
1102+ // Add fake objects to the remainder chunk if specified
1103+ if let Some ( num_objects) = num_fake_objects {
1104+ for i in 0 ..num_objects {
1105+ let checksum = format ! ( "checksum_{}" , i) ;
1106+ chunking
1107+ . remainder
1108+ . content
1109+ . insert ( RcStr :: from ( checksum) , ( 1000 , vec ! [ ] ) ) ;
1110+ chunking. remainder . size += 1000 ;
1111+ }
1112+ }
1113+
1114+ Ok ( ( contentmeta, regular_meta, exclusive_meta, chunking) )
1115+ }
1116+
1117+ #[ test]
1118+ fn test_exclusive_chunks ( ) -> Result < ( ) > {
1119+ // Test that exclusive chunks are created first and get their own layers
1120+ let component_data = [
1121+ ( 1 , 100 , 50000 ) ,
1122+ ( 2 , 200 , 40000 ) ,
1123+ ( 3 , 300 , 30000 ) ,
1124+ ( 4 , 400 , 20000 ) ,
1125+ ( 5 , 500 , 10000 ) ,
1126+ ] ;
1127+
1128+ let ( contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1129+ setup_exclusive_test ( & component_data, 8 , Some ( 5 ) ) ?;
1130+
1131+ // Create exclusive content metadata for pkg1 and pkg2
1132+ let exclusive_content: Vec < ObjectSourceMetaSized > =
1133+ vec ! [ contentmeta[ 0 ] . clone( ) , contentmeta[ 1 ] . clone( ) ] ;
1134+ exclusive_meta. sizes = exclusive_content;
1135+
1136+ chunking. process_mapping (
1137+ & regular_meta,
1138+ & Some ( NonZeroU32 :: new ( 8 ) . unwrap ( ) ) ,
1139+ None ,
1140+ Some ( & exclusive_meta) ,
1141+ ) ?;
1142+
1143+ // Verify exclusive chunks are created first
1144+ assert ! ( chunking. chunks. len( ) >= 2 ) ;
1145+ assert_eq ! ( chunking. chunks[ 0 ] . name, "pkg1" ) ;
1146+ assert_eq ! ( chunking. chunks[ 1 ] . name, "pkg2" ) ;
1147+ assert_eq ! ( chunking. chunks[ 0 ] . packages, vec![ "pkg1" . to_string( ) ] ) ;
1148+ assert_eq ! ( chunking. chunks[ 1 ] . packages, vec![ "pkg2" . to_string( ) ] ) ;
1149+
1150+ Ok ( ( ) )
1151+ }
1152+
1153+ #[ test]
1154+ fn test_exclusive_chunks_with_regular_packing ( ) -> Result < ( ) > {
1155+ // Test that exclusive chunks are created first, then regular packing continues
1156+ let component_data = [
1157+ ( 1 , 100 , 50000 ) , // exclusive
1158+ ( 2 , 200 , 40000 ) , // exclusive
1159+ ( 3 , 300 , 30000 ) , // regular
1160+ ( 4 , 400 , 20000 ) , // regular
1161+ ( 5 , 500 , 10000 ) , // regular
1162+ ( 6 , 600 , 5000 ) , // regular
1163+ ] ;
1164+
1165+ let ( contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1166+ setup_exclusive_test ( & component_data, 8 , Some ( 6 ) ) ?;
1167+
1168+ // Create exclusive content metadata for pkg1 and pkg2
1169+ let exclusive_content: Vec < ObjectSourceMetaSized > =
1170+ vec ! [ contentmeta[ 0 ] . clone( ) , contentmeta[ 1 ] . clone( ) ] ;
1171+ exclusive_meta. sizes = exclusive_content;
1172+
1173+ chunking. process_mapping (
1174+ & regular_meta,
1175+ & Some ( NonZeroU32 :: new ( 8 ) . unwrap ( ) ) ,
1176+ None ,
1177+ Some ( & exclusive_meta) ,
1178+ ) ?;
1179+
1180+ // Verify exclusive chunks are created first
1181+ assert ! ( chunking. chunks. len( ) >= 2 ) ;
1182+ assert_eq ! ( chunking. chunks[ 0 ] . name, "pkg1" ) ;
1183+ assert_eq ! ( chunking. chunks[ 1 ] . name, "pkg2" ) ;
1184+ assert_eq ! ( chunking. chunks[ 0 ] . packages, vec![ "pkg1" . to_string( ) ] ) ;
1185+ assert_eq ! ( chunking. chunks[ 1 ] . packages, vec![ "pkg2" . to_string( ) ] ) ;
1186+
1187+ // Verify regular components are not in exclusive chunks
1188+ for chunk in & chunking. chunks [ 2 ..] {
1189+ assert ! ( !chunk. packages. contains( & "pkg1" . to_string( ) ) ) ;
1190+ assert ! ( !chunk. packages. contains( & "pkg2" . to_string( ) ) ) ;
1191+ }
1192+
1193+ Ok ( ( ) )
1194+ }
1195+
1196+ #[ test]
1197+ fn test_exclusive_chunks_isolation ( ) -> Result < ( ) > {
1198+ // Test that exclusive chunks properly isolate components
1199+ let component_data = [ ( 1 , 100 , 50000 ) , ( 2 , 200 , 40000 ) , ( 3 , 300 , 30000 ) ] ;
1200+
1201+ let ( contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1202+ setup_exclusive_test ( & component_data, 8 , Some ( 3 ) ) ?;
1203+
1204+ // Create exclusive content metadata for pkg1 only
1205+ let exclusive_content: Vec < ObjectSourceMetaSized > = vec ! [ contentmeta[ 0 ] . clone( ) ] ;
1206+ exclusive_meta. sizes = exclusive_content;
1207+
1208+ chunking. process_mapping (
1209+ & regular_meta,
1210+ & Some ( NonZeroU32 :: new ( 8 ) . unwrap ( ) ) ,
1211+ None ,
1212+ Some ( & exclusive_meta) ,
1213+ ) ?;
1214+
1215+ // Verify pkg1 is in its own exclusive chunk
1216+ assert ! ( chunking. chunks. len( ) >= 1 ) ;
1217+ assert_eq ! ( chunking. chunks[ 0 ] . name, "pkg1" ) ;
1218+ assert_eq ! ( chunking. chunks[ 0 ] . packages, vec![ "pkg1" . to_string( ) ] ) ;
1219+
1220+ // Verify pkg2 and pkg3 are in regular chunks, not mixed with pkg1
1221+ let mut found_pkg2 = false ;
1222+ let mut found_pkg3 = false ;
1223+ for chunk in & chunking. chunks [ 1 ..] {
1224+ if chunk. packages . contains ( & "pkg2" . to_string ( ) ) {
1225+ found_pkg2 = true ;
1226+ assert ! ( !chunk. packages. contains( & "pkg1" . to_string( ) ) ) ;
1227+ }
1228+ if chunk. packages . contains ( & "pkg3" . to_string ( ) ) {
1229+ found_pkg3 = true ;
1230+ assert ! ( !chunk. packages. contains( & "pkg1" . to_string( ) ) ) ;
1231+ }
1232+ }
1233+ assert ! ( found_pkg2 && found_pkg3) ;
1234+
1235+ Ok ( ( ) )
1236+ }
10061237}
0 commit comments