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}