1use std::collections::HashMap;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct RefEntry {
11 pub cid: String,
13 pub ref_count: u32,
15 pub first_seen_secs: u64,
17 pub last_seen_secs: u64,
19 pub size_bytes: u64,
21}
22
23impl RefEntry {
24 pub fn is_shared(&self) -> bool {
26 self.ref_count > 1
27 }
28
29 pub fn savings_bytes(&self) -> u64 {
33 (self.ref_count.saturating_sub(1) as u64).saturating_mul(self.size_bytes)
34 }
35}
36
37#[derive(Debug, Clone, PartialEq)]
39pub struct DedupStats {
40 pub total_blocks: usize,
42 pub shared_blocks: usize,
44 pub unique_blocks: usize,
46 pub total_ref_count: u64,
48 pub total_bytes: u64,
50 pub saved_bytes: u64,
52}
53
54impl DedupStats {
55 pub fn dedup_ratio(&self) -> f64 {
59 self.shared_blocks as f64 / self.total_blocks.max(1) as f64
60 }
61}
62
63#[derive(Debug, Default)]
69pub struct BlockDeduplicationTracker {
70 entries: HashMap<String, RefEntry>,
71}
72
73impl BlockDeduplicationTracker {
74 pub fn new() -> Self {
76 Self {
77 entries: HashMap::new(),
78 }
79 }
80
81 pub fn add_ref(&mut self, cid: &str, size_bytes: u64, now_secs: u64) {
87 if let Some(entry) = self.entries.get_mut(cid) {
88 entry.ref_count = entry.ref_count.saturating_add(1);
89 entry.last_seen_secs = now_secs;
90 } else {
91 self.entries.insert(
92 cid.to_owned(),
93 RefEntry {
94 cid: cid.to_owned(),
95 ref_count: 1,
96 first_seen_secs: now_secs,
97 last_seen_secs: now_secs,
98 size_bytes,
99 },
100 );
101 }
102 }
103
104 pub fn remove_ref(&mut self, cid: &str) -> bool {
111 match self.entries.get_mut(cid) {
112 Some(entry) if entry.ref_count > 1 => {
113 entry.ref_count -= 1;
114 true
115 }
116 Some(_) => {
117 self.entries.remove(cid);
118 true
119 }
120 None => false,
121 }
122 }
123
124 pub fn ref_count(&self, cid: &str) -> u32 {
126 self.entries.get(cid).map_or(0, |e| e.ref_count)
127 }
128
129 pub fn is_safe_to_delete(&self, cid: &str) -> bool {
134 match self.entries.get(cid) {
135 None => true,
136 Some(entry) => entry.ref_count == 0,
137 }
138 }
139
140 pub fn shared_blocks(&self) -> Vec<&RefEntry> {
142 let mut shared: Vec<&RefEntry> = self.entries.values().filter(|e| e.is_shared()).collect();
143 shared.sort_by_key(|b| std::cmp::Reverse(b.savings_bytes()));
144 shared
145 }
146
147 pub fn unique_blocks(&self) -> Vec<&RefEntry> {
149 self.entries.values().filter(|e| e.ref_count == 1).collect()
150 }
151
152 pub fn stats(&self) -> DedupStats {
154 let total_blocks = self.entries.len();
155 let mut shared_blocks = 0usize;
156 let mut unique_blocks = 0usize;
157 let mut total_ref_count = 0u64;
158 let mut total_bytes = 0u64;
159 let mut saved_bytes = 0u64;
160
161 for entry in self.entries.values() {
162 if entry.is_shared() {
163 shared_blocks += 1;
164 } else {
165 unique_blocks += 1;
166 }
167 total_ref_count += entry.ref_count as u64;
168 total_bytes += entry.size_bytes;
169 saved_bytes += entry.savings_bytes();
170 }
171
172 DedupStats {
173 total_blocks,
174 shared_blocks,
175 unique_blocks,
176 total_ref_count,
177 total_bytes,
178 saved_bytes,
179 }
180 }
181
182 pub fn top_savings(&self, n: usize) -> Vec<&RefEntry> {
184 let mut all: Vec<&RefEntry> = self.entries.values().collect();
185 all.sort_by_key(|b| std::cmp::Reverse(b.savings_bytes()));
186 all.truncate(n);
187 all
188 }
189
190 pub fn prune_unreferenced(&mut self) -> usize {
194 let before = self.entries.len();
195 self.entries.retain(|_, e| e.ref_count > 0);
196 before - self.entries.len()
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 fn tracker_with_entries() -> BlockDeduplicationTracker {
207 let mut t = BlockDeduplicationTracker::new();
208 t.add_ref("a", 100, 10);
210 t.add_ref("a", 100, 20);
211 t.add_ref("a", 100, 30);
212 t.add_ref("b", 50, 10);
214 t.add_ref("b", 50, 20);
215 t.add_ref("c", 200, 10);
217 t
218 }
219
220 #[test]
223 fn test_new_empty() {
224 let t = BlockDeduplicationTracker::new();
225 assert_eq!(t.entries.len(), 0);
226 }
227
228 #[test]
231 fn test_add_ref_new_block() {
232 let mut t = BlockDeduplicationTracker::new();
233 t.add_ref("cid1", 512, 100);
234 let entry = t.entries.get("cid1").expect("entry must exist");
235 assert_eq!(entry.ref_count, 1);
236 assert_eq!(entry.size_bytes, 512);
237 assert_eq!(entry.first_seen_secs, 100);
238 assert_eq!(entry.last_seen_secs, 100);
239 assert_eq!(entry.cid, "cid1");
240 }
241
242 #[test]
245 fn test_add_ref_increments_ref_count() {
246 let mut t = BlockDeduplicationTracker::new();
247 t.add_ref("cid1", 512, 100);
248 t.add_ref("cid1", 512, 200);
249 t.add_ref("cid1", 512, 300);
250 assert_eq!(t.ref_count("cid1"), 3);
251 }
252
253 #[test]
256 fn test_add_ref_updates_last_seen() {
257 let mut t = BlockDeduplicationTracker::new();
258 t.add_ref("cid1", 512, 100);
259 t.add_ref("cid1", 512, 999);
260 let entry = t.entries.get("cid1").expect("entry must exist");
261 assert_eq!(entry.first_seen_secs, 100);
262 assert_eq!(entry.last_seen_secs, 999);
263 }
264
265 #[test]
268 fn test_remove_ref_decrements() {
269 let mut t = BlockDeduplicationTracker::new();
270 t.add_ref("x", 64, 1);
271 t.add_ref("x", 64, 2);
272 assert!(t.remove_ref("x"));
273 assert_eq!(t.ref_count("x"), 1);
274 assert!(t.entries.contains_key("x"));
275 }
276
277 #[test]
280 fn test_remove_ref_removes_entry() {
281 let mut t = BlockDeduplicationTracker::new();
282 t.add_ref("x", 64, 1);
283 assert!(t.remove_ref("x"));
284 assert!(!t.entries.contains_key("x"));
285 }
286
287 #[test]
290 fn test_remove_ref_not_found() {
291 let mut t = BlockDeduplicationTracker::new();
292 assert!(!t.remove_ref("nonexistent"));
293 }
294
295 #[test]
298 fn test_ref_count_unknown() {
299 let t = BlockDeduplicationTracker::new();
300 assert_eq!(t.ref_count("ghost"), 0);
301 }
302
303 #[test]
306 fn test_ref_count_after_add_remove() {
307 let mut t = BlockDeduplicationTracker::new();
308 t.add_ref("y", 32, 1);
309 t.add_ref("y", 32, 2);
310 t.add_ref("y", 32, 3);
311 t.remove_ref("y");
312 assert_eq!(t.ref_count("y"), 2);
313 t.remove_ref("y");
314 assert_eq!(t.ref_count("y"), 1);
315 t.remove_ref("y");
316 assert_eq!(t.ref_count("y"), 0); }
318
319 #[test]
322 fn test_is_safe_to_delete_not_in_map() {
323 let t = BlockDeduplicationTracker::new();
324 assert!(t.is_safe_to_delete("absent"));
325 }
326
327 #[test]
330 fn test_is_safe_to_delete_has_refs() {
331 let mut t = BlockDeduplicationTracker::new();
332 t.add_ref("live", 128, 1);
333 assert!(!t.is_safe_to_delete("live"));
334 }
335
336 #[test]
339 fn test_is_shared() {
340 let e1 = RefEntry {
341 cid: "a".to_owned(),
342 ref_count: 1,
343 first_seen_secs: 0,
344 last_seen_secs: 0,
345 size_bytes: 100,
346 };
347 let e2 = RefEntry {
348 ref_count: 2,
349 ..e1.clone()
350 };
351 assert!(!e1.is_shared());
352 assert!(e2.is_shared());
353 }
354
355 #[test]
358 fn test_savings_bytes() {
359 let entry = RefEntry {
360 cid: "z".to_owned(),
361 ref_count: 5,
362 first_seen_secs: 0,
363 last_seen_secs: 0,
364 size_bytes: 1000,
365 };
366 assert_eq!(entry.savings_bytes(), 4000);
367
368 let single = RefEntry {
369 ref_count: 1,
370 ..entry.clone()
371 };
372 assert_eq!(single.savings_bytes(), 0);
373 }
374
375 #[test]
378 fn test_shared_blocks_sorted() {
379 let t = tracker_with_entries();
380 let shared = t.shared_blocks();
381 assert_eq!(shared.len(), 2);
383 assert_eq!(shared[0].cid, "a");
384 assert_eq!(shared[1].cid, "b");
385 }
386
387 #[test]
390 fn test_unique_blocks() {
391 let t = tracker_with_entries();
392 let unique = t.unique_blocks();
393 assert_eq!(unique.len(), 1);
394 assert_eq!(unique[0].cid, "c");
395 }
396
397 #[test]
400 fn test_top_savings() {
401 let t = tracker_with_entries();
402 let top1 = t.top_savings(1);
403 assert_eq!(top1.len(), 1);
404 assert_eq!(top1[0].cid, "a");
405
406 let top2 = t.top_savings(2);
407 assert_eq!(top2.len(), 2);
408 assert_eq!(top2[0].cid, "a");
409 assert_eq!(top2[1].cid, "b");
410
411 let top10 = t.top_savings(10);
413 assert_eq!(top10.len(), 3);
414 }
415
416 #[test]
419 fn test_stats_all_fields() {
420 let t = tracker_with_entries();
421 let s = t.stats();
422 assert_eq!(s.total_blocks, 3);
423 assert_eq!(s.shared_blocks, 2); assert_eq!(s.unique_blocks, 1); assert_eq!(s.total_ref_count, 6); assert_eq!(s.total_bytes, 350); assert_eq!(s.saved_bytes, 250); }
429
430 #[test]
433 fn test_dedup_ratio() {
434 let t = tracker_with_entries();
435 let s = t.stats();
436 let ratio = s.dedup_ratio();
438 assert!((ratio - 2.0 / 3.0).abs() < f64::EPSILON);
439 }
440
441 #[test]
442 fn test_dedup_ratio_empty() {
443 let t = BlockDeduplicationTracker::new();
444 let s = t.stats();
445 assert_eq!(s.dedup_ratio(), 0.0);
446 }
447
448 #[test]
451 fn test_prune_unreferenced() {
452 let mut t = BlockDeduplicationTracker::new();
453 t.add_ref("keep", 10, 1);
454 t.add_ref("remove_me", 20, 1);
455
456 t.entries
458 .get_mut("remove_me")
459 .expect("must exist")
460 .ref_count = 0;
461
462 let pruned = t.prune_unreferenced();
463 assert_eq!(pruned, 1);
464 assert!(t.entries.contains_key("keep"));
465 assert!(!t.entries.contains_key("remove_me"));
466 }
467
468 #[test]
469 fn test_prune_unreferenced_none() {
470 let mut t = tracker_with_entries();
471 let pruned = t.prune_unreferenced();
472 assert_eq!(pruned, 0);
473 }
474}