@@ -49,7 +49,7 @@ pub(crate) struct Chunk {
49
49
pub ( crate ) packages : Vec < String > ,
50
50
}
51
51
52
- #[ derive( Debug , Deserialize , Serialize ) ]
52
+ #[ derive( Debug , Clone , Deserialize , Serialize ) ]
53
53
/// Object metadata, but with additional size data
54
54
pub struct ObjectSourceMetaSized {
55
55
/// The original metadata
@@ -276,9 +276,10 @@ impl Chunking {
276
276
meta : & ObjectMetaSized ,
277
277
max_layers : & Option < NonZeroU32 > ,
278
278
prior_build_metadata : Option < & oci_spec:: image:: ImageManifest > ,
279
+ specific_contentmeta : Option < & ObjectMetaSized > ,
279
280
) -> Result < Self > {
280
281
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 ) ?;
282
283
Ok ( r)
283
284
}
284
285
@@ -294,6 +295,7 @@ impl Chunking {
294
295
meta : & ObjectMetaSized ,
295
296
max_layers : & Option < NonZeroU32 > ,
296
297
prior_build_metadata : Option < & oci_spec:: image:: ImageManifest > ,
298
+ specific_contentmeta : Option < & ObjectMetaSized > ,
297
299
) -> Result < ( ) > {
298
300
self . max = max_layers
299
301
. unwrap_or ( NonZeroU32 :: new ( MAX_CHUNKS ) . unwrap ( ) )
@@ -314,6 +316,25 @@ impl Chunking {
314
316
rmap. entry ( Rc :: clone ( contentid) ) . or_default ( ) . push ( checksum) ;
315
317
}
316
318
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
+
317
338
// Safety: Let's assume no one has over 4 billion components.
318
339
self . n_provided_components = meta. sizes . len ( ) . try_into ( ) . unwrap ( ) ;
319
340
self . n_sized_components = sizes
@@ -323,49 +344,59 @@ impl Chunking {
323
344
. try_into ( )
324
345
. unwrap ( ) ;
325
346
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
+ . cloned ( )
354
+ . collect ( ) ;
355
+
356
+ // Process regular components with bin packing if we have remaining layers
357
+ if let Some ( remaining) = NonZeroU32 :: new ( self . remaining ( ) ) {
358
+ let start = Instant :: now ( ) ;
359
+ let packing = basic_packing ( & regular_sizes, remaining, prior_build_metadata) ?;
360
+ let duration = start. elapsed ( ) ;
361
+ tracing:: debug!( "Time elapsed in packing: {:#?}" , duration) ;
362
+
363
+ for bin in packing. into_iter ( ) {
364
+ let name = match bin. len ( ) {
365
+ 0 => Cow :: Borrowed ( "Reserved for new packages" ) ,
366
+ 1 => {
367
+ let first = bin[ 0 ] ;
368
+ let first_name = & * first. meta . identifier ;
369
+ Cow :: Borrowed ( first_name)
370
+ }
371
+ 2 ..=5 => {
372
+ let first = bin[ 0 ] ;
373
+ let first_name = & * first. meta . identifier ;
374
+ let r = bin. iter ( ) . map ( |v| & * v. meta . identifier ) . skip ( 1 ) . fold (
375
+ String :: from ( first_name) ,
376
+ |mut acc, v| {
377
+ write ! ( acc, " and {}" , v) . unwrap ( ) ;
378
+ acc
379
+ } ,
380
+ ) ;
381
+ Cow :: Owned ( r)
382
+ }
383
+ n => Cow :: Owned ( format ! ( "{n} components" ) ) ,
384
+ } ;
385
+ let mut chunk = Chunk :: new ( & name) ;
386
+ chunk. packages = bin. iter ( ) . map ( |v| String :: from ( & * v. meta . name ) ) . collect ( ) ;
387
+ for szmeta in bin {
388
+ for & obj in rmap. get ( & szmeta. meta . identifier ) . unwrap ( ) {
389
+ self . remainder . move_obj ( & mut chunk, obj. as_str ( ) ) ;
390
+ }
363
391
}
392
+ self . chunks . push ( chunk) ;
364
393
}
365
- self . chunks . push ( chunk) ;
366
394
}
367
395
368
- assert_eq ! ( self . remainder. content. len( ) , 0 ) ;
396
+ // Check that all objects have been processed
397
+ if !processed_specific_components. is_empty ( ) || !regular_sizes. is_empty ( ) {
398
+ assert_eq ! ( self . remainder. content. len( ) , 0 ) ;
399
+ }
369
400
370
401
Ok ( ( ) )
371
402
}
@@ -1003,4 +1034,191 @@ mod test {
1003
1034
assert_eq ! ( structure_derived, v2_expected_structure) ;
1004
1035
Ok ( ( ) )
1005
1036
}
1037
+
1038
+ fn setup_exclusive_test (
1039
+ component_data : & [ ( u32 , u32 , u64 ) ] ,
1040
+ max_layers : u32 ,
1041
+ num_fake_objects : Option < usize > ,
1042
+ ) -> Result < (
1043
+ Vec < ObjectSourceMetaSized > ,
1044
+ ObjectMetaSized ,
1045
+ ObjectMetaSized ,
1046
+ Chunking ,
1047
+ ) > {
1048
+ // Create content metadata from provided data
1049
+ let contentmeta: Vec < ObjectSourceMetaSized > = component_data
1050
+ . iter ( )
1051
+ . map ( |& ( id, freq, size) | ObjectSourceMetaSized {
1052
+ meta : ObjectSourceMeta {
1053
+ identifier : RcStr :: from ( format ! ( "pkg{}.0" , id) ) ,
1054
+ name : RcStr :: from ( format ! ( "pkg{}" , id) ) ,
1055
+ srcid : RcStr :: from ( format ! ( "srcpkg{}" , id) ) ,
1056
+ change_time_offset : 0 ,
1057
+ change_frequency : freq,
1058
+ } ,
1059
+ size,
1060
+ } )
1061
+ . collect ( ) ;
1062
+
1063
+ // Create object maps with fake checksums
1064
+ let mut object_map = IndexMap :: new ( ) ;
1065
+ let mut regular_map = IndexMap :: new ( ) ;
1066
+
1067
+ for ( i, component) in contentmeta. iter ( ) . enumerate ( ) {
1068
+ let checksum = format ! ( "checksum_{}" , i) ;
1069
+ regular_map. insert ( checksum. clone ( ) , component. meta . identifier . clone ( ) ) ;
1070
+ object_map. insert ( checksum, component. meta . identifier . clone ( ) ) ;
1071
+ }
1072
+
1073
+ let regular_meta = ObjectMetaSized {
1074
+ map : regular_map,
1075
+ sizes : contentmeta. clone ( ) ,
1076
+ } ;
1077
+
1078
+ // Create exclusive metadata (initially empty, to be populated by individual tests)
1079
+ let exclusive_meta = ObjectMetaSized {
1080
+ map : object_map,
1081
+ sizes : Vec :: new ( ) ,
1082
+ } ;
1083
+
1084
+ // Set up chunking with remainder chunk
1085
+ let mut chunking = Chunking :: default ( ) ;
1086
+ chunking. max = max_layers;
1087
+ chunking. remainder = Chunk :: new ( "remainder" ) ;
1088
+
1089
+ // Add fake objects to the remainder chunk if specified
1090
+ if let Some ( num_objects) = num_fake_objects {
1091
+ for i in 0 ..num_objects {
1092
+ let checksum = format ! ( "checksum_{}" , i) ;
1093
+ chunking
1094
+ . remainder
1095
+ . content
1096
+ . insert ( RcStr :: from ( checksum) , ( 1000 , vec ! [ ] ) ) ;
1097
+ chunking. remainder . size += 1000 ;
1098
+ }
1099
+ }
1100
+
1101
+ Ok ( ( contentmeta, regular_meta, exclusive_meta, chunking) )
1102
+ }
1103
+
1104
+ #[ test]
1105
+ fn test_exclusive_chunks ( ) -> Result < ( ) > {
1106
+ // Test that exclusive chunks are created first and get their own layers
1107
+ let component_data = [
1108
+ ( 1 , 100 , 50000 ) ,
1109
+ ( 2 , 200 , 40000 ) ,
1110
+ ( 3 , 300 , 30000 ) ,
1111
+ ( 4 , 400 , 20000 ) ,
1112
+ ( 5 , 500 , 10000 ) ,
1113
+ ] ;
1114
+
1115
+ let ( contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1116
+ setup_exclusive_test ( & component_data, 8 , Some ( 5 ) ) ?;
1117
+
1118
+ // Create exclusive content metadata for pkg1 and pkg2
1119
+ let exclusive_content: Vec < ObjectSourceMetaSized > =
1120
+ vec ! [ contentmeta[ 0 ] . clone( ) , contentmeta[ 1 ] . clone( ) ] ;
1121
+ exclusive_meta. sizes = exclusive_content;
1122
+
1123
+ chunking. process_mapping (
1124
+ & regular_meta,
1125
+ & Some ( NonZeroU32 :: new ( 8 ) . unwrap ( ) ) ,
1126
+ None ,
1127
+ Some ( & exclusive_meta) ,
1128
+ ) ?;
1129
+
1130
+ // Verify exclusive chunks are created first
1131
+ assert ! ( chunking. chunks. len( ) >= 2 ) ;
1132
+ assert_eq ! ( chunking. chunks[ 0 ] . name, "pkg1" ) ;
1133
+ assert_eq ! ( chunking. chunks[ 1 ] . name, "pkg2" ) ;
1134
+ assert_eq ! ( chunking. chunks[ 0 ] . packages, vec![ "pkg1" . to_string( ) ] ) ;
1135
+ assert_eq ! ( chunking. chunks[ 1 ] . packages, vec![ "pkg2" . to_string( ) ] ) ;
1136
+
1137
+ Ok ( ( ) )
1138
+ }
1139
+
1140
+ #[ test]
1141
+ fn test_exclusive_chunks_with_regular_packing ( ) -> Result < ( ) > {
1142
+ // Test that exclusive chunks are created first, then regular packing continues
1143
+ let component_data = [
1144
+ ( 1 , 100 , 50000 ) , // exclusive
1145
+ ( 2 , 200 , 40000 ) , // exclusive
1146
+ ( 3 , 300 , 30000 ) , // regular
1147
+ ( 4 , 400 , 20000 ) , // regular
1148
+ ( 5 , 500 , 10000 ) , // regular
1149
+ ( 6 , 600 , 5000 ) , // regular
1150
+ ] ;
1151
+
1152
+ let ( contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1153
+ setup_exclusive_test ( & component_data, 8 , Some ( 6 ) ) ?;
1154
+
1155
+ // Create exclusive content metadata for pkg1 and pkg2
1156
+ let exclusive_content: Vec < ObjectSourceMetaSized > =
1157
+ vec ! [ contentmeta[ 0 ] . clone( ) , contentmeta[ 1 ] . clone( ) ] ;
1158
+ exclusive_meta. sizes = exclusive_content;
1159
+
1160
+ chunking. process_mapping (
1161
+ & regular_meta,
1162
+ & Some ( NonZeroU32 :: new ( 8 ) . unwrap ( ) ) ,
1163
+ None ,
1164
+ Some ( & exclusive_meta) ,
1165
+ ) ?;
1166
+
1167
+ // Verify exclusive chunks are created first
1168
+ assert ! ( chunking. chunks. len( ) >= 2 ) ;
1169
+ assert_eq ! ( chunking. chunks[ 0 ] . name, "pkg1" ) ;
1170
+ assert_eq ! ( chunking. chunks[ 1 ] . name, "pkg2" ) ;
1171
+ assert_eq ! ( chunking. chunks[ 0 ] . packages, vec![ "pkg1" . to_string( ) ] ) ;
1172
+ assert_eq ! ( chunking. chunks[ 1 ] . packages, vec![ "pkg2" . to_string( ) ] ) ;
1173
+
1174
+ // Verify regular components are not in exclusive chunks
1175
+ for chunk in & chunking. chunks [ 2 ..] {
1176
+ assert ! ( !chunk. packages. contains( & "pkg1" . to_string( ) ) ) ;
1177
+ assert ! ( !chunk. packages. contains( & "pkg2" . to_string( ) ) ) ;
1178
+ }
1179
+
1180
+ Ok ( ( ) )
1181
+ }
1182
+
1183
+ #[ test]
1184
+ fn test_exclusive_chunks_isolation ( ) -> Result < ( ) > {
1185
+ // Test that exclusive chunks properly isolate components
1186
+ let component_data = [ ( 1 , 100 , 50000 ) , ( 2 , 200 , 40000 ) , ( 3 , 300 , 30000 ) ] ;
1187
+
1188
+ let ( contentmeta, regular_meta, mut exclusive_meta, mut chunking) =
1189
+ setup_exclusive_test ( & component_data, 8 , Some ( 3 ) ) ?;
1190
+
1191
+ // Create exclusive content metadata for pkg1 only
1192
+ let exclusive_content: Vec < ObjectSourceMetaSized > = vec ! [ contentmeta[ 0 ] . clone( ) ] ;
1193
+ exclusive_meta. sizes = exclusive_content;
1194
+
1195
+ chunking. process_mapping (
1196
+ & regular_meta,
1197
+ & Some ( NonZeroU32 :: new ( 8 ) . unwrap ( ) ) ,
1198
+ None ,
1199
+ Some ( & exclusive_meta) ,
1200
+ ) ?;
1201
+
1202
+ // Verify pkg1 is in its own exclusive chunk
1203
+ assert ! ( chunking. chunks. len( ) >= 1 ) ;
1204
+ assert_eq ! ( chunking. chunks[ 0 ] . name, "pkg1" ) ;
1205
+ assert_eq ! ( chunking. chunks[ 0 ] . packages, vec![ "pkg1" . to_string( ) ] ) ;
1206
+
1207
+ // Verify pkg2 and pkg3 are in regular chunks, not mixed with pkg1
1208
+ let mut found_pkg2 = false ;
1209
+ let mut found_pkg3 = false ;
1210
+ for chunk in & chunking. chunks [ 1 ..] {
1211
+ if chunk. packages . contains ( & "pkg2" . to_string ( ) ) {
1212
+ found_pkg2 = true ;
1213
+ assert ! ( !chunk. packages. contains( & "pkg1" . to_string( ) ) ) ;
1214
+ }
1215
+ if chunk. packages . contains ( & "pkg3" . to_string ( ) ) {
1216
+ found_pkg3 = true ;
1217
+ assert ! ( !chunk. packages. contains( & "pkg1" . to_string( ) ) ) ;
1218
+ }
1219
+ }
1220
+ assert ! ( found_pkg2 && found_pkg3) ;
1221
+
1222
+ Ok ( ( ) )
1223
+ }
1006
1224
}
0 commit comments