Skip to main content

rustpython_vm/
gc_state.rs

1//! Garbage Collection State and Algorithm
2//!
3//! Generational garbage collection using an intrusive doubly-linked list.
4
5use crate::common::linked_list::LinkedList;
6use crate::common::lock::{PyMutex, PyRwLock};
7use crate::object::{GC_PERMANENT, GC_UNTRACKED, GcLink};
8use crate::{AsObject, PyObject, PyObjectRef};
9use core::ptr::NonNull;
10use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
11use std::collections::HashSet;
12
13#[cfg(not(target_arch = "wasm32"))]
14fn elapsed_secs(start: &std::time::Instant) -> f64 {
15    start.elapsed().as_secs_f64()
16}
17
18#[cfg(target_arch = "wasm32")]
19fn elapsed_secs(_start: &()) -> f64 {
20    0.0
21}
22
23bitflags::bitflags! {
24    /// GC debug flags (see Include/internal/pycore_gc.h)
25    #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
26    pub struct GcDebugFlags: u32 {
27        /// Print collection statistics
28        const STATS         = 1 << 0;
29        /// Print collectable objects
30        const COLLECTABLE   = 1 << 1;
31        /// Print uncollectable objects
32        const UNCOLLECTABLE = 1 << 2;
33        /// Save all garbage in gc.garbage
34        const SAVEALL       = 1 << 5;
35        /// DEBUG_COLLECTABLE | DEBUG_UNCOLLECTABLE | DEBUG_SAVEALL
36        const LEAK = Self::COLLECTABLE.bits() | Self::UNCOLLECTABLE.bits() | Self::SAVEALL.bits();
37    }
38}
39
40/// Result from a single collection run
41#[derive(Debug, Default)]
42pub struct CollectResult {
43    pub collected: usize,
44    pub uncollectable: usize,
45    pub candidates: usize,
46    pub duration: f64,
47}
48
49/// Statistics for a single generation (gc_generation_stats)
50#[derive(Debug, Default)]
51pub struct GcStats {
52    pub collections: usize,
53    pub collected: usize,
54    pub uncollectable: usize,
55    pub candidates: usize,
56    pub duration: f64,
57}
58
59/// A single GC generation with intrusive linked list
60pub struct GcGeneration {
61    /// Number of objects in this generation
62    count: AtomicUsize,
63    /// Threshold for triggering collection
64    threshold: AtomicU32,
65    /// Collection statistics
66    stats: PyMutex<GcStats>,
67}
68
69impl GcGeneration {
70    pub const fn new(threshold: u32) -> Self {
71        Self {
72            count: AtomicUsize::new(0),
73            threshold: AtomicU32::new(threshold),
74            stats: PyMutex::new(GcStats {
75                collections: 0,
76                collected: 0,
77                uncollectable: 0,
78                candidates: 0,
79                duration: 0.0,
80            }),
81        }
82    }
83
84    pub fn count(&self) -> usize {
85        self.count.load(Ordering::SeqCst)
86    }
87
88    pub fn threshold(&self) -> u32 {
89        self.threshold.load(Ordering::SeqCst)
90    }
91
92    pub fn set_threshold(&self, value: u32) {
93        self.threshold.store(value, Ordering::SeqCst);
94    }
95
96    pub fn stats(&self) -> GcStats {
97        let guard = self.stats.lock();
98        GcStats {
99            collections: guard.collections,
100            collected: guard.collected,
101            uncollectable: guard.uncollectable,
102            candidates: guard.candidates,
103            duration: guard.duration,
104        }
105    }
106
107    pub fn update_stats(
108        &self,
109        collected: usize,
110        uncollectable: usize,
111        candidates: usize,
112        duration: f64,
113    ) {
114        let mut guard = self.stats.lock();
115        guard.collections += 1;
116        guard.collected += collected;
117        guard.uncollectable += uncollectable;
118        guard.candidates += candidates;
119        guard.duration += duration;
120    }
121
122    /// Reset the stats mutex to unlocked state after fork().
123    ///
124    /// # Safety
125    /// Must only be called after fork() in the child process when no other
126    /// threads exist.
127    #[cfg(all(unix, feature = "threading"))]
128    unsafe fn reinit_stats_after_fork(&self) {
129        unsafe { crate::common::lock::reinit_mutex_after_fork(&self.stats) };
130    }
131}
132
133/// Wrapper for NonNull<PyObject> to impl Hash/Eq for use in temporary collection sets.
134/// Only used within collect_inner, never shared across threads.
135#[derive(Clone, Copy, PartialEq, Eq, Hash)]
136struct GcPtr(NonNull<PyObject>);
137
138/// Global GC state
139pub struct GcState {
140    /// 3 generations (0 = youngest, 2 = oldest)
141    pub generations: [GcGeneration; 3],
142    /// Permanent generation (frozen objects)
143    pub permanent: GcGeneration,
144    /// GC enabled flag
145    pub enabled: AtomicBool,
146    /// Per-generation intrusive linked lists for object tracking.
147    /// Objects start in gen0, survivors are promoted to gen1, then gen2.
148    generation_lists: [PyRwLock<LinkedList<GcLink, PyObject>>; 3],
149    /// Frozen/permanent objects (excluded from normal GC)
150    permanent_list: PyRwLock<LinkedList<GcLink, PyObject>>,
151    /// Debug flags
152    pub debug: AtomicU32,
153    /// gc.garbage list (uncollectable objects with __del__)
154    pub garbage: PyMutex<Vec<PyObjectRef>>,
155    /// gc.callbacks list
156    pub callbacks: PyMutex<Vec<PyObjectRef>>,
157    /// Mutex for collection (prevents concurrent collections)
158    collecting: PyMutex<()>,
159    /// Allocation counter for gen0
160    alloc_count: AtomicUsize,
161}
162
163// SAFETY: All fields are either inherently Send/Sync (atomics, RwLock, Mutex) or protected by PyMutex.
164// LinkedList<GcLink, PyObject> is Send+Sync because GcLink's Target (PyObject) is Send+Sync.
165#[cfg(feature = "threading")]
166unsafe impl Send for GcState {}
167#[cfg(feature = "threading")]
168unsafe impl Sync for GcState {}
169
170impl Default for GcState {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176impl GcState {
177    pub fn new() -> Self {
178        Self {
179            generations: [
180                GcGeneration::new(2000), // young
181                GcGeneration::new(10),   // old[0]
182                GcGeneration::new(0),    // old[1]
183            ],
184            permanent: GcGeneration::new(0),
185            enabled: AtomicBool::new(true),
186            generation_lists: [
187                PyRwLock::new(LinkedList::new()),
188                PyRwLock::new(LinkedList::new()),
189                PyRwLock::new(LinkedList::new()),
190            ],
191            permanent_list: PyRwLock::new(LinkedList::new()),
192            debug: AtomicU32::new(0),
193            garbage: PyMutex::new(Vec::new()),
194            callbacks: PyMutex::new(Vec::new()),
195            collecting: PyMutex::new(()),
196            alloc_count: AtomicUsize::new(0),
197        }
198    }
199
200    /// Check if GC is enabled
201    pub fn is_enabled(&self) -> bool {
202        self.enabled.load(Ordering::SeqCst)
203    }
204
205    /// Enable GC
206    pub fn enable(&self) {
207        self.enabled.store(true, Ordering::SeqCst);
208    }
209
210    /// Disable GC
211    pub fn disable(&self) {
212        self.enabled.store(false, Ordering::SeqCst);
213    }
214
215    /// Get debug flags
216    pub fn get_debug(&self) -> GcDebugFlags {
217        GcDebugFlags::from_bits_truncate(self.debug.load(Ordering::SeqCst))
218    }
219
220    /// Set debug flags
221    pub fn set_debug(&self, flags: GcDebugFlags) {
222        self.debug.store(flags.bits(), Ordering::SeqCst);
223    }
224
225    /// Get thresholds for all generations
226    pub fn get_threshold(&self) -> (u32, u32, u32) {
227        (
228            self.generations[0].threshold(),
229            self.generations[1].threshold(),
230            self.generations[2].threshold(),
231        )
232    }
233
234    /// Set thresholds
235    pub fn set_threshold(&self, t0: u32, t1: Option<u32>, t2: Option<u32>) {
236        self.generations[0].set_threshold(t0);
237        if let Some(t1) = t1 {
238            self.generations[1].set_threshold(t1);
239        }
240        if let Some(t2) = t2 {
241            self.generations[2].set_threshold(t2);
242        }
243    }
244
245    /// Get counts for all generations
246    pub fn get_count(&self) -> (usize, usize, usize) {
247        (
248            self.generations[0].count(),
249            self.generations[1].count(),
250            self.generations[2].count(),
251        )
252    }
253
254    /// Get statistics for all generations
255    pub fn get_stats(&self) -> [GcStats; 3] {
256        [
257            self.generations[0].stats(),
258            self.generations[1].stats(),
259            self.generations[2].stats(),
260        ]
261    }
262
263    /// Track a new object (add to gen0).
264    /// O(1) — intrusive linked list push_front, no hashing.
265    ///
266    /// # Safety
267    /// obj must be a valid pointer to a PyObject
268    pub unsafe fn track_object(&self, obj: NonNull<PyObject>) {
269        let obj_ref = unsafe { obj.as_ref() };
270        obj_ref.set_gc_tracked();
271        obj_ref.set_gc_generation(0);
272
273        self.generation_lists[0].write().push_front(obj);
274        self.generations[0].count.fetch_add(1, Ordering::SeqCst);
275        self.alloc_count.fetch_add(1, Ordering::SeqCst);
276    }
277
278    /// Untrack an object (remove from GC lists).
279    /// O(1) — intrusive linked list remove by node pointer.
280    ///
281    /// # Safety
282    /// obj must be a valid pointer to a PyObject that is currently tracked.
283    /// The object's memory must still be valid (pointers are read).
284    pub unsafe fn untrack_object(&self, obj: NonNull<PyObject>) {
285        let obj_ref = unsafe { obj.as_ref() };
286
287        loop {
288            let obj_gen = obj_ref.gc_generation();
289
290            let (list_lock, count) = if obj_gen <= 2 {
291                (
292                    &self.generation_lists[obj_gen as usize]
293                        as &PyRwLock<LinkedList<GcLink, PyObject>>,
294                    &self.generations[obj_gen as usize].count,
295                )
296            } else if obj_gen == GC_PERMANENT {
297                (&self.permanent_list, &self.permanent.count)
298            } else {
299                return; // GC_UNTRACKED or unknown — already untracked
300            };
301
302            let mut list = list_lock.write();
303            // Re-check generation under lock (may have changed due to promotion)
304            if obj_ref.gc_generation() != obj_gen {
305                drop(list);
306                continue; // Retry with the updated generation
307            }
308            if unsafe { list.remove(obj) }.is_some() {
309                count.fetch_sub(1, Ordering::SeqCst);
310                obj_ref.clear_gc_tracked();
311                obj_ref.set_gc_generation(GC_UNTRACKED);
312            } else {
313                // Object claims to be in this generation but wasn't found in the list.
314                // This indicates a bug: the object was already removed from the list
315                // without updating gc_generation, or was never inserted.
316                eprintln!(
317                    "GC WARNING: untrack_object failed to remove obj={obj:p} from gen={obj_gen}, \
318                     tracked={}, gc_gen={}",
319                    obj_ref.is_gc_tracked(),
320                    obj_ref.gc_generation()
321                );
322            }
323            return;
324        }
325    }
326
327    /// Get tracked objects (for gc.get_objects)
328    /// If generation is None, returns all tracked objects.
329    /// If generation is Some(n), returns objects in generation n only.
330    pub fn get_objects(&self, generation: Option<i32>) -> Vec<PyObjectRef> {
331        fn collect_from_list(
332            list: &LinkedList<GcLink, PyObject>,
333        ) -> impl Iterator<Item = PyObjectRef> + '_ {
334            list.iter().filter_map(|obj| obj.try_to_owned())
335        }
336
337        match generation {
338            None => {
339                // Return all tracked objects from all generations + permanent
340                let mut result = Vec::new();
341                for gen_list in &self.generation_lists {
342                    result.extend(collect_from_list(&gen_list.read()));
343                }
344                result.extend(collect_from_list(&self.permanent_list.read()));
345                result
346            }
347            Some(g) if (0..=2).contains(&g) => {
348                let guard = self.generation_lists[g as usize].read();
349                collect_from_list(&guard).collect()
350            }
351            _ => Vec::new(),
352        }
353    }
354
355    /// Check if automatic GC should run and run it if needed.
356    /// Called after object allocation.
357    /// Returns true if GC was run, false otherwise.
358    pub fn maybe_collect(&self) -> bool {
359        if !self.is_enabled() {
360            return false;
361        }
362
363        // Check gen0 threshold
364        let count0 = self.generations[0].count.load(Ordering::SeqCst) as u32;
365        let threshold0 = self.generations[0].threshold();
366        if threshold0 > 0 && count0 >= threshold0 {
367            self.collect(0);
368            return true;
369        }
370
371        false
372    }
373
374    /// Perform garbage collection on the given generation
375    pub fn collect(&self, generation: usize) -> CollectResult {
376        self.collect_inner(generation, false)
377    }
378
379    /// Force collection even if GC is disabled (for manual gc.collect() calls)
380    pub fn collect_force(&self, generation: usize) -> CollectResult {
381        self.collect_inner(generation, true)
382    }
383
384    fn collect_inner(&self, generation: usize, force: bool) -> CollectResult {
385        if !force && !self.is_enabled() {
386            return CollectResult::default();
387        }
388
389        // Try to acquire the collecting lock
390        let Some(_guard) = self.collecting.try_lock() else {
391            return CollectResult::default();
392        };
393
394        #[cfg(not(target_arch = "wasm32"))]
395        let start_time = std::time::Instant::now();
396        #[cfg(target_arch = "wasm32")]
397        let start_time = ();
398
399        // Memory barrier to ensure visibility of all reference count updates
400        // from other threads before we start analyzing the object graph.
401        core::sync::atomic::fence(Ordering::SeqCst);
402
403        let generation = generation.min(2);
404        let debug = self.get_debug();
405
406        // Clear the method cache to release strong references that
407        // might prevent cycle collection (_PyType_ClearCache).
408        crate::builtins::type_::type_cache_clear();
409
410        // Step 1: Gather objects from generations 0..=generation
411        // Hold read locks for the entire scan to prevent concurrent modifications.
412        let gen_locks: Vec<_> = (0..=generation)
413            .map(|i| self.generation_lists[i].read())
414            .collect();
415
416        let mut collecting: HashSet<GcPtr> = HashSet::new();
417        for gen_list in &gen_locks {
418            for obj in gen_list.iter() {
419                if obj.strong_count() > 0 {
420                    collecting.insert(GcPtr(NonNull::from(obj)));
421                }
422            }
423        }
424
425        if collecting.is_empty() {
426            // Reset counts for generations whose objects were promoted away.
427            // For gen2 (oldest), survivors stay in-place so don't reset gen2 count.
428            let reset_end = if generation >= 2 { 2 } else { generation + 1 };
429            for i in 0..reset_end {
430                self.generations[i].count.store(0, Ordering::SeqCst);
431            }
432            let duration = elapsed_secs(&start_time);
433            self.generations[generation].update_stats(0, 0, 0, duration);
434            return CollectResult {
435                collected: 0,
436                uncollectable: 0,
437                candidates: 0,
438                duration,
439            };
440        }
441
442        let candidates = collecting.len();
443
444        if debug.contains(GcDebugFlags::STATS) {
445            eprintln!(
446                "gc: collecting {} objects from generations 0..={}",
447                collecting.len(),
448                generation
449            );
450        }
451
452        // Step 2: Build gc_refs map (copy reference counts)
453        let mut gc_refs: std::collections::HashMap<GcPtr, usize> = std::collections::HashMap::new();
454        for &ptr in &collecting {
455            let obj = unsafe { ptr.0.as_ref() };
456            gc_refs.insert(ptr, obj.strong_count());
457        }
458
459        // Step 3: Subtract internal references
460        // Pre-compute referent pointers once per object so that both step 3
461        // (subtract refs) and step 4 (BFS reachability) see the same snapshot
462        // of each object's children. Without this, a dict whose write lock is
463        // held during one traversal but not the other can yield inconsistent
464        // results, causing live objects to be incorrectly collected.
465        let mut referents_map: std::collections::HashMap<GcPtr, Vec<NonNull<PyObject>>> =
466            std::collections::HashMap::new();
467        for &ptr in &collecting {
468            let obj = unsafe { ptr.0.as_ref() };
469            if obj.strong_count() == 0 {
470                continue;
471            }
472            let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
473            referents_map.insert(ptr, referent_ptrs.clone());
474            for child_ptr in referent_ptrs {
475                let gc_ptr = GcPtr(child_ptr);
476                if collecting.contains(&gc_ptr)
477                    && let Some(refs) = gc_refs.get_mut(&gc_ptr)
478                {
479                    *refs = refs.saturating_sub(1);
480                }
481            }
482        }
483
484        // Step 4: Find reachable objects (gc_refs > 0) and traverse from them
485        let mut reachable: HashSet<GcPtr> = HashSet::new();
486        let mut worklist: Vec<GcPtr> = Vec::new();
487
488        for (&ptr, &refs) in &gc_refs {
489            if refs > 0 {
490                reachable.insert(ptr);
491                worklist.push(ptr);
492            }
493        }
494
495        while let Some(ptr) = worklist.pop() {
496            let obj = unsafe { ptr.0.as_ref() };
497            if obj.is_gc_tracked() {
498                // Reuse the pre-computed referent pointers from step 3.
499                // For objects that were skipped in step 3 (strong_count was 0),
500                // compute them now as a fallback.
501                let referent_ptrs = referents_map
502                    .get(&ptr)
503                    .cloned()
504                    .unwrap_or_else(|| unsafe { obj.gc_get_referent_ptrs() });
505                for child_ptr in referent_ptrs {
506                    let gc_ptr = GcPtr(child_ptr);
507                    if collecting.contains(&gc_ptr) && reachable.insert(gc_ptr) {
508                        worklist.push(gc_ptr);
509                    }
510                }
511            }
512        }
513
514        // Step 5: Find unreachable objects
515        let unreachable: Vec<GcPtr> = collecting.difference(&reachable).copied().collect();
516
517        if debug.contains(GcDebugFlags::STATS) {
518            eprintln!(
519                "gc: {} reachable, {} unreachable",
520                reachable.len(),
521                unreachable.len()
522            );
523        }
524
525        // Create strong references while read locks are still held.
526        // After dropping gen_locks, other threads can untrack+free objects,
527        // making the raw pointers in `reachable`/`unreachable` dangling.
528        // Strong refs keep objects alive for later phases.
529        //
530        // Use try_to_owned() (CAS-based) instead of strong_count()+to_owned()
531        // to prevent a TOCTOU race: another thread can dec() the count to 0
532        // between the check and the increment, causing a use-after-free when
533        // the destroying thread eventually frees the memory.
534        let survivor_refs: Vec<PyObjectRef> = reachable
535            .iter()
536            .filter_map(|ptr| {
537                let obj = unsafe { ptr.0.as_ref() };
538                obj.try_to_owned()
539            })
540            .collect();
541
542        let unreachable_refs: Vec<crate::PyObjectRef> = unreachable
543            .iter()
544            .filter_map(|ptr| {
545                let obj = unsafe { ptr.0.as_ref() };
546                obj.try_to_owned()
547            })
548            .collect();
549
550        if unreachable.is_empty() {
551            drop(gen_locks);
552            self.promote_survivors(generation, &survivor_refs);
553            let reset_end = if generation >= 2 { 2 } else { generation + 1 };
554            for i in 0..reset_end {
555                self.generations[i].count.store(0, Ordering::SeqCst);
556            }
557            let duration = elapsed_secs(&start_time);
558            self.generations[generation].update_stats(0, 0, candidates, duration);
559            return CollectResult {
560                collected: 0,
561                uncollectable: 0,
562                candidates,
563                duration,
564            };
565        }
566
567        // Release read locks before finalization phase.
568        drop(gen_locks);
569
570        // Step 6: Finalize unreachable objects and handle resurrection
571
572        if unreachable_refs.is_empty() {
573            self.promote_survivors(generation, &survivor_refs);
574            let reset_end = if generation >= 2 { 2 } else { generation + 1 };
575            for i in 0..reset_end {
576                self.generations[i].count.store(0, Ordering::SeqCst);
577            }
578            let duration = elapsed_secs(&start_time);
579            self.generations[generation].update_stats(0, 0, candidates, duration);
580            return CollectResult {
581                collected: 0,
582                uncollectable: 0,
583                candidates,
584                duration,
585            };
586        }
587
588        // 6b: Record initial strong counts (for resurrection detection)
589        let initial_counts: std::collections::HashMap<GcPtr, usize> = unreachable_refs
590            .iter()
591            .map(|obj| {
592                let ptr = GcPtr(core::ptr::NonNull::from(obj.as_ref()));
593                (ptr, obj.strong_count())
594            })
595            .collect();
596
597        // 6c: Clear existing weakrefs BEFORE calling __del__
598        let mut all_callbacks: Vec<(crate::PyRef<crate::object::PyWeak>, crate::PyObjectRef)> =
599            Vec::new();
600        for obj_ref in &unreachable_refs {
601            let callbacks = obj_ref.gc_clear_weakrefs_collect_callbacks();
602            all_callbacks.extend(callbacks);
603        }
604        for (wr, cb) in all_callbacks {
605            if let Some(Err(e)) = crate::vm::thread::with_vm(&cb, |vm| cb.call((wr.clone(),), vm)) {
606                crate::vm::thread::with_vm(&cb, |vm| {
607                    vm.run_unraisable(e.clone(), Some("weakref callback".to_owned()), cb.clone());
608                });
609            }
610        }
611
612        // 6d: Call __del__ on unreachable objects (skip already-finalized).
613        // try_call_finalizer() internally checks gc_finalized() and sets it,
614        // so we must NOT set it beforehand.
615        for obj_ref in &unreachable_refs {
616            obj_ref.try_call_finalizer();
617        }
618
619        // Detect resurrection
620        let mut resurrected_set: HashSet<GcPtr> = HashSet::new();
621        let unreachable_set: HashSet<GcPtr> = unreachable.iter().copied().collect();
622
623        for obj in &unreachable_refs {
624            let ptr = GcPtr(core::ptr::NonNull::from(obj.as_ref()));
625            let initial = initial_counts.get(&ptr).copied().unwrap_or(1);
626            if obj.strong_count() > initial {
627                resurrected_set.insert(ptr);
628            }
629        }
630
631        // Transitive resurrection
632        let mut worklist: Vec<GcPtr> = resurrected_set.iter().copied().collect();
633        while let Some(ptr) = worklist.pop() {
634            let obj = unsafe { ptr.0.as_ref() };
635            let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
636            for child_ptr in referent_ptrs {
637                let child_gc_ptr = GcPtr(child_ptr);
638                if unreachable_set.contains(&child_gc_ptr) && resurrected_set.insert(child_gc_ptr) {
639                    worklist.push(child_gc_ptr);
640                }
641            }
642        }
643
644        // Partition into resurrected and truly dead
645        let (resurrected, truly_dead): (Vec<_>, Vec<_>) =
646            unreachable_refs.into_iter().partition(|obj| {
647                let ptr = GcPtr(core::ptr::NonNull::from(obj.as_ref()));
648                resurrected_set.contains(&ptr)
649            });
650
651        if debug.contains(GcDebugFlags::STATS) {
652            eprintln!(
653                "gc: {} resurrected, {} truly dead",
654                resurrected.len(),
655                truly_dead.len()
656            );
657        }
658
659        // Compute collected count (exclude instance dicts in truly_dead)
660        let collected = {
661            let dead_ptrs: HashSet<usize> = truly_dead
662                .iter()
663                .map(|obj| obj.as_ref() as *const PyObject as usize)
664                .collect();
665            let instance_dict_count = truly_dead
666                .iter()
667                .filter(|obj| {
668                    if let Some(dict_ref) = obj.dict() {
669                        dead_ptrs.contains(&(dict_ref.as_object() as *const PyObject as usize))
670                    } else {
671                        false
672                    }
673                })
674                .count();
675            truly_dead.len() - instance_dict_count
676        };
677
678        // Promote survivors to next generation BEFORE tp_clear.
679        // move_legacy_finalizer_reachable → delete_garbage order ensures
680        // survivor_refs are dropped before tp_clear, so reachable objects
681        // aren't kept alive beyond the deferred-drop phase.
682        self.promote_survivors(generation, &survivor_refs);
683        drop(survivor_refs);
684
685        // Resurrected objects stay tracked — just drop our references
686        drop(resurrected);
687
688        if debug.contains(GcDebugFlags::COLLECTABLE) {
689            for obj in &truly_dead {
690                eprintln!(
691                    "gc: collectable <{} {:p}>",
692                    obj.class().name(),
693                    obj.as_ref()
694                );
695            }
696        }
697
698        if debug.contains(GcDebugFlags::SAVEALL) {
699            let mut garbage_guard = self.garbage.lock();
700            for obj_ref in truly_dead.iter() {
701                garbage_guard.push(obj_ref.clone());
702            }
703        }
704
705        if !truly_dead.is_empty() {
706            // Break cycles by clearing references (tp_clear)
707            // Use deferred drop context to prevent stack overflow.
708            rustpython_common::refcount::with_deferred_drops(|| {
709                for obj_ref in truly_dead.iter() {
710                    if obj_ref.gc_has_clear() {
711                        let edges = unsafe { obj_ref.gc_clear() };
712                        drop(edges);
713                    }
714                }
715                drop(truly_dead);
716            });
717        }
718
719        // Reset counts for generations whose objects were promoted away.
720        // For gen2 (oldest), survivors stay in-place so don't reset gen2 count.
721        let reset_end = if generation >= 2 { 2 } else { generation + 1 };
722        for i in 0..reset_end {
723            self.generations[i].count.store(0, Ordering::SeqCst);
724        }
725
726        let duration = elapsed_secs(&start_time);
727        self.generations[generation].update_stats(collected, 0, candidates, duration);
728
729        CollectResult {
730            collected,
731            uncollectable: 0,
732            candidates,
733            duration,
734        }
735    }
736
737    /// Promote surviving objects to the next generation.
738    ///
739    /// `survivors` must be strong references (`PyObjectRef`) to keep objects alive,
740    /// since the generation read locks are released before this is called.
741    ///
742    /// Holds both source and destination list locks simultaneously to prevent
743    /// a race where concurrent `untrack_object` reads a stale `gc_generation`
744    /// and operates on the wrong list.
745    fn promote_survivors(&self, from_gen: usize, survivors: &[PyObjectRef]) {
746        if from_gen >= 2 {
747            return; // Already in oldest generation
748        }
749
750        let next_gen = from_gen + 1;
751
752        for obj_ref in survivors {
753            let obj = obj_ref.as_ref();
754            let ptr = NonNull::from(obj);
755            let obj_gen = obj.gc_generation();
756            if obj_gen as usize <= from_gen && obj_gen <= 2 {
757                let src_gen = obj_gen as usize;
758
759                // Lock both source and destination lists simultaneously.
760                // Always ascending order (src_gen < next_gen) → no deadlock.
761                let mut src = self.generation_lists[src_gen].write();
762                let mut dst = self.generation_lists[next_gen].write();
763
764                // Re-check under locks: object might have been untracked concurrently
765                if obj.gc_generation() != obj_gen || !obj.is_gc_tracked() {
766                    continue;
767                }
768
769                if unsafe { src.remove(ptr) }.is_some() {
770                    self.generations[src_gen]
771                        .count
772                        .fetch_sub(1, Ordering::SeqCst);
773
774                    dst.push_front(ptr);
775                    self.generations[next_gen]
776                        .count
777                        .fetch_add(1, Ordering::SeqCst);
778
779                    obj.set_gc_generation(next_gen as u8);
780                }
781            }
782        }
783    }
784
785    /// Get count of frozen objects
786    pub fn get_freeze_count(&self) -> usize {
787        self.permanent.count()
788    }
789
790    /// Freeze all tracked objects (move to permanent generation).
791    /// Lock order: generation_lists[i] → permanent_list (consistent with unfreeze).
792    pub fn freeze(&self) {
793        let mut count = 0usize;
794
795        for (gen_idx, gen_list) in self.generation_lists.iter().enumerate() {
796            let mut list = gen_list.write();
797            let mut perm = self.permanent_list.write();
798            while let Some(ptr) = list.pop_front() {
799                perm.push_front(ptr);
800                unsafe { ptr.as_ref().set_gc_generation(GC_PERMANENT) };
801                count += 1;
802            }
803            self.generations[gen_idx].count.store(0, Ordering::SeqCst);
804        }
805
806        self.permanent.count.fetch_add(count, Ordering::SeqCst);
807    }
808
809    /// Unfreeze all objects (move from permanent to gen2).
810    /// Lock order: generation_lists[2] → permanent_list (consistent with freeze).
811    pub fn unfreeze(&self) {
812        let mut count = 0usize;
813
814        {
815            let mut gen2 = self.generation_lists[2].write();
816            let mut perm_list = self.permanent_list.write();
817            while let Some(ptr) = perm_list.pop_front() {
818                gen2.push_front(ptr);
819                unsafe { ptr.as_ref().set_gc_generation(2) };
820                count += 1;
821            }
822            self.permanent.count.store(0, Ordering::SeqCst);
823        }
824
825        self.generations[2].count.fetch_add(count, Ordering::SeqCst);
826    }
827
828    /// Reset all locks to unlocked state after fork().
829    ///
830    /// After fork(), only the forking thread survives. Any lock held by another
831    /// thread is permanently stuck. This resets them by zeroing the raw bytes.
832    ///
833    /// # Safety
834    /// Must only be called after fork() in the child process when no other
835    /// threads exist. The calling thread must NOT hold any of these locks.
836    #[cfg(all(unix, feature = "threading"))]
837    pub unsafe fn reinit_after_fork(&self) {
838        use crate::common::lock::{reinit_mutex_after_fork, reinit_rwlock_after_fork};
839
840        unsafe {
841            reinit_mutex_after_fork(&self.collecting);
842            reinit_mutex_after_fork(&self.garbage);
843            reinit_mutex_after_fork(&self.callbacks);
844
845            for generation in &self.generations {
846                generation.reinit_stats_after_fork();
847            }
848            self.permanent.reinit_stats_after_fork();
849
850            for rw in &self.generation_lists {
851                reinit_rwlock_after_fork(rw);
852            }
853            reinit_rwlock_after_fork(&self.permanent_list);
854        }
855    }
856}
857
858/// Get a reference to the GC state.
859///
860/// In threading mode this is a true global (OnceLock).
861/// In non-threading mode this is thread-local, because PyRwLock/PyMutex
862/// use Cell-based locks that are not Sync.
863pub fn gc_state() -> &'static GcState {
864    rustpython_common::static_cell! {
865        static GC_STATE: GcState;
866    }
867    GC_STATE.get_or_init(GcState::new)
868}
869
870#[cfg(test)]
871mod tests {
872    use super::*;
873
874    #[test]
875    fn test_gc_state_default() {
876        let state = GcState::new();
877        assert!(state.is_enabled());
878        assert_eq!(state.get_debug(), GcDebugFlags::empty());
879        assert_eq!(state.get_threshold(), (2000, 10, 0));
880        assert_eq!(state.get_count(), (0, 0, 0));
881    }
882
883    #[test]
884    fn test_gc_enable_disable() {
885        let state = GcState::new();
886        assert!(state.is_enabled());
887        state.disable();
888        assert!(!state.is_enabled());
889        state.enable();
890        assert!(state.is_enabled());
891    }
892
893    #[test]
894    fn test_gc_threshold() {
895        let state = GcState::new();
896        state.set_threshold(100, Some(20), Some(30));
897        assert_eq!(state.get_threshold(), (100, 20, 30));
898    }
899
900    #[test]
901    fn test_gc_debug_flags() {
902        let state = GcState::new();
903        state.set_debug(GcDebugFlags::STATS | GcDebugFlags::COLLECTABLE);
904        assert_eq!(
905            state.get_debug(),
906            GcDebugFlags::STATS | GcDebugFlags::COLLECTABLE
907        );
908    }
909}