Skip to main content

ipfrs_tensorlogic/
tensor_gc.rs

1//! Garbage collector for unreachable tensor allocations.
2//!
3//! Identifies and collects unreachable tensor allocations using reference
4//! counting and reachability analysis from named roots. The collector
5//! implements a classic mark-and-sweep algorithm operating in three phases:
6//!
7//! 1. **MarkRoots** — seed the reachable set with explicit roots and pinned tensors.
8//! 2. **Trace** — BFS-expand the reachable set through dependency edges.
9//! 3. **Sweep** — remove every tensor that is neither reachable nor ref-counted.
10//!
11//! # Example
12//!
13//! ```
14//! use ipfrs_tensorlogic::tensor_gc::{TensorGarbageCollector, TensorRef};
15//!
16//! let mut gc = TensorGarbageCollector::new();
17//!
18//! // Register two tensors: A depends on B.
19//! gc.register(TensorRef {
20//!     tensor_id: 1,
21//!     name: Some("A".to_string()),
22//!     size_bytes: 1024,
23//!     ref_count: 0,
24//!     dependencies: vec![2],
25//!     pinned: false,
26//! });
27//! gc.register(TensorRef {
28//!     tensor_id: 2,
29//!     name: Some("B".to_string()),
30//!     size_bytes: 512,
31//!     ref_count: 0,
32//!     dependencies: vec![],
33//!     pinned: false,
34//! });
35//!
36//! // Make A a root; B is reachable through A's dependency edge.
37//! gc.add_root(1);
38//! let stats = gc.collect();
39//! assert_eq!(stats.collected, 0);
40//! assert_eq!(stats.reachable, 2);
41//! ```
42
43use std::collections::{HashMap, HashSet, VecDeque};
44
45// ---------------------------------------------------------------------------
46// TensorRef
47// ---------------------------------------------------------------------------
48
49/// Descriptor for a single tensor allocation tracked by the GC.
50#[derive(Debug, Clone)]
51pub struct TensorRef {
52    /// Unique identifier for this tensor.
53    pub tensor_id: u64,
54    /// Optional human-readable name.
55    pub name: Option<String>,
56    /// Size of the tensor allocation in bytes.
57    pub size_bytes: u64,
58    /// External reference count.  A tensor with `ref_count > 0` is never
59    /// collected even if it is unreachable from the root set.
60    pub ref_count: u32,
61    /// IDs of tensors that this tensor depends on (outgoing dependency edges).
62    pub dependencies: Vec<u64>,
63    /// If `true` the tensor is never collected regardless of reachability or
64    /// reference count.
65    pub pinned: bool,
66}
67
68// ---------------------------------------------------------------------------
69// GcPhase
70// ---------------------------------------------------------------------------
71
72/// The current phase of a mark-and-sweep garbage collection cycle.
73#[derive(Clone, Debug, PartialEq)]
74pub enum GcPhase {
75    /// Phase 1 — seed the reachable set from roots and pinned tensors.
76    MarkRoots,
77    /// Phase 2 — BFS/DFS expansion of the reachable set through dependency edges.
78    Trace,
79    /// Phase 3 — remove tensors absent from the reachable set that have no
80    /// external references.
81    Sweep,
82}
83
84// ---------------------------------------------------------------------------
85// GcStats
86// ---------------------------------------------------------------------------
87
88/// Statistics produced at the end of a collection cycle.
89#[derive(Debug, Clone, Default)]
90pub struct GcStats {
91    /// Total number of tensors registered at the time of collection.
92    pub total_tensors: usize,
93    /// Number of tensors that were reachable from roots or pinned.
94    pub reachable: usize,
95    /// Number of tensors reclaimed during the sweep phase.
96    pub collected: usize,
97    /// Total bytes freed during the sweep phase.
98    pub freed_bytes: u64,
99    /// Number of tensors with `pinned == true`.
100    pub pinned_tensors: usize,
101}
102
103impl GcStats {
104    /// Fraction of registered tensors that were collected.
105    ///
106    /// Returns a value in `[0.0, 1.0]`.  Returns `0.0` when there are no
107    /// registered tensors.
108    pub fn collection_rate(&self) -> f64 {
109        self.collected as f64 / self.total_tensors.max(1) as f64
110    }
111}
112
113// ---------------------------------------------------------------------------
114// TensorGarbageCollector
115// ---------------------------------------------------------------------------
116
117/// Mark-and-sweep garbage collector for tensor allocations.
118///
119/// Tracks a set of [`TensorRef`] descriptors connected by dependency edges,
120/// and periodically reclaims those that are unreachable from the named root
121/// set and carry no external references.
122pub struct TensorGarbageCollector {
123    /// All registered tensor descriptors keyed by `tensor_id`.
124    pub tensors: HashMap<u64, TensorRef>,
125    /// Tensor IDs that are always considered reachable (GC roots).
126    pub roots: Vec<u64>,
127}
128
129impl TensorGarbageCollector {
130    /// Create a new, empty garbage collector.
131    pub fn new() -> Self {
132        Self {
133            tensors: HashMap::new(),
134            roots: Vec::new(),
135        }
136    }
137
138    /// Register a tensor with the collector.
139    ///
140    /// If a tensor with the same `tensor_id` already exists it is replaced.
141    pub fn register(&mut self, tensor: TensorRef) {
142        self.tensors.insert(tensor.tensor_id, tensor);
143    }
144
145    /// Add `tensor_id` to the GC root set.
146    ///
147    /// Roots are always considered reachable regardless of incoming edges.
148    /// Duplicates are silently ignored.
149    pub fn add_root(&mut self, tensor_id: u64) {
150        if !self.roots.contains(&tensor_id) {
151            self.roots.push(tensor_id);
152        }
153    }
154
155    /// Remove `tensor_id` from the GC root set.
156    ///
157    /// Does nothing if the ID is not present.
158    pub fn remove_root(&mut self, tensor_id: u64) {
159        self.roots.retain(|&id| id != tensor_id);
160    }
161
162    /// Mark a registered tensor as pinned so it is never collected.
163    ///
164    /// Does nothing if `tensor_id` is not registered.
165    pub fn pin(&mut self, tensor_id: u64) {
166        if let Some(t) = self.tensors.get_mut(&tensor_id) {
167            t.pinned = true;
168        }
169    }
170
171    /// Increment the external reference count for `tensor_id`.
172    ///
173    /// Does nothing if `tensor_id` is not registered.
174    pub fn add_ref(&mut self, tensor_id: u64) {
175        if let Some(t) = self.tensors.get_mut(&tensor_id) {
176            t.ref_count = t.ref_count.saturating_add(1);
177        }
178    }
179
180    /// Decrement the external reference count for `tensor_id` (saturating at 0).
181    ///
182    /// Does nothing if `tensor_id` is not registered.
183    pub fn remove_ref(&mut self, tensor_id: u64) {
184        if let Some(t) = self.tensors.get_mut(&tensor_id) {
185            t.ref_count = t.ref_count.saturating_sub(1);
186        }
187    }
188
189    /// Compute the set of tensor IDs reachable from roots and pinned tensors
190    /// without mutating collector state.
191    ///
192    /// Reachability is determined by BFS expansion through
193    /// [`TensorRef::dependencies`] edges.
194    pub fn reachable_set(&self) -> Vec<u64> {
195        let mut visited: HashSet<u64> = HashSet::new();
196        let mut queue: VecDeque<u64> = VecDeque::new();
197
198        // Phase 1 — seed from roots.
199        for &id in &self.roots {
200            if self.tensors.contains_key(&id) && visited.insert(id) {
201                queue.push_back(id);
202            }
203        }
204
205        // Seed from pinned tensors.
206        for (id, tensor) in &self.tensors {
207            if tensor.pinned && visited.insert(*id) {
208                queue.push_back(*id);
209            }
210        }
211
212        // Phase 2 — BFS trace through dependency edges.
213        while let Some(current) = queue.pop_front() {
214            if let Some(tensor) = self.tensors.get(&current) {
215                for &dep in &tensor.dependencies {
216                    if self.tensors.contains_key(&dep) && visited.insert(dep) {
217                        queue.push_back(dep);
218                    }
219                }
220            }
221        }
222
223        visited.into_iter().collect()
224    }
225
226    /// Run a complete mark-and-sweep garbage collection cycle.
227    ///
228    /// The collection proceeds through three [`GcPhase`]s:
229    ///
230    /// * [`GcPhase::MarkRoots`] — seed the reachable set from roots and pinned tensors.
231    /// * [`GcPhase::Trace`] — BFS-expand through dependency edges.
232    /// * [`GcPhase::Sweep`] — remove unreachable tensors with `ref_count == 0`.
233    ///
234    /// Returns [`GcStats`] describing the result.
235    pub fn collect(&mut self) -> GcStats {
236        let total_tensors = self.tensors.len();
237        let pinned_tensors = self.tensors.values().filter(|t| t.pinned).count();
238
239        // ----- Phase 1: MarkRoots -----
240        let _phase = GcPhase::MarkRoots;
241        let mut visited: HashSet<u64> = HashSet::new();
242        let mut queue: VecDeque<u64> = VecDeque::new();
243
244        for &id in &self.roots {
245            if self.tensors.contains_key(&id) && visited.insert(id) {
246                queue.push_back(id);
247            }
248        }
249
250        for (id, tensor) in &self.tensors {
251            if tensor.pinned && visited.insert(*id) {
252                queue.push_back(*id);
253            }
254        }
255
256        // ----- Phase 2: Trace -----
257        let _phase = GcPhase::Trace;
258        while let Some(current) = queue.pop_front() {
259            if let Some(tensor) = self.tensors.get(&current) {
260                for &dep in &tensor.dependencies {
261                    if self.tensors.contains_key(&dep) && visited.insert(dep) {
262                        queue.push_back(dep);
263                    }
264                }
265            }
266        }
267
268        let reachable = visited.len();
269
270        // ----- Phase 3: Sweep -----
271        let _phase = GcPhase::Sweep;
272        let mut collected = 0usize;
273        let mut freed_bytes = 0u64;
274
275        // Collect the IDs to remove first to avoid borrow conflicts.
276        let to_remove: Vec<u64> = self
277            .tensors
278            .iter()
279            .filter_map(|(id, tensor)| {
280                if !visited.contains(id) && tensor.ref_count == 0 {
281                    Some(*id)
282                } else {
283                    None
284                }
285            })
286            .collect();
287
288        for id in to_remove {
289            if let Some(tensor) = self.tensors.remove(&id) {
290                freed_bytes += tensor.size_bytes;
291                collected += 1;
292            }
293        }
294
295        GcStats {
296            total_tensors,
297            reachable,
298            collected,
299            freed_bytes,
300            pinned_tensors,
301        }
302    }
303}
304
305impl Default for TensorGarbageCollector {
306    fn default() -> Self {
307        Self::new()
308    }
309}
310
311// ---------------------------------------------------------------------------
312// Tests
313// ---------------------------------------------------------------------------
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    /// Helper to build a simple TensorRef with no dependencies.
320    fn make_tensor(id: u64, size: u64) -> TensorRef {
321        TensorRef {
322            tensor_id: id,
323            name: None,
324            size_bytes: size,
325            ref_count: 0,
326            dependencies: vec![],
327            pinned: false,
328        }
329    }
330
331    // ------------------------------------------------------------------
332    // 1. register
333    // ------------------------------------------------------------------
334
335    #[test]
336    fn test_register_inserts_tensor() {
337        let mut gc = TensorGarbageCollector::new();
338        gc.register(make_tensor(1, 128));
339        assert!(gc.tensors.contains_key(&1));
340    }
341
342    #[test]
343    fn test_register_overwrites_existing() {
344        let mut gc = TensorGarbageCollector::new();
345        gc.register(make_tensor(1, 128));
346        gc.register(TensorRef {
347            tensor_id: 1,
348            name: Some("updated".to_string()),
349            size_bytes: 256,
350            ref_count: 0,
351            dependencies: vec![],
352            pinned: false,
353        });
354        assert_eq!(gc.tensors[&1].size_bytes, 256);
355        assert_eq!(gc.tensors[&1].name.as_deref(), Some("updated"));
356    }
357
358    // ------------------------------------------------------------------
359    // 2. add_root / remove_root
360    // ------------------------------------------------------------------
361
362    #[test]
363    fn test_add_root_appends() {
364        let mut gc = TensorGarbageCollector::new();
365        gc.add_root(42);
366        assert!(gc.roots.contains(&42));
367    }
368
369    #[test]
370    fn test_add_root_no_duplicates() {
371        let mut gc = TensorGarbageCollector::new();
372        gc.add_root(7);
373        gc.add_root(7);
374        assert_eq!(gc.roots.iter().filter(|&&id| id == 7).count(), 1);
375    }
376
377    #[test]
378    fn test_remove_root_removes_entry() {
379        let mut gc = TensorGarbageCollector::new();
380        gc.add_root(5);
381        gc.add_root(6);
382        gc.remove_root(5);
383        assert!(!gc.roots.contains(&5));
384        assert!(gc.roots.contains(&6));
385    }
386
387    #[test]
388    fn test_remove_root_nonexistent_is_noop() {
389        let mut gc = TensorGarbageCollector::new();
390        gc.add_root(3);
391        gc.remove_root(999); // should not panic
392        assert_eq!(gc.roots.len(), 1);
393    }
394
395    // ------------------------------------------------------------------
396    // 3. collect: unreachable tensors are removed
397    // ------------------------------------------------------------------
398
399    #[test]
400    fn test_collect_removes_unreachable() {
401        let mut gc = TensorGarbageCollector::new();
402        gc.register(make_tensor(1, 100)); // root
403        gc.register(make_tensor(2, 200)); // unreachable
404        gc.add_root(1);
405
406        let stats = gc.collect();
407
408        assert_eq!(stats.collected, 1);
409        assert_eq!(stats.freed_bytes, 200);
410        assert!(gc.tensors.contains_key(&1));
411        assert!(!gc.tensors.contains_key(&2));
412    }
413
414    #[test]
415    fn test_collect_keeps_reachable_tensors() {
416        let mut gc = TensorGarbageCollector::new();
417        gc.register(TensorRef {
418            tensor_id: 1,
419            name: None,
420            size_bytes: 50,
421            ref_count: 0,
422            dependencies: vec![2],
423            pinned: false,
424        });
425        gc.register(make_tensor(2, 75));
426        gc.add_root(1);
427
428        let stats = gc.collect();
429        assert_eq!(stats.collected, 0);
430        assert!(gc.tensors.contains_key(&2));
431    }
432
433    // ------------------------------------------------------------------
434    // 4. pinned tensors are never collected
435    // ------------------------------------------------------------------
436
437    #[test]
438    fn test_pinned_never_collected() {
439        let mut gc = TensorGarbageCollector::new();
440        gc.register(TensorRef {
441            tensor_id: 10,
442            name: None,
443            size_bytes: 512,
444            ref_count: 0,
445            dependencies: vec![],
446            pinned: true,
447        });
448        // No roots — tensor 10 is only reachable because it is pinned.
449        let stats = gc.collect();
450        assert_eq!(stats.collected, 0);
451        assert!(gc.tensors.contains_key(&10));
452    }
453
454    #[test]
455    fn test_pin_method_marks_tensor() {
456        let mut gc = TensorGarbageCollector::new();
457        gc.register(make_tensor(99, 1024));
458        assert!(!gc.tensors[&99].pinned);
459        gc.pin(99);
460        assert!(gc.tensors[&99].pinned);
461    }
462
463    #[test]
464    fn test_pin_nonexistent_is_noop() {
465        let mut gc = TensorGarbageCollector::new();
466        gc.pin(1234); // must not panic
467    }
468
469    // ------------------------------------------------------------------
470    // 5. ref_count > 0 prevents collection
471    // ------------------------------------------------------------------
472
473    #[test]
474    fn test_ref_count_prevents_collection() {
475        let mut gc = TensorGarbageCollector::new();
476        gc.register(TensorRef {
477            tensor_id: 5,
478            name: None,
479            size_bytes: 300,
480            ref_count: 1, // externally referenced
481            dependencies: vec![],
482            pinned: false,
483        });
484        // No roots — tensor 5 would be unreachable but ref_count keeps it alive.
485        let stats = gc.collect();
486        assert_eq!(stats.collected, 0);
487        assert!(gc.tensors.contains_key(&5));
488    }
489
490    // ------------------------------------------------------------------
491    // 6. BFS traces multi-level dependency chains
492    // ------------------------------------------------------------------
493
494    #[test]
495    fn test_bfs_traces_multi_level_chain() {
496        // Chain: root(1) → 2 → 3 → 4  plus isolated 5
497        let mut gc = TensorGarbageCollector::new();
498        for (id, dep) in [(1u64, Some(2u64)), (2, Some(3)), (3, Some(4)), (4, None)] {
499            gc.register(TensorRef {
500                tensor_id: id,
501                name: None,
502                size_bytes: id * 10,
503                ref_count: 0,
504                dependencies: dep.into_iter().collect(),
505                pinned: false,
506            });
507        }
508        gc.register(make_tensor(5, 999)); // isolated
509        gc.add_root(1);
510
511        let stats = gc.collect();
512        assert_eq!(stats.reachable, 4);
513        assert_eq!(stats.collected, 1);
514        assert_eq!(stats.freed_bytes, 999);
515        assert!(!gc.tensors.contains_key(&5));
516    }
517
518    // ------------------------------------------------------------------
519    // 7. reachable_set is correct and non-mutating
520    // ------------------------------------------------------------------
521
522    #[test]
523    fn test_reachable_set_correct() {
524        let mut gc = TensorGarbageCollector::new();
525        gc.register(TensorRef {
526            tensor_id: 1,
527            name: None,
528            size_bytes: 10,
529            ref_count: 0,
530            dependencies: vec![2],
531            pinned: false,
532        });
533        gc.register(make_tensor(2, 20));
534        gc.register(make_tensor(3, 30)); // unreachable
535        gc.add_root(1);
536
537        let mut reachable = gc.reachable_set();
538        reachable.sort();
539        assert_eq!(reachable, vec![1, 2]);
540        // State unchanged (tensors still present)
541        assert_eq!(gc.tensors.len(), 3);
542    }
543
544    #[test]
545    fn test_reachable_set_includes_pinned() {
546        let mut gc = TensorGarbageCollector::new();
547        gc.register(make_tensor(1, 10));
548        gc.register(TensorRef {
549            tensor_id: 2,
550            name: None,
551            size_bytes: 20,
552            ref_count: 0,
553            dependencies: vec![],
554            pinned: true,
555        });
556        // No roots; tensor 2 is pinned.
557        let reachable = gc.reachable_set();
558        assert!(reachable.contains(&2));
559        assert!(!reachable.contains(&1));
560    }
561
562    // ------------------------------------------------------------------
563    // 8. GcStats fields and collection_rate
564    // ------------------------------------------------------------------
565
566    #[test]
567    fn test_gc_stats_fields() {
568        let mut gc = TensorGarbageCollector::new();
569        gc.register(make_tensor(1, 100));
570        gc.register(make_tensor(2, 200));
571        gc.register(TensorRef {
572            tensor_id: 3,
573            name: None,
574            size_bytes: 50,
575            ref_count: 0,
576            dependencies: vec![],
577            pinned: true,
578        });
579        gc.add_root(1);
580
581        let stats = gc.collect();
582        assert_eq!(stats.total_tensors, 3);
583        assert_eq!(stats.reachable, 2); // 1 (root) + 3 (pinned)
584        assert_eq!(stats.collected, 1); // tensor 2
585        assert_eq!(stats.freed_bytes, 200);
586        assert_eq!(stats.pinned_tensors, 1);
587    }
588
589    #[test]
590    fn test_collection_rate_correct() {
591        let stats = GcStats {
592            total_tensors: 10,
593            reachable: 6,
594            collected: 4,
595            freed_bytes: 0,
596            pinned_tensors: 0,
597        };
598        let rate = stats.collection_rate();
599        assert!((rate - 0.4).abs() < f64::EPSILON * 10.0);
600    }
601
602    #[test]
603    fn test_collection_rate_zero_total() {
604        let stats = GcStats::default();
605        // Must not divide by zero.
606        assert_eq!(stats.collection_rate(), 0.0);
607    }
608
609    // ------------------------------------------------------------------
610    // 9. add_ref / remove_ref
611    // ------------------------------------------------------------------
612
613    #[test]
614    fn test_add_ref_increments() {
615        let mut gc = TensorGarbageCollector::new();
616        gc.register(make_tensor(1, 64));
617        assert_eq!(gc.tensors[&1].ref_count, 0);
618        gc.add_ref(1);
619        assert_eq!(gc.tensors[&1].ref_count, 1);
620        gc.add_ref(1);
621        assert_eq!(gc.tensors[&1].ref_count, 2);
622    }
623
624    #[test]
625    fn test_remove_ref_decrements_saturating() {
626        let mut gc = TensorGarbageCollector::new();
627        gc.register(make_tensor(1, 64));
628        gc.add_ref(1);
629        gc.remove_ref(1);
630        assert_eq!(gc.tensors[&1].ref_count, 0);
631        // Saturating — should not underflow.
632        gc.remove_ref(1);
633        assert_eq!(gc.tensors[&1].ref_count, 0);
634    }
635
636    #[test]
637    fn test_add_ref_remove_ref_nonexistent_is_noop() {
638        let mut gc = TensorGarbageCollector::new();
639        gc.add_ref(9999); // must not panic
640        gc.remove_ref(9999); // must not panic
641    }
642
643    // ------------------------------------------------------------------
644    // 10. Diamond dependency graph
645    // ------------------------------------------------------------------
646
647    #[test]
648    fn test_diamond_dependency_all_reachable() {
649        // Diamond: root(1) → {2, 3} → 4
650        let mut gc = TensorGarbageCollector::new();
651        gc.register(TensorRef {
652            tensor_id: 1,
653            name: None,
654            size_bytes: 10,
655            ref_count: 0,
656            dependencies: vec![2, 3],
657            pinned: false,
658        });
659        gc.register(TensorRef {
660            tensor_id: 2,
661            name: None,
662            size_bytes: 20,
663            ref_count: 0,
664            dependencies: vec![4],
665            pinned: false,
666        });
667        gc.register(TensorRef {
668            tensor_id: 3,
669            name: None,
670            size_bytes: 30,
671            ref_count: 0,
672            dependencies: vec![4],
673            pinned: false,
674        });
675        gc.register(make_tensor(4, 40));
676        gc.add_root(1);
677
678        let stats = gc.collect();
679        assert_eq!(stats.reachable, 4);
680        assert_eq!(stats.collected, 0);
681    }
682
683    // ------------------------------------------------------------------
684    // 11. Multiple roots
685    // ------------------------------------------------------------------
686
687    #[test]
688    fn test_multiple_roots() {
689        let mut gc = TensorGarbageCollector::new();
690        gc.register(make_tensor(1, 10));
691        gc.register(make_tensor(2, 20));
692        gc.register(make_tensor(3, 30)); // unreachable
693        gc.add_root(1);
694        gc.add_root(2);
695
696        let stats = gc.collect();
697        assert_eq!(stats.reachable, 2);
698        assert_eq!(stats.collected, 1);
699    }
700
701    // ------------------------------------------------------------------
702    // 12. Remove root makes previously reachable tensor collectable
703    // ------------------------------------------------------------------
704
705    #[test]
706    fn test_remove_root_enables_collection() {
707        let mut gc = TensorGarbageCollector::new();
708        gc.register(make_tensor(1, 100));
709        gc.register(make_tensor(2, 200));
710        gc.add_root(1);
711        gc.add_root(2);
712
713        // First collect — both survive.
714        let s1 = gc.collect();
715        assert_eq!(s1.collected, 0);
716
717        // Remove root 2 — tensor 2 is now unreachable.
718        gc.remove_root(2);
719        let s2 = gc.collect();
720        assert_eq!(s2.collected, 1);
721        assert!(!gc.tensors.contains_key(&2));
722    }
723
724    // ------------------------------------------------------------------
725    // 13. Named tensors
726    // ------------------------------------------------------------------
727
728    #[test]
729    fn test_named_tensor_survives_as_root() {
730        let mut gc = TensorGarbageCollector::new();
731        gc.register(TensorRef {
732            tensor_id: 7,
733            name: Some("embedding_weights".to_string()),
734            size_bytes: 4096,
735            ref_count: 0,
736            dependencies: vec![],
737            pinned: false,
738        });
739        gc.add_root(7);
740        let stats = gc.collect();
741        assert_eq!(stats.collected, 0);
742        assert_eq!(gc.tensors[&7].name.as_deref(), Some("embedding_weights"));
743    }
744
745    // ------------------------------------------------------------------
746    // 14. Empty GC collect
747    // ------------------------------------------------------------------
748
749    #[test]
750    fn test_empty_gc_collect_returns_zero_stats() {
751        let mut gc = TensorGarbageCollector::new();
752        let stats = gc.collect();
753        assert_eq!(stats.total_tensors, 0);
754        assert_eq!(stats.reachable, 0);
755        assert_eq!(stats.collected, 0);
756        assert_eq!(stats.freed_bytes, 0);
757        assert_eq!(stats.pinned_tensors, 0);
758    }
759
760    // ------------------------------------------------------------------
761    // 15. GcPhase derives
762    // ------------------------------------------------------------------
763
764    #[test]
765    fn test_gc_phase_clone_debug_partialeq() {
766        let p = GcPhase::Trace;
767        let q = p.clone();
768        assert_eq!(p, q);
769        let r = GcPhase::Sweep;
770        assert_ne!(p, r);
771        let debug_str = format!("{:?}", GcPhase::MarkRoots);
772        assert!(debug_str.contains("MarkRoots"));
773    }
774}