1#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct StorageExtent {
9 pub start_offset: u64,
11 pub length_bytes: u64,
13 pub cid: Option<String>,
15}
16
17impl StorageExtent {
18 #[inline]
20 pub fn is_free(&self) -> bool {
21 self.cid.is_none()
22 }
23
24 #[inline]
26 pub fn end_offset(&self) -> u64 {
27 self.start_offset + self.length_bytes
28 }
29}
30
31#[derive(Debug, Clone)]
33pub struct FragmentationReport {
34 pub total_bytes: u64,
36 pub used_bytes: u64,
38 pub free_bytes: u64,
40 pub free_extent_count: usize,
42 pub largest_free_extent: u64,
44 pub fragmentation_score: f64,
51}
52
53impl FragmentationReport {
54 pub fn utilization(&self) -> f64 {
56 self.used_bytes as f64 / self.total_bytes.max(1) as f64
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct CompactionCandidate {
63 pub cid: String,
65 pub current_offset: u64,
67 pub target_offset: u64,
69 pub size_bytes: u64,
71}
72
73impl CompactionCandidate {
74 pub fn bytes_saved(&self) -> u64 {
79 self.current_offset.saturating_sub(self.target_offset)
80 }
81}
82
83pub struct StorageFragmentationAnalyzer {
88 pub extents: Vec<StorageExtent>,
90}
91
92impl StorageFragmentationAnalyzer {
93 pub fn new() -> Self {
95 Self {
96 extents: Vec::new(),
97 }
98 }
99
100 pub fn add_extent(&mut self, extent: StorageExtent) {
102 let pos = self
104 .extents
105 .partition_point(|e| e.start_offset < extent.start_offset);
106 self.extents.insert(pos, extent);
107 }
108
109 pub fn free_extent(&mut self, cid: &str) {
113 for extent in &mut self.extents {
114 if extent.cid.as_deref() == Some(cid) {
115 extent.cid = None;
116 }
117 }
118 }
119
120 pub fn analyze(&self) -> FragmentationReport {
122 let mut total_bytes: u64 = 0;
123 let mut used_bytes: u64 = 0;
124 let mut free_bytes: u64 = 0;
125 let mut free_extent_count: usize = 0;
126 let mut largest_free_extent: u64 = 0;
127
128 for extent in &self.extents {
129 total_bytes += extent.length_bytes;
130 if extent.is_free() {
131 free_bytes += extent.length_bytes;
132 free_extent_count += 1;
133 if extent.length_bytes > largest_free_extent {
134 largest_free_extent = extent.length_bytes;
135 }
136 } else {
137 used_bytes += extent.length_bytes;
138 }
139 }
140
141 let fragmentation_score = if free_bytes > 0 {
142 1.0 - (largest_free_extent as f64 / free_bytes as f64)
143 } else {
144 0.0
145 };
146
147 FragmentationReport {
148 total_bytes,
149 used_bytes,
150 free_bytes,
151 free_extent_count,
152 largest_free_extent,
153 fragmentation_score,
154 }
155 }
156
157 pub fn compaction_plan(&self) -> Vec<CompactionCandidate> {
163 let mut candidates: Vec<CompactionCandidate> = Vec::new();
164 let mut write_cursor: u64 = 0;
165
166 for extent in &self.extents {
167 if !extent.is_free() {
168 let target_offset = write_cursor;
169 let current_offset = extent.start_offset;
170
171 if target_offset < current_offset {
172 candidates.push(CompactionCandidate {
173 cid: extent.cid.clone().unwrap_or_default(),
174 current_offset,
175 target_offset,
176 size_bytes: extent.length_bytes,
177 });
178 }
179
180 write_cursor += extent.length_bytes;
181 }
182 }
183
184 candidates.sort_by_key(|c| std::cmp::Reverse(c.bytes_saved()));
186 candidates
187 }
188
189 pub fn merge_free_extents(&mut self) {
193 if self.extents.is_empty() {
194 return;
195 }
196
197 let mut merged: Vec<StorageExtent> = Vec::with_capacity(self.extents.len());
198
199 for extent in self.extents.drain(..) {
200 if let Some(last) = merged.last_mut() {
201 if last.is_free() && extent.is_free() && last.end_offset() == extent.start_offset {
203 last.length_bytes += extent.length_bytes;
204 continue;
205 }
206 }
207 merged.push(extent);
208 }
209
210 self.extents = merged;
211 }
212
213 pub fn total_extents(&self) -> usize {
215 self.extents.len()
216 }
217}
218
219impl Default for StorageFragmentationAnalyzer {
220 fn default() -> Self {
221 Self::new()
222 }
223}
224
225#[cfg(test)]
230mod tests {
231 use super::*;
232
233 fn used(start: u64, len: u64, cid: &str) -> StorageExtent {
234 StorageExtent {
235 start_offset: start,
236 length_bytes: len,
237 cid: Some(cid.to_string()),
238 }
239 }
240
241 fn free(start: u64, len: u64) -> StorageExtent {
242 StorageExtent {
243 start_offset: start,
244 length_bytes: len,
245 cid: None,
246 }
247 }
248
249 #[test]
252 fn test_extent_is_free_true_for_none_cid() {
253 let e = free(0, 512);
254 assert!(e.is_free());
255 }
256
257 #[test]
258 fn test_extent_is_free_false_for_some_cid() {
259 let e = used(0, 512, "QmA");
260 assert!(!e.is_free());
261 }
262
263 #[test]
264 fn test_extent_end_offset() {
265 let e = used(100, 400, "QmA");
266 assert_eq!(e.end_offset(), 500);
267 }
268
269 #[test]
272 fn test_add_extent_maintains_sorted_order() {
273 let mut analyzer = StorageFragmentationAnalyzer::new();
274 analyzer.add_extent(used(1000, 256, "QmC"));
276 analyzer.add_extent(used(0, 512, "QmA"));
277 analyzer.add_extent(used(512, 488, "QmB"));
278
279 let offsets: Vec<u64> = analyzer.extents.iter().map(|e| e.start_offset).collect();
280 assert_eq!(offsets, vec![0, 512, 1000]);
281 }
282
283 #[test]
284 fn test_add_extent_single_element() {
285 let mut analyzer = StorageFragmentationAnalyzer::new();
286 analyzer.add_extent(used(42, 100, "QmX"));
287 assert_eq!(analyzer.total_extents(), 1);
288 assert_eq!(analyzer.extents[0].start_offset, 42);
289 }
290
291 #[test]
294 fn test_free_extent_marks_as_free() {
295 let mut analyzer = StorageFragmentationAnalyzer::new();
296 analyzer.add_extent(used(0, 512, "QmA"));
297 analyzer.add_extent(used(512, 512, "QmB"));
298
299 analyzer.free_extent("QmA");
300
301 assert!(analyzer.extents[0].is_free());
302 assert!(!analyzer.extents[1].is_free());
303 }
304
305 #[test]
306 fn test_free_extent_noop_for_unknown_cid() {
307 let mut analyzer = StorageFragmentationAnalyzer::new();
308 analyzer.add_extent(used(0, 512, "QmA"));
309 analyzer.free_extent("QmZZZ");
310 assert!(!analyzer.extents[0].is_free());
312 }
313
314 #[test]
317 fn test_analyze_score_zero_when_no_free_space() {
318 let mut analyzer = StorageFragmentationAnalyzer::new();
319 analyzer.add_extent(used(0, 1024, "QmA"));
320 analyzer.add_extent(used(1024, 1024, "QmB"));
321
322 let report = analyzer.analyze();
323 assert_eq!(report.fragmentation_score, 0.0);
324 assert_eq!(report.free_extent_count, 0);
325 }
326
327 #[test]
328 fn test_analyze_score_zero_when_free_space_is_contiguous() {
329 let mut analyzer = StorageFragmentationAnalyzer::new();
330 analyzer.add_extent(used(0, 512, "QmA"));
332 analyzer.add_extent(free(512, 1024));
333
334 let report = analyzer.analyze();
335 assert_eq!(report.fragmentation_score, 0.0);
336 assert_eq!(report.free_extent_count, 1);
337 assert_eq!(report.largest_free_extent, 1024);
338 }
339
340 #[test]
343 fn test_analyze_score_positive_when_fragmented() {
344 let mut analyzer = StorageFragmentationAnalyzer::new();
345 analyzer.add_extent(used(0, 100, "QmA"));
347 analyzer.add_extent(free(100, 100));
348 analyzer.add_extent(used(200, 100, "QmB"));
349 analyzer.add_extent(free(300, 100));
350 analyzer.add_extent(used(400, 100, "QmC"));
351 analyzer.add_extent(free(500, 100));
352
353 let report = analyzer.analyze();
354 assert!(report.fragmentation_score > 0.0);
357 assert!(report.fragmentation_score < 1.0);
358 assert_eq!(report.free_extent_count, 3);
359 }
360
361 #[test]
364 fn test_analyze_largest_free_extent() {
365 let mut analyzer = StorageFragmentationAnalyzer::new();
366 analyzer.add_extent(free(0, 50));
367 analyzer.add_extent(used(50, 100, "QmA"));
368 analyzer.add_extent(free(150, 200));
369 analyzer.add_extent(used(350, 100, "QmB"));
370 analyzer.add_extent(free(450, 30));
371
372 let report = analyzer.analyze();
373 assert_eq!(report.largest_free_extent, 200);
374 assert_eq!(report.free_bytes, 280); assert_eq!(report.free_extent_count, 3);
376 }
377
378 #[test]
381 fn test_analyze_utilization() {
382 let mut analyzer = StorageFragmentationAnalyzer::new();
383 analyzer.add_extent(used(0, 750, "QmA"));
384 analyzer.add_extent(free(750, 250));
385
386 let report = analyzer.analyze();
387 assert!((report.utilization() - 0.75).abs() < 1e-10);
388 }
389
390 #[test]
391 fn test_analyze_utilization_full() {
392 let mut analyzer = StorageFragmentationAnalyzer::new();
393 analyzer.add_extent(used(0, 1024, "QmA"));
394
395 let report = analyzer.analyze();
396 assert!((report.utilization() - 1.0).abs() < 1e-10);
397 }
398
399 #[test]
402 fn test_compaction_plan_ordered_by_bytes_saved_desc() {
403 let mut analyzer = StorageFragmentationAnalyzer::new();
404 analyzer.add_extent(used(0, 100, "QmA"));
409 analyzer.add_extent(free(100, 500));
410 analyzer.add_extent(used(600, 100, "QmB"));
411 analyzer.add_extent(free(700, 100));
412 analyzer.add_extent(used(800, 100, "QmC"));
413
414 let plan = analyzer.compaction_plan();
415 assert_eq!(plan.len(), 2);
417 assert_eq!(plan[0].cid, "QmC");
418 assert_eq!(plan[0].bytes_saved(), 600);
419 assert_eq!(plan[1].cid, "QmB");
420 assert_eq!(plan[1].bytes_saved(), 500);
421 }
422
423 #[test]
426 fn test_compaction_candidate_bytes_saved() {
427 let c = CompactionCandidate {
428 cid: "QmA".to_string(),
429 current_offset: 1000,
430 target_offset: 300,
431 size_bytes: 200,
432 };
433 assert_eq!(c.bytes_saved(), 700);
434 }
435
436 #[test]
437 fn test_compaction_candidate_bytes_saved_saturating() {
438 let c = CompactionCandidate {
440 cid: "QmA".to_string(),
441 current_offset: 100,
442 target_offset: 200,
443 size_bytes: 50,
444 };
445 assert_eq!(c.bytes_saved(), 0);
446 }
447
448 #[test]
451 fn test_merge_free_extents_combines_adjacent() {
452 let mut analyzer = StorageFragmentationAnalyzer::new();
453 analyzer.add_extent(free(0, 100));
455 analyzer.add_extent(free(100, 200));
456 analyzer.add_extent(used(300, 50, "QmA"));
457
458 analyzer.merge_free_extents();
459
460 assert_eq!(analyzer.total_extents(), 2);
461 let first = &analyzer.extents[0];
462 assert!(first.is_free());
463 assert_eq!(first.length_bytes, 300); assert_eq!(first.start_offset, 0);
465 }
466
467 #[test]
468 fn test_merge_free_extents_does_not_merge_separated() {
469 let mut analyzer = StorageFragmentationAnalyzer::new();
470 analyzer.add_extent(free(0, 100));
471 analyzer.add_extent(used(100, 50, "QmA"));
472 analyzer.add_extent(free(150, 100));
473
474 analyzer.merge_free_extents();
475
476 assert_eq!(analyzer.total_extents(), 3);
478 }
479
480 #[test]
481 fn test_merge_free_extents_multiple_runs() {
482 let mut analyzer = StorageFragmentationAnalyzer::new();
483 analyzer.add_extent(free(0, 50));
485 analyzer.add_extent(free(50, 50));
486 analyzer.add_extent(used(100, 100, "QmA"));
487 analyzer.add_extent(free(200, 60));
488 analyzer.add_extent(free(260, 40));
489 analyzer.add_extent(free(300, 100));
490
491 analyzer.merge_free_extents();
492
493 assert_eq!(analyzer.total_extents(), 3);
495 assert_eq!(analyzer.extents[0].length_bytes, 100); assert_eq!(analyzer.extents[2].length_bytes, 200); }
498
499 #[test]
502 fn test_total_extents_empty() {
503 let analyzer = StorageFragmentationAnalyzer::new();
504 assert_eq!(analyzer.total_extents(), 0);
505 }
506
507 #[test]
508 fn test_total_extents_counts_all() {
509 let mut analyzer = StorageFragmentationAnalyzer::new();
510 analyzer.add_extent(used(0, 100, "QmA"));
511 analyzer.add_extent(free(100, 50));
512 analyzer.add_extent(used(150, 200, "QmB"));
513 assert_eq!(analyzer.total_extents(), 3);
514 }
515
516 #[test]
519 fn test_analyze_free_bytes_count() {
520 let mut analyzer = StorageFragmentationAnalyzer::new();
521 analyzer.add_extent(used(0, 400, "QmA"));
522 analyzer.add_extent(free(400, 100));
523 analyzer.add_extent(used(500, 400, "QmB"));
524 analyzer.add_extent(free(900, 100));
525
526 let report = analyzer.analyze();
527 assert_eq!(report.free_bytes, 200);
528 assert_eq!(report.used_bytes, 800);
529 assert_eq!(report.total_bytes, 1000);
530 }
531}