1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum CompactionState {
10 Idle,
12 Analyzing,
14 Compacting,
16 Completed,
18 Failed,
20}
21
22#[derive(Debug, Clone)]
24pub struct CompactorConfig {
25 pub fragmentation_threshold: f64,
28 pub max_budget_bytes: u64,
31 pub min_interval_ticks: u64,
34}
35
36impl Default for CompactorConfig {
37 fn default() -> Self {
38 Self {
39 fragmentation_threshold: 0.3,
40 max_budget_bytes: 100 * 1024 * 1024, min_interval_ticks: 100,
42 }
43 }
44}
45
46#[derive(Debug, Clone)]
48pub struct FragmentationReport {
49 pub total_bytes: u64,
51 pub used_bytes: u64,
53 pub fragmentation_ratio: f64,
55 pub fragmented_regions: usize,
57}
58
59#[derive(Debug, Clone)]
61pub struct CompactionResult {
62 pub regions_merged: usize,
64 pub bytes_reclaimed: u64,
66 pub duration_ticks: u64,
68}
69
70#[derive(Debug, Clone)]
72pub struct CompactorStats {
73 pub state: CompactionState,
75 pub runs_completed: u64,
77 pub bytes_reclaimed_total: u64,
79 pub current_fragmentation: f64,
81}
82
83pub struct StorageCompactor {
88 config: CompactorConfig,
89 state: CompactionState,
90 last_run_tick: u64,
91 current_tick: u64,
92 runs_completed: u64,
93 bytes_reclaimed_total: u64,
94 regions: Vec<(u64, u64, bool)>,
96 last_fragmentation: f64,
98}
99
100impl StorageCompactor {
101 pub fn new(config: CompactorConfig) -> Self {
103 Self {
104 config,
105 state: CompactionState::Idle,
106 last_run_tick: 0,
107 current_tick: 0,
108 runs_completed: 0,
109 bytes_reclaimed_total: 0,
110 regions: Vec::new(),
111 last_fragmentation: 0.0,
112 }
113 }
114
115 pub fn add_region(&mut self, offset: u64, size: u64, is_used: bool) {
117 self.regions.push((offset, size, is_used));
118 }
119
120 pub fn remove_region(&mut self, offset: u64) -> bool {
123 if let Some(pos) = self.regions.iter().position(|r| r.0 == offset) {
124 self.regions.remove(pos);
125 true
126 } else {
127 false
128 }
129 }
130
131 pub fn analyze(&mut self) -> FragmentationReport {
137 self.state = CompactionState::Analyzing;
138
139 let total_bytes: u64 = self.regions.iter().map(|r| r.1).sum();
140 let used_bytes: u64 = self.regions.iter().filter(|r| r.2).map(|r| r.1).sum();
141 let fragmented_regions = self.regions.iter().filter(|r| !r.2).count();
142
143 let fragmentation_ratio = if total_bytes == 0 {
144 0.0
145 } else {
146 (total_bytes - used_bytes) as f64 / total_bytes as f64
147 };
148
149 self.last_fragmentation = fragmentation_ratio;
150
151 self.state = CompactionState::Idle;
153
154 FragmentationReport {
155 total_bytes,
156 used_bytes,
157 fragmentation_ratio,
158 fragmented_regions,
159 }
160 }
161
162 pub fn should_compact(&self) -> bool {
165 self.last_fragmentation > self.config.fragmentation_threshold
166 && (self.current_tick.saturating_sub(self.last_run_tick)
167 >= self.config.min_interval_ticks)
168 }
169
170 pub fn compact(&mut self) -> Result<CompactionResult, String> {
174 if self.state == CompactionState::Compacting {
175 return Err("compaction already in progress".to_string());
176 }
177
178 self.state = CompactionState::Compacting;
179
180 self.regions.sort_by_key(|r| r.0);
182
183 let mut merged: Vec<(u64, u64, bool)> = Vec::with_capacity(self.regions.len());
184 let mut regions_merged: usize = 0;
185 let mut bytes_reclaimed: u64 = 0;
186 let mut budget_remaining = self.config.max_budget_bytes;
187
188 for ®ion in &self.regions {
189 if let Some(last) = merged.last_mut() {
190 let adjacent = last.0 + last.1 == region.0;
193 let both_free = !last.2 && !region.2;
194
195 if adjacent && both_free && budget_remaining >= region.1 {
196 bytes_reclaimed += region.1;
198 budget_remaining = budget_remaining.saturating_sub(region.1);
199 last.1 += region.1;
200 regions_merged += 1;
201 continue;
202 }
203 }
204 merged.push(region);
205 }
206
207 self.regions = merged;
208 self.bytes_reclaimed_total += bytes_reclaimed;
209 self.runs_completed += 1;
210 self.last_run_tick = self.current_tick;
211 self.state = CompactionState::Completed;
212
213 Ok(CompactionResult {
214 regions_merged,
215 bytes_reclaimed,
216 duration_ticks: 1,
217 })
218 }
219
220 pub fn tick(&mut self) {
222 self.current_tick += 1;
223 }
224
225 pub fn reclaimed_bytes(&self) -> u64 {
227 self.bytes_reclaimed_total
228 }
229
230 pub fn stats(&self) -> CompactorStats {
232 CompactorStats {
233 state: self.state,
234 runs_completed: self.runs_completed,
235 bytes_reclaimed_total: self.bytes_reclaimed_total,
236 current_fragmentation: self.last_fragmentation,
237 }
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 fn default_compactor() -> StorageCompactor {
246 StorageCompactor::new(CompactorConfig::default())
247 }
248
249 #[test]
252 fn empty_compactor_zero_fragmentation() {
253 let mut c = default_compactor();
254 let report = c.analyze();
255 assert_eq!(report.total_bytes, 0);
256 assert_eq!(report.used_bytes, 0);
257 assert!((report.fragmentation_ratio - 0.0).abs() < f64::EPSILON);
258 assert_eq!(report.fragmented_regions, 0);
259 }
260
261 #[test]
262 fn empty_compactor_state_idle() {
263 let c = default_compactor();
264 assert_eq!(c.state, CompactionState::Idle);
265 }
266
267 #[test]
268 fn empty_compactor_stats() {
269 let c = default_compactor();
270 let s = c.stats();
271 assert_eq!(s.runs_completed, 0);
272 assert_eq!(s.bytes_reclaimed_total, 0);
273 assert!((s.current_fragmentation - 0.0).abs() < f64::EPSILON);
274 }
275
276 #[test]
279 fn add_regions_and_analyze_all_used() {
280 let mut c = default_compactor();
281 c.add_region(0, 1000, true);
282 c.add_region(1000, 2000, true);
283 let report = c.analyze();
284 assert_eq!(report.total_bytes, 3000);
285 assert_eq!(report.used_bytes, 3000);
286 assert!((report.fragmentation_ratio - 0.0).abs() < f64::EPSILON);
287 assert_eq!(report.fragmented_regions, 0);
288 }
289
290 #[test]
291 fn add_regions_and_analyze_half_free() {
292 let mut c = default_compactor();
293 c.add_region(0, 500, true);
294 c.add_region(500, 500, false);
295 let report = c.analyze();
296 assert_eq!(report.total_bytes, 1000);
297 assert_eq!(report.used_bytes, 500);
298 assert!((report.fragmentation_ratio - 0.5).abs() < f64::EPSILON);
299 assert_eq!(report.fragmented_regions, 1);
300 }
301
302 #[test]
303 fn analyze_multiple_free_regions() {
304 let mut c = default_compactor();
305 c.add_region(0, 100, false);
306 c.add_region(100, 200, true);
307 c.add_region(300, 100, false);
308 c.add_region(400, 100, false);
309 let report = c.analyze();
310 assert_eq!(report.total_bytes, 500);
311 assert_eq!(report.used_bytes, 200);
312 assert_eq!(report.fragmented_regions, 3);
313 let expected = 300.0 / 500.0;
314 assert!((report.fragmentation_ratio - expected).abs() < f64::EPSILON);
315 }
316
317 #[test]
320 fn should_compact_below_threshold() {
321 let mut c = StorageCompactor::new(CompactorConfig {
322 fragmentation_threshold: 0.5,
323 min_interval_ticks: 0,
324 ..CompactorConfig::default()
325 });
326 c.add_region(0, 800, true);
327 c.add_region(800, 200, false);
328 c.analyze(); assert!(!c.should_compact());
330 }
331
332 #[test]
333 fn should_compact_above_threshold() {
334 let mut c = StorageCompactor::new(CompactorConfig {
335 fragmentation_threshold: 0.1,
336 min_interval_ticks: 0,
337 ..CompactorConfig::default()
338 });
339 c.add_region(0, 200, true);
340 c.add_region(200, 800, false);
341 c.analyze(); assert!(c.should_compact());
343 }
344
345 #[test]
346 fn should_compact_respects_interval() {
347 let mut c = StorageCompactor::new(CompactorConfig {
348 fragmentation_threshold: 0.1,
349 min_interval_ticks: 50,
350 ..CompactorConfig::default()
351 });
352 c.add_region(0, 100, true);
353 c.add_region(100, 900, false);
354 c.analyze();
355 assert!(!c.should_compact());
358
359 for _ in 0..50 {
360 c.tick();
361 }
362 assert!(c.should_compact());
363 }
364
365 #[test]
366 fn should_compact_not_after_recent_run() {
367 let mut c = StorageCompactor::new(CompactorConfig {
368 fragmentation_threshold: 0.1,
369 min_interval_ticks: 10,
370 ..CompactorConfig::default()
371 });
372 c.add_region(0, 100, false);
373 c.add_region(100, 100, false);
374 for _ in 0..20 {
375 c.tick();
376 }
377 c.analyze();
378 assert!(c.should_compact());
379 let _ = c.compact();
380 c.add_region(200, 100, false);
382 c.analyze();
383 assert!(!c.should_compact());
385 }
386
387 #[test]
390 fn compact_merges_adjacent_free_regions() {
391 let mut c = default_compactor();
392 c.add_region(0, 100, false);
393 c.add_region(100, 200, false);
394 c.add_region(300, 300, false);
395 let result = c.compact().expect("compaction should succeed");
396 assert_eq!(result.regions_merged, 2);
397 assert_eq!(result.bytes_reclaimed, 500); assert_eq!(c.regions.len(), 1);
400 assert_eq!(c.regions[0], (0, 600, false));
401 }
402
403 #[test]
404 fn compact_does_not_merge_used_regions() {
405 let mut c = default_compactor();
406 c.add_region(0, 100, true);
407 c.add_region(100, 200, true);
408 let result = c.compact().expect("compaction should succeed");
409 assert_eq!(result.regions_merged, 0);
410 assert_eq!(result.bytes_reclaimed, 0);
411 assert_eq!(c.regions.len(), 2);
412 }
413
414 #[test]
415 fn compact_non_adjacent_free_regions_stay_separate() {
416 let mut c = default_compactor();
417 c.add_region(0, 100, false);
418 c.add_region(200, 100, false); let result = c.compact().expect("compaction should succeed");
420 assert_eq!(result.regions_merged, 0);
421 assert_eq!(result.bytes_reclaimed, 0);
422 assert_eq!(c.regions.len(), 2);
423 }
424
425 #[test]
426 fn compact_mixed_regions() {
427 let mut c = default_compactor();
428 c.add_region(0, 100, false);
430 c.add_region(100, 100, false);
431 c.add_region(200, 100, true);
432 c.add_region(300, 100, false);
433 c.add_region(400, 100, false);
434 let result = c.compact().expect("compaction should succeed");
435 assert_eq!(result.regions_merged, 2);
437 assert_eq!(result.bytes_reclaimed, 200); assert_eq!(c.regions.len(), 3);
439 }
440
441 #[test]
442 fn compact_free_used_alternating() {
443 let mut c = default_compactor();
444 c.add_region(0, 50, false);
445 c.add_region(50, 50, true);
446 c.add_region(100, 50, false);
447 c.add_region(150, 50, true);
448 c.add_region(200, 50, false);
449 let result = c.compact().expect("compaction should succeed");
450 assert_eq!(result.regions_merged, 0);
451 assert_eq!(c.regions.len(), 5);
452 }
453
454 #[test]
457 fn state_idle_to_analyzing_to_idle() {
458 let mut c = default_compactor();
459 assert_eq!(c.state, CompactionState::Idle);
460 c.add_region(0, 100, true);
462 let _ = c.analyze();
463 assert_eq!(c.state, CompactionState::Idle);
464 }
465
466 #[test]
467 fn state_compacting_to_completed() {
468 let mut c = default_compactor();
469 c.add_region(0, 100, false);
470 let _ = c.compact();
471 assert_eq!(c.state, CompactionState::Completed);
472 }
473
474 #[test]
475 fn cannot_compact_while_compacting() {
476 let mut c = default_compactor();
479 c.state = CompactionState::Compacting;
480 let result = c.compact();
481 assert!(result.is_err());
482 assert_eq!(
483 result.expect_err("should be error"),
484 "compaction already in progress"
485 );
486 }
487
488 #[test]
491 fn remove_region_existing() {
492 let mut c = default_compactor();
493 c.add_region(0, 100, true);
494 c.add_region(100, 200, false);
495 assert!(c.remove_region(0));
496 assert_eq!(c.regions.len(), 1);
497 assert_eq!(c.regions[0].0, 100);
498 }
499
500 #[test]
501 fn remove_region_nonexistent() {
502 let mut c = default_compactor();
503 c.add_region(0, 100, true);
504 assert!(!c.remove_region(999));
505 assert_eq!(c.regions.len(), 1);
506 }
507
508 #[test]
511 fn reclaimed_bytes_zero_initially() {
512 let c = default_compactor();
513 assert_eq!(c.reclaimed_bytes(), 0);
514 }
515
516 #[test]
517 fn reclaimed_bytes_after_compact() {
518 let mut c = default_compactor();
519 c.add_region(0, 100, false);
520 c.add_region(100, 200, false);
521 let _ = c.compact();
522 assert_eq!(c.reclaimed_bytes(), 200);
523 }
524
525 #[test]
528 fn multiple_runs_accumulate_stats() {
529 let mut c = default_compactor();
530 c.add_region(0, 100, false);
532 c.add_region(100, 100, false);
533 let r1 = c.compact().expect("run 1");
534 assert_eq!(r1.bytes_reclaimed, 100);
535 assert_eq!(c.runs_completed, 1);
536
537 c.add_region(200, 50, false);
539 c.add_region(250, 50, false);
540 let r2 = c.compact().expect("run 2");
541 assert_eq!(r2.bytes_reclaimed, 100);
543 assert_eq!(c.runs_completed, 2);
544 assert_eq!(c.reclaimed_bytes(), 200);
545 }
546
547 #[test]
548 fn stats_reflect_multiple_runs() {
549 let mut c = default_compactor();
550 c.add_region(0, 100, false);
551 c.add_region(100, 100, false);
552 let _ = c.compact(); c.add_region(500, 100, false);
555 c.add_region(600, 100, false);
556 let _ = c.compact(); let s = c.stats();
559 assert_eq!(s.runs_completed, 2);
560 assert_eq!(s.bytes_reclaimed_total, 200);
561 assert_eq!(s.state, CompactionState::Completed);
562 }
563
564 #[test]
567 fn tick_increments() {
568 let mut c = default_compactor();
569 assert_eq!(c.current_tick, 0);
570 c.tick();
571 c.tick();
572 c.tick();
573 assert_eq!(c.current_tick, 3);
574 }
575
576 #[test]
579 fn compact_respects_budget() {
580 let mut c = StorageCompactor::new(CompactorConfig {
581 max_budget_bytes: 150,
582 ..CompactorConfig::default()
583 });
584 c.add_region(0, 100, false);
586 c.add_region(100, 100, false);
587 c.add_region(200, 100, false);
588 let result = c.compact().expect("compaction should succeed");
589 assert_eq!(result.regions_merged, 1);
591 assert_eq!(result.bytes_reclaimed, 100);
592 }
593
594 #[test]
597 fn compact_single_free_region() {
598 let mut c = default_compactor();
599 c.add_region(0, 500, false);
600 let result = c.compact().expect("compaction should succeed");
601 assert_eq!(result.regions_merged, 0);
602 assert_eq!(result.bytes_reclaimed, 0);
603 assert_eq!(c.regions.len(), 1);
604 }
605
606 #[test]
607 fn compact_single_used_region() {
608 let mut c = default_compactor();
609 c.add_region(0, 500, true);
610 let result = c.compact().expect("compaction should succeed");
611 assert_eq!(result.regions_merged, 0);
612 assert_eq!(c.regions.len(), 1);
613 }
614
615 #[test]
616 fn analyze_only_free_regions() {
617 let mut c = default_compactor();
618 c.add_region(0, 1000, false);
619 let report = c.analyze();
620 assert_eq!(report.fragmentation_ratio, 1.0);
621 assert_eq!(report.fragmented_regions, 1);
622 }
623
624 #[test]
625 fn compact_many_adjacent_free() {
626 let mut c = default_compactor();
627 for i in 0..10 {
628 c.add_region(i * 100, 100, false);
629 }
630 let result = c.compact().expect("compaction should succeed");
631 assert_eq!(result.regions_merged, 9);
632 assert_eq!(result.bytes_reclaimed, 900);
633 assert_eq!(c.regions.len(), 1);
634 assert_eq!(c.regions[0], (0, 1000, false));
635 }
636
637 #[test]
638 fn compact_unsorted_regions() {
639 let mut c = default_compactor();
640 c.add_region(200, 100, false);
642 c.add_region(0, 100, false);
643 c.add_region(100, 100, false);
644 let result = c.compact().expect("compaction should succeed");
645 assert_eq!(result.regions_merged, 2);
646 assert_eq!(c.regions.len(), 1);
647 assert_eq!(c.regions[0], (0, 300, false));
648 }
649
650 #[test]
651 fn default_config_values() {
652 let cfg = CompactorConfig::default();
653 assert!((cfg.fragmentation_threshold - 0.3).abs() < f64::EPSILON);
654 assert_eq!(cfg.max_budget_bytes, 100 * 1024 * 1024);
655 assert_eq!(cfg.min_interval_ticks, 100);
656 }
657
658 #[test]
659 fn compaction_result_fields() {
660 let mut c = default_compactor();
661 c.add_region(0, 64, false);
662 c.add_region(64, 64, false);
663 let result = c.compact().expect("compaction should succeed");
664 assert_eq!(result.duration_ticks, 1);
665 assert_eq!(result.regions_merged, 1);
666 assert_eq!(result.bytes_reclaimed, 64);
667 }
668
669 #[test]
670 fn state_transitions_full_cycle() {
671 let mut c = default_compactor();
672 assert_eq!(c.state, CompactionState::Idle);
673 c.add_region(0, 100, false);
674 c.add_region(100, 100, false);
675 let _ = c.analyze();
676 assert_eq!(c.state, CompactionState::Idle);
677 let _ = c.compact();
678 assert_eq!(c.state, CompactionState::Completed);
679 }
680
681 #[test]
682 fn remove_region_then_analyze() {
683 let mut c = default_compactor();
684 c.add_region(0, 100, true);
685 c.add_region(100, 100, false);
686 c.remove_region(100);
687 let report = c.analyze();
688 assert_eq!(report.total_bytes, 100);
689 assert_eq!(report.used_bytes, 100);
690 assert_eq!(report.fragmented_regions, 0);
691 }
692
693 #[test]
694 fn compact_free_between_used() {
695 let mut c = default_compactor();
696 c.add_region(0, 100, true);
697 c.add_region(100, 100, false);
698 c.add_region(200, 100, true);
699 let result = c.compact().expect("compaction should succeed");
700 assert_eq!(result.regions_merged, 0);
701 assert_eq!(c.regions.len(), 3);
702 }
703}