1use std::collections::{HashMap, HashSet, VecDeque};
42
43use thiserror::Error;
44
45#[derive(Debug, Error, Clone, PartialEq, Eq)]
49pub enum GcError {
50 #[error("object already exists: {0}")]
52 ObjectAlreadyExists(String),
53
54 #[error("object not found: {0}")]
56 ObjectNotFound(String),
57
58 #[error("object is pinned: {0}")]
60 ObjectPinned(String),
61
62 #[error("object {id} is still referenced (ref_count={ref_count})")]
64 ObjectReferenced {
65 id: String,
67 ref_count: u32,
69 },
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
76pub struct GcObjectId(pub String);
77
78impl GcObjectId {
79 pub fn new(id: impl Into<String>) -> Self {
81 Self(id.into())
82 }
83
84 pub fn as_str(&self) -> &str {
86 &self.0
87 }
88}
89
90impl std::fmt::Display for GcObjectId {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.write_str(&self.0)
93 }
94}
95
96#[derive(Debug, Clone)]
98pub struct GcObject {
99 pub id: GcObjectId,
101 pub size_bytes: u64,
103 pub ref_count: u32,
105 pub pinned: bool,
107 pub created_at: u64,
109 pub last_marked: Option<u64>,
111 pub links: Vec<GcObjectId>,
113}
114
115impl GcObject {
116 fn is_sweepable(&self, mark_epoch: u64) -> bool {
119 !self.pinned && self.ref_count == 0 && self.last_marked != Some(mark_epoch)
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum GcPhase {
126 Idle,
128 Marking,
130 Sweeping,
132 Compacting,
134}
135
136#[derive(Debug, Clone)]
140pub struct StorageGcConfig {
141 pub max_live_bytes: u64,
143 pub sweep_threshold_fraction: f64,
146 pub pin_preserve: bool,
148 pub batch_size: usize,
150}
151
152impl Default for StorageGcConfig {
153 fn default() -> Self {
154 Self {
155 max_live_bytes: 1024 * 1024 * 1024, sweep_threshold_fraction: 0.8,
157 pin_preserve: true,
158 batch_size: 1000,
159 }
160 }
161}
162
163#[derive(Debug, Clone)]
167pub struct StorageGcRun {
168 pub id: u64,
170 pub started_at: u64,
172 pub completed_at: Option<u64>,
174 pub objects_marked: usize,
176 pub objects_swept: usize,
178 pub bytes_freed: u64,
180 pub phase: GcPhase,
182}
183
184#[derive(Debug, Clone)]
188pub struct StorageGcStats {
189 pub total_objects: usize,
191 pub root_count: usize,
193 pub live_bytes: u64,
195 pub pinned_count: usize,
197 pub total_runs: usize,
199 pub last_run_freed_bytes: u64,
201}
202
203pub struct StorageGarbageCollector {
212 pub config: StorageGcConfig,
214 pub objects: HashMap<GcObjectId, GcObject>,
216 pub roots: HashSet<GcObjectId>,
218 pub gc_runs: VecDeque<StorageGcRun>,
220 pub phase: GcPhase,
222 pub next_run_id: u64,
224}
225
226impl StorageGarbageCollector {
227 pub fn new(config: StorageGcConfig) -> Self {
231 Self {
232 config,
233 objects: HashMap::new(),
234 roots: HashSet::new(),
235 gc_runs: VecDeque::new(),
236 phase: GcPhase::Idle,
237 next_run_id: 1,
238 }
239 }
240
241 pub fn add_object(&mut self, object: GcObject) -> Result<(), GcError> {
246 if self.objects.contains_key(&object.id) {
247 return Err(GcError::ObjectAlreadyExists(object.id.0.clone()));
248 }
249 self.objects.insert(object.id.clone(), object);
250 Ok(())
251 }
252
253 pub fn remove_object(&mut self, id: &GcObjectId) -> Result<GcObject, GcError> {
260 let obj = self
261 .objects
262 .get(id)
263 .ok_or_else(|| GcError::ObjectNotFound(id.0.clone()))?;
264
265 if obj.pinned {
266 return Err(GcError::ObjectPinned(id.0.clone()));
267 }
268 if obj.ref_count > 0 {
269 return Err(GcError::ObjectReferenced {
270 id: id.0.clone(),
271 ref_count: obj.ref_count,
272 });
273 }
274
275 Ok(self.objects.remove(id).unwrap_or_else(|| {
276 panic!("object disappeared between check and remove")
278 }))
279 }
280
281 pub fn add_root(&mut self, id: &GcObjectId) -> bool {
288 if self.objects.contains_key(id) {
289 self.roots.insert(id.clone());
290 true
291 } else {
292 false
293 }
294 }
295
296 pub fn remove_root(&mut self, id: &GcObjectId) -> bool {
300 self.roots.remove(id)
301 }
302
303 pub fn increment_ref(&mut self, id: &GcObjectId) -> bool {
309 if let Some(obj) = self.objects.get_mut(id) {
310 obj.ref_count = obj.ref_count.saturating_add(1);
311 true
312 } else {
313 false
314 }
315 }
316
317 pub fn decrement_ref(&mut self, id: &GcObjectId) -> bool {
323 if let Some(obj) = self.objects.get_mut(id) {
324 obj.ref_count = obj.ref_count.saturating_sub(1);
325 true
326 } else {
327 false
328 }
329 }
330
331 pub fn pin(&mut self, id: &GcObjectId) -> bool {
337 if let Some(obj) = self.objects.get_mut(id) {
338 obj.pinned = true;
339 true
340 } else {
341 false
342 }
343 }
344
345 pub fn unpin(&mut self, id: &GcObjectId) -> bool {
349 if let Some(obj) = self.objects.get_mut(id) {
350 obj.pinned = false;
351 true
352 } else {
353 false
354 }
355 }
356
357 pub fn should_run(&self) -> bool {
361 let live = self.live_bytes();
362 let max = self.config.max_live_bytes;
363 if max == 0 {
364 return false;
365 }
366 let ratio = live as f64 / max as f64;
367 ratio >= self.config.sweep_threshold_fraction
368 }
369
370 pub fn mark_reachable(&mut self, now: u64) -> HashSet<GcObjectId> {
377 let mut queue: VecDeque<GcObjectId> = VecDeque::new();
379 let mut visited: HashSet<GcObjectId> = HashSet::new();
380
381 for id in &self.roots {
382 if !visited.contains(id) {
383 visited.insert(id.clone());
384 queue.push_back(id.clone());
385 }
386 }
387
388 for (id, obj) in &self.objects {
390 if obj.pinned && !visited.contains(id) {
391 visited.insert(id.clone());
392 queue.push_back(id.clone());
393 }
394 }
395
396 while let Some(current_id) = queue.pop_front() {
399 if let Some(obj) = self.objects.get_mut(¤t_id) {
401 obj.last_marked = Some(now);
402
403 let links: Vec<GcObjectId> = obj.links.clone();
405 for link in links {
406 if !visited.contains(&link) {
407 visited.insert(link.clone());
408 queue.push_back(link);
409 }
410 }
411 }
412 }
413
414 visited
415 }
416
417 pub fn run_gc(&mut self, now: u64) -> StorageGcRun {
426 let run_id = self.next_run_id;
427 self.next_run_id += 1;
428
429 self.phase = GcPhase::Marking;
431 let reachable = self.mark_reachable(now);
432 let objects_marked = reachable.len();
433
434 self.phase = GcPhase::Sweeping;
436 let sweep_ids: Vec<GcObjectId> = self
437 .objects
438 .iter()
439 .filter_map(|(id, obj)| {
440 if obj.is_sweepable(now) {
441 Some(id.clone())
442 } else {
443 None
444 }
445 })
446 .collect();
447
448 let mut objects_swept = 0usize;
449 let mut bytes_freed = 0u64;
450 for id in &sweep_ids {
451 if let Some(obj) = self.objects.remove(id) {
452 objects_swept += 1;
453 bytes_freed += obj.size_bytes;
454 self.roots.remove(id);
456 }
457 }
458
459 self.phase = GcPhase::Compacting;
461
462 self.phase = GcPhase::Idle;
464
465 let run = StorageGcRun {
466 id: run_id,
467 started_at: now,
468 completed_at: Some(now),
469 objects_marked,
470 objects_swept,
471 bytes_freed,
472 phase: GcPhase::Idle,
473 };
474
475 if self.gc_runs.len() >= 256 {
477 self.gc_runs.pop_front();
478 }
479 self.gc_runs.push_back(run.clone());
480
481 run
482 }
483
484 pub fn live_bytes(&self) -> u64 {
488 self.objects.values().map(|o| o.size_bytes).sum()
489 }
490
491 pub fn reachable_bytes(&self, reachable: &HashSet<GcObjectId>) -> u64 {
493 reachable
494 .iter()
495 .filter_map(|id| self.objects.get(id))
496 .map(|o| o.size_bytes)
497 .sum()
498 }
499
500 pub fn object_count(&self) -> usize {
502 self.objects.len()
503 }
504
505 pub fn root_count(&self) -> usize {
507 self.roots.len()
508 }
509
510 pub fn stats(&self) -> StorageGcStats {
512 let pinned_count = self.objects.values().filter(|o| o.pinned).count();
513 let last_run_freed_bytes = self.gc_runs.back().map(|r| r.bytes_freed).unwrap_or(0);
514
515 StorageGcStats {
516 total_objects: self.objects.len(),
517 root_count: self.roots.len(),
518 live_bytes: self.live_bytes(),
519 pinned_count,
520 total_runs: self.gc_runs.len(),
521 last_run_freed_bytes,
522 }
523 }
524}
525
526impl GcObject {
529 pub fn new(id: GcObjectId, size_bytes: u64, created_at: u64, links: Vec<GcObjectId>) -> Self {
531 Self {
532 id,
533 size_bytes,
534 ref_count: 0,
535 pinned: false,
536 created_at,
537 last_marked: None,
538 links,
539 }
540 }
541
542 pub fn with_ref_count(mut self, ref_count: u32) -> Self {
544 self.ref_count = ref_count;
545 self
546 }
547
548 pub fn pinned(mut self) -> Self {
550 self.pinned = true;
551 self
552 }
553}
554
555#[cfg(test)]
558mod tests {
559 use std::collections::HashSet;
560
561 use crate::garbage_collector::{
562 GcError, GcObject, GcObjectId, GcPhase, StorageGarbageCollector, StorageGcConfig,
563 };
564
565 fn default_gc() -> StorageGarbageCollector {
568 StorageGarbageCollector::new(StorageGcConfig::default())
569 }
570
571 fn make_obj(id: &str, size: u64) -> GcObject {
572 GcObject::new(GcObjectId::new(id), size, 0, vec![])
573 }
574
575 fn make_obj_with_links(id: &str, size: u64, links: Vec<&str>) -> GcObject {
576 let link_ids = links.into_iter().map(GcObjectId::new).collect();
577 GcObject::new(GcObjectId::new(id), size, 0, link_ids)
578 }
579
580 fn oid(s: &str) -> GcObjectId {
581 GcObjectId::new(s)
582 }
583
584 #[test]
587 fn test_new_is_idle() {
588 let gc = default_gc();
589 assert_eq!(gc.phase, GcPhase::Idle);
590 assert_eq!(gc.object_count(), 0);
591 assert_eq!(gc.root_count(), 0);
592 }
593
594 #[test]
595 fn test_default_config_values() {
596 let cfg = StorageGcConfig::default();
597 assert!(cfg.max_live_bytes > 0);
598 assert!(cfg.sweep_threshold_fraction > 0.0);
599 assert!(cfg.sweep_threshold_fraction <= 1.0);
600 assert_eq!(cfg.batch_size, 1000);
601 assert!(cfg.pin_preserve);
602 }
603
604 #[test]
607 fn test_add_object_succeeds() {
608 let mut gc = default_gc();
609 gc.add_object(make_obj("a", 100)).expect("add object");
610 assert_eq!(gc.object_count(), 1);
611 }
612
613 #[test]
614 fn test_add_duplicate_fails() {
615 let mut gc = default_gc();
616 gc.add_object(make_obj("a", 100)).expect("first add");
617 let err = gc.add_object(make_obj("a", 200)).unwrap_err();
618 assert!(matches!(err, GcError::ObjectAlreadyExists(_)));
619 }
620
621 #[test]
622 fn test_add_multiple_objects() {
623 let mut gc = default_gc();
624 for i in 0..10u32 {
625 gc.add_object(make_obj(&format!("obj{i}"), 64))
626 .expect("add");
627 }
628 assert_eq!(gc.object_count(), 10);
629 }
630
631 #[test]
634 fn test_remove_object_succeeds() {
635 let mut gc = default_gc();
636 gc.add_object(make_obj("a", 100)).expect("add");
637 let removed = gc.remove_object(&oid("a")).expect("remove");
638 assert_eq!(removed.id, oid("a"));
639 assert_eq!(gc.object_count(), 0);
640 }
641
642 #[test]
643 fn test_remove_nonexistent_fails() {
644 let mut gc = default_gc();
645 let err = gc.remove_object(&oid("nope")).unwrap_err();
646 assert!(matches!(err, GcError::ObjectNotFound(_)));
647 }
648
649 #[test]
650 fn test_remove_pinned_fails() {
651 let mut gc = default_gc();
652 gc.add_object(make_obj("a", 100).pinned()).expect("add");
653 let err = gc.remove_object(&oid("a")).unwrap_err();
654 assert!(matches!(err, GcError::ObjectPinned(_)));
655 }
656
657 #[test]
658 fn test_remove_referenced_fails() {
659 let mut gc = default_gc();
660 gc.add_object(make_obj("a", 100).with_ref_count(2))
661 .expect("add");
662 let err = gc.remove_object(&oid("a")).unwrap_err();
663 assert!(
664 matches!(err, GcError::ObjectReferenced { ref_count: 2, .. }),
665 "got: {err:?}"
666 );
667 }
668
669 #[test]
672 fn test_add_root_existing_object() {
673 let mut gc = default_gc();
674 gc.add_object(make_obj("r", 50)).expect("add");
675 assert!(gc.add_root(&oid("r")));
676 assert_eq!(gc.root_count(), 1);
677 }
678
679 #[test]
680 fn test_add_root_missing_object_returns_false() {
681 let mut gc = default_gc();
682 assert!(!gc.add_root(&oid("ghost")));
683 assert_eq!(gc.root_count(), 0);
684 }
685
686 #[test]
687 fn test_remove_root() {
688 let mut gc = default_gc();
689 gc.add_object(make_obj("r", 50)).expect("add");
690 gc.add_root(&oid("r"));
691 assert!(gc.remove_root(&oid("r")));
692 assert_eq!(gc.root_count(), 0);
693 }
694
695 #[test]
696 fn test_remove_root_not_present() {
697 let mut gc = default_gc();
698 assert!(!gc.remove_root(&oid("none")));
699 }
700
701 #[test]
704 fn test_increment_ref() {
705 let mut gc = default_gc();
706 gc.add_object(make_obj("a", 10)).expect("add");
707 assert!(gc.increment_ref(&oid("a")));
708 let obj = gc.objects.get(&oid("a")).expect("obj");
709 assert_eq!(obj.ref_count, 1);
710 }
711
712 #[test]
713 fn test_decrement_ref() {
714 let mut gc = default_gc();
715 gc.add_object(make_obj("a", 10).with_ref_count(3))
716 .expect("add");
717 assert!(gc.decrement_ref(&oid("a")));
718 let obj = gc.objects.get(&oid("a")).expect("obj");
719 assert_eq!(obj.ref_count, 2);
720 }
721
722 #[test]
723 fn test_decrement_ref_floors_at_zero() {
724 let mut gc = default_gc();
725 gc.add_object(make_obj("a", 10)).expect("add");
726 gc.decrement_ref(&oid("a")); let obj = gc.objects.get(&oid("a")).expect("obj");
728 assert_eq!(obj.ref_count, 0);
729 }
730
731 #[test]
732 fn test_increment_ref_missing_returns_false() {
733 let mut gc = default_gc();
734 assert!(!gc.increment_ref(&oid("nope")));
735 }
736
737 #[test]
740 fn test_pin_and_unpin() {
741 let mut gc = default_gc();
742 gc.add_object(make_obj("a", 10)).expect("add");
743 assert!(gc.pin(&oid("a")));
744 assert!(gc.objects[&oid("a")].pinned);
745 assert!(gc.unpin(&oid("a")));
746 assert!(!gc.objects[&oid("a")].pinned);
747 }
748
749 #[test]
750 fn test_pin_missing_returns_false() {
751 let mut gc = default_gc();
752 assert!(!gc.pin(&oid("nope")));
753 }
754
755 #[test]
758 fn test_live_bytes_empty() {
759 let gc = default_gc();
760 assert_eq!(gc.live_bytes(), 0);
761 }
762
763 #[test]
764 fn test_live_bytes_sum() {
765 let mut gc = default_gc();
766 gc.add_object(make_obj("a", 100)).expect("add");
767 gc.add_object(make_obj("b", 200)).expect("add");
768 assert_eq!(gc.live_bytes(), 300);
769 }
770
771 #[test]
772 fn test_reachable_bytes() {
773 let mut gc = default_gc();
774 gc.add_object(make_obj("a", 100)).expect("add");
775 gc.add_object(make_obj("b", 200)).expect("add");
776 let mut reachable = HashSet::new();
777 reachable.insert(oid("a"));
778 assert_eq!(gc.reachable_bytes(&reachable), 100);
779 }
780
781 #[test]
784 fn test_should_run_below_threshold() {
785 let cfg = StorageGcConfig {
786 max_live_bytes: 1000,
787 sweep_threshold_fraction: 0.8,
788 ..StorageGcConfig::default()
789 };
790 let mut gc = StorageGarbageCollector::new(cfg);
791 gc.add_object(make_obj("a", 500)).expect("add"); assert!(!gc.should_run());
793 }
794
795 #[test]
796 fn test_should_run_at_threshold() {
797 let cfg = StorageGcConfig {
798 max_live_bytes: 1000,
799 sweep_threshold_fraction: 0.8,
800 ..StorageGcConfig::default()
801 };
802 let mut gc = StorageGarbageCollector::new(cfg);
803 gc.add_object(make_obj("a", 800)).expect("add"); assert!(gc.should_run());
805 }
806
807 #[test]
808 fn test_should_run_zero_max_bytes() {
809 let cfg = StorageGcConfig {
810 max_live_bytes: 0,
811 ..StorageGcConfig::default()
812 };
813 let mut gc = StorageGarbageCollector::new(cfg);
814 gc.add_object(make_obj("a", 1)).expect("add");
815 assert!(!gc.should_run()); }
817
818 #[test]
821 fn test_mark_reachable_from_root() {
822 let mut gc = default_gc();
823 gc.add_object(make_obj("root", 10)).expect("add");
824 gc.add_object(make_obj("child", 10)).expect("add");
825 gc.add_root(&oid("root"));
827 let reachable = gc.mark_reachable(1);
828 assert!(reachable.contains(&oid("root")));
829 assert!(!reachable.contains(&oid("child")));
830 }
831
832 #[test]
833 fn test_mark_reachable_follows_links() {
834 let mut gc = default_gc();
835 gc.add_object(make_obj_with_links("root", 10, vec!["child"]))
836 .expect("add");
837 gc.add_object(make_obj("child", 10)).expect("add");
838 gc.add_root(&oid("root"));
839 let reachable = gc.mark_reachable(1);
840 assert!(reachable.contains(&oid("child")));
841 }
842
843 #[test]
844 fn test_mark_reachable_deep_chain() {
845 let mut gc = default_gc();
846 gc.add_object(make_obj_with_links("a", 10, vec!["b"]))
848 .expect("add");
849 gc.add_object(make_obj_with_links("b", 10, vec!["c"]))
850 .expect("add");
851 gc.add_object(make_obj_with_links("c", 10, vec!["d"]))
852 .expect("add");
853 gc.add_object(make_obj("d", 10)).expect("add");
854 gc.add_root(&oid("a"));
855 let reachable = gc.mark_reachable(1);
856 for id in &["a", "b", "c", "d"] {
857 assert!(reachable.contains(&oid(id)), "missing: {id}");
858 }
859 }
860
861 #[test]
862 fn test_mark_reachable_pinned_as_seed() {
863 let mut gc = default_gc();
864 gc.add_object(make_obj("pinned", 10).pinned()).expect("add");
866 let reachable = gc.mark_reachable(1);
867 assert!(reachable.contains(&oid("pinned")));
868 }
869
870 #[test]
871 fn test_mark_sets_last_marked() {
872 let mut gc = default_gc();
873 gc.add_object(make_obj("a", 10)).expect("add");
874 gc.add_root(&oid("a"));
875 gc.mark_reachable(42);
876 assert_eq!(gc.objects[&oid("a")].last_marked, Some(42));
877 }
878
879 #[test]
882 fn test_run_gc_sweeps_unreachable() {
883 let mut gc = default_gc();
884 gc.add_object(make_obj("orphan", 512)).expect("add");
885 let run = gc.run_gc(1);
886 assert_eq!(run.objects_swept, 1);
887 assert_eq!(run.bytes_freed, 512);
888 assert_eq!(gc.object_count(), 0);
889 }
890
891 #[test]
892 fn test_run_gc_keeps_roots() {
893 let mut gc = default_gc();
894 gc.add_object(make_obj("root", 100)).expect("add");
895 gc.add_root(&oid("root"));
896 let run = gc.run_gc(1);
897 assert_eq!(run.objects_swept, 0);
898 assert_eq!(run.bytes_freed, 0);
899 assert_eq!(gc.object_count(), 1);
900 }
901
902 #[test]
903 fn test_run_gc_keeps_pinned() {
904 let mut gc = default_gc();
905 gc.add_object(make_obj("pin", 100).pinned()).expect("add");
906 let run = gc.run_gc(1);
907 assert_eq!(run.objects_swept, 0);
908 assert_eq!(gc.object_count(), 1);
909 }
910
911 #[test]
912 fn test_run_gc_keeps_referenced() {
913 let mut gc = default_gc();
914 gc.add_object(make_obj("ref", 100).with_ref_count(1))
915 .expect("add");
916 let run = gc.run_gc(1);
917 assert_eq!(run.objects_swept, 0);
919 }
920
921 #[test]
922 fn test_run_gc_increments_run_id() {
923 let mut gc = default_gc();
924 let r1 = gc.run_gc(1);
925 let r2 = gc.run_gc(2);
926 assert_eq!(r1.id + 1, r2.id);
927 }
928
929 #[test]
930 fn test_run_gc_records_stats() {
931 let mut gc = default_gc();
932 gc.add_object(make_obj("orphan", 1024)).expect("add");
933 gc.run_gc(1);
934 let stats = gc.stats();
935 assert_eq!(stats.total_runs, 1);
936 assert_eq!(stats.last_run_freed_bytes, 1024);
937 }
938
939 #[test]
940 fn test_run_gc_marks_objects_count() {
941 let mut gc = default_gc();
942 gc.add_object(make_obj("a", 10)).expect("add");
943 gc.add_object(make_obj("b", 10)).expect("add");
944 gc.add_root(&oid("a"));
945 let run = gc.run_gc(1);
946 assert_eq!(run.objects_marked, 1); assert_eq!(run.objects_swept, 1); }
949
950 #[test]
951 fn test_run_gc_completed_at() {
952 let mut gc = default_gc();
953 let run = gc.run_gc(999);
954 assert_eq!(run.completed_at, Some(999));
955 assert_eq!(run.started_at, 999);
956 }
957
958 #[test]
959 fn test_run_gc_idempotent_on_empty_store() {
960 let mut gc = default_gc();
961 for epoch in 0..5u64 {
962 let run = gc.run_gc(epoch);
963 assert_eq!(run.objects_swept, 0);
964 }
965 }
966
967 #[test]
970 fn test_diamond_dag() {
971 let mut gc = default_gc();
973 gc.add_object(make_obj_with_links("root", 10, vec!["left", "right"]))
974 .expect("add");
975 gc.add_object(make_obj_with_links("left", 10, vec!["leaf"]))
976 .expect("add");
977 gc.add_object(make_obj_with_links("right", 10, vec!["leaf"]))
978 .expect("add");
979 gc.add_object(make_obj("leaf", 10)).expect("add");
980 gc.add_root(&oid("root"));
981
982 let run = gc.run_gc(1);
983 assert_eq!(run.objects_swept, 0);
984 assert_eq!(gc.object_count(), 4);
985 }
986
987 #[test]
988 fn test_orphaned_subgraph() {
989 let mut gc = default_gc();
991 gc.add_object(make_obj_with_links("root", 10, vec!["child"]))
992 .expect("add");
993 gc.add_object(make_obj("child", 20)).expect("add");
994 gc.add_object(make_obj_with_links("orphan_a", 30, vec!["orphan_b"]))
995 .expect("add");
996 gc.add_object(make_obj("orphan_b", 40)).expect("add");
997 gc.add_root(&oid("root"));
998
999 let run = gc.run_gc(1);
1000 assert_eq!(run.objects_swept, 2);
1001 assert_eq!(run.bytes_freed, 70);
1002 }
1003
1004 #[test]
1005 fn test_multiple_roots() {
1006 let mut gc = default_gc();
1007 gc.add_object(make_obj("r1", 10)).expect("add");
1008 gc.add_object(make_obj("r2", 10)).expect("add");
1009 gc.add_object(make_obj("orphan", 10)).expect("add");
1010 gc.add_root(&oid("r1"));
1011 gc.add_root(&oid("r2"));
1012
1013 let run = gc.run_gc(1);
1014 assert_eq!(run.objects_swept, 1);
1015 assert_eq!(gc.object_count(), 2);
1016 }
1017
1018 #[test]
1021 fn test_stats_pinned_count() {
1022 let mut gc = default_gc();
1023 gc.add_object(make_obj("a", 10).pinned()).expect("add");
1024 gc.add_object(make_obj("b", 10)).expect("add");
1025 let stats = gc.stats();
1026 assert_eq!(stats.pinned_count, 1);
1027 }
1028
1029 #[test]
1030 fn test_stats_zero_runs() {
1031 let gc = default_gc();
1032 let stats = gc.stats();
1033 assert_eq!(stats.total_runs, 0);
1034 assert_eq!(stats.last_run_freed_bytes, 0);
1035 }
1036
1037 #[test]
1040 fn test_gc_object_new_defaults() {
1041 let obj = GcObject::new(oid("x"), 64, 100, vec![]);
1042 assert_eq!(obj.ref_count, 0);
1043 assert!(!obj.pinned);
1044 assert!(obj.last_marked.is_none());
1045 }
1046
1047 #[test]
1048 fn test_gc_object_with_ref_count() {
1049 let obj = GcObject::new(oid("x"), 64, 0, vec![]).with_ref_count(5);
1050 assert_eq!(obj.ref_count, 5);
1051 }
1052
1053 #[test]
1054 fn test_gc_object_pinned_builder() {
1055 let obj = GcObject::new(oid("x"), 64, 0, vec![]).pinned();
1056 assert!(obj.pinned);
1057 }
1058
1059 #[test]
1062 fn test_gc_object_id_display() {
1063 let id = GcObjectId::new("bafy123");
1064 assert_eq!(id.to_string(), "bafy123");
1065 }
1066
1067 #[test]
1068 fn test_gc_object_id_as_str() {
1069 let id = GcObjectId::new("bafy123");
1070 assert_eq!(id.as_str(), "bafy123");
1071 }
1072
1073 #[test]
1076 fn test_gc_run_history_capped_at_256() {
1077 let mut gc = default_gc();
1078 for epoch in 0..300u64 {
1079 gc.run_gc(epoch);
1080 }
1081 assert!(gc.gc_runs.len() <= 256, "history exceeded 256 entries");
1082 }
1083
1084 #[test]
1087 fn test_full_lifecycle() {
1088 let mut gc = default_gc();
1089
1090 gc.add_object(make_obj_with_links("root", 100, vec!["a"]))
1092 .expect("add root");
1093 gc.add_object(make_obj_with_links("a", 200, vec!["b"]))
1094 .expect("add a");
1095 gc.add_object(make_obj("b", 300)).expect("add b");
1096 gc.add_object(make_obj("c", 400)).expect("add c");
1097
1098 gc.add_root(&oid("root"));
1099
1100 let run1 = gc.run_gc(1);
1102 assert_eq!(run1.objects_swept, 1);
1103 assert_eq!(run1.bytes_freed, 400);
1104
1105 gc.remove_root(&oid("root"));
1107 let run2 = gc.run_gc(2);
1108 assert_eq!(run2.objects_swept, 3);
1109 assert_eq!(run2.bytes_freed, 600);
1110
1111 assert_eq!(gc.object_count(), 0);
1112 let stats = gc.stats();
1113 assert_eq!(stats.total_runs, 2);
1114 }
1115}