1use std::collections::HashMap;
7
8#[derive(Debug, Clone)]
10pub struct BlockFragment {
11 pub block_id: u64,
13 pub cid: String,
15 pub size_bytes: u64,
17 pub created_at_tick: u64,
19 pub last_accessed_tick: u64,
21}
22
23#[derive(Debug, Clone)]
25pub struct CompactionSegment {
26 pub segment_id: u64,
28 pub block_ids: Vec<u64>,
30 pub total_bytes: u64,
32 pub target_size_bytes: u64,
34}
35
36impl CompactionSegment {
37 pub fn fill_ratio(&self) -> f64 {
40 if self.target_size_bytes == 0 {
41 0.0
42 } else {
43 self.total_bytes as f64 / self.target_size_bytes as f64
44 }
45 }
46
47 pub fn is_full(&self) -> bool {
49 self.total_bytes >= self.target_size_bytes
50 }
51}
52
53#[derive(Debug, Clone)]
55pub struct CompactionPlan {
56 pub segments: Vec<CompactionSegment>,
58 pub blocks_compacted: usize,
60 pub bytes_compacted: u64,
62 pub estimated_savings_bytes: u64,
64}
65
66impl CompactionPlan {
67 pub fn segment_count(&self) -> usize {
69 self.segments.len()
70 }
71}
72
73#[derive(Debug, Clone)]
75pub struct CompactorConfig {
76 pub target_segment_bytes: u64,
78 pub min_block_size_to_compact: u64,
80 pub max_blocks_per_segment: usize,
82}
83
84impl Default for CompactorConfig {
85 fn default() -> Self {
86 Self {
87 target_segment_bytes: 4_194_304, min_block_size_to_compact: 65_536, max_blocks_per_segment: 128,
90 }
91 }
92}
93
94#[derive(Debug, Clone, Default)]
96pub struct CompactorStats {
97 pub total_plans_generated: u64,
99 pub total_blocks_compacted: u64,
101 pub total_bytes_compacted: u64,
103 pub total_segments_created: u64,
105}
106
107pub struct StorageBlockCompactor {
109 pub fragments: HashMap<u64, BlockFragment>,
111 pub config: CompactorConfig,
113 pub stats: CompactorStats,
115 pub next_segment_id: u64,
117}
118
119impl StorageBlockCompactor {
120 pub fn new(config: CompactorConfig) -> Self {
122 Self {
123 fragments: HashMap::new(),
124 config,
125 stats: CompactorStats::default(),
126 next_segment_id: 0,
127 }
128 }
129
130 pub fn register_block(&mut self, block_id: u64, cid: String, size_bytes: u64, tick: u64) {
135 if size_bytes >= self.config.min_block_size_to_compact {
136 return;
137 }
138 let fragment = BlockFragment {
139 block_id,
140 cid,
141 size_bytes,
142 created_at_tick: tick,
143 last_accessed_tick: tick,
144 };
145 self.fragments.insert(block_id, fragment);
146 }
147
148 pub fn touch(&mut self, block_id: u64, tick: u64) -> bool {
152 match self.fragments.get_mut(&block_id) {
153 Some(fragment) => {
154 fragment.last_accessed_tick = tick;
155 true
156 }
157 None => false,
158 }
159 }
160
161 pub fn plan_compaction(&mut self) -> CompactionPlan {
166 let mut sorted: Vec<BlockFragment> = self.fragments.values().cloned().collect();
168 sorted.sort_by(|a, b| {
169 a.size_bytes
170 .cmp(&b.size_bytes)
171 .then_with(|| a.block_id.cmp(&b.block_id))
172 });
173
174 let target = self.config.target_segment_bytes;
175 let max_per_seg = self.config.max_blocks_per_segment;
176
177 let mut segments: Vec<CompactionSegment> = Vec::new();
178
179 let mut idx = 0;
180 while idx < sorted.len() {
181 let mut seg_block_ids: Vec<u64> = Vec::new();
182 let mut seg_bytes: u64 = 0;
183
184 while idx < sorted.len() && seg_bytes < target && seg_block_ids.len() < max_per_seg {
186 let frag = &sorted[idx];
187 seg_block_ids.push(frag.block_id);
188 seg_bytes += frag.size_bytes;
189 idx += 1;
190 }
191
192 if seg_block_ids.len() < 2 {
194 continue;
195 }
196
197 seg_block_ids.sort_unstable();
198
199 let seg = CompactionSegment {
200 segment_id: self.next_segment_id,
201 block_ids: seg_block_ids,
202 total_bytes: seg_bytes,
203 target_size_bytes: target,
204 };
205 self.next_segment_id += 1;
206 segments.push(seg);
207 }
208
209 let blocks_compacted: usize = segments.iter().map(|s| s.block_ids.len()).sum();
210 let bytes_compacted: u64 = segments.iter().map(|s| s.total_bytes).sum();
211 let estimated_savings_bytes = 64 * blocks_compacted as u64;
212
213 self.stats.total_plans_generated += 1;
215 self.stats.total_blocks_compacted += blocks_compacted as u64;
216 self.stats.total_bytes_compacted += bytes_compacted;
217 self.stats.total_segments_created += segments.len() as u64;
218
219 CompactionPlan {
220 segments,
221 blocks_compacted,
222 bytes_compacted,
223 estimated_savings_bytes,
224 }
225 }
226
227 pub fn remove_block(&mut self, block_id: u64) -> bool {
231 self.fragments.remove(&block_id).is_some()
232 }
233
234 pub fn fragmentation_ratio(&self) -> f64 {
239 let total = self.fragments.len();
240 if total == 0 {
241 return 0.0;
242 }
243 let threshold = self.config.target_segment_bytes / 2;
244 let small_count = self
245 .fragments
246 .values()
247 .filter(|f| f.size_bytes < threshold)
248 .count();
249 small_count as f64 / total as f64
250 }
251
252 pub fn stats(&self) -> &CompactorStats {
254 &self.stats
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 fn default_compactor() -> StorageBlockCompactor {
263 StorageBlockCompactor::new(CompactorConfig::default())
264 }
265
266 #[test]
269 fn test_register_small_block_accepted() {
270 let mut c = default_compactor();
271 c.register_block(1, "cid1".into(), 1024, 10);
272 assert_eq!(c.fragments.len(), 1);
273 }
274
275 #[test]
276 fn test_register_block_at_boundary_excluded() {
277 let mut c = default_compactor();
278 c.register_block(1, "cid1".into(), 65_536, 10);
280 assert!(c.fragments.is_empty());
281 }
282
283 #[test]
284 fn test_register_large_block_not_registered() {
285 let mut c = default_compactor();
286 c.register_block(99, "cid99".into(), 1_000_000, 5);
287 assert!(c.fragments.is_empty());
288 }
289
290 #[test]
291 fn test_register_block_stores_fields() {
292 let mut c = default_compactor();
293 c.register_block(42, "bafycid".into(), 512, 100);
294 let frag = c.fragments.get(&42).expect("fragment should exist");
295 assert_eq!(frag.block_id, 42);
296 assert_eq!(frag.cid, "bafycid");
297 assert_eq!(frag.size_bytes, 512);
298 assert_eq!(frag.created_at_tick, 100);
299 assert_eq!(frag.last_accessed_tick, 100);
300 }
301
302 #[test]
303 fn test_register_multiple_small_blocks() {
304 let mut c = default_compactor();
305 for i in 0..10_u64 {
306 c.register_block(i, format!("cid{i}"), 1024 * (i + 1), i);
307 }
308 assert_eq!(c.fragments.len(), 10);
309 }
310
311 #[test]
314 fn test_touch_updates_last_accessed_tick() {
315 let mut c = default_compactor();
316 c.register_block(1, "cid1".into(), 512, 5);
317 let updated = c.touch(1, 99);
318 assert!(updated);
319 let frag = c.fragments.get(&1).expect("fragment should exist");
320 assert_eq!(frag.last_accessed_tick, 99);
321 assert_eq!(frag.created_at_tick, 5); }
323
324 #[test]
325 fn test_touch_returns_false_for_missing_block() {
326 let mut c = default_compactor();
327 assert!(!c.touch(999, 10));
328 }
329
330 #[test]
331 fn test_touch_does_not_change_created_at() {
332 let mut c = default_compactor();
333 c.register_block(7, "cid7".into(), 256, 1);
334 c.touch(7, 500);
335 let frag = c.fragments.get(&7).unwrap();
336 assert_eq!(frag.created_at_tick, 1);
337 }
338
339 #[test]
342 fn test_remove_existing_block_returns_true() {
343 let mut c = default_compactor();
344 c.register_block(5, "cid5".into(), 1000, 0);
345 assert!(c.remove_block(5));
346 assert!(c.fragments.is_empty());
347 }
348
349 #[test]
350 fn test_remove_missing_block_returns_false() {
351 let mut c = default_compactor();
352 assert!(!c.remove_block(404));
353 }
354
355 #[test]
358 fn test_fill_ratio_normal() {
359 let seg = CompactionSegment {
360 segment_id: 0,
361 block_ids: vec![1, 2],
362 total_bytes: 2_097_152,
363 target_size_bytes: 4_194_304,
364 };
365 let ratio = seg.fill_ratio();
366 assert!((ratio - 0.5).abs() < 1e-9);
367 }
368
369 #[test]
370 fn test_fill_ratio_zero_target() {
371 let seg = CompactionSegment {
372 segment_id: 0,
373 block_ids: vec![1],
374 total_bytes: 1024,
375 target_size_bytes: 0,
376 };
377 assert_eq!(seg.fill_ratio(), 0.0);
378 }
379
380 #[test]
381 fn test_is_full_true() {
382 let seg = CompactionSegment {
383 segment_id: 0,
384 block_ids: vec![1, 2],
385 total_bytes: 4_194_304,
386 target_size_bytes: 4_194_304,
387 };
388 assert!(seg.is_full());
389 }
390
391 #[test]
392 fn test_is_full_false() {
393 let seg = CompactionSegment {
394 segment_id: 0,
395 block_ids: vec![1, 2],
396 total_bytes: 1024,
397 target_size_bytes: 4_194_304,
398 };
399 assert!(!seg.is_full());
400 }
401
402 #[test]
405 fn test_plan_groups_small_blocks_into_segment() {
406 let mut c = default_compactor();
407 for i in 0..5_u64 {
408 c.register_block(i, format!("cid{i}"), 8_192, i);
409 }
410 let plan = c.plan_compaction();
411 assert!(!plan.segments.is_empty());
412 assert_eq!(plan.blocks_compacted, 5);
413 }
414
415 #[test]
416 fn test_plan_excludes_singleton_segment() {
417 let mut c = default_compactor();
419 c.register_block(1, "cid1".into(), 1024, 0);
420 let plan = c.plan_compaction();
421 assert_eq!(plan.segment_count(), 0);
422 assert_eq!(plan.blocks_compacted, 0);
423 assert_eq!(plan.bytes_compacted, 0);
424 assert_eq!(plan.estimated_savings_bytes, 0);
425 }
426
427 #[test]
428 fn test_plan_blocks_compacted_total() {
429 let mut c = default_compactor();
430 for i in 0..10_u64 {
431 c.register_block(i, format!("cid{i}"), 4096, i);
432 }
433 let plan = c.plan_compaction();
434 assert_eq!(plan.blocks_compacted, 10);
435 }
436
437 #[test]
438 fn test_plan_bytes_compacted_total() {
439 let mut c = default_compactor();
440 for i in 0..4_u64 {
441 c.register_block(i, format!("cid{i}"), 1000, 0);
442 }
443 let plan = c.plan_compaction();
444 assert_eq!(plan.bytes_compacted, 4000);
445 }
446
447 #[test]
448 fn test_estimated_savings_bytes() {
449 let mut c = default_compactor();
450 for i in 0..6_u64 {
451 c.register_block(i, format!("cid{i}"), 512, 0);
452 }
453 let plan = c.plan_compaction();
454 assert_eq!(
455 plan.estimated_savings_bytes,
456 64 * plan.blocks_compacted as u64
457 );
458 }
459
460 #[test]
461 fn test_segment_count() {
462 let mut c = default_compactor();
463 for i in 0..130_u64 {
467 c.register_block(i, format!("cid{i}"), 65_535, i);
468 }
469 let plan = c.plan_compaction();
470 assert!(plan.segment_count() >= 2);
471 }
472
473 #[test]
474 fn test_max_blocks_per_segment_cap() {
475 let config = CompactorConfig {
476 max_blocks_per_segment: 4,
477 target_segment_bytes: 4_194_304,
478 ..CompactorConfig::default()
479 };
480 let mut c = StorageBlockCompactor::new(config);
481 for i in 0..10_u64 {
482 c.register_block(i, format!("cid{i}"), 256, 0);
483 }
484 let plan = c.plan_compaction();
485 for seg in &plan.segments {
486 assert!(seg.block_ids.len() <= 4);
487 }
488 }
489
490 #[test]
491 fn test_segment_block_ids_sorted_ascending() {
492 let mut c = default_compactor();
493 for i in (0_u64..5).rev() {
495 c.register_block(i, format!("cid{i}"), 1024, 0);
496 }
497 let plan = c.plan_compaction();
498 for seg in &plan.segments {
499 let sorted = {
500 let mut ids = seg.block_ids.clone();
501 ids.sort_unstable();
502 ids
503 };
504 assert_eq!(seg.block_ids, sorted);
505 }
506 }
507
508 #[test]
509 fn test_plan_with_no_fragments_returns_empty_plan() {
510 let mut c = default_compactor();
511 let plan = c.plan_compaction();
512 assert_eq!(plan.segment_count(), 0);
513 assert_eq!(plan.blocks_compacted, 0);
514 assert_eq!(plan.bytes_compacted, 0);
515 assert_eq!(plan.estimated_savings_bytes, 0);
516 }
517
518 #[test]
521 fn test_fragmentation_ratio_no_fragments() {
522 let c = default_compactor();
523 assert_eq!(c.fragmentation_ratio(), 0.0);
524 }
525
526 #[test]
527 fn test_fragmentation_ratio_all_small() {
528 let mut c = default_compactor();
529 for i in 0..5_u64 {
531 c.register_block(i, format!("cid{i}"), 1024, 0);
532 }
533 let ratio = c.fragmentation_ratio();
534 assert!((ratio - 1.0).abs() < 1e-9);
535 }
536
537 #[test]
538 fn test_fragmentation_ratio_mixed() {
539 let config = CompactorConfig {
540 target_segment_bytes: 4_000,
541 min_block_size_to_compact: 10_000,
542 max_blocks_per_segment: 128,
543 };
544 let mut c = StorageBlockCompactor::new(config);
545 c.register_block(1, "a".into(), 500, 0); c.register_block(2, "b".into(), 1000, 0); c.register_block(3, "c".into(), 1500, 0); c.register_block(4, "d".into(), 2000, 0); c.register_block(5, "e".into(), 3000, 0); let ratio = c.fragmentation_ratio();
553 assert!((ratio - 3.0 / 5.0).abs() < 1e-9);
554 }
555
556 #[test]
559 fn test_stats_accumulate_across_plans() {
560 let mut c = default_compactor();
561 for i in 0..4_u64 {
562 c.register_block(i, format!("cid{i}"), 1024, 0);
563 }
564 c.plan_compaction();
565 c.plan_compaction(); let s = c.stats();
568 assert_eq!(s.total_plans_generated, 2);
569 assert_eq!(s.total_blocks_compacted, 8);
571 }
572
573 #[test]
574 fn test_stats_total_segments_created() {
575 let mut c = default_compactor();
576 for i in 0..4_u64 {
577 c.register_block(i, format!("cid{i}"), 512, 0);
578 }
579 let plan = c.plan_compaction();
580 let seg_count = plan.segment_count() as u64;
581 assert_eq!(c.stats().total_segments_created, seg_count);
582 }
583
584 #[test]
585 fn test_stats_bytes_compacted_accumulate() {
586 let mut c = default_compactor();
587 for i in 0..4_u64 {
588 c.register_block(i, format!("cid{i}"), 1000, 0);
589 }
590 c.plan_compaction(); c.plan_compaction(); assert_eq!(c.stats().total_bytes_compacted, 8000);
594 }
595
596 #[test]
597 fn test_stats_initial_zero() {
598 let c = default_compactor();
599 let s = c.stats();
600 assert_eq!(s.total_plans_generated, 0);
601 assert_eq!(s.total_blocks_compacted, 0);
602 assert_eq!(s.total_bytes_compacted, 0);
603 assert_eq!(s.total_segments_created, 0);
604 }
605}