1use std::collections::{BTreeSet, HashMap, HashSet, VecDeque};
40
41pub type BgcBlockCid = [u8; 32];
45
46#[inline]
49fn xorshift64(state: &mut u64) -> u64 {
50 let mut x = *state;
51 x ^= x << 13;
52 x ^= x >> 7;
53 x ^= x << 17;
54 *state = x;
55 x
56}
57
58#[inline]
59fn fnv1a_64(data: &[u8]) -> u64 {
60 let mut h: u64 = 14695981039346656037;
61 for &b in data {
62 h ^= b as u64;
63 h = h.wrapping_mul(1099511628211);
64 }
65 h
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
72pub enum BgcGcPhase {
73 Idle,
75 Mark,
77 Sweep,
79 Compact,
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
87pub enum BgcGcPolicy {
88 MarkAndSweep,
90 ReferenceCounting,
92 TriColor,
94 Generational,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
102pub enum BgcError {
103 UnknownBlock(BgcBlockCid),
105 BlockIsPinned(BgcBlockCid),
107 MarkTimeout,
109 RefCountOverflow(BgcBlockCid),
111}
112
113impl std::fmt::Display for BgcError {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 match self {
116 BgcError::UnknownBlock(c) => write!(f, "unknown block {:?}", c),
117 BgcError::BlockIsPinned(c) => write!(f, "block is pinned {:?}", c),
118 BgcError::MarkTimeout => write!(f, "mark phase timed out"),
119 BgcError::RefCountOverflow(c) => write!(f, "ref-count overflow {:?}", c),
120 }
121 }
122}
123
124impl std::error::Error for BgcError {}
125
126#[derive(Debug, Clone, PartialEq, Eq)]
130pub struct BgcBlockRecord {
131 pub cid: BgcBlockCid,
133 pub size_bytes: u64,
135 pub ref_count: u32,
137 pub created_at: u64,
139 pub last_accessed: u64,
141 pub is_pinned: bool,
143 pub generation: u32,
145}
146
147pub type BlockRecord = BgcBlockRecord;
149
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154enum TriColor {
155 White,
157 Grey,
159 Black,
161}
162
163#[derive(Debug, Clone)]
167pub struct BgcGcLogEntry {
168 pub ts: u64,
170 pub phase: BgcGcPhase,
172 pub blocks_visited: u64,
174 pub blocks_freed: u64,
176 pub bytes_freed: u64,
178}
179
180#[derive(Debug, Clone)]
184pub struct BgcCollectorConfig {
185 pub dry_run: bool,
187 pub min_age_secs: u64,
189 pub batch_size: usize,
191 pub mark_timeout_ms: u64,
193 pub generational_threshold_secs: u64,
195}
196
197impl Default for BgcCollectorConfig {
198 fn default() -> Self {
199 Self {
200 dry_run: false,
201 min_age_secs: 300,
202 batch_size: 1024,
203 mark_timeout_ms: 5_000,
204 generational_threshold_secs: 3600,
205 }
206 }
207}
208
209#[derive(Debug, Clone, Default)]
213pub struct BgcSweepResult {
214 pub removed: Vec<BgcBlockCid>,
216 pub bytes_freed: u64,
218 pub dry_run: bool,
220}
221
222#[derive(Debug, Clone, Default)]
226pub struct BgcGcResult {
227 pub policy: Option<BgcGcPolicy>,
229 pub live_blocks: u64,
231 pub blocks_freed: u64,
233 pub bytes_freed: u64,
235 pub dry_run: bool,
237 pub mark_duration_us: u64,
239 pub sweep_duration_us: u64,
241}
242
243#[derive(Debug, Clone, Default)]
247pub struct BgcCollectorStats {
248 pub total_blocks: u64,
250 pub total_bytes: u64,
252 pub pinned_count: u64,
254 pub root_count: u64,
256 pub orphan_estimate: u64,
258 pub gc_cycles: u64,
260 pub total_bytes_freed: u64,
262 pub total_blocks_freed: u64,
264}
265
266pub struct BlockGarbageCollector {
273 registry: HashMap<BgcBlockCid, BgcBlockRecord>,
275 pin_set: HashSet<BgcBlockCid>,
277 root_set: HashSet<BgcBlockCid>,
279 edges: HashMap<BgcBlockCid, Vec<BgcBlockCid>>,
281 gc_log: VecDeque<BgcGcLogEntry>,
283 config: BgcCollectorConfig,
285 clock_seed: u64,
287 prng_state: u64,
289 gc_cycles: u64,
291 total_bytes_freed: u64,
293 total_blocks_freed: u64,
295}
296
297impl BlockGarbageCollector {
302 fn now_secs(&mut self) -> u64 {
305 let jitter = xorshift64(&mut self.prng_state) & 0x0000_0000_0000_000F;
307 self.clock_seed + jitter
308 }
309
310 pub fn set_clock(&mut self, secs: u64) {
312 self.clock_seed = secs;
313 }
314
315 pub fn cid_hash(cid: &BgcBlockCid) -> u64 {
317 fnv1a_64(cid.as_ref())
318 }
319}
320
321impl BlockGarbageCollector {
324 pub fn new(config: BgcCollectorConfig) -> Self {
326 let seed = fnv1a_64(b"bgc-seed-v1");
327 Self {
328 registry: HashMap::new(),
329 pin_set: HashSet::new(),
330 root_set: HashSet::new(),
331 edges: HashMap::new(),
332 gc_log: VecDeque::with_capacity(64),
333 config,
334 clock_seed: 1_700_000_000,
335 prng_state: seed,
336 gc_cycles: 0,
337 total_bytes_freed: 0,
338 total_blocks_freed: 0,
339 }
340 }
341}
342
343impl BlockGarbageCollector {
346 pub fn register_block(
350 &mut self,
351 cid: BgcBlockCid,
352 size_bytes: u64,
353 refs: Vec<BgcBlockCid>,
354 ) -> Result<(), BgcError> {
355 let now = self.now_secs();
356 let record = BgcBlockRecord {
357 cid,
358 size_bytes,
359 ref_count: 0,
360 created_at: now,
361 last_accessed: now,
362 is_pinned: false,
363 generation: 0,
364 };
365 self.registry.insert(cid, record);
366 self.edges.insert(cid, refs);
367 Ok(())
368 }
369
370 pub fn unregister_block(&mut self, cid: BgcBlockCid) -> Result<(), BgcError> {
374 if self.pin_set.contains(&cid) {
375 return Err(BgcError::BlockIsPinned(cid));
376 }
377 self.registry.remove(&cid);
378 self.edges.remove(&cid);
379 self.root_set.remove(&cid);
380 Ok(())
381 }
382
383 pub fn touch(&mut self, cid: &BgcBlockCid) -> Result<(), BgcError> {
385 let now = self.now_secs();
386 let record = self
387 .registry
388 .get_mut(cid)
389 .ok_or(BgcError::UnknownBlock(*cid))?;
390 record.last_accessed = now;
391 Ok(())
392 }
393
394 pub fn get_block(&self, cid: &BgcBlockCid) -> Option<&BgcBlockRecord> {
396 self.registry.get(cid)
397 }
398}
399
400impl BlockGarbageCollector {
403 pub fn pin(&mut self, cid: BgcBlockCid) -> Result<(), BgcError> {
405 if !self.registry.contains_key(&cid) {
406 return Err(BgcError::UnknownBlock(cid));
407 }
408 self.pin_set.insert(cid);
409 if let Some(rec) = self.registry.get_mut(&cid) {
410 rec.is_pinned = true;
411 }
412 Ok(())
413 }
414
415 pub fn unpin(&mut self, cid: BgcBlockCid) -> Result<(), BgcError> {
417 if !self.registry.contains_key(&cid) {
418 return Err(BgcError::UnknownBlock(cid));
419 }
420 self.pin_set.remove(&cid);
421 if let Some(rec) = self.registry.get_mut(&cid) {
422 rec.is_pinned = false;
423 }
424 Ok(())
425 }
426
427 pub fn add_root(&mut self, cid: BgcBlockCid) {
429 self.root_set.insert(cid);
430 }
431
432 pub fn remove_root(&mut self, cid: &BgcBlockCid) {
434 self.root_set.remove(cid);
435 }
436
437 pub fn is_pinned(&self, cid: &BgcBlockCid) -> bool {
439 self.pin_set.contains(cid)
440 }
441
442 pub fn is_root(&self, cid: &BgcBlockCid) -> bool {
444 self.root_set.contains(cid)
445 }
446}
447
448impl BlockGarbageCollector {
451 pub fn increment_ref(&mut self, cid: &BgcBlockCid) -> Result<(), BgcError> {
453 let rec = self
454 .registry
455 .get_mut(cid)
456 .ok_or(BgcError::UnknownBlock(*cid))?;
457 rec.ref_count = rec
458 .ref_count
459 .checked_add(1)
460 .ok_or(BgcError::RefCountOverflow(*cid))?;
461 Ok(())
462 }
463
464 pub fn decrement_ref(&mut self, cid: &BgcBlockCid) -> Result<bool, BgcError> {
469 let rec = self
470 .registry
471 .get_mut(cid)
472 .ok_or(BgcError::UnknownBlock(*cid))?;
473 if rec.ref_count == 0 {
474 return Ok(true);
476 }
477 rec.ref_count -= 1;
478 Ok(rec.ref_count == 0)
479 }
480}
481
482impl BlockGarbageCollector {
485 pub fn mark_phase(&self) -> BTreeSet<BgcBlockCid> {
489 let mut reachable: BTreeSet<BgcBlockCid> = BTreeSet::new();
490 let mut queue: VecDeque<BgcBlockCid> = VecDeque::new();
491
492 for &cid in &self.root_set {
494 if !reachable.contains(&cid) {
495 reachable.insert(cid);
496 queue.push_back(cid);
497 }
498 }
499 for &cid in &self.pin_set {
501 if !reachable.contains(&cid) {
502 reachable.insert(cid);
503 queue.push_back(cid);
504 }
505 }
506
507 while let Some(current) = queue.pop_front() {
509 if let Some(children) = self.edges.get(¤t) {
510 for &child in children {
511 if !reachable.contains(&child) {
512 reachable.insert(child);
513 queue.push_back(child);
514 }
515 }
516 }
517 }
518
519 reachable
520 }
521
522 fn mark_phase_tricolor(&self) -> BTreeSet<BgcBlockCid> {
528 let mut color: HashMap<BgcBlockCid, TriColor> = self
530 .registry
531 .keys()
532 .map(|&cid| (cid, TriColor::White))
533 .collect();
534
535 let mut grey_queue: VecDeque<BgcBlockCid> = VecDeque::new();
536
537 let seeds: Vec<BgcBlockCid> = self
539 .root_set
540 .iter()
541 .chain(self.pin_set.iter())
542 .copied()
543 .collect();
544
545 for cid in seeds {
546 if color.get(&cid).copied() == Some(TriColor::White) {
547 color.insert(cid, TriColor::Grey);
548 grey_queue.push_back(cid);
549 }
550 }
551
552 while let Some(current) = grey_queue.pop_front() {
554 if let Some(children) = self.edges.get(¤t) {
555 for &child in children {
556 if color.get(&child).copied() == Some(TriColor::White) {
557 color.insert(child, TriColor::Grey);
558 grey_queue.push_back(child);
559 }
560 }
561 }
562 color.insert(current, TriColor::Black);
563 }
564
565 color
567 .into_iter()
568 .filter(|(_, c)| *c == TriColor::Black)
569 .map(|(cid, _)| cid)
570 .collect()
571 }
572
573 fn mark_phase_generational(&self, now: u64) -> (BTreeSet<BgcBlockCid>, BTreeSet<BgcBlockCid>) {
578 let threshold = self.config.generational_threshold_secs;
579 let all_reachable = self.mark_phase();
580
581 let mut young: BTreeSet<BgcBlockCid> = BTreeSet::new();
582 let mut old: BTreeSet<BgcBlockCid> = BTreeSet::new();
583
584 for &cid in &all_reachable {
585 if let Some(rec) = self.registry.get(&cid) {
586 let age = now.saturating_sub(rec.created_at);
587 if rec.generation == 0 && age < threshold {
588 young.insert(cid);
589 } else {
590 old.insert(cid);
591 }
592 }
593 }
594 (young, old)
595 }
596}
597
598impl BlockGarbageCollector {
601 pub fn sweep_phase(&mut self, reachable: &BTreeSet<BgcBlockCid>) -> BgcSweepResult {
605 let now = self.now_secs();
606 let min_age = self.config.min_age_secs;
607 let dry_run = self.config.dry_run;
608 let batch_size = self.config.batch_size;
609
610 let mut result = BgcSweepResult {
611 dry_run,
612 ..Default::default()
613 };
614
615 let candidates: Vec<BgcBlockCid> = self
616 .registry
617 .iter()
618 .filter(|(cid, rec)| {
619 !reachable.contains(*cid)
620 && !rec.is_pinned
621 && now.saturating_sub(rec.created_at) >= min_age
622 })
623 .map(|(&cid, _)| cid)
624 .take(batch_size)
625 .collect();
626
627 for cid in candidates {
628 if let Some(rec) = self.registry.get(&cid) {
629 result.bytes_freed += rec.size_bytes;
630 result.removed.push(cid);
631 }
632 }
633
634 if !dry_run {
635 for cid in &result.removed {
636 self.registry.remove(cid);
637 self.edges.remove(cid);
638 self.root_set.remove(cid);
639 self.pin_set.remove(cid);
640 }
641 }
642
643 result
644 }
645
646 fn sweep_refcount(&mut self) -> BgcSweepResult {
648 let now = self.now_secs();
649 let min_age = self.config.min_age_secs;
650 let dry_run = self.config.dry_run;
651 let batch_size = self.config.batch_size;
652
653 let mut result = BgcSweepResult {
654 dry_run,
655 ..Default::default()
656 };
657
658 let candidates: Vec<BgcBlockCid> = self
659 .registry
660 .iter()
661 .filter(|(cid, rec)| {
662 rec.ref_count == 0
663 && !rec.is_pinned
664 && !self.root_set.contains(*cid)
665 && now.saturating_sub(rec.created_at) >= min_age
666 })
667 .map(|(&cid, _)| cid)
668 .take(batch_size)
669 .collect();
670
671 for cid in candidates {
672 if let Some(rec) = self.registry.get(&cid) {
673 result.bytes_freed += rec.size_bytes;
674 result.removed.push(cid);
675 }
676 }
677
678 if !dry_run {
679 for cid in &result.removed {
680 self.registry.remove(cid);
681 self.edges.remove(cid);
682 }
683 }
684
685 result
686 }
687
688 fn sweep_young_generation(
690 &mut self,
691 now: u64,
692 reachable: &BTreeSet<BgcBlockCid>,
693 ) -> BgcSweepResult {
694 let min_age = self.config.min_age_secs;
695 let threshold = self.config.generational_threshold_secs;
696 let dry_run = self.config.dry_run;
697 let batch_size = self.config.batch_size;
698
699 let mut result = BgcSweepResult {
700 dry_run,
701 ..Default::default()
702 };
703
704 let candidates: Vec<BgcBlockCid> = self
705 .registry
706 .iter()
707 .filter(|(cid, rec)| {
708 let age = now.saturating_sub(rec.created_at);
709 rec.generation == 0
710 && age < threshold
711 && !reachable.contains(*cid)
712 && !rec.is_pinned
713 && age >= min_age
714 })
715 .map(|(&cid, _)| cid)
716 .take(batch_size)
717 .collect();
718
719 for cid in candidates {
720 if let Some(rec) = self.registry.get(&cid) {
721 result.bytes_freed += rec.size_bytes;
722 result.removed.push(cid);
723 }
724 }
725
726 if !dry_run {
727 for cid in &result.removed {
728 self.registry.remove(cid);
729 self.edges.remove(cid);
730 self.root_set.remove(cid);
731 self.pin_set.remove(cid);
732 }
733 let promote: Vec<BgcBlockCid> = self
735 .registry
736 .iter()
737 .filter(|(_, rec)| {
738 rec.generation == 0 && now.saturating_sub(rec.created_at) < threshold
739 })
740 .map(|(&cid, _)| cid)
741 .collect();
742 for cid in promote {
743 if let Some(rec) = self.registry.get_mut(&cid) {
744 rec.generation = 1;
745 }
746 }
747 }
748
749 result
750 }
751}
752
753impl BlockGarbageCollector {
756 pub fn run_gc(&mut self, policy: BgcGcPolicy) -> Result<BgcGcResult, BgcError> {
758 let now = self.now_secs();
759 self.gc_cycles += 1;
760
761 let mark_start = Self::pseudo_clock(&mut 0u64, now);
762 let sweep_result = match policy {
763 BgcGcPolicy::MarkAndSweep => {
764 let reachable = self.mark_phase();
765 let _ = mark_start;
766 self.sweep_phase(&reachable)
767 }
768 BgcGcPolicy::ReferenceCounting => self.sweep_refcount(),
769 BgcGcPolicy::TriColor => {
770 let reachable = self.mark_phase_tricolor();
771 self.sweep_phase(&reachable)
772 }
773 BgcGcPolicy::Generational => {
774 let (young_reachable, old_reachable) = self.mark_phase_generational(now);
775 let mut combined = young_reachable;
776 combined.extend(old_reachable.iter());
777 self.sweep_young_generation(now, &combined)
778 }
779 };
780
781 self.total_bytes_freed += sweep_result.bytes_freed;
782 self.total_blocks_freed += sweep_result.removed.len() as u64;
783
784 let log_entry = BgcGcLogEntry {
785 ts: now,
786 phase: BgcGcPhase::Sweep,
787 blocks_visited: self.registry.len() as u64,
788 blocks_freed: sweep_result.removed.len() as u64,
789 bytes_freed: sweep_result.bytes_freed,
790 };
791 self.append_log(log_entry);
792
793 Ok(BgcGcResult {
794 policy: Some(policy),
795 live_blocks: self.registry.len() as u64,
796 blocks_freed: sweep_result.removed.len() as u64,
797 bytes_freed: sweep_result.bytes_freed,
798 dry_run: self.config.dry_run,
799 mark_duration_us: 0,
800 sweep_duration_us: 0,
801 })
802 }
803
804 #[inline]
806 fn pseudo_clock(_state: &mut u64, seed: u64) -> u64 {
807 seed
808 }
809}
810
811impl BlockGarbageCollector {
814 pub fn collect_orphans(&self, min_age_secs: u64) -> Vec<BgcBlockCid> {
817 let reachable = self.mark_phase();
818 let now = self.clock_seed; self.registry
821 .iter()
822 .filter(|(cid, rec)| {
823 !reachable.contains(*cid)
824 && !rec.is_pinned
825 && now.saturating_sub(rec.created_at) >= min_age_secs
826 })
827 .map(|(&cid, _)| cid)
828 .collect()
829 }
830
831 pub fn collect_orphans_default(&self) -> Vec<BgcBlockCid> {
833 self.collect_orphans(self.config.min_age_secs)
834 }
835}
836
837impl BlockGarbageCollector {
840 pub fn gc_stats(&self) -> BgcCollectorStats {
842 let total_blocks = self.registry.len() as u64;
843 let total_bytes: u64 = self.registry.values().map(|r| r.size_bytes).sum();
844 let pinned_count = self.pin_set.len() as u64;
845 let root_count = self.root_set.len() as u64;
846
847 let reachable = self.mark_phase();
848 let orphan_estimate = self
849 .registry
850 .keys()
851 .filter(|cid| !reachable.contains(*cid))
852 .count() as u64;
853
854 BgcCollectorStats {
855 total_blocks,
856 total_bytes,
857 pinned_count,
858 root_count,
859 orphan_estimate,
860 gc_cycles: self.gc_cycles,
861 total_bytes_freed: self.total_bytes_freed,
862 total_blocks_freed: self.total_blocks_freed,
863 }
864 }
865
866 pub fn block_count(&self) -> usize {
868 self.registry.len()
869 }
870
871 pub fn log_len(&self) -> usize {
873 self.gc_log.len()
874 }
875
876 pub fn log_entries(&self) -> impl Iterator<Item = &BgcGcLogEntry> {
878 self.gc_log.iter()
879 }
880
881 pub fn gc_log(&self) -> &VecDeque<BgcGcLogEntry> {
883 &self.gc_log
884 }
885}
886
887impl BlockGarbageCollector {
890 fn append_log(&mut self, entry: BgcGcLogEntry) {
892 if self.gc_log.len() >= 1000 {
893 self.gc_log.pop_front();
894 }
895 self.gc_log.push_back(entry);
896 }
897
898 pub fn random_cid(&mut self) -> BgcBlockCid {
900 let mut cid = [0u8; 32];
901 for chunk in cid.chunks_mut(8) {
902 let v = xorshift64(&mut self.prng_state);
903 let bytes = v.to_le_bytes();
904 let len = chunk.len().min(8);
905 chunk[..len].copy_from_slice(&bytes[..len]);
906 }
907 cid
908 }
909
910 pub fn cid_from_bytes(data: &[u8]) -> BgcBlockCid {
912 let mut cid = [0u8; 32];
913 let h1 = fnv1a_64(data);
914 let h2 = fnv1a_64(&h1.to_le_bytes());
915 let h3 = fnv1a_64(&h2.to_le_bytes());
916 let h4 = fnv1a_64(&h3.to_le_bytes());
917 cid[0..8].copy_from_slice(&h1.to_le_bytes());
918 cid[8..16].copy_from_slice(&h2.to_le_bytes());
919 cid[16..24].copy_from_slice(&h3.to_le_bytes());
920 cid[24..32].copy_from_slice(&h4.to_le_bytes());
921 cid
922 }
923
924 pub fn config(&self) -> &BgcCollectorConfig {
926 &self.config
927 }
928
929 pub fn config_mut(&mut self) -> &mut BgcCollectorConfig {
931 &mut self.config
932 }
933
934 pub fn update_edges(
936 &mut self,
937 cid: BgcBlockCid,
938 new_refs: Vec<BgcBlockCid>,
939 ) -> Result<(), BgcError> {
940 if !self.registry.contains_key(&cid) {
941 return Err(BgcError::UnknownBlock(cid));
942 }
943 self.edges.insert(cid, new_refs);
944 Ok(())
945 }
946
947 pub fn children_of(&self, cid: &BgcBlockCid) -> &[BgcBlockCid] {
949 self.edges.get(cid).map(|v| v.as_slice()).unwrap_or(&[])
950 }
951
952 pub fn reconcile_pin_flags(&mut self) {
954 let pinned: HashSet<BgcBlockCid> = self.pin_set.clone();
955 for (cid, rec) in &mut self.registry {
956 rec.is_pinned = pinned.contains(cid);
957 }
958 }
959
960 pub fn promote_generation(&mut self) {
962 for rec in self.registry.values_mut() {
963 if rec.generation == 0 {
964 rec.generation = 1;
965 }
966 }
967 }
968
969 pub fn compact_edges(&mut self) {
971 let known: HashSet<BgcBlockCid> = self.registry.keys().copied().collect();
972 self.edges.retain(|cid, _| known.contains(cid));
973 for children in self.edges.values_mut() {
974 children.retain(|c| known.contains(c));
975 }
976 }
977
978 pub fn clear(&mut self) {
980 self.registry.clear();
981 self.pin_set.clear();
982 self.root_set.clear();
983 self.edges.clear();
984 self.gc_log.clear();
985 self.gc_cycles = 0;
986 self.total_bytes_freed = 0;
987 self.total_blocks_freed = 0;
988 }
989
990 pub fn all_cids(&self) -> Vec<BgcBlockCid> {
992 self.registry.keys().copied().collect()
993 }
994
995 pub fn reachable_set(&self) -> BTreeSet<BgcBlockCid> {
997 self.mark_phase()
998 }
999}
1000
1001#[cfg(test)]
1004mod tests {
1005 use super::*;
1006
1007 fn make_cid(n: u8) -> BgcBlockCid {
1010 let mut c = [0u8; 32];
1011 c[0] = n;
1012 c
1013 }
1014
1015 fn make_cid_u16(n: u16) -> BgcBlockCid {
1016 let mut c = [0u8; 32];
1017 c[0] = (n >> 8) as u8;
1018 c[1] = n as u8;
1019 c
1020 }
1021
1022 fn default_gc() -> BlockGarbageCollector {
1023 let cfg = BgcCollectorConfig {
1024 min_age_secs: 0,
1025 ..BgcCollectorConfig::default()
1026 }; let mut gc = BlockGarbageCollector::new(cfg);
1028 gc.set_clock(1_700_100_000);
1029 gc
1030 }
1031
1032 #[test]
1035 fn test_new_empty() {
1036 let gc = BlockGarbageCollector::new(BgcCollectorConfig::default());
1037 assert_eq!(gc.block_count(), 0);
1038 assert_eq!(gc.log_len(), 0);
1039 }
1040
1041 #[test]
1042 fn test_default_config() {
1043 let cfg = BgcCollectorConfig::default();
1044 assert!(!cfg.dry_run);
1045 assert_eq!(cfg.min_age_secs, 300);
1046 assert_eq!(cfg.batch_size, 1024);
1047 assert_eq!(cfg.mark_timeout_ms, 5_000);
1048 }
1049
1050 #[test]
1053 fn test_register_block() {
1054 let mut gc = default_gc();
1055 let cid = make_cid(1);
1056 gc.register_block(cid, 512, vec![]).unwrap();
1057 assert_eq!(gc.block_count(), 1);
1058 }
1059
1060 #[test]
1061 fn test_register_multiple_blocks() {
1062 let mut gc = default_gc();
1063 for i in 0..10u8 {
1064 gc.register_block(make_cid(i), 100, vec![]).unwrap();
1065 }
1066 assert_eq!(gc.block_count(), 10);
1067 }
1068
1069 #[test]
1070 fn test_unregister_block() {
1071 let mut gc = default_gc();
1072 let cid = make_cid(1);
1073 gc.register_block(cid, 100, vec![]).unwrap();
1074 gc.unregister_block(cid).unwrap();
1075 assert_eq!(gc.block_count(), 0);
1076 }
1077
1078 #[test]
1079 fn test_unregister_unknown_is_ok() {
1080 let mut gc = default_gc();
1081 let result = gc.unregister_block(make_cid(99));
1083 assert!(result.is_ok());
1084 }
1085
1086 #[test]
1087 fn test_unregister_pinned_fails() {
1088 let mut gc = default_gc();
1089 let cid = make_cid(1);
1090 gc.register_block(cid, 100, vec![]).unwrap();
1091 gc.pin(cid).unwrap();
1092 let result = gc.unregister_block(cid);
1093 assert!(matches!(result, Err(BgcError::BlockIsPinned(_))));
1094 }
1095
1096 #[test]
1097 fn test_get_block_returns_record() {
1098 let mut gc = default_gc();
1099 let cid = make_cid(7);
1100 gc.register_block(cid, 4096, vec![]).unwrap();
1101 let rec = gc.get_block(&cid).unwrap();
1102 assert_eq!(rec.size_bytes, 4096);
1103 assert_eq!(rec.ref_count, 0);
1104 }
1105
1106 #[test]
1109 fn test_pin_unknown_fails() {
1110 let mut gc = default_gc();
1111 let result = gc.pin(make_cid(42));
1112 assert!(matches!(result, Err(BgcError::UnknownBlock(_))));
1113 }
1114
1115 #[test]
1116 fn test_pin_and_is_pinned() {
1117 let mut gc = default_gc();
1118 let cid = make_cid(2);
1119 gc.register_block(cid, 100, vec![]).unwrap();
1120 gc.pin(cid).unwrap();
1121 assert!(gc.is_pinned(&cid));
1122 assert!(gc.get_block(&cid).unwrap().is_pinned);
1123 }
1124
1125 #[test]
1126 fn test_unpin() {
1127 let mut gc = default_gc();
1128 let cid = make_cid(3);
1129 gc.register_block(cid, 100, vec![]).unwrap();
1130 gc.pin(cid).unwrap();
1131 gc.unpin(cid).unwrap();
1132 assert!(!gc.is_pinned(&cid));
1133 }
1134
1135 #[test]
1136 fn test_unpin_unknown_fails() {
1137 let mut gc = default_gc();
1138 let result = gc.unpin(make_cid(99));
1139 assert!(matches!(result, Err(BgcError::UnknownBlock(_))));
1140 }
1141
1142 #[test]
1145 fn test_add_remove_root() {
1146 let mut gc = default_gc();
1147 let cid = make_cid(5);
1148 gc.add_root(cid);
1149 assert!(gc.is_root(&cid));
1150 gc.remove_root(&cid);
1151 assert!(!gc.is_root(&cid));
1152 }
1153
1154 #[test]
1155 fn test_root_not_collected() {
1156 let mut gc = default_gc();
1157 let cid = make_cid(10);
1158 gc.register_block(cid, 100, vec![]).unwrap();
1159 gc.add_root(cid);
1160 let result = gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1161 assert_eq!(result.blocks_freed, 0);
1162 assert_eq!(gc.block_count(), 1);
1163 }
1164
1165 #[test]
1168 fn test_increment_ref() {
1169 let mut gc = default_gc();
1170 let cid = make_cid(1);
1171 gc.register_block(cid, 100, vec![]).unwrap();
1172 gc.increment_ref(&cid).unwrap();
1173 assert_eq!(gc.get_block(&cid).unwrap().ref_count, 1);
1174 }
1175
1176 #[test]
1177 fn test_decrement_ref() {
1178 let mut gc = default_gc();
1179 let cid = make_cid(1);
1180 gc.register_block(cid, 100, vec![]).unwrap();
1181 gc.increment_ref(&cid).unwrap();
1182 let zero = gc.decrement_ref(&cid).unwrap();
1183 assert!(zero);
1184 assert_eq!(gc.get_block(&cid).unwrap().ref_count, 0);
1185 }
1186
1187 #[test]
1188 fn test_decrement_ref_already_zero() {
1189 let mut gc = default_gc();
1190 let cid = make_cid(1);
1191 gc.register_block(cid, 100, vec![]).unwrap();
1192 let zero = gc.decrement_ref(&cid).unwrap();
1193 assert!(zero); }
1195
1196 #[test]
1197 fn test_increment_ref_unknown_fails() {
1198 let mut gc = default_gc();
1199 let result = gc.increment_ref(&make_cid(200));
1200 assert!(matches!(result, Err(BgcError::UnknownBlock(_))));
1201 }
1202
1203 #[test]
1204 fn test_decrement_ref_unknown_fails() {
1205 let mut gc = default_gc();
1206 let result = gc.decrement_ref(&make_cid(200));
1207 assert!(matches!(result, Err(BgcError::UnknownBlock(_))));
1208 }
1209
1210 #[test]
1213 fn test_mark_phase_empty() {
1214 let gc = default_gc();
1215 let reachable = gc.mark_phase();
1216 assert!(reachable.is_empty());
1217 }
1218
1219 #[test]
1220 fn test_mark_phase_root_and_child() {
1221 let mut gc = default_gc();
1222 let root = make_cid(0);
1223 let child = make_cid(1);
1224 gc.register_block(root, 100, vec![child]).unwrap();
1225 gc.register_block(child, 200, vec![]).unwrap();
1226 gc.add_root(root);
1227
1228 let reachable = gc.mark_phase();
1229 assert!(reachable.contains(&root));
1230 assert!(reachable.contains(&child));
1231 }
1232
1233 #[test]
1234 fn test_mark_phase_unreachable_excluded() {
1235 let mut gc = default_gc();
1236 let root = make_cid(0);
1237 let orphan = make_cid(99);
1238 gc.register_block(root, 100, vec![]).unwrap();
1239 gc.register_block(orphan, 50, vec![]).unwrap();
1240 gc.add_root(root);
1241
1242 let reachable = gc.mark_phase();
1243 assert!(reachable.contains(&root));
1244 assert!(!reachable.contains(&orphan));
1245 }
1246
1247 #[test]
1248 fn test_mark_phase_pinned_included() {
1249 let mut gc = default_gc();
1250 let cid = make_cid(5);
1251 gc.register_block(cid, 100, vec![]).unwrap();
1252 gc.pin(cid).unwrap();
1253
1254 let reachable = gc.mark_phase();
1255 assert!(reachable.contains(&cid));
1256 }
1257
1258 #[test]
1259 fn test_mark_phase_chain() {
1260 let mut gc = default_gc();
1261 let a = make_cid(0);
1263 let b = make_cid(1);
1264 let c = make_cid(2);
1265 let d = make_cid(3);
1266 gc.register_block(a, 1, vec![b]).unwrap();
1267 gc.register_block(b, 1, vec![c]).unwrap();
1268 gc.register_block(c, 1, vec![d]).unwrap();
1269 gc.register_block(d, 1, vec![]).unwrap();
1270 gc.add_root(a);
1271
1272 let reachable = gc.mark_phase();
1273 assert_eq!(reachable.len(), 4);
1274 }
1275
1276 #[test]
1279 fn test_sweep_removes_orphan() {
1280 let mut gc = default_gc();
1281 let root = make_cid(0);
1282 let orphan = make_cid(1);
1283 gc.register_block(root, 100, vec![]).unwrap();
1284 gc.register_block(orphan, 200, vec![]).unwrap();
1285 gc.add_root(root);
1286
1287 let reachable = gc.mark_phase();
1288 let sweep = gc.sweep_phase(&reachable);
1289
1290 assert_eq!(sweep.removed.len(), 1);
1291 assert_eq!(sweep.bytes_freed, 200);
1292 assert!(sweep.removed.contains(&orphan));
1293 }
1294
1295 #[test]
1296 fn test_sweep_dry_run() {
1297 let mut gc = default_gc();
1298 gc.config_mut().dry_run = true;
1299 let orphan = make_cid(1);
1300 gc.register_block(orphan, 300, vec![]).unwrap();
1301
1302 let reachable = gc.mark_phase();
1303 let sweep = gc.sweep_phase(&reachable);
1304
1305 assert_eq!(sweep.removed.len(), 1);
1306 assert!(sweep.dry_run);
1307 assert_eq!(gc.block_count(), 1);
1309 }
1310
1311 #[test]
1312 fn test_sweep_respects_min_age() {
1313 let mut gc = BlockGarbageCollector::new(BgcCollectorConfig {
1314 min_age_secs: 1_000,
1315 ..BgcCollectorConfig::default()
1316 });
1317 gc.set_clock(1_700_100_000);
1318 let cid = make_cid(1);
1319 gc.register_block(cid, 100, vec![]).unwrap();
1320
1321 let reachable = gc.mark_phase();
1322 let sweep = gc.sweep_phase(&reachable);
1323 assert_eq!(sweep.removed.len(), 0);
1325 }
1326
1327 #[test]
1328 fn test_sweep_pinned_not_collected() {
1329 let mut gc = default_gc();
1330 let cid = make_cid(1);
1331 gc.register_block(cid, 100, vec![]).unwrap();
1332 gc.pin(cid).unwrap();
1333
1334 let reachable = gc.mark_phase(); let sweep = gc.sweep_phase(&reachable);
1336 assert_eq!(sweep.removed.len(), 0);
1337 }
1338
1339 #[test]
1342 fn test_run_gc_mark_and_sweep() {
1343 let mut gc = default_gc();
1344 let root = make_cid(0);
1345 let orphan = make_cid(1);
1346 gc.register_block(root, 100, vec![]).unwrap();
1347 gc.register_block(orphan, 200, vec![]).unwrap();
1348 gc.add_root(root);
1349
1350 let result = gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1351 assert_eq!(result.blocks_freed, 1);
1352 assert_eq!(result.bytes_freed, 200);
1353 }
1354
1355 #[test]
1356 fn test_run_gc_reference_counting() {
1357 let mut gc = default_gc();
1358 let alive = make_cid(0);
1359 let dead = make_cid(1);
1360 gc.register_block(alive, 100, vec![]).unwrap();
1361 gc.register_block(dead, 50, vec![]).unwrap();
1362 gc.increment_ref(&alive).unwrap();
1363 let result = gc.run_gc(BgcGcPolicy::ReferenceCounting).unwrap();
1366 assert_eq!(result.blocks_freed, 1);
1367 assert_eq!(result.bytes_freed, 50);
1368 }
1369
1370 #[test]
1371 fn test_run_gc_tricolor() {
1372 let mut gc = default_gc();
1373 let root = make_cid(0);
1374 let orphan = make_cid(1);
1375 gc.register_block(root, 100, vec![]).unwrap();
1376 gc.register_block(orphan, 200, vec![]).unwrap();
1377 gc.add_root(root);
1378
1379 let result = gc.run_gc(BgcGcPolicy::TriColor).unwrap();
1380 assert_eq!(result.blocks_freed, 1);
1381 }
1382
1383 #[test]
1384 fn test_run_gc_generational() {
1385 let mut gc = default_gc();
1386 let old_root = make_cid(0);
1387 let young_orphan = make_cid(1);
1388 gc.register_block(old_root, 100, vec![]).unwrap();
1389 gc.register_block(young_orphan, 200, vec![]).unwrap();
1390 gc.add_root(old_root);
1391
1392 if let Some(rec) = gc.registry.get_mut(&old_root) {
1394 rec.generation = 1;
1395 }
1396
1397 let result = gc.run_gc(BgcGcPolicy::Generational).unwrap();
1398 assert_eq!(result.blocks_freed, 1);
1400 }
1401
1402 #[test]
1403 fn test_run_gc_logs_entry() {
1404 let mut gc = default_gc();
1405 let cid = make_cid(0);
1406 gc.register_block(cid, 100, vec![]).unwrap();
1407 gc.add_root(cid);
1408 gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1409 assert_eq!(gc.log_len(), 1);
1410 }
1411
1412 #[test]
1413 fn test_run_gc_increments_cycle_count() {
1414 let mut gc = default_gc();
1415 gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1416 gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1417 let stats = gc.gc_stats();
1418 assert_eq!(stats.gc_cycles, 2);
1419 }
1420
1421 #[test]
1422 fn test_run_gc_accumulates_bytes_freed() {
1423 let mut gc = default_gc();
1424 let a = make_cid(0);
1425 let b = make_cid(1);
1426 gc.register_block(a, 100, vec![]).unwrap();
1427 gc.register_block(b, 200, vec![]).unwrap();
1428 gc.add_root(a);
1429
1430 gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1431 let stats = gc.gc_stats();
1432 assert_eq!(stats.total_bytes_freed, 200);
1433 }
1434
1435 #[test]
1438 fn test_collect_orphans_basic() {
1439 let mut gc = default_gc();
1440 let root = make_cid(0);
1441 let orphan = make_cid(1);
1442 gc.register_block(root, 100, vec![]).unwrap();
1443 gc.register_block(orphan, 50, vec![]).unwrap();
1444 gc.add_root(root);
1445
1446 let orphans = gc.collect_orphans(0);
1447 assert_eq!(orphans.len(), 1);
1448 assert!(orphans.contains(&orphan));
1449 }
1450
1451 #[test]
1452 fn test_collect_orphans_respects_age() {
1453 let mut gc = BlockGarbageCollector::new(BgcCollectorConfig {
1454 min_age_secs: 9999,
1455 ..BgcCollectorConfig::default()
1456 });
1457 gc.set_clock(1_700_100_000);
1458 let orphan = make_cid(1);
1459 gc.register_block(orphan, 50, vec![]).unwrap();
1460
1461 let orphans = gc.collect_orphans(9999);
1462 assert_eq!(orphans.len(), 0);
1464 }
1465
1466 #[test]
1467 fn test_collect_orphans_pinned_excluded() {
1468 let mut gc = default_gc();
1469 let cid = make_cid(1);
1470 gc.register_block(cid, 50, vec![]).unwrap();
1471 gc.pin(cid).unwrap();
1472
1473 let orphans = gc.collect_orphans(0);
1474 assert_eq!(orphans.len(), 0);
1475 }
1476
1477 #[test]
1480 fn test_gc_stats_total_bytes() {
1481 let mut gc = default_gc();
1482 gc.register_block(make_cid(0), 1000, vec![]).unwrap();
1483 gc.register_block(make_cid(1), 2000, vec![]).unwrap();
1484 let stats = gc.gc_stats();
1485 assert_eq!(stats.total_bytes, 3000);
1486 }
1487
1488 #[test]
1489 fn test_gc_stats_pinned_count() {
1490 let mut gc = default_gc();
1491 let cid = make_cid(0);
1492 gc.register_block(cid, 100, vec![]).unwrap();
1493 gc.pin(cid).unwrap();
1494 let stats = gc.gc_stats();
1495 assert_eq!(stats.pinned_count, 1);
1496 }
1497
1498 #[test]
1499 fn test_gc_stats_root_count() {
1500 let mut gc = default_gc();
1501 let cid = make_cid(0);
1502 gc.register_block(cid, 100, vec![]).unwrap();
1503 gc.add_root(cid);
1504 let stats = gc.gc_stats();
1505 assert_eq!(stats.root_count, 1);
1506 }
1507
1508 #[test]
1509 fn test_gc_stats_orphan_estimate() {
1510 let mut gc = default_gc();
1511 gc.register_block(make_cid(0), 100, vec![]).unwrap(); gc.register_block(make_cid(1), 100, vec![]).unwrap(); gc.add_root(make_cid(0));
1514 let stats = gc.gc_stats();
1515 assert_eq!(stats.orphan_estimate, 1);
1516 }
1517
1518 #[test]
1521 fn test_children_of() {
1522 let mut gc = default_gc();
1523 let parent = make_cid(0);
1524 let child = make_cid(1);
1525 gc.register_block(parent, 100, vec![child]).unwrap();
1526 let children = gc.children_of(&parent);
1527 assert_eq!(children.len(), 1);
1528 assert_eq!(children[0], child);
1529 }
1530
1531 #[test]
1532 fn test_update_edges() {
1533 let mut gc = default_gc();
1534 let parent = make_cid(0);
1535 let child1 = make_cid(1);
1536 let child2 = make_cid(2);
1537 gc.register_block(parent, 100, vec![child1]).unwrap();
1538 gc.update_edges(parent, vec![child1, child2]).unwrap();
1539 assert_eq!(gc.children_of(&parent).len(), 2);
1540 }
1541
1542 #[test]
1543 fn test_update_edges_unknown_fails() {
1544 let mut gc = default_gc();
1545 let result = gc.update_edges(make_cid(99), vec![]);
1546 assert!(matches!(result, Err(BgcError::UnknownBlock(_))));
1547 }
1548
1549 #[test]
1550 fn test_compact_edges_removes_dangling() {
1551 let mut gc = default_gc();
1552 let parent = make_cid(0);
1553 let child = make_cid(1);
1554 gc.register_block(parent, 100, vec![child]).unwrap();
1555 gc.register_block(child, 50, vec![]).unwrap();
1556 gc.registry.remove(&child);
1558 gc.compact_edges();
1559 assert_eq!(gc.children_of(&parent).len(), 0);
1560 }
1561
1562 #[test]
1565 fn test_random_cid_differs() {
1566 let mut gc = default_gc();
1567 let a = gc.random_cid();
1568 let b = gc.random_cid();
1569 assert_ne!(a, b);
1570 }
1571
1572 #[test]
1573 fn test_cid_from_bytes_deterministic() {
1574 let a = BlockGarbageCollector::cid_from_bytes(b"hello");
1575 let b = BlockGarbageCollector::cid_from_bytes(b"hello");
1576 assert_eq!(a, b);
1577 }
1578
1579 #[test]
1580 fn test_cid_from_bytes_distinct() {
1581 let a = BlockGarbageCollector::cid_from_bytes(b"hello");
1582 let b = BlockGarbageCollector::cid_from_bytes(b"world");
1583 assert_ne!(a, b);
1584 }
1585
1586 #[test]
1587 fn test_cid_hash_stable() {
1588 let cid = make_cid(42);
1589 let h1 = BlockGarbageCollector::cid_hash(&cid);
1590 let h2 = BlockGarbageCollector::cid_hash(&cid);
1591 assert_eq!(h1, h2);
1592 }
1593
1594 #[test]
1597 fn test_all_cids() {
1598 let mut gc = default_gc();
1599 for i in 0..5u8 {
1600 gc.register_block(make_cid(i), 10, vec![]).unwrap();
1601 }
1602 assert_eq!(gc.all_cids().len(), 5);
1603 }
1604
1605 #[test]
1606 fn test_reachable_set() {
1607 let mut gc = default_gc();
1608 let root = make_cid(0);
1609 gc.register_block(root, 100, vec![]).unwrap();
1610 gc.add_root(root);
1611 let rs = gc.reachable_set();
1612 assert!(rs.contains(&root));
1613 }
1614
1615 #[test]
1616 fn test_clear_resets_state() {
1617 let mut gc = default_gc();
1618 gc.register_block(make_cid(0), 100, vec![]).unwrap();
1619 gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1620 gc.clear();
1621 assert_eq!(gc.block_count(), 0);
1622 assert_eq!(gc.log_len(), 0);
1623 assert_eq!(gc.gc_stats().gc_cycles, 0);
1624 }
1625
1626 #[test]
1627 fn test_reconcile_pin_flags() {
1628 let mut gc = default_gc();
1629 let cid = make_cid(1);
1630 gc.register_block(cid, 100, vec![]).unwrap();
1631 gc.pin_set.insert(cid); gc.reconcile_pin_flags();
1633 assert!(gc.get_block(&cid).unwrap().is_pinned);
1634 }
1635
1636 #[test]
1637 fn test_promote_generation() {
1638 let mut gc = default_gc();
1639 let cid = make_cid(1);
1640 gc.register_block(cid, 100, vec![]).unwrap();
1641 gc.promote_generation();
1642 assert_eq!(gc.get_block(&cid).unwrap().generation, 1);
1643 }
1644
1645 #[test]
1648 fn test_large_registry_gc() {
1649 let mut gc = default_gc();
1650 let root = make_cid(0);
1652 gc.register_block(root, 100, vec![]).unwrap();
1653 gc.add_root(root);
1654 for i in 1..=900u16 {
1656 gc.register_block(make_cid_u16(i), 10, vec![]).unwrap();
1657 }
1658 let result = gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1659 assert!(result.blocks_freed > 0);
1660 }
1661
1662 #[test]
1663 fn test_gc_preserves_reachable_tree() {
1664 let mut gc = default_gc();
1665 let root = make_cid(0);
1667 let a = make_cid(1);
1668 let b = make_cid(2);
1669 let c = make_cid(3);
1670 let d = make_cid(4);
1671 gc.register_block(root, 1, vec![a, b]).unwrap();
1672 gc.register_block(a, 1, vec![c]).unwrap();
1673 gc.register_block(b, 1, vec![d]).unwrap();
1674 gc.register_block(c, 1, vec![]).unwrap();
1675 gc.register_block(d, 1, vec![]).unwrap();
1676 gc.add_root(root);
1677
1678 let result = gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1679 assert_eq!(result.blocks_freed, 0);
1680 assert_eq!(gc.block_count(), 5);
1681 }
1682
1683 #[test]
1684 fn test_multiple_roots() {
1685 let mut gc = default_gc();
1686 let r1 = make_cid(0);
1687 let r2 = make_cid(1);
1688 let orphan = make_cid(2);
1689 gc.register_block(r1, 100, vec![]).unwrap();
1690 gc.register_block(r2, 100, vec![]).unwrap();
1691 gc.register_block(orphan, 100, vec![]).unwrap();
1692 gc.add_root(r1);
1693 gc.add_root(r2);
1694 let result = gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1695 assert_eq!(result.blocks_freed, 1);
1696 }
1697
1698 #[test]
1699 fn test_gc_log_bounded_at_1000() {
1700 let mut gc = default_gc();
1701 for _ in 0..1010 {
1702 gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1703 }
1704 assert!(gc.log_len() <= 1000);
1705 }
1706
1707 #[test]
1708 fn test_batch_size_limits_sweep() {
1709 let mut gc = default_gc();
1710 gc.config_mut().batch_size = 5;
1711 for i in 0..20u8 {
1712 gc.register_block(make_cid(i), 10, vec![]).unwrap();
1713 }
1714 let result = gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1715 assert!(result.blocks_freed <= 5);
1717 }
1718
1719 #[test]
1720 fn test_tricolor_matches_mark_and_sweep() {
1721 let mut gc1 = default_gc();
1722 let mut gc2 = default_gc();
1723
1724 let root = make_cid(0);
1725 let child = make_cid(1);
1726 let orphan = make_cid(2);
1727
1728 for gc in [&mut gc1, &mut gc2] {
1729 gc.register_block(root, 100, vec![child]).unwrap();
1730 gc.register_block(child, 50, vec![]).unwrap();
1731 gc.register_block(orphan, 75, vec![]).unwrap();
1732 gc.add_root(root);
1733 }
1734
1735 let r1 = gc1.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1736 let r2 = gc2.run_gc(BgcGcPolicy::TriColor).unwrap();
1737
1738 assert_eq!(r1.blocks_freed, r2.blocks_freed);
1739 assert_eq!(r1.bytes_freed, r2.bytes_freed);
1740 }
1741
1742 #[test]
1743 fn test_touch_updates_last_accessed() {
1744 let mut gc = default_gc();
1745 let cid = make_cid(1);
1746 gc.register_block(cid, 100, vec![]).unwrap();
1747 let before = gc.get_block(&cid).unwrap().last_accessed;
1748 gc.set_clock(before + 5000);
1749 gc.touch(&cid).unwrap();
1750 let after = gc.get_block(&cid).unwrap().last_accessed;
1751 assert!(after >= before);
1752 }
1753
1754 #[test]
1755 fn test_touch_unknown_fails() {
1756 let mut gc = default_gc();
1757 let result = gc.touch(&make_cid(99));
1758 assert!(matches!(result, Err(BgcError::UnknownBlock(_))));
1759 }
1760
1761 #[test]
1764 fn test_fnv1a_64_empty() {
1765 let h = fnv1a_64(b"");
1766 assert_eq!(h, 14695981039346656037u64);
1767 }
1768
1769 #[test]
1770 fn test_fnv1a_64_hello() {
1771 let h = fnv1a_64(b"hello");
1772 assert_ne!(h, 0);
1773 }
1774
1775 #[test]
1776 fn test_xorshift64_non_zero() {
1777 let mut state: u64 = 12345;
1778 let v = xorshift64(&mut state);
1779 assert_ne!(v, 0);
1780 }
1781
1782 #[test]
1783 fn test_xorshift64_advances_state() {
1784 let mut state: u64 = 1;
1785 let v1 = xorshift64(&mut state);
1786 let v2 = xorshift64(&mut state);
1787 assert_ne!(v1, v2);
1788 }
1789
1790 #[test]
1793 fn test_error_display_unknown() {
1794 let e = BgcError::UnknownBlock(make_cid(1));
1795 assert!(e.to_string().contains("unknown block"));
1796 }
1797
1798 #[test]
1799 fn test_error_display_pinned() {
1800 let e = BgcError::BlockIsPinned(make_cid(1));
1801 assert!(e.to_string().contains("pinned"));
1802 }
1803
1804 #[test]
1805 fn test_error_display_timeout() {
1806 let e = BgcError::MarkTimeout;
1807 assert!(e.to_string().contains("timed out"));
1808 }
1809
1810 #[test]
1811 fn test_error_display_overflow() {
1812 let e = BgcError::RefCountOverflow(make_cid(1));
1813 assert!(e.to_string().contains("overflow"));
1814 }
1815
1816 #[test]
1819 fn test_gcphase_debug() {
1820 let p = BgcGcPhase::Mark;
1821 assert_eq!(format!("{:?}", p), "Mark");
1822 }
1823
1824 #[test]
1825 fn test_gcpolicy_debug() {
1826 let p = BgcGcPolicy::Generational;
1827 assert_eq!(format!("{:?}", p), "Generational");
1828 }
1829
1830 #[test]
1831 fn test_gcphase_eq() {
1832 assert_eq!(BgcGcPhase::Idle, BgcGcPhase::Idle);
1833 assert_ne!(BgcGcPhase::Sweep, BgcGcPhase::Mark);
1834 }
1835
1836 #[test]
1837 fn test_gcpolicy_eq() {
1838 assert_eq!(BgcGcPolicy::MarkAndSweep, BgcGcPolicy::MarkAndSweep);
1839 assert_ne!(BgcGcPolicy::TriColor, BgcGcPolicy::Generational);
1840 }
1841
1842 #[test]
1845 fn test_gc_result_default() {
1846 let r = BgcGcResult::default();
1847 assert_eq!(r.blocks_freed, 0);
1848 assert_eq!(r.bytes_freed, 0);
1849 assert!(!r.dry_run);
1850 assert!(r.policy.is_none());
1851 }
1852
1853 #[test]
1856 fn test_sweep_result_default() {
1857 let r = BgcSweepResult::default();
1858 assert!(r.removed.is_empty());
1859 assert_eq!(r.bytes_freed, 0);
1860 }
1861
1862 #[test]
1865 fn test_stats_accumulate_over_multiple_gc() {
1866 let mut gc = default_gc();
1867 gc.register_block(make_cid(1), 100, vec![]).unwrap();
1869 gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1870 gc.register_block(make_cid(2), 200, vec![]).unwrap();
1871 gc.run_gc(BgcGcPolicy::MarkAndSweep).unwrap();
1872 let stats = gc.gc_stats();
1873 assert!(stats.total_bytes_freed >= 100);
1874 assert!(stats.total_blocks_freed >= 1);
1875 }
1876}