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_NO_OWNER, GC_PERMANENT, GC_REACHABLE, GC_UNTRACKED, GcLink, GcOwner};
8use crate::{AsObject, PyObject, PyObjectRef};
9use core::ptr::NonNull;
10use core::sync::atomic::{AtomicBool, AtomicU16, AtomicU32, AtomicUsize, Ordering};
11
12fn elapsed_secs(
13    #[cfg(target_arch = "wasm32")] _start: (),
14    #[cfg(not(target_arch = "wasm32"))] start: std::time::Instant,
15) -> f64 {
16    cfg_select! {
17        target_arch = "wasm32" => 0.0,
18        _ => start.elapsed().as_secs_f64(),
19    }
20}
21
22bitflags::bitflags! {
23    /// GC debug flags (see Include/internal/pycore_gc.h)
24    #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
25    pub struct GcDebugFlags: u32 {
26        /// Print collection statistics
27        const STATS         = 1 << 0;
28        /// Print collectable objects
29        const COLLECTABLE   = 1 << 1;
30        /// Print uncollectable objects
31        const UNCOLLECTABLE = 1 << 2;
32        /// Save all garbage in gc.garbage
33        const SAVEALL       = 1 << 5;
34        /// DEBUG_COLLECTABLE | DEBUG_UNCOLLECTABLE | DEBUG_SAVEALL
35        const LEAK = Self::COLLECTABLE.bits() | Self::UNCOLLECTABLE.bits() | Self::SAVEALL.bits();
36    }
37}
38
39/// Result from a single collection run
40#[derive(Clone, Copy, Debug, Default)]
41pub struct CollectResult {
42    pub collected: usize,
43    pub uncollectable: usize,
44    pub candidates: usize,
45    pub duration: f64,
46}
47
48/// Statistics for a single generation (gc_generation_stats)
49#[derive(Clone, Copy, Debug, Default)]
50pub struct GcStats {
51    pub collections: usize,
52    pub collected: usize,
53    pub uncollectable: usize,
54    pub candidates: usize,
55    pub duration: f64,
56}
57
58/// One generation's collection policy and statistics, per interpreter.
59///
60/// The objects themselves live in the process-wide lists on [`GcState`], so the
61/// occupancy count sits there; what an interpreter owns is when to collect and
62/// what its own collections have done.
63pub struct GcGeneration {
64    /// Threshold for triggering collection
65    threshold: AtomicU32,
66    /// Collection statistics
67    stats: PyMutex<GcStats>,
68}
69
70impl GcGeneration {
71    #[must_use]
72    pub const fn new(threshold: u32) -> Self {
73        Self {
74            threshold: AtomicU32::new(threshold),
75            stats: PyMutex::new(GcStats {
76                collections: 0,
77                collected: 0,
78                uncollectable: 0,
79                candidates: 0,
80                duration: 0.0,
81            }),
82        }
83    }
84
85    /// Relaxed: this is policy read once per allocation, and a collection
86    /// racing `gc.set_threshold()` may use either value.
87    pub fn threshold(&self) -> u32 {
88        self.threshold.load(Ordering::Relaxed)
89    }
90
91    pub fn set_threshold(&self, value: u32) {
92        self.threshold.store(value, Ordering::Relaxed);
93    }
94
95    pub fn stats(&self) -> GcStats {
96        let guard = self.stats.lock();
97        GcStats {
98            collections: guard.collections,
99            collected: guard.collected,
100            uncollectable: guard.uncollectable,
101            candidates: guard.candidates,
102            duration: guard.duration,
103        }
104    }
105
106    pub fn update_stats(
107        &self,
108        collected: usize,
109        uncollectable: usize,
110        candidates: usize,
111        duration: f64,
112    ) {
113        let mut guard = self.stats.lock();
114        guard.collections += 1;
115        guard.collected += collected;
116        guard.uncollectable += uncollectable;
117        guard.candidates += candidates;
118        guard.duration += duration;
119    }
120
121    /// Reset the stats mutex to unlocked state after fork().
122    ///
123    /// # Safety
124    /// Must only be called after fork() in the child process when no other
125    /// threads exist.
126    #[cfg(all(unix, feature = "threading"))]
127    unsafe fn reinit_stats_after_fork(&self) {
128        unsafe { crate::common::lock::reinit_mutex_after_fork(&self.stats) };
129    }
130}
131
132/// Drop one from a generation's occupancy.
133///
134/// A collection resets the counts of the generations it emptied, but it only
135/// empties its own interpreter's objects; another interpreter's stay behind with
136/// the count already zeroed, and untracking one of those must not wrap.
137fn release_count(count: &AtomicUsize) {
138    let _ = count.try_update(Ordering::Relaxed, Ordering::Relaxed, |n| n.checked_sub(1));
139}
140
141/// Whether `owner`'s collections act on `obj`.
142///
143/// Objects with no owner — everything the shared context allocates, and anything
144/// allocated with no interpreter current — belong to all of them.
145fn is_owned_by(obj: &PyObject, owner: GcOwner) -> bool {
146    let obj_owner = obj.gc_owner();
147    obj_owner == owner || obj_owner == GC_NO_OWNER
148}
149
150/// Wrapper for NonNull<PyObject> to impl Hash/Eq for use in temporary collection sets.
151/// Only used within collect_inner, never shared across threads.
152#[derive(Clone, Copy, PartialEq, Eq, Hash)]
153struct GcPtr(NonNull<PyObject>);
154
155/// Hashing for the tables a collection keys by an object's address.
156///
157/// The default hasher is SipHash, which buys resistance against a caller
158/// choosing keys that collide. Nothing chooses these keys: they are addresses
159/// this process handed out, and the tables live and die inside one collection.
160/// What a collection needs from them is speed -- it hashes every tracked
161/// object -- so this runs the address through a handful of multiplies and
162/// shifts instead. The shifts are what earns the speed: a table picks its
163/// bucket from the low bits, and an address arrives with its low bits zeroed
164/// by alignment, so entropy has to be carried downward or every object lands
165/// in the same few buckets.
166#[derive(Default)]
167struct GcPtrHasher(u64);
168
169impl core::hash::Hasher for GcPtrHasher {
170    fn finish(&self) -> u64 {
171        self.0
172    }
173
174    fn write_usize(&mut self, value: usize) {
175        let mut z = (value as u64).wrapping_add(0x9E37_79B9_7F4A_7C15);
176        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
177        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
178        self.0 = z ^ (z >> 31);
179    }
180
181    fn write(&mut self, bytes: &[u8]) {
182        // Addresses reach this hasher through `write_usize`; a key hashed any
183        // other way still has to land somewhere sensible.
184        for &byte in bytes {
185            self.0 = (self.0 ^ u64::from(byte)).wrapping_mul(0x0100_0000_01B3);
186        }
187    }
188}
189
190type GcBuildHasher = core::hash::BuildHasherDefault<GcPtrHasher>;
191type GcSet<T> = std::collections::HashSet<T, GcBuildHasher>;
192type GcMap<K, V> = std::collections::HashMap<K, V, GcBuildHasher>;
193
194/// RAII barrier that parks every other thread for the pointer-reading phases
195/// of a collection and lets them run again before finalizers execute.
196///
197/// Reference subtraction, the reachability walk and the strong-reference
198/// snapshot dereference the interpreter state of every tracked object,
199/// including the `localsplus` of frames that other threads are actively
200/// executing. Those writes carry no synchronization, so the reads are only
201/// well-defined while all other threads are parked at a safepoint. Restarting
202/// happens explicitly once the snapshot has pinned every object; `Drop` is a
203/// backstop that also restarts on the early-return paths.
204///
205/// A collection acts on one interpreter's objects, but its candidates include
206/// the ones no interpreter owns, which every interpreter can reference and so
207/// incref. Reading a refcount that another interpreter is changing is what
208/// makes an object look unreachable when it is not, so every live interpreter
209/// is stopped, not just the collecting one. Stopping in `runtime` id order
210/// keeps exclusion acquisition ordered; the `collecting` mutex additionally
211/// serializes collections process-wide, so no second collector can take these
212/// exclusions in another order.
213#[cfg(feature = "threading")]
214struct CollectStopTheWorld {
215    /// Stopped interpreter states, in stop order. Held as strong references so
216    /// an interpreter cannot be dropped between stop and restart, and kept past
217    /// the restart so that releasing the last one — which frees that
218    /// interpreter's objects, and so removes them from these lists — happens
219    /// after the collection has let go of the generation locks.
220    stopped: Vec<crate::common::rc::PyRc<crate::vm::PyGlobalState>>,
221    /// Keeps interpreters from registering between the snapshot below and the
222    /// restart. One registered in that window would be missing from `stopped`,
223    /// so its bootstrap would keep running — and mutating the shared generation
224    /// lists — while this collection reads them.
225    admission: Option<parking_lot::MutexGuard<'static, ()>>,
226    restarted: bool,
227}
228
229#[cfg(feature = "threading")]
230impl CollectStopTheWorld {
231    /// Request stop-the-world on every live interpreter when the current thread
232    /// has an attached VM. Falls back to no barrier when no VM is attached (the
233    /// tracked-object reads then run without other threads only if the caller
234    /// guarantees it).
235    fn new() -> Self {
236        // No attached VM means no interpreter is running Python on this thread;
237        // keep the historical no-barrier fallback.
238        if !crate::vm::thread::current_vm_is_set() {
239            return Self {
240                stopped: Vec::new(),
241                admission: None,
242                restarted: true,
243            };
244        }
245
246        // Accumulate into a live `Self` rather than a bare Vec: if a later
247        // `stop_the_world` unwinds, dropping this guard restarts the
248        // interpreters already stopped, instead of leaving their threads parked
249        // and their exclusion held forever.
250        let mut guard = Self {
251            stopped: Vec::new(),
252            admission: Some(crate::vm::runtime::lock_admission_for_stop()),
253            restarted: false,
254        };
255        for state in crate::vm::runtime::live_interpreter_states() {
256            state.stop_the_world.stop_the_world(&state);
257            guard.stopped.push(state);
258        }
259        guard
260    }
261
262    /// Restart the world. Idempotent.
263    fn restart(&mut self) {
264        if self.restarted {
265            return;
266        }
267        self.restarted = true;
268        // Reverse of the stop order. The references stay until this guard is
269        // dropped; see the field comment.
270        for state in self.stopped.iter().rev() {
271            state.stop_the_world.start_the_world(state);
272        }
273        // Nothing is parked any more, so registration may resume.
274        self.admission = None;
275    }
276
277    /// Whether this collection actually stopped the world.
278    #[cfg(all(unix, debug_assertions))]
279    fn is_stopped(&self) -> bool {
280        !self.stopped.is_empty()
281    }
282}
283
284#[cfg(feature = "threading")]
285impl Drop for CollectStopTheWorld {
286    fn drop(&mut self) {
287        self.restart();
288    }
289}
290
291/// The process-wide object lists every interpreter's collections walk.
292///
293/// Interpreter-owned policy and results live in [`GcInterpreterState`]; what is
294/// here is shared because the lists are: an object is untracked from
295/// `default_dealloc`, where no interpreter is in scope, so it has to be findable
296/// without one.
297pub struct GcState {
298    /// Per-generation intrusive linked lists for object tracking.
299    /// Objects start in gen0, survivors are promoted to gen1, then gen2.
300    generation_lists: [PyRwLock<LinkedList<GcLink>>; 3],
301    /// Frozen/permanent objects (excluded from normal GC)
302    permanent_list: PyRwLock<LinkedList<GcLink>>,
303    /// Number of tracked objects per generation, across all interpreters.
304    ///
305    /// Advisory: they drive the collection threshold and `gc.get_count()`, and
306    /// the generation locks — not these counters — order the list changes they
307    /// describe. Every access is therefore relaxed, which keeps the tracking and
308    /// untracking of every object off the barrier path.
309    counts: [AtomicUsize; 3],
310    /// Number of frozen objects. Advisory, like `counts`.
311    permanent_count: AtomicUsize,
312    /// Mutex for collection (prevents concurrent collections)
313    collecting: PyMutex<()>,
314    /// Next `gc_owner` tag to hand to an interpreter.
315    next_owner: AtomicU16,
316    /// Tags of interpreters that are gone. Their objects outlived them, so a
317    /// collection adopts them — tags them `GC_NO_OWNER` again — as it walks,
318    /// rather than leaving them for a collector that will never come.
319    retired: PyMutex<Vec<GcOwner>>,
320}
321
322// SAFETY: All fields are either inherently Send/Sync (atomics, RwLock, Mutex) or protected by PyMutex.
323// LinkedList<GcLink> is Send+Sync because GcLink's Target (PyObject) is Send+Sync.
324#[cfg(feature = "threading")]
325unsafe impl Send for GcState {}
326#[cfg(feature = "threading")]
327unsafe impl Sync for GcState {}
328
329impl Default for GcState {
330    fn default() -> Self {
331        Self::new()
332    }
333}
334
335impl GcState {
336    #[must_use]
337    pub const fn new() -> Self {
338        Self {
339            generation_lists: [
340                PyRwLock::new(LinkedList::new()),
341                PyRwLock::new(LinkedList::new()),
342                PyRwLock::new(LinkedList::new()),
343            ],
344            permanent_list: PyRwLock::new(LinkedList::new()),
345            counts: [
346                AtomicUsize::new(0),
347                AtomicUsize::new(0),
348                AtomicUsize::new(0),
349            ],
350            permanent_count: AtomicUsize::new(0),
351            collecting: PyMutex::new(()),
352            next_owner: AtomicU16::new(GC_NO_OWNER + 1),
353            retired: PyMutex::new(Vec::new()),
354        }
355    }
356
357    /// Reserve a tag for a new interpreter. Tags are never reused; exhausting
358    /// the tag space falls back to `GC_NO_OWNER`, which costs isolation but
359    /// stays correct, rather than aliasing a live interpreter.
360    fn alloc_owner(&self) -> GcOwner {
361        self.next_owner
362            .try_update(Ordering::Relaxed, Ordering::Relaxed, |next| {
363                next.checked_add(1)
364            })
365            .unwrap_or(GC_NO_OWNER)
366    }
367
368    /// Record that `owner`'s interpreter is gone, so the next collection adopts
369    /// whatever it left behind. Retagging the objects here would mean walking
370    /// every list under an interpreter drop, which happens while a collection
371    /// holds the collecting lock.
372    fn retire_owner(&self, owner: GcOwner) {
373        if owner == GC_NO_OWNER {
374            return;
375        }
376        self.retired.lock().push(owner);
377    }
378
379    /// Get counts for all generations. Tracked objects are shared, so these are
380    /// process-wide even though the thresholds they are compared against are
381    /// per interpreter.
382    pub fn get_count(&self) -> (usize, usize, usize) {
383        (
384            self.counts[0].load(Ordering::Relaxed),
385            self.counts[1].load(Ordering::Relaxed),
386            self.counts[2].load(Ordering::Relaxed),
387        )
388    }
389
390    /// Track a new object (add to gen0) as owned by `owner`.
391    /// O(1) — intrusive linked list push_front, no hashing.
392    ///
393    /// # Safety
394    /// obj must be a valid pointer to a PyObject
395    pub unsafe fn track_object(&self, obj: NonNull<PyObject>, owner: GcOwner) {
396        let obj_ref = unsafe { obj.as_ref() };
397        obj_ref.set_gc_tracked();
398        obj_ref.set_gc_generation(0);
399        obj_ref.set_gc_owner(owner);
400
401        self.generation_lists[0].write().push_front(obj);
402        self.counts[0].fetch_add(1, Ordering::Relaxed);
403    }
404
405    /// Track a freshly allocated object (add to gen0) as owned by `owner`.
406    ///
407    /// Like [`Self::track_object`], but for the hot allocation path only:
408    /// `obj`'s `gc_bits` must still hold its freshly-initialized value of `0`
409    /// (true right after `Py::new` or a freelist pop, both of which zero
410    /// it), so the tracked bit can go in with a plain store instead of the
411    /// `fetch_or` `set_gc_tracked()` needs to be safe for the general case
412    /// (e.g. re-tracking a resurrected object, whose bits are not zero — it
413    /// may carry `FINALIZED`). A plain relaxed store compiles to a single
414    /// store instruction; `fetch_or` is a read-modify-write that, even
415    /// without contention, is measurably pricier on a hot per-allocation path.
416    ///
417    /// # Safety
418    /// obj must be a valid pointer to a PyObject whose `gc_bits` is still `0`.
419    unsafe fn track_object_fresh(&self, obj: NonNull<PyObject>, owner: GcOwner) {
420        let obj_ref = unsafe { obj.as_ref() };
421        obj_ref.init_gc_tracked_bit();
422        obj_ref.set_gc_generation(0);
423        obj_ref.set_gc_owner(owner);
424
425        self.generation_lists[0].write().push_front(obj);
426        self.counts[0].fetch_add(1, Ordering::Relaxed);
427    }
428
429    /// Track two freshly allocated objects as one push under one list lock.
430    ///
431    /// Like [`Self::track_object_fresh`] twice over, for a pair that is always
432    /// born together: a generator and the frame it owns. Doing it in one go
433    /// saves the second lock round trip on a path that runs per generator.
434    ///
435    /// # Safety
436    /// Both must be valid pointers to PyObjects whose `gc_bits` is still `0`.
437    unsafe fn track_pair_fresh(&self, a: NonNull<PyObject>, b: NonNull<PyObject>, owner: GcOwner) {
438        debug_assert_ne!(a, b);
439        for obj in [a, b] {
440            let obj_ref = unsafe { obj.as_ref() };
441            obj_ref.init_gc_tracked_bit();
442            obj_ref.set_gc_generation(0);
443            obj_ref.set_gc_owner(owner);
444        }
445
446        {
447            let mut list = self.generation_lists[0].write();
448            list.push_front(a);
449            list.push_front(b);
450        }
451        self.counts[0].fetch_add(2, Ordering::Relaxed);
452    }
453
454    /// Untrack an object (remove from GC lists).
455    /// O(1) — intrusive linked list remove by node pointer.
456    ///
457    /// # Safety
458    /// obj must be a valid pointer to a PyObject that is currently tracked.
459    /// The object's memory must still be valid (pointers are read).
460    pub unsafe fn untrack_object(&self, obj: NonNull<PyObject>) {
461        let obj_ref = unsafe { obj.as_ref() };
462
463        loop {
464            let obj_gen = obj_ref.gc_generation();
465
466            let (list_lock, count) = if obj_gen <= 2 {
467                (
468                    &self.generation_lists[obj_gen as usize] as &PyRwLock<LinkedList<GcLink>>,
469                    &self.counts[obj_gen as usize],
470                )
471            } else if obj_gen == GC_PERMANENT {
472                (&self.permanent_list, &self.permanent_count)
473            } else {
474                return; // GC_UNTRACKED or unknown — already untracked
475            };
476
477            let mut list = list_lock.write();
478            // Re-check generation under lock (may have changed due to promotion)
479            if obj_ref.gc_generation() != obj_gen {
480                drop(list);
481                continue; // Retry with the updated generation
482            }
483            if unsafe { list.remove(obj) }.is_some() {
484                release_count(count);
485                obj_ref.clear_gc_tracked();
486                obj_ref.set_gc_generation(GC_UNTRACKED);
487            } else {
488                // Object claims to be in this generation but wasn't found in the list.
489                // This indicates a bug: the object was already removed from the list
490                // without updating gc_generation, or was never inserted.
491                eprintln!(
492                    "GC WARNING: untrack_object failed to remove obj={obj:p} from gen={obj_gen}, \
493                     tracked={}, gc_gen={}",
494                    obj_ref.is_gc_tracked(),
495                    obj_ref.gc_generation()
496                );
497            }
498            return;
499        }
500    }
501
502    /// Get the objects `owner` tracks (for gc.get_objects), plus the ones no
503    /// interpreter owns.
504    /// If generation is None, returns all such objects.
505    /// If generation is Some(n), returns those in generation n only.
506    pub fn get_objects(&self, generation: Option<i32>, owner: GcOwner) -> Vec<PyObjectRef> {
507        fn collect_from_list(
508            list: &LinkedList<GcLink>,
509            owner: GcOwner,
510        ) -> impl Iterator<Item = PyObjectRef> + '_ {
511            list.iter()
512                .filter(move |obj| is_owned_by(obj, owner))
513                .filter_map(|obj| obj.try_to_owned())
514        }
515
516        match generation {
517            None => {
518                // Return all tracked objects from all generations + permanent
519                let mut result = Vec::new();
520                for gen_list in &self.generation_lists {
521                    result.extend(collect_from_list(&gen_list.read(), owner));
522                }
523                result.extend(collect_from_list(&self.permanent_list.read(), owner));
524                result
525            }
526            Some(g) if (0..=2).contains(&g) => {
527                let guard = self.generation_lists[g as usize].read();
528                collect_from_list(&guard, owner).collect()
529            }
530            _ => Vec::new(),
531        }
532    }
533
534    /// Check if automatic GC should run and run it if needed.
535    /// Called after object allocation.
536    /// Returns true if GC was run, false otherwise.
537    fn maybe_collect(&self, gc: &GcInterpreterState) -> bool {
538        if !gc.is_enabled() {
539            return false;
540        }
541
542        // Check gen0 threshold
543        let count0 = self.counts[0].load(Ordering::Relaxed) as u32;
544        let threshold0 = gc.generations[0].threshold();
545        if threshold0 > 0 && count0 >= threshold0 {
546            #[cfg(feature = "threading")]
547            {
548                // Defer to the next bytecode safepoint. Collecting here would
549                // stop the world while this thread may hold an internal lock
550                // (e.g. a lazily-initialized frame locals cell) that another
551                // thread is blocked on with no way to reach a safepoint —
552                // a deadlock. At a safepoint no such lock is held.
553                gc.scheduled.store(true, Ordering::Relaxed);
554                return false;
555            }
556            // Without threading there is no safepoint to defer to and no other
557            // thread whose frames could be read mid-mutation, so collect inline.
558            #[cfg(not(feature = "threading"))]
559            {
560                self.collect_inner(gc, 0, false);
561                return true;
562            }
563        }
564
565        false
566    }
567
568    fn collect_inner(
569        &self,
570        gc: &GcInterpreterState,
571        generation: usize,
572        force: bool,
573    ) -> CollectResult {
574        if !force && !gc.is_enabled() {
575            #[cfg(feature = "threading")]
576            gc.scheduled.store(false, Ordering::Relaxed);
577            return CollectResult::default();
578        }
579
580        // Try to acquire the collecting lock
581        let Some(_guard) = self.collecting.try_lock() else {
582            return CollectResult::default();
583        };
584
585        // A busy collector must not consume another interpreter's request.
586        // Clear only after acquiring the lock, before callbacks can request
587        // a later collection.
588        #[cfg(feature = "threading")]
589        gc.scheduled.store(false, Ordering::Relaxed);
590
591        let start_time = cfg_select! {
592            target_arch = "wasm32" => (),
593            _ => std::time::Instant::now(),
594        };
595
596        // Memory barrier to ensure visibility of all reference count updates
597        // from other threads before we start analyzing the object graph.
598        core::sync::atomic::fence(Ordering::SeqCst);
599
600        let generation = generation.min(2);
601        let debug = gc.get_debug();
602
603        // Clear the method cache to release strong references that
604        // might prevent cycle collection (_PyType_ClearCache).
605        crate::builtins::type_::type_cache_clear();
606
607        // Backstop for QSBR reclamation (threads may have missed requests).
608        #[cfg(feature = "threading")]
609        crate::object::qsbr::QSBR.process();
610
611        // Stop the world before reading any tracked object's interpreter
612        // state. Requested *before* the generation read locks are taken: a
613        // thread parking at a safepoint may still hold a generation write lock
614        // (track/untrack/promote) and must be able to release it to reach the
615        // safepoint. It could not do so if this thread already held a read
616        // lock it was waiting behind — hence the ordering.
617        //
618        // Auto-collection is deferred to a bytecode safepoint (see
619        // `maybe_collect`), where no internal lock is held, so it never stops
620        // the world under a lock. Explicit `gc.collect()` runs synchronously
621        // here; a re-entrant call from a finalizer during an in-progress
622        // collection is turned into a no-op by the `collecting` try_lock above.
623        // The one residual is an explicit `gc.collect()` reached from a
624        // finalizer/`__del__` that runs inline while a non-generation internal
625        // lock is still held (e.g. a container write lock during element
626        // replacement) with another thread blocked on that same lock: stopping
627        // the world then waits for a thread that cannot reach a safepoint.
628        // Closing it fully would require making those locks stop-the-world
629        // aware; the exclusion above only serializes the fork/GC requesters.
630        #[cfg(feature = "threading")]
631        let mut stw = CollectStopTheWorld::new();
632
633        // Step 1: Gather objects from generations 0..=generation
634        // Hold read locks for the entire scan to prevent concurrent modifications.
635        let gen_locks: Vec<_> = (0..=generation)
636            .map(|i| self.generation_lists[i].read())
637            .collect();
638
639        // Only this interpreter's objects, plus the ones no interpreter owns.
640        // Another interpreter's objects stay out of the candidate set, so they
641        // act as external roots: anything they reference survives this pass.
642        let owner = gc.owner;
643        // Sorted so that the test below, which every scanned object pays for,
644        // stays logarithmic in the number of interpreters that have been
645        // dropped instead of linear.
646        let retired = {
647            let mut retired = self.retired.lock().clone();
648            retired.sort_unstable();
649            retired
650        };
651        // Each candidate carries its own count, with `GcBits::COLLECTING`
652        // saying the count is there. Every edge in the heap is answered from
653        // that bit and that field; a table keyed by address turned each of
654        // those answers into a hash of the address instead. `candidate_ptrs`
655        // keeps the candidates in a walkable order, and the bit is what keeps
656        // an object that appears in two generation lists out of it twice.
657        let mut candidate_ptrs: Vec<GcPtr> = Vec::new();
658        for gen_list in &gen_locks {
659            for obj in gen_list.iter() {
660                if retired.binary_search(&obj.gc_owner()).is_ok() {
661                    obj.set_gc_owner(GC_NO_OWNER);
662                }
663                let strong_count = obj.strong_count();
664                if strong_count > 0 && is_owned_by(obj, owner) && !obj.is_gc_collecting() {
665                    obj.start_gc_refs(strong_count);
666                    candidate_ptrs.push(GcPtr(NonNull::from(obj)));
667                }
668            }
669        }
670
671        // A full collection is the only one that sees every generation, so it
672        // is where adoption finishes and the tags stop being tracked.
673        if generation == 2 && !retired.is_empty() {
674            for obj in self.permanent_list.read().iter() {
675                if retired.binary_search(&obj.gc_owner()).is_ok() {
676                    obj.set_gc_owner(GC_NO_OWNER);
677                }
678            }
679            // Only the tags this scan saw: one retired while it ran still has
680            // objects nobody has adopted.
681            self.retired
682                .lock()
683                .retain(|tag| retired.binary_search(tag).is_err());
684        }
685
686        if candidate_ptrs.is_empty() {
687            // Reset counts for generations whose objects were promoted away.
688            // For gen2 (oldest), survivors stay in-place so don't reset gen2 count.
689            let reset_end = if generation >= 2 { 2 } else { generation + 1 };
690            for count in self.counts.iter().take(reset_end) {
691                count.store(0, Ordering::Relaxed);
692            }
693
694            let duration = elapsed_secs(start_time);
695
696            gc.generations[generation].update_stats(0, 0, 0, duration);
697            return CollectResult {
698                collected: 0,
699                uncollectable: 0,
700                candidates: 0,
701                duration,
702            };
703        }
704
705        let candidates = candidate_ptrs.len();
706
707        if debug.contains(GcDebugFlags::STATS) {
708            eprintln!("gc: collecting {candidates} objects from generations 0..={generation}");
709        }
710
711        // Step 3: Subtract internal references
712        // Pre-compute referent pointers once per object so that both step 3
713        // (subtract refs) and step 4 (BFS reachability) see the same snapshot
714        // of each object's children. Without this, a dict whose write lock is
715        // held during one traversal but not the other can yield inconsistent
716        // results, causing live objects to be incorrectly collected.
717        //
718        // Every object's referents go in one buffer, with each object holding
719        // the range that is its own: a vector each would be an allocation per
720        // tracked object, and the collection wants them all at once anyway.
721        let mut referent_ptrs: Vec<NonNull<PyObject>> = Vec::new();
722        let mut referent_ranges: GcMap<GcPtr, (usize, usize)> = GcMap::default();
723
724        for &ptr in &candidate_ptrs {
725            let obj = unsafe { ptr.0.as_ref() };
726            if obj.strong_count() == 0 {
727                continue;
728            }
729            let start = referent_ptrs.len();
730            unsafe { obj.gc_extend_referent_ptrs(&mut referent_ptrs) };
731            let end = referent_ptrs.len();
732            for &child_ptr in &referent_ptrs[start..end] {
733                // SAFETY: the referents came from `traverse`, which handed out
734                // live references to them, and the world is stopped.
735                let child = unsafe { child_ptr.as_ref() };
736                if child.is_gc_collecting() {
737                    child.subtract_gc_ref();
738                }
739            }
740            referent_ranges.insert(ptr, (start, end));
741        }
742
743        // Step 4: Find reachable objects (gc_refs > 0) and traverse from them
744        let mut worklist: Vec<GcPtr> = Vec::new();
745
746        for &ptr in &candidate_ptrs {
747            let obj = unsafe { ptr.0.as_ref() };
748            if obj.gc_refs() > 0 {
749                obj.mark_gc_reachable();
750                worklist.push(ptr);
751            }
752        }
753
754        while let Some(ptr) = worklist.pop() {
755            let obj = unsafe { ptr.0.as_ref() };
756            if obj.is_gc_tracked() {
757                // Reuse the pre-computed referent pointers from step 3, in
758                // place: copying them out again costs a second pass over every
759                // edge in the heap. Objects skipped in step 3 (strong_count was
760                // 0) have none stored and are traversed here instead.
761                let computed;
762                let children: &[NonNull<PyObject>] = match referent_ranges.get(&ptr) {
763                    Some(&(start, end)) => &referent_ptrs[start..end],
764                    None => {
765                        computed = unsafe { obj.gc_get_referent_ptrs() };
766                        &computed
767                    }
768                };
769                for &child_ptr in children {
770                    // SAFETY: as in step 3, the referents are live.
771                    let child = unsafe { child_ptr.as_ref() };
772                    if child.is_gc_collecting() && child.mark_gc_reachable() {
773                        worklist.push(GcPtr(child_ptr));
774                    }
775                }
776            }
777        }
778
779        // Step 5: Split the candidates on what step 4 concluded, and hand the
780        // headers back: nothing past here reads `gc_refs`, and a candidate that
781        // kept the bit would be passed over by every later collection.
782        let mut reachable: Vec<GcPtr> = Vec::new();
783        let mut unreachable: Vec<GcPtr> = Vec::new();
784        for &ptr in &candidate_ptrs {
785            let obj = unsafe { ptr.0.as_ref() };
786            if obj.gc_refs() == GC_REACHABLE {
787                reachable.push(ptr);
788            } else {
789                unreachable.push(ptr);
790            }
791            obj.end_gc_refs();
792        }
793
794        // With the world stopped, every frame on any thread's call stack is a
795        // live root that is externally referenced and must have been
796        // classified reachable. A running frame appearing in `unreachable`
797        // would mean the reachability analysis observed its interpreter state
798        // as garbage — the exact hazard the barrier exists to prevent.
799        // Verify no running frame is classified unreachable.
800        // Walk the TLS frame chain (CURRENT_FRAME) instead of top_frame,
801        // because stack-allocated frames update only CURRENT_FRAME (via
802        // set_current_frame_nosave), not top_frame.
803        #[cfg(all(unix, feature = "threading", debug_assertions))]
804        if stw.is_stopped() {
805            let unreachable_set: GcSet<GcPtr> = unreachable.iter().copied().collect();
806            let mut cur = crate::vm::thread::get_current_frame();
807            while !cur.is_null() {
808                let iframe = unsafe { &*cur };
809                if let Some(fo) = iframe.frame_obj() {
810                    let obj = fo.as_object();
811                    let ptr = GcPtr(NonNull::from(obj));
812                    debug_assert!(
813                        !unreachable_set.contains(&ptr),
814                        "running frame {obj:p} classified unreachable during GC"
815                    );
816                }
817                cur = iframe.previous();
818            }
819        }
820
821        if debug.contains(GcDebugFlags::STATS) {
822            eprintln!(
823                "gc: {} reachable, {} unreachable",
824                reachable.len(),
825                unreachable.len()
826            );
827        }
828
829        // Create strong references while read locks are still held.
830        // After dropping gen_locks, other threads can untrack+free objects,
831        // making the raw pointers in `reachable`/`unreachable` dangling.
832        // Strong refs keep objects alive for later phases.
833        //
834        // Use try_to_owned() (CAS-based) instead of strong_count()+to_owned()
835        // to prevent a TOCTOU race: another thread can dec() the count to 0
836        // between the check and the increment, causing a use-after-free when
837        // the destroying thread eventually frees the memory.
838        let survivor_refs: Vec<PyObjectRef> = reachable
839            .iter()
840            .filter_map(|ptr| {
841                let obj = unsafe { ptr.0.as_ref() };
842                obj.try_to_owned()
843            })
844            .collect();
845
846        let unreachable_refs: Vec<crate::PyObjectRef> = unreachable
847            .iter()
848            .filter_map(|ptr| {
849                let obj = unsafe { ptr.0.as_ref() };
850                obj.try_to_owned()
851            })
852            .collect();
853
854        // The pointer-reading phases are done: strong references now pin every
855        // survivor and unreachable object, so the remaining phases can run with
856        // the world restarted. Finalizers and tp_clear must not run under
857        // stop-the-world — they execute arbitrary Python — and they only touch
858        // dead/husk objects, never a running frame.
859        #[cfg(feature = "threading")]
860        stw.restart();
861
862        if unreachable.is_empty() {
863            drop(gen_locks);
864            self.promote_survivors(generation, &survivor_refs);
865            let reset_end = if generation >= 2 { 2 } else { generation + 1 };
866            for count in self.counts.iter().take(reset_end) {
867                count.store(0, Ordering::Relaxed);
868            }
869
870            let duration = elapsed_secs(start_time);
871
872            gc.generations[generation].update_stats(0, 0, candidates, duration);
873            return CollectResult {
874                collected: 0,
875                uncollectable: 0,
876                candidates,
877                duration,
878            };
879        }
880
881        // Release read locks before finalization phase.
882        drop(gen_locks);
883
884        // Step 6: Finalize unreachable objects and handle resurrection
885
886        if unreachable_refs.is_empty() {
887            self.promote_survivors(generation, &survivor_refs);
888            let reset_end = if generation >= 2 { 2 } else { generation + 1 };
889            for count in self.counts.iter().take(reset_end) {
890                count.store(0, Ordering::Relaxed);
891            }
892
893            let duration = elapsed_secs(start_time);
894
895            gc.generations[generation].update_stats(0, 0, candidates, duration);
896            return CollectResult {
897                collected: 0,
898                uncollectable: 0,
899                candidates,
900                duration,
901            };
902        }
903
904        // 6b: Record initial strong counts (for resurrection detection)
905        let initial_counts: GcMap<GcPtr, usize> = unreachable_refs
906            .iter()
907            .map(|obj| {
908                let ptr = GcPtr(core::ptr::NonNull::from(obj.as_ref()));
909                (ptr, obj.strong_count())
910            })
911            .collect();
912
913        // 6c: Clear existing weakrefs BEFORE calling __del__
914        let mut all_callbacks: Vec<(crate::PyRef<crate::object::PyWeak>, crate::PyObjectRef)> =
915            Vec::new();
916        for obj_ref in &unreachable_refs {
917            let callbacks = obj_ref.gc_clear_weakrefs_collect_callbacks();
918            all_callbacks.extend(callbacks);
919        }
920        for (wr, cb) in all_callbacks {
921            if let Some(Err(e)) = crate::vm::thread::with_vm(&cb, |vm| cb.call((wr.clone(),), vm)) {
922                crate::vm::thread::with_vm(&cb, |vm| {
923                    vm.run_unraisable(e.clone(), Some("weakref callback".to_owned()), cb.clone());
924                });
925            }
926        }
927
928        // 6d: Call __del__ on unreachable objects (skip already-finalized).
929        // try_call_finalizer() internally checks gc_finalized() and sets it,
930        // so we must NOT set it beforehand.
931        for obj_ref in &unreachable_refs {
932            obj_ref.try_call_finalizer();
933        }
934
935        // Detect resurrection
936        let mut resurrected_set: GcSet<GcPtr> = GcSet::default();
937        let unreachable_set: GcSet<GcPtr> = unreachable.iter().copied().collect();
938
939        for obj in &unreachable_refs {
940            let ptr = GcPtr(core::ptr::NonNull::from(obj.as_ref()));
941            let initial = initial_counts.get(&ptr).copied().unwrap_or(1);
942            if obj.strong_count() > initial {
943                resurrected_set.insert(ptr);
944            }
945        }
946
947        // Transitive resurrection
948        let mut worklist: Vec<GcPtr> = resurrected_set.iter().copied().collect();
949        while let Some(ptr) = worklist.pop() {
950            let obj = unsafe { ptr.0.as_ref() };
951            let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
952            for child_ptr in referent_ptrs {
953                let child_gc_ptr = GcPtr(child_ptr);
954                if unreachable_set.contains(&child_gc_ptr) && resurrected_set.insert(child_gc_ptr) {
955                    worklist.push(child_gc_ptr);
956                }
957            }
958        }
959
960        // Partition into resurrected and truly dead
961        let (resurrected, truly_dead): (Vec<_>, Vec<_>) =
962            unreachable_refs.into_iter().partition(|obj| {
963                let ptr = GcPtr(core::ptr::NonNull::from(obj.as_ref()));
964                resurrected_set.contains(&ptr)
965            });
966
967        if debug.contains(GcDebugFlags::STATS) {
968            eprintln!(
969                "gc: {} resurrected, {} truly dead",
970                resurrected.len(),
971                truly_dead.len()
972            );
973        }
974
975        // Compute collected count (exclude instance dicts in truly_dead)
976        let collected = {
977            let dead_ptrs: GcSet<usize> = truly_dead
978                .iter()
979                .map(|obj| obj.as_ref() as *const PyObject as usize)
980                .collect();
981            let instance_dict_count = truly_dead
982                .iter()
983                .filter(|obj| {
984                    if let Some(dict_ref) = obj.dict() {
985                        dead_ptrs.contains(&(dict_ref.as_object() as *const PyObject as usize))
986                    } else {
987                        false
988                    }
989                })
990                .count();
991            truly_dead.len() - instance_dict_count
992        };
993
994        // Promote survivors to next generation BEFORE tp_clear.
995        // move_legacy_finalizer_reachable → delete_garbage order ensures
996        // survivor_refs are dropped before tp_clear, so reachable objects
997        // aren't kept alive beyond the deferred-drop phase.
998        self.promote_survivors(generation, &survivor_refs);
999        drop(survivor_refs);
1000
1001        // Resurrected objects stay tracked and survive this collection too.
1002        self.promote_survivors(generation, &resurrected);
1003        drop(resurrected);
1004
1005        if debug.contains(GcDebugFlags::COLLECTABLE) {
1006            for obj in &truly_dead {
1007                eprintln!(
1008                    "gc: collectable <{} {:p}>",
1009                    obj.class().name(),
1010                    obj.as_ref()
1011                );
1012            }
1013        }
1014
1015        if debug.contains(GcDebugFlags::SAVEALL) {
1016            self.promote_survivors(generation, &truly_dead);
1017            let mut garbage_guard = gc.garbage.lock();
1018            for obj_ref in &truly_dead {
1019                garbage_guard.push(obj_ref.clone());
1020            }
1021        }
1022
1023        if !truly_dead.is_empty() {
1024            // Break cycles by clearing references (tp_clear)
1025            // Use deferred drop context to prevent stack overflow.
1026            // With DEBUG_SAVEALL the objects stay reachable through
1027            // gc.garbage, so they must not be cleared (delete_garbage
1028            // skips tp_clear for saved objects).
1029            let save_all = debug.contains(GcDebugFlags::SAVEALL);
1030
1031            // Untrack dead objects BEFORE clearing them, mirroring the
1032            // untrack-then-clear ordering of the refcount dealloc path.
1033            // A cleared object (e.g. a frame husk with iframe == None) must
1034            // never be observable through the generation lists, or another
1035            // thread could obtain a strong reference via gc.get_objects()
1036            // and access the cleared payload.
1037            let mut late_resurrected: GcSet<GcPtr> = GcSet::default();
1038            if !save_all {
1039                let mut expected_counts: GcMap<GcPtr, usize> = GcMap::default();
1040                for obj_ref in &truly_dead {
1041                    let obj = obj_ref.as_ref();
1042                    if obj.is_gc_tracked() {
1043                        unsafe { self.untrack_object(NonNull::from(obj)) };
1044                    }
1045                    // One strong reference held by the `truly_dead` vec itself.
1046                    expected_counts.insert(GcPtr(NonNull::from(obj)), 1);
1047                }
1048                // With the objects out of the generation lists, no new external
1049                // reference can appear. Count the references coming from within
1050                // the dead set; any surplus in strong_count means another thread
1051                // grabbed a reference before untracking (late resurrection) and
1052                // the object must not be cleared.
1053                let mut referents: GcMap<GcPtr, Vec<NonNull<PyObject>>> = GcMap::default();
1054                for obj_ref in &truly_dead {
1055                    let referent_ptrs = unsafe { obj_ref.gc_get_referent_ptrs() };
1056                    for child_ptr in &referent_ptrs {
1057                        if let Some(n) = expected_counts.get_mut(&GcPtr(*child_ptr)) {
1058                            *n += 1;
1059                        }
1060                    }
1061                    referents.insert(GcPtr(NonNull::from(obj_ref.as_ref())), referent_ptrs);
1062                }
1063                let mut worklist: Vec<GcPtr> = Vec::new();
1064                for obj_ref in &truly_dead {
1065                    let ptr = GcPtr(NonNull::from(obj_ref.as_ref()));
1066                    if obj_ref.strong_count() > expected_counts[&ptr]
1067                        && late_resurrected.insert(ptr)
1068                    {
1069                        worklist.push(ptr);
1070                    }
1071                }
1072                // A holder of a late-resurrected object can reach its referents,
1073                // so everything reachable from it must stay intact as well.
1074                while let Some(ptr) = worklist.pop() {
1075                    let Some(referent_ptrs) = referents.get(&ptr) else {
1076                        continue;
1077                    };
1078                    for child_ptr in referent_ptrs {
1079                        let child = GcPtr(*child_ptr);
1080                        if expected_counts.contains_key(&child) && late_resurrected.insert(child) {
1081                            worklist.push(child);
1082                        }
1083                    }
1084                }
1085                // Re-track late-resurrected objects so a future collection can
1086                // retry once the external references are released.
1087                #[expect(
1088                    clippy::iter_over_hash_type,
1089                    reason = "Iteration order doesn't matter here"
1090                )]
1091                for &ptr in &late_resurrected {
1092                    // Re-tracking a resurrected object: it keeps the owner it
1093                    // was allocated under.
1094                    let owner = unsafe { ptr.0.as_ref() }.gc_owner();
1095                    unsafe { self.track_object(ptr.0, owner) };
1096                }
1097            }
1098            rustpython_common::refcount::with_deferred_drops(|| {
1099                if !save_all {
1100                    for obj_ref in &truly_dead {
1101                        let obj = obj_ref.as_ref();
1102                        if late_resurrected.contains(&GcPtr(NonNull::from(obj))) {
1103                            continue;
1104                        }
1105                        if obj.gc_has_clear() {
1106                            let edges = unsafe { obj.gc_clear() };
1107                            drop(edges);
1108                        }
1109                    }
1110                }
1111                drop(truly_dead);
1112            });
1113        }
1114
1115        // Reset counts for generations whose objects were promoted away.
1116        // For gen2 (oldest), survivors stay in-place so don't reset gen2 count.
1117        let reset_end = if generation >= 2 { 2 } else { generation + 1 };
1118        for count in self.counts.iter().take(reset_end) {
1119            count.store(0, Ordering::Relaxed);
1120        }
1121
1122        let duration = elapsed_secs(start_time);
1123
1124        gc.generations[generation].update_stats(collected, 0, candidates, duration);
1125
1126        CollectResult {
1127            collected,
1128            uncollectable: 0,
1129            candidates,
1130            duration,
1131        }
1132    }
1133
1134    /// Promote surviving objects to the next generation, or gen2 for a full collection.
1135    ///
1136    /// `survivors` must be strong references (`PyObjectRef`) to keep objects alive,
1137    /// since the generation read locks are released before this is called.
1138    ///
1139    /// Holds both source and destination list locks simultaneously to prevent
1140    /// a race where concurrent `untrack_object` reads a stale `gc_generation`
1141    /// and operates on the wrong list.
1142    fn promote_survivors(&self, from_gen: usize, survivors: &[PyObjectRef]) {
1143        let next_gen = (from_gen + 1).min(2);
1144
1145        // The world has restarted by this point. Batch lock acquisition and
1146        // counter updates, but bound each batch so other interpreters can keep
1147        // tracking and untracking objects between batches.
1148        for batch in survivors.chunks(256) {
1149            for src_gen in 0..next_gen {
1150                // Lock both source and destination lists simultaneously.
1151                // Always ascending order (src_gen < next_gen) → no deadlock.
1152                let mut src = self.generation_lists[src_gen].write();
1153                let mut dst = self.generation_lists[next_gen].write();
1154                let mut promoted = 0;
1155
1156                for obj_ref in batch {
1157                    let obj = obj_ref.as_ref();
1158                    // Re-check under locks: object might have been untracked concurrently
1159                    if obj.gc_generation() as usize != src_gen || !obj.is_gc_tracked() {
1160                        continue;
1161                    }
1162
1163                    let ptr = NonNull::from(obj);
1164                    if unsafe { src.remove(ptr) }.is_some() {
1165                        dst.push_front(ptr);
1166                        obj.set_gc_generation(next_gen as u8);
1167                        promoted += 1;
1168                    }
1169                }
1170
1171                if promoted != 0 {
1172                    let _ = self.counts[src_gen].try_update(
1173                        Ordering::Relaxed,
1174                        Ordering::Relaxed,
1175                        |count| Some(count.saturating_sub(promoted)),
1176                    );
1177                    self.counts[next_gen].fetch_add(promoted, Ordering::Relaxed);
1178                }
1179            }
1180        }
1181    }
1182
1183    /// Get count of frozen objects
1184    pub fn get_freeze_count(&self) -> usize {
1185        self.permanent_count.load(Ordering::Relaxed)
1186    }
1187
1188    /// Freeze the objects `owner` could collect (move them to the permanent
1189    /// generation).
1190    /// Lock order: generation_lists[i] → permanent_list (consistent with unfreeze).
1191    fn freeze(&self, owner: GcOwner) {
1192        let mut count = 0usize;
1193
1194        for (gen_idx, gen_list) in self.generation_lists.iter().enumerate() {
1195            let mut list = gen_list.write();
1196            let mut perm = self.permanent_list.write();
1197            let moving: Vec<_> = list
1198                .iter()
1199                .filter(|obj| is_owned_by(obj, owner))
1200                .map(NonNull::from)
1201                .collect();
1202            for ptr in moving {
1203                if unsafe { list.remove(ptr) }.is_none() {
1204                    continue;
1205                }
1206                perm.push_front(ptr);
1207                unsafe { ptr.as_ref().set_gc_generation(GC_PERMANENT) };
1208                count += 1;
1209                release_count(&self.counts[gen_idx]);
1210            }
1211        }
1212
1213        self.permanent_count.fetch_add(count, Ordering::Relaxed);
1214    }
1215
1216    /// Unfreeze the objects `owner` froze (move them from permanent to gen2).
1217    /// Lock order: generation_lists[2] → permanent_list (consistent with freeze).
1218    fn unfreeze(&self, owner: GcOwner) {
1219        let mut count = 0usize;
1220
1221        {
1222            let mut gen2 = self.generation_lists[2].write();
1223            let mut perm_list = self.permanent_list.write();
1224            let moving: Vec<_> = perm_list
1225                .iter()
1226                .filter(|obj| is_owned_by(obj, owner))
1227                .map(NonNull::from)
1228                .collect();
1229            for ptr in moving {
1230                if unsafe { perm_list.remove(ptr) }.is_none() {
1231                    continue;
1232                }
1233                gen2.push_front(ptr);
1234                unsafe { ptr.as_ref().set_gc_generation(2) };
1235                count += 1;
1236            }
1237            let _ = self.permanent_count.try_update(
1238                Ordering::Relaxed,
1239                Ordering::Relaxed,
1240                |permanent| Some(permanent.saturating_sub(count)),
1241            );
1242        }
1243
1244        self.counts[2].fetch_add(count, Ordering::Relaxed);
1245    }
1246
1247    /// Reset all locks to unlocked state after fork().
1248    ///
1249    /// After fork(), only the forking thread survives. Any lock held by another
1250    /// thread is permanently stuck. This resets them by zeroing the raw bytes.
1251    ///
1252    /// # Safety
1253    /// Must only be called after fork() in the child process when no other
1254    /// threads exist. The calling thread must NOT hold any of these locks.
1255    #[cfg(all(unix, feature = "threading"))]
1256    pub unsafe fn reinit_after_fork(&self) {
1257        use crate::common::lock::{reinit_mutex_after_fork, reinit_rwlock_after_fork};
1258
1259        unsafe {
1260            reinit_mutex_after_fork(&self.collecting);
1261            reinit_mutex_after_fork(&self.retired);
1262
1263            for rw in &self.generation_lists {
1264                reinit_rwlock_after_fork(rw);
1265            }
1266            reinit_rwlock_after_fork(&self.permanent_list);
1267        }
1268    }
1269}
1270
1271/// Per-interpreter garbage collector state (≈ `PyInterpreterState.gc`).
1272///
1273/// The generation lists are process-wide (see [`GcState`]); what an interpreter
1274/// owns is the policy applied to them and the results — which objects its
1275/// collections consider, whether they run automatically, and where uncollectable
1276/// objects end up.
1277pub struct GcInterpreterState {
1278    /// Tag written into every object this interpreter tracks.
1279    owner: GcOwner,
1280    /// Per-generation thresholds and statistics.
1281    pub generations: [GcGeneration; 3],
1282    /// GC enabled flag
1283    enabled: AtomicBool,
1284    /// Automatic collection requested at this interpreter's next safepoint.
1285    #[cfg(feature = "threading")]
1286    scheduled: AtomicBool,
1287    /// Debug flags
1288    debug: AtomicU32,
1289    /// Uncollectable objects saved by this interpreter's collections, drained
1290    /// into `py_garbage` by `gc.collect()`.
1291    pub garbage: PyMutex<Vec<PyObjectRef>>,
1292    /// `gc.garbage`
1293    pub py_garbage: crate::builtins::PyListRef,
1294    /// `gc.callbacks`
1295    pub py_callbacks: crate::builtins::PyListRef,
1296}
1297
1298impl GcInterpreterState {
1299    pub fn new(ctx: &crate::vm::Context) -> Self {
1300        Self {
1301            owner: gc_state().alloc_owner(),
1302            generations: [
1303                GcGeneration::new(2000), // young
1304                GcGeneration::new(10),   // old[0]
1305                GcGeneration::new(0),    // old[1]
1306            ],
1307            enabled: AtomicBool::new(true),
1308            #[cfg(feature = "threading")]
1309            scheduled: AtomicBool::new(false),
1310            debug: AtomicU32::new(0),
1311            garbage: PyMutex::new(Vec::new()),
1312            py_garbage: ctx.new_list(Vec::new()),
1313            py_callbacks: ctx.new_list(Vec::new()),
1314        }
1315    }
1316
1317    /// Check if GC is enabled.
1318    ///
1319    /// Relaxed, like [`GcGeneration::threshold`]: it is read once per
1320    /// allocation, and an allocation racing `gc.disable()` may use either value.
1321    pub fn is_enabled(&self) -> bool {
1322        self.enabled.load(Ordering::Relaxed)
1323    }
1324
1325    /// Leave requests pending while a collector is busy, without repeatedly
1326    /// entering the bytecode loop's slow path during its Python callbacks.
1327    #[cfg(feature = "threading")]
1328    #[inline]
1329    pub(crate) fn collection_ready(&self) -> bool {
1330        self.scheduled.load(Ordering::Relaxed) && !gc_state().collecting.is_locked()
1331    }
1332
1333    /// Enable GC
1334    pub fn enable(&self) {
1335        self.enabled.store(true, Ordering::Relaxed);
1336    }
1337
1338    /// Disable GC
1339    pub fn disable(&self) {
1340        self.enabled.store(false, Ordering::Relaxed);
1341    }
1342
1343    /// Get debug flags
1344    pub fn get_debug(&self) -> GcDebugFlags {
1345        GcDebugFlags::from_bits_truncate(self.debug.load(Ordering::SeqCst))
1346    }
1347
1348    /// Set debug flags
1349    pub fn set_debug(&self, flags: GcDebugFlags) {
1350        self.debug.store(flags.bits(), Ordering::SeqCst);
1351    }
1352
1353    /// Get thresholds for all generations
1354    pub fn get_threshold(&self) -> (u32, u32, u32) {
1355        (
1356            self.generations[0].threshold(),
1357            self.generations[1].threshold(),
1358            self.generations[2].threshold(),
1359        )
1360    }
1361
1362    /// Set thresholds
1363    pub fn set_threshold(&self, t0: u32, t1: Option<u32>, t2: Option<u32>) {
1364        self.generations[0].set_threshold(t0);
1365        if let Some(t1) = t1 {
1366            self.generations[1].set_threshold(t1);
1367        }
1368        if let Some(t2) = t2 {
1369            self.generations[2].set_threshold(t2);
1370        }
1371    }
1372
1373    /// Get statistics for all generations
1374    pub fn get_stats(&self) -> [GcStats; 3] {
1375        [
1376            self.generations[0].stats(),
1377            self.generations[1].stats(),
1378            self.generations[2].stats(),
1379        ]
1380    }
1381
1382    /// Perform garbage collection on the given generation
1383    pub fn collect(&self, generation: usize) -> CollectResult {
1384        gc_state().collect_inner(self, generation, false)
1385    }
1386
1387    /// Force collection even if GC is disabled (for manual gc.collect() calls)
1388    pub fn collect_force(&self, generation: usize) -> CollectResult {
1389        gc_state().collect_inner(self, generation, true)
1390    }
1391
1392    /// The tracked objects this interpreter can reach (for gc.get_objects).
1393    pub fn get_objects(&self, generation: Option<i32>) -> Vec<PyObjectRef> {
1394        gc_state().get_objects(generation, self.owner)
1395    }
1396
1397    /// Move the objects this interpreter could collect into the permanent
1398    /// generation.
1399    pub fn freeze(&self) {
1400        gc_state().freeze(self.owner);
1401    }
1402
1403    /// Move them back out of it.
1404    pub fn unfreeze(&self) {
1405        gc_state().unfreeze(self.owner);
1406    }
1407
1408    /// Reset this interpreter's GC locks to unlocked state after fork().
1409    ///
1410    /// # Safety
1411    /// Must only be called after fork() in the child process when no other
1412    /// threads exist. The calling thread must NOT hold any of these locks.
1413    #[cfg(all(unix, feature = "threading"))]
1414    pub unsafe fn reinit_after_fork(&self) {
1415        unsafe {
1416            crate::common::lock::reinit_mutex_after_fork(&self.garbage);
1417            for generation in &self.generations {
1418                generation.reinit_stats_after_fork();
1419            }
1420        }
1421    }
1422}
1423
1424impl Drop for GcInterpreterState {
1425    fn drop(&mut self) {
1426        // Objects this interpreter tracked can outlive it (another interpreter
1427        // may still hold one). Clearing the tag hands them to every collection
1428        // instead of stranding them. The tag itself is not handed back: it stays
1429        // retired so that a later interpreter cannot inherit these objects.
1430        gc_state().retire_owner(self.owner);
1431    }
1432}
1433
1434/// The tag `track_object` should write for the interpreter running now.
1435#[must_use]
1436pub fn current_owner() -> GcOwner {
1437    // SAFETY: the pointee is owned by the `PyGlobalState` of the VM on top of
1438    // this thread's VM stack, which outlives the section this call runs in.
1439    crate::vm::thread::current_gc_state().map_or(GC_NO_OWNER, |gc| unsafe { gc.as_ref() }.owner)
1440}
1441
1442/// Track a freshly allocated object under the interpreter running now, and let
1443/// it collect if the allocation pushed gen0 past its threshold.
1444///
1445/// # Safety
1446/// obj must be a valid pointer to a PyObject that is not already tracked.
1447pub(crate) unsafe fn track_new_object(obj: NonNull<PyObject>) {
1448    let state = gc_state();
1449    let Some(gc) = crate::vm::thread::current_gc_state() else {
1450        // No interpreter is running: the shared context builds its own objects
1451        // this way. They are left unowned, so every interpreter collects them.
1452        unsafe { state.track_object_fresh(obj, GC_NO_OWNER) };
1453        return;
1454    };
1455    // SAFETY: as in `current_owner`.
1456    let gc = unsafe { gc.as_ref() };
1457    unsafe { state.track_object_fresh(obj, gc.owner) };
1458    state.maybe_collect(gc);
1459}
1460
1461/// Track a generator (or coroutine, or async generator) together with the
1462/// frame it owns, and let the pair collect if it pushed gen0 past its
1463/// threshold.
1464///
1465/// # Safety
1466/// Both must be valid pointers to distinct PyObjects that are not already
1467/// tracked and whose `gc_bits` is still `0`.
1468pub(crate) unsafe fn track_new_pair(obj: NonNull<PyObject>, frame: NonNull<PyObject>) {
1469    let state = gc_state();
1470    let Some(gc) = crate::vm::thread::current_gc_state() else {
1471        unsafe { state.track_pair_fresh(obj, frame, GC_NO_OWNER) };
1472        return;
1473    };
1474    // SAFETY: as in `current_owner`.
1475    let gc = unsafe { gc.as_ref() };
1476    unsafe { state.track_pair_fresh(obj, frame, gc.owner) };
1477    state.maybe_collect(gc);
1478}
1479
1480/// Get a reference to the GC state.
1481///
1482/// In threading mode this is a true global (OnceLock).
1483/// In non-threading mode this is thread-local, because PyRwLock/PyMutex
1484/// use Cell-based locks that are not Sync.
1485///
1486/// Every interpreter's tracked objects live in these lists, because untracking
1487/// happens in `default_dealloc`, where no interpreter is in scope to route to.
1488/// What a collection *acts on* is still one interpreter's own objects, selected
1489/// by the `gc_owner` tag; [`GcInterpreterState`] holds the rest of the state
1490/// that goes with that. The counts here, and so `gc.get_count()` and
1491/// `gc.get_freeze_count()`, stay process-wide: they measure how full these
1492/// lists are.
1493pub fn gc_state() -> &'static GcState {
1494    rustpython_common::static_cell! {
1495        static GC_STATE: GcState;
1496    }
1497    GC_STATE.get_or_init(GcState::new)
1498}
1499
1500#[cfg(test)]
1501mod tests {
1502    use super::*;
1503
1504    fn interpreter_state() -> GcInterpreterState {
1505        GcInterpreterState::new(crate::vm::Context::genesis())
1506    }
1507
1508    #[test]
1509    fn gc_state_default() {
1510        let state = interpreter_state();
1511        assert!(state.is_enabled());
1512        assert_eq!(state.get_debug(), GcDebugFlags::empty());
1513        assert_eq!(state.get_threshold(), (2000, 10, 0));
1514    }
1515
1516    #[test]
1517    fn gc_enable_disable() {
1518        let state = interpreter_state();
1519        assert!(state.is_enabled());
1520        state.disable();
1521        assert!(!state.is_enabled());
1522        state.enable();
1523        assert!(state.is_enabled());
1524    }
1525
1526    #[test]
1527    fn gc_threshold() {
1528        let state = interpreter_state();
1529        state.set_threshold(100, Some(20), Some(30));
1530        assert_eq!(state.get_threshold(), (100, 20, 30));
1531    }
1532
1533    #[test]
1534    fn gc_debug_flags() {
1535        let state = interpreter_state();
1536        state.set_debug(GcDebugFlags::STATS | GcDebugFlags::COLLECTABLE);
1537        assert_eq!(
1538            state.get_debug(),
1539            GcDebugFlags::STATS | GcDebugFlags::COLLECTABLE
1540        );
1541    }
1542
1543    /// Live interpreters never share an owner tag, or their collections would
1544    /// reach each other's objects.
1545    #[test]
1546    fn gc_owner_tags_are_distinct_while_live() {
1547        let first = interpreter_state();
1548        let second = interpreter_state();
1549        assert_ne!(first.owner, second.owner);
1550        assert_ne!(first.owner, GC_NO_OWNER);
1551        assert_ne!(second.owner, GC_NO_OWNER);
1552    }
1553
1554    #[cfg(feature = "threading")]
1555    #[test]
1556    fn automatic_gc_request_stays_with_allocating_interpreter() {
1557        let first = crate::Interpreter::without_stdlib(Default::default());
1558        let second = crate::Interpreter::without_stdlib(Default::default());
1559        first.enter(|vm| vm.state.gc.scheduled.store(false, Ordering::Relaxed));
1560        second.enter(|vm| vm.state.gc.scheduled.store(false, Ordering::Relaxed));
1561        // An isolated allocation counter makes crossing the threshold
1562        // deterministic without depending on the rest of the test process.
1563        let allocations = GcState::new();
1564        allocations.counts[0].store(1, Ordering::Relaxed);
1565        first.enter(|vm| {
1566            vm.state.gc.set_threshold(1, None, None);
1567            assert!(!allocations.maybe_collect(&vm.state.gc));
1568        });
1569        second.enter(|vm| {
1570            assert!(!vm.state.gc.scheduled.load(Ordering::Relaxed));
1571            vm.run_scheduled_gc();
1572        });
1573        first.enter(|vm| {
1574            assert!(vm.state.gc.scheduled.swap(false, Ordering::Relaxed));
1575        });
1576    }
1577
1578    #[cfg(feature = "threading")]
1579    #[test]
1580    fn automatic_gc_request_survives_busy_collector() {
1581        let state = interpreter_state();
1582        let _guard = gc_state().collecting.lock();
1583        state.scheduled.store(true, Ordering::Relaxed);
1584        state.collect(0);
1585        assert!(state.scheduled.load(Ordering::Relaxed));
1586        assert!(!state.collection_ready());
1587
1588        state.disable();
1589        state.collect(0);
1590        assert!(!state.scheduled.load(Ordering::Relaxed));
1591    }
1592
1593    #[test]
1594    fn release_count_does_not_wrap_during_reset() {
1595        use std::sync::Barrier;
1596
1597        let count = AtomicUsize::new(1);
1598        let start = Barrier::new(3);
1599        let finish = Barrier::new(3);
1600        let mut underflows = 0;
1601        std::thread::scope(|scope| {
1602            for reset in [false, true] {
1603                let (count, start, finish) = (&count, &start, &finish);
1604                scope.spawn(move || {
1605                    for _ in 0..10_000 {
1606                        start.wait();
1607                        if reset {
1608                            count.store(0, Ordering::Relaxed);
1609                        } else {
1610                            release_count(count);
1611                        }
1612                        finish.wait();
1613                    }
1614                });
1615            }
1616            for _ in 0..10_000 {
1617                count.store(1, Ordering::Relaxed);
1618                start.wait();
1619                finish.wait();
1620                underflows += usize::from(count.load(Ordering::Relaxed) > 1);
1621            }
1622        });
1623        assert_eq!(underflows, 0);
1624    }
1625
1626    #[test]
1627    fn survivor_promotion_handles_mixed_and_stale_generations() {
1628        // Isolate list membership and counters from other tests' collections.
1629        // Detach every object before its normal deallocator consults gc_state().
1630        struct Heap {
1631            state: GcState,
1632            objects: Vec<PyObjectRef>,
1633        }
1634
1635        impl Drop for Heap {
1636            fn drop(&mut self) {
1637                for obj in &self.objects {
1638                    unsafe { self.state.untrack_object(NonNull::from(obj.as_ref())) };
1639                }
1640            }
1641        }
1642
1643        let ctx = crate::vm::Context::genesis();
1644        // A global collector must not retain a promotion snapshot containing
1645        // objects that this test has moved into its private lists.
1646        let _collector = gc_state().collecting.lock();
1647        let mut heap = Heap {
1648            state: GcState::new(),
1649            objects: Vec::new(),
1650        };
1651        for _ in 0..600 {
1652            let obj: PyObjectRef = ctx.new_list(Vec::new()).into();
1653            let ptr = NonNull::from(obj.as_ref());
1654            unsafe {
1655                gc_state().untrack_object(ptr);
1656                heap.state.track_object(ptr, 1);
1657            }
1658            heap.objects.push(obj);
1659        }
1660
1661        let Heap { state, objects } = &heap;
1662        // More than one batch, with survivors initially in all three generations.
1663        state.promote_survivors(0, &objects[..200]);
1664        state.promote_survivors(1, &objects[..100]);
1665        assert_eq!(state.get_count(), (400, 100, 100));
1666
1667        // Finalizers or another thread may freeze/untrack a survivor after the
1668        // collection took its snapshot, but before promotion acquires the locks.
1669        objects[250].set_gc_owner(2);
1670        state.freeze(2);
1671        unsafe { state.untrack_object(NonNull::from(objects[500].as_ref())) };
1672        // Counts are advisory and may have been reset by an earlier collection.
1673        state.counts[0].store(0, Ordering::Relaxed);
1674
1675        state.promote_survivors(2, objects);
1676        assert_eq!(state.get_count(), (0, 0, 598));
1677        assert_eq!(state.get_freeze_count(), 1);
1678        for (index, obj) in objects.iter().enumerate() {
1679            let expected = match index {
1680                250 => GC_PERMANENT,
1681                500 => GC_UNTRACKED,
1682                _ => 2,
1683            };
1684            assert_eq!(obj.gc_generation(), expected);
1685        }
1686        assert_eq!(state.generation_lists[0].read().iter().count(), 0);
1687        assert_eq!(state.generation_lists[1].read().iter().count(), 0);
1688        assert_eq!(state.generation_lists[2].read().iter().count(), 598);
1689        // Each node must occur exactly once, with its intrusive links intact.
1690        let promoted: GcSet<_> = state.generation_lists[2]
1691            .read()
1692            .iter()
1693            .map(NonNull::from)
1694            .collect();
1695        for obj in objects.iter().filter(|obj| obj.gc_generation() == 2) {
1696            assert!(promoted.contains(&NonNull::from(obj.as_ref())));
1697        }
1698    }
1699}