1use crate::common::linked_list::LinkedList;
6use crate::common::lock::{PyMutex, PyRwLock};
7use crate::object::{GC_PERMANENT, GC_UNTRACKED, GcLink};
8use crate::{AsObject, PyObject, PyObjectRef};
9use core::ptr::NonNull;
10use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
11use std::collections::HashSet;
12
13#[cfg(not(target_arch = "wasm32"))]
14fn elapsed_secs(start: &std::time::Instant) -> f64 {
15 start.elapsed().as_secs_f64()
16}
17
18#[cfg(target_arch = "wasm32")]
19fn elapsed_secs(_start: &()) -> f64 {
20 0.0
21}
22
23bitflags::bitflags! {
24 #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
26 pub struct GcDebugFlags: u32 {
27 const STATS = 1 << 0;
29 const COLLECTABLE = 1 << 1;
31 const UNCOLLECTABLE = 1 << 2;
33 const SAVEALL = 1 << 5;
35 const LEAK = Self::COLLECTABLE.bits() | Self::UNCOLLECTABLE.bits() | Self::SAVEALL.bits();
37 }
38}
39
40#[derive(Debug, Default)]
42pub struct CollectResult {
43 pub collected: usize,
44 pub uncollectable: usize,
45 pub candidates: usize,
46 pub duration: f64,
47}
48
49#[derive(Debug, Default)]
51pub struct GcStats {
52 pub collections: usize,
53 pub collected: usize,
54 pub uncollectable: usize,
55 pub candidates: usize,
56 pub duration: f64,
57}
58
59pub struct GcGeneration {
61 count: AtomicUsize,
63 threshold: AtomicU32,
65 stats: PyMutex<GcStats>,
67}
68
69impl GcGeneration {
70 pub const fn new(threshold: u32) -> Self {
71 Self {
72 count: AtomicUsize::new(0),
73 threshold: AtomicU32::new(threshold),
74 stats: PyMutex::new(GcStats {
75 collections: 0,
76 collected: 0,
77 uncollectable: 0,
78 candidates: 0,
79 duration: 0.0,
80 }),
81 }
82 }
83
84 pub fn count(&self) -> usize {
85 self.count.load(Ordering::SeqCst)
86 }
87
88 pub fn threshold(&self) -> u32 {
89 self.threshold.load(Ordering::SeqCst)
90 }
91
92 pub fn set_threshold(&self, value: u32) {
93 self.threshold.store(value, Ordering::SeqCst);
94 }
95
96 pub fn stats(&self) -> GcStats {
97 let guard = self.stats.lock();
98 GcStats {
99 collections: guard.collections,
100 collected: guard.collected,
101 uncollectable: guard.uncollectable,
102 candidates: guard.candidates,
103 duration: guard.duration,
104 }
105 }
106
107 pub fn update_stats(
108 &self,
109 collected: usize,
110 uncollectable: usize,
111 candidates: usize,
112 duration: f64,
113 ) {
114 let mut guard = self.stats.lock();
115 guard.collections += 1;
116 guard.collected += collected;
117 guard.uncollectable += uncollectable;
118 guard.candidates += candidates;
119 guard.duration += duration;
120 }
121
122 #[cfg(all(unix, feature = "threading"))]
128 unsafe fn reinit_stats_after_fork(&self) {
129 unsafe { crate::common::lock::reinit_mutex_after_fork(&self.stats) };
130 }
131}
132
133#[derive(Clone, Copy, PartialEq, Eq, Hash)]
136struct GcPtr(NonNull<PyObject>);
137
138pub struct GcState {
140 pub generations: [GcGeneration; 3],
142 pub permanent: GcGeneration,
144 pub enabled: AtomicBool,
146 generation_lists: [PyRwLock<LinkedList<GcLink, PyObject>>; 3],
149 permanent_list: PyRwLock<LinkedList<GcLink, PyObject>>,
151 pub debug: AtomicU32,
153 pub garbage: PyMutex<Vec<PyObjectRef>>,
155 pub callbacks: PyMutex<Vec<PyObjectRef>>,
157 collecting: PyMutex<()>,
159 alloc_count: AtomicUsize,
161}
162
163#[cfg(feature = "threading")]
166unsafe impl Send for GcState {}
167#[cfg(feature = "threading")]
168unsafe impl Sync for GcState {}
169
170impl Default for GcState {
171 fn default() -> Self {
172 Self::new()
173 }
174}
175
176impl GcState {
177 pub fn new() -> Self {
178 Self {
179 generations: [
180 GcGeneration::new(2000), GcGeneration::new(10), GcGeneration::new(0), ],
184 permanent: GcGeneration::new(0),
185 enabled: AtomicBool::new(true),
186 generation_lists: [
187 PyRwLock::new(LinkedList::new()),
188 PyRwLock::new(LinkedList::new()),
189 PyRwLock::new(LinkedList::new()),
190 ],
191 permanent_list: PyRwLock::new(LinkedList::new()),
192 debug: AtomicU32::new(0),
193 garbage: PyMutex::new(Vec::new()),
194 callbacks: PyMutex::new(Vec::new()),
195 collecting: PyMutex::new(()),
196 alloc_count: AtomicUsize::new(0),
197 }
198 }
199
200 pub fn is_enabled(&self) -> bool {
202 self.enabled.load(Ordering::SeqCst)
203 }
204
205 pub fn enable(&self) {
207 self.enabled.store(true, Ordering::SeqCst);
208 }
209
210 pub fn disable(&self) {
212 self.enabled.store(false, Ordering::SeqCst);
213 }
214
215 pub fn get_debug(&self) -> GcDebugFlags {
217 GcDebugFlags::from_bits_truncate(self.debug.load(Ordering::SeqCst))
218 }
219
220 pub fn set_debug(&self, flags: GcDebugFlags) {
222 self.debug.store(flags.bits(), Ordering::SeqCst);
223 }
224
225 pub fn get_threshold(&self) -> (u32, u32, u32) {
227 (
228 self.generations[0].threshold(),
229 self.generations[1].threshold(),
230 self.generations[2].threshold(),
231 )
232 }
233
234 pub fn set_threshold(&self, t0: u32, t1: Option<u32>, t2: Option<u32>) {
236 self.generations[0].set_threshold(t0);
237 if let Some(t1) = t1 {
238 self.generations[1].set_threshold(t1);
239 }
240 if let Some(t2) = t2 {
241 self.generations[2].set_threshold(t2);
242 }
243 }
244
245 pub fn get_count(&self) -> (usize, usize, usize) {
247 (
248 self.generations[0].count(),
249 self.generations[1].count(),
250 self.generations[2].count(),
251 )
252 }
253
254 pub fn get_stats(&self) -> [GcStats; 3] {
256 [
257 self.generations[0].stats(),
258 self.generations[1].stats(),
259 self.generations[2].stats(),
260 ]
261 }
262
263 pub unsafe fn track_object(&self, obj: NonNull<PyObject>) {
269 let obj_ref = unsafe { obj.as_ref() };
270 obj_ref.set_gc_tracked();
271 obj_ref.set_gc_generation(0);
272
273 self.generation_lists[0].write().push_front(obj);
274 self.generations[0].count.fetch_add(1, Ordering::SeqCst);
275 self.alloc_count.fetch_add(1, Ordering::SeqCst);
276 }
277
278 pub unsafe fn untrack_object(&self, obj: NonNull<PyObject>) {
285 let obj_ref = unsafe { obj.as_ref() };
286
287 loop {
288 let obj_gen = obj_ref.gc_generation();
289
290 let (list_lock, count) = if obj_gen <= 2 {
291 (
292 &self.generation_lists[obj_gen as usize]
293 as &PyRwLock<LinkedList<GcLink, PyObject>>,
294 &self.generations[obj_gen as usize].count,
295 )
296 } else if obj_gen == GC_PERMANENT {
297 (&self.permanent_list, &self.permanent.count)
298 } else {
299 return; };
301
302 let mut list = list_lock.write();
303 if obj_ref.gc_generation() != obj_gen {
305 drop(list);
306 continue; }
308 if unsafe { list.remove(obj) }.is_some() {
309 count.fetch_sub(1, Ordering::SeqCst);
310 obj_ref.clear_gc_tracked();
311 obj_ref.set_gc_generation(GC_UNTRACKED);
312 } else {
313 eprintln!(
317 "GC WARNING: untrack_object failed to remove obj={obj:p} from gen={obj_gen}, \
318 tracked={}, gc_gen={}",
319 obj_ref.is_gc_tracked(),
320 obj_ref.gc_generation()
321 );
322 }
323 return;
324 }
325 }
326
327 pub fn get_objects(&self, generation: Option<i32>) -> Vec<PyObjectRef> {
331 fn collect_from_list(
332 list: &LinkedList<GcLink, PyObject>,
333 ) -> impl Iterator<Item = PyObjectRef> + '_ {
334 list.iter().filter_map(|obj| obj.try_to_owned())
335 }
336
337 match generation {
338 None => {
339 let mut result = Vec::new();
341 for gen_list in &self.generation_lists {
342 result.extend(collect_from_list(&gen_list.read()));
343 }
344 result.extend(collect_from_list(&self.permanent_list.read()));
345 result
346 }
347 Some(g) if (0..=2).contains(&g) => {
348 let guard = self.generation_lists[g as usize].read();
349 collect_from_list(&guard).collect()
350 }
351 _ => Vec::new(),
352 }
353 }
354
355 pub fn maybe_collect(&self) -> bool {
359 if !self.is_enabled() {
360 return false;
361 }
362
363 let count0 = self.generations[0].count.load(Ordering::SeqCst) as u32;
365 let threshold0 = self.generations[0].threshold();
366 if threshold0 > 0 && count0 >= threshold0 {
367 self.collect(0);
368 return true;
369 }
370
371 false
372 }
373
374 pub fn collect(&self, generation: usize) -> CollectResult {
376 self.collect_inner(generation, false)
377 }
378
379 pub fn collect_force(&self, generation: usize) -> CollectResult {
381 self.collect_inner(generation, true)
382 }
383
384 fn collect_inner(&self, generation: usize, force: bool) -> CollectResult {
385 if !force && !self.is_enabled() {
386 return CollectResult::default();
387 }
388
389 let Some(_guard) = self.collecting.try_lock() else {
391 return CollectResult::default();
392 };
393
394 #[cfg(not(target_arch = "wasm32"))]
395 let start_time = std::time::Instant::now();
396 #[cfg(target_arch = "wasm32")]
397 let start_time = ();
398
399 core::sync::atomic::fence(Ordering::SeqCst);
402
403 let generation = generation.min(2);
404 let debug = self.get_debug();
405
406 crate::builtins::type_::type_cache_clear();
409
410 let gen_locks: Vec<_> = (0..=generation)
413 .map(|i| self.generation_lists[i].read())
414 .collect();
415
416 let mut collecting: HashSet<GcPtr> = HashSet::new();
417 for gen_list in &gen_locks {
418 for obj in gen_list.iter() {
419 if obj.strong_count() > 0 {
420 collecting.insert(GcPtr(NonNull::from(obj)));
421 }
422 }
423 }
424
425 if collecting.is_empty() {
426 let reset_end = if generation >= 2 { 2 } else { generation + 1 };
429 for i in 0..reset_end {
430 self.generations[i].count.store(0, Ordering::SeqCst);
431 }
432 let duration = elapsed_secs(&start_time);
433 self.generations[generation].update_stats(0, 0, 0, duration);
434 return CollectResult {
435 collected: 0,
436 uncollectable: 0,
437 candidates: 0,
438 duration,
439 };
440 }
441
442 let candidates = collecting.len();
443
444 if debug.contains(GcDebugFlags::STATS) {
445 eprintln!(
446 "gc: collecting {} objects from generations 0..={}",
447 collecting.len(),
448 generation
449 );
450 }
451
452 let mut gc_refs: std::collections::HashMap<GcPtr, usize> = std::collections::HashMap::new();
454 for &ptr in &collecting {
455 let obj = unsafe { ptr.0.as_ref() };
456 gc_refs.insert(ptr, obj.strong_count());
457 }
458
459 let mut referents_map: std::collections::HashMap<GcPtr, Vec<NonNull<PyObject>>> =
466 std::collections::HashMap::new();
467 for &ptr in &collecting {
468 let obj = unsafe { ptr.0.as_ref() };
469 if obj.strong_count() == 0 {
470 continue;
471 }
472 let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
473 referents_map.insert(ptr, referent_ptrs.clone());
474 for child_ptr in referent_ptrs {
475 let gc_ptr = GcPtr(child_ptr);
476 if collecting.contains(&gc_ptr)
477 && let Some(refs) = gc_refs.get_mut(&gc_ptr)
478 {
479 *refs = refs.saturating_sub(1);
480 }
481 }
482 }
483
484 let mut reachable: HashSet<GcPtr> = HashSet::new();
486 let mut worklist: Vec<GcPtr> = Vec::new();
487
488 for (&ptr, &refs) in &gc_refs {
489 if refs > 0 {
490 reachable.insert(ptr);
491 worklist.push(ptr);
492 }
493 }
494
495 while let Some(ptr) = worklist.pop() {
496 let obj = unsafe { ptr.0.as_ref() };
497 if obj.is_gc_tracked() {
498 let referent_ptrs = referents_map
502 .get(&ptr)
503 .cloned()
504 .unwrap_or_else(|| unsafe { obj.gc_get_referent_ptrs() });
505 for child_ptr in referent_ptrs {
506 let gc_ptr = GcPtr(child_ptr);
507 if collecting.contains(&gc_ptr) && reachable.insert(gc_ptr) {
508 worklist.push(gc_ptr);
509 }
510 }
511 }
512 }
513
514 let unreachable: Vec<GcPtr> = collecting.difference(&reachable).copied().collect();
516
517 if debug.contains(GcDebugFlags::STATS) {
518 eprintln!(
519 "gc: {} reachable, {} unreachable",
520 reachable.len(),
521 unreachable.len()
522 );
523 }
524
525 let survivor_refs: Vec<PyObjectRef> = reachable
535 .iter()
536 .filter_map(|ptr| {
537 let obj = unsafe { ptr.0.as_ref() };
538 obj.try_to_owned()
539 })
540 .collect();
541
542 let unreachable_refs: Vec<crate::PyObjectRef> = unreachable
543 .iter()
544 .filter_map(|ptr| {
545 let obj = unsafe { ptr.0.as_ref() };
546 obj.try_to_owned()
547 })
548 .collect();
549
550 if unreachable.is_empty() {
551 drop(gen_locks);
552 self.promote_survivors(generation, &survivor_refs);
553 let reset_end = if generation >= 2 { 2 } else { generation + 1 };
554 for i in 0..reset_end {
555 self.generations[i].count.store(0, Ordering::SeqCst);
556 }
557 let duration = elapsed_secs(&start_time);
558 self.generations[generation].update_stats(0, 0, candidates, duration);
559 return CollectResult {
560 collected: 0,
561 uncollectable: 0,
562 candidates,
563 duration,
564 };
565 }
566
567 drop(gen_locks);
569
570 if unreachable_refs.is_empty() {
573 self.promote_survivors(generation, &survivor_refs);
574 let reset_end = if generation >= 2 { 2 } else { generation + 1 };
575 for i in 0..reset_end {
576 self.generations[i].count.store(0, Ordering::SeqCst);
577 }
578 let duration = elapsed_secs(&start_time);
579 self.generations[generation].update_stats(0, 0, candidates, duration);
580 return CollectResult {
581 collected: 0,
582 uncollectable: 0,
583 candidates,
584 duration,
585 };
586 }
587
588 let initial_counts: std::collections::HashMap<GcPtr, usize> = unreachable_refs
590 .iter()
591 .map(|obj| {
592 let ptr = GcPtr(core::ptr::NonNull::from(obj.as_ref()));
593 (ptr, obj.strong_count())
594 })
595 .collect();
596
597 let mut all_callbacks: Vec<(crate::PyRef<crate::object::PyWeak>, crate::PyObjectRef)> =
599 Vec::new();
600 for obj_ref in &unreachable_refs {
601 let callbacks = obj_ref.gc_clear_weakrefs_collect_callbacks();
602 all_callbacks.extend(callbacks);
603 }
604 for (wr, cb) in all_callbacks {
605 if let Some(Err(e)) = crate::vm::thread::with_vm(&cb, |vm| cb.call((wr.clone(),), vm)) {
606 crate::vm::thread::with_vm(&cb, |vm| {
607 vm.run_unraisable(e.clone(), Some("weakref callback".to_owned()), cb.clone());
608 });
609 }
610 }
611
612 for obj_ref in &unreachable_refs {
616 obj_ref.try_call_finalizer();
617 }
618
619 let mut resurrected_set: HashSet<GcPtr> = HashSet::new();
621 let unreachable_set: HashSet<GcPtr> = unreachable.iter().copied().collect();
622
623 for obj in &unreachable_refs {
624 let ptr = GcPtr(core::ptr::NonNull::from(obj.as_ref()));
625 let initial = initial_counts.get(&ptr).copied().unwrap_or(1);
626 if obj.strong_count() > initial {
627 resurrected_set.insert(ptr);
628 }
629 }
630
631 let mut worklist: Vec<GcPtr> = resurrected_set.iter().copied().collect();
633 while let Some(ptr) = worklist.pop() {
634 let obj = unsafe { ptr.0.as_ref() };
635 let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
636 for child_ptr in referent_ptrs {
637 let child_gc_ptr = GcPtr(child_ptr);
638 if unreachable_set.contains(&child_gc_ptr) && resurrected_set.insert(child_gc_ptr) {
639 worklist.push(child_gc_ptr);
640 }
641 }
642 }
643
644 let (resurrected, truly_dead): (Vec<_>, Vec<_>) =
646 unreachable_refs.into_iter().partition(|obj| {
647 let ptr = GcPtr(core::ptr::NonNull::from(obj.as_ref()));
648 resurrected_set.contains(&ptr)
649 });
650
651 if debug.contains(GcDebugFlags::STATS) {
652 eprintln!(
653 "gc: {} resurrected, {} truly dead",
654 resurrected.len(),
655 truly_dead.len()
656 );
657 }
658
659 let collected = {
661 let dead_ptrs: HashSet<usize> = truly_dead
662 .iter()
663 .map(|obj| obj.as_ref() as *const PyObject as usize)
664 .collect();
665 let instance_dict_count = truly_dead
666 .iter()
667 .filter(|obj| {
668 if let Some(dict_ref) = obj.dict() {
669 dead_ptrs.contains(&(dict_ref.as_object() as *const PyObject as usize))
670 } else {
671 false
672 }
673 })
674 .count();
675 truly_dead.len() - instance_dict_count
676 };
677
678 self.promote_survivors(generation, &survivor_refs);
683 drop(survivor_refs);
684
685 drop(resurrected);
687
688 if debug.contains(GcDebugFlags::COLLECTABLE) {
689 for obj in &truly_dead {
690 eprintln!(
691 "gc: collectable <{} {:p}>",
692 obj.class().name(),
693 obj.as_ref()
694 );
695 }
696 }
697
698 if debug.contains(GcDebugFlags::SAVEALL) {
699 let mut garbage_guard = self.garbage.lock();
700 for obj_ref in truly_dead.iter() {
701 garbage_guard.push(obj_ref.clone());
702 }
703 }
704
705 if !truly_dead.is_empty() {
706 rustpython_common::refcount::with_deferred_drops(|| {
709 for obj_ref in truly_dead.iter() {
710 if obj_ref.gc_has_clear() {
711 let edges = unsafe { obj_ref.gc_clear() };
712 drop(edges);
713 }
714 }
715 drop(truly_dead);
716 });
717 }
718
719 let reset_end = if generation >= 2 { 2 } else { generation + 1 };
722 for i in 0..reset_end {
723 self.generations[i].count.store(0, Ordering::SeqCst);
724 }
725
726 let duration = elapsed_secs(&start_time);
727 self.generations[generation].update_stats(collected, 0, candidates, duration);
728
729 CollectResult {
730 collected,
731 uncollectable: 0,
732 candidates,
733 duration,
734 }
735 }
736
737 fn promote_survivors(&self, from_gen: usize, survivors: &[PyObjectRef]) {
746 if from_gen >= 2 {
747 return; }
749
750 let next_gen = from_gen + 1;
751
752 for obj_ref in survivors {
753 let obj = obj_ref.as_ref();
754 let ptr = NonNull::from(obj);
755 let obj_gen = obj.gc_generation();
756 if obj_gen as usize <= from_gen && obj_gen <= 2 {
757 let src_gen = obj_gen as usize;
758
759 let mut src = self.generation_lists[src_gen].write();
762 let mut dst = self.generation_lists[next_gen].write();
763
764 if obj.gc_generation() != obj_gen || !obj.is_gc_tracked() {
766 continue;
767 }
768
769 if unsafe { src.remove(ptr) }.is_some() {
770 self.generations[src_gen]
771 .count
772 .fetch_sub(1, Ordering::SeqCst);
773
774 dst.push_front(ptr);
775 self.generations[next_gen]
776 .count
777 .fetch_add(1, Ordering::SeqCst);
778
779 obj.set_gc_generation(next_gen as u8);
780 }
781 }
782 }
783 }
784
785 pub fn get_freeze_count(&self) -> usize {
787 self.permanent.count()
788 }
789
790 pub fn freeze(&self) {
793 let mut count = 0usize;
794
795 for (gen_idx, gen_list) in self.generation_lists.iter().enumerate() {
796 let mut list = gen_list.write();
797 let mut perm = self.permanent_list.write();
798 while let Some(ptr) = list.pop_front() {
799 perm.push_front(ptr);
800 unsafe { ptr.as_ref().set_gc_generation(GC_PERMANENT) };
801 count += 1;
802 }
803 self.generations[gen_idx].count.store(0, Ordering::SeqCst);
804 }
805
806 self.permanent.count.fetch_add(count, Ordering::SeqCst);
807 }
808
809 pub fn unfreeze(&self) {
812 let mut count = 0usize;
813
814 {
815 let mut gen2 = self.generation_lists[2].write();
816 let mut perm_list = self.permanent_list.write();
817 while let Some(ptr) = perm_list.pop_front() {
818 gen2.push_front(ptr);
819 unsafe { ptr.as_ref().set_gc_generation(2) };
820 count += 1;
821 }
822 self.permanent.count.store(0, Ordering::SeqCst);
823 }
824
825 self.generations[2].count.fetch_add(count, Ordering::SeqCst);
826 }
827
828 #[cfg(all(unix, feature = "threading"))]
837 pub unsafe fn reinit_after_fork(&self) {
838 use crate::common::lock::{reinit_mutex_after_fork, reinit_rwlock_after_fork};
839
840 unsafe {
841 reinit_mutex_after_fork(&self.collecting);
842 reinit_mutex_after_fork(&self.garbage);
843 reinit_mutex_after_fork(&self.callbacks);
844
845 for generation in &self.generations {
846 generation.reinit_stats_after_fork();
847 }
848 self.permanent.reinit_stats_after_fork();
849
850 for rw in &self.generation_lists {
851 reinit_rwlock_after_fork(rw);
852 }
853 reinit_rwlock_after_fork(&self.permanent_list);
854 }
855 }
856}
857
858pub fn gc_state() -> &'static GcState {
864 rustpython_common::static_cell! {
865 static GC_STATE: GcState;
866 }
867 GC_STATE.get_or_init(GcState::new)
868}
869
870#[cfg(test)]
871mod tests {
872 use super::*;
873
874 #[test]
875 fn test_gc_state_default() {
876 let state = GcState::new();
877 assert!(state.is_enabled());
878 assert_eq!(state.get_debug(), GcDebugFlags::empty());
879 assert_eq!(state.get_threshold(), (2000, 10, 0));
880 assert_eq!(state.get_count(), (0, 0, 0));
881 }
882
883 #[test]
884 fn test_gc_enable_disable() {
885 let state = GcState::new();
886 assert!(state.is_enabled());
887 state.disable();
888 assert!(!state.is_enabled());
889 state.enable();
890 assert!(state.is_enabled());
891 }
892
893 #[test]
894 fn test_gc_threshold() {
895 let state = GcState::new();
896 state.set_threshold(100, Some(20), Some(30));
897 assert_eq!(state.get_threshold(), (100, 20, 30));
898 }
899
900 #[test]
901 fn test_gc_debug_flags() {
902 let state = GcState::new();
903 state.set_debug(GcDebugFlags::STATS | GcDebugFlags::COLLECTABLE);
904 assert_eq!(
905 state.get_debug(),
906 GcDebugFlags::STATS | GcDebugFlags::COLLECTABLE
907 );
908 }
909}