11//! Garbage Collection State and Algorithm
22//!
3- //! This module implements CPython-compatible generational garbage collection
4- //! for RustPython, using an intrusive doubly-linked list approach.
3+ //! Generational garbage collection using an intrusive doubly-linked list.
54
65use crate :: common:: linked_list:: LinkedList ;
76use crate :: common:: lock:: { PyMutex , PyRwLock } ;
@@ -28,12 +27,23 @@ bitflags::bitflags! {
2827 }
2928}
3029
30+ /// Result from a single collection run
31+ #[ derive( Debug , Default ) ]
32+ pub struct CollectResult {
33+ pub collected : usize ,
34+ pub uncollectable : usize ,
35+ pub candidates : usize ,
36+ pub duration : f64 ,
37+ }
38+
3139/// Statistics for a single generation (gc_generation_stats)
3240#[ derive( Debug , Default ) ]
3341pub struct GcStats {
3442 pub collections : usize ,
3543 pub collected : usize ,
3644 pub uncollectable : usize ,
45+ pub candidates : usize ,
46+ pub duration : f64 ,
3747}
3848
3949/// A single GC generation with intrusive linked list
@@ -55,6 +65,8 @@ impl GcGeneration {
5565 collections : 0 ,
5666 collected : 0 ,
5767 uncollectable : 0 ,
68+ candidates : 0 ,
69+ duration : 0.0 ,
5870 } ) ,
5971 }
6072 }
@@ -77,14 +89,24 @@ impl GcGeneration {
7789 collections : guard. collections ,
7890 collected : guard. collected ,
7991 uncollectable : guard. uncollectable ,
92+ candidates : guard. candidates ,
93+ duration : guard. duration ,
8094 }
8195 }
8296
83- pub fn update_stats ( & self , collected : usize , uncollectable : usize ) {
97+ pub fn update_stats (
98+ & self ,
99+ collected : usize ,
100+ uncollectable : usize ,
101+ candidates : usize ,
102+ duration : f64 ,
103+ ) {
84104 let mut guard = self . stats . lock ( ) ;
85105 guard. collections += 1 ;
86106 guard. collected += collected;
87107 guard. uncollectable += uncollectable;
108+ guard. candidates += candidates;
109+ guard. duration += duration;
88110 }
89111
90112 /// Reset the stats mutex to unlocked state after fork().
@@ -340,25 +362,27 @@ impl GcState {
340362 }
341363
342364 /// Perform garbage collection on the given generation
343- pub fn collect ( & self , generation : usize ) -> ( usize , usize ) {
365+ pub fn collect ( & self , generation : usize ) -> CollectResult {
344366 self . collect_inner ( generation, false )
345367 }
346368
347369 /// Force collection even if GC is disabled (for manual gc.collect() calls)
348- pub fn collect_force ( & self , generation : usize ) -> ( usize , usize ) {
370+ pub fn collect_force ( & self , generation : usize ) -> CollectResult {
349371 self . collect_inner ( generation, true )
350372 }
351373
352- fn collect_inner ( & self , generation : usize , force : bool ) -> ( usize , usize ) {
374 + fn collect_inner ( & self , generation : usize , force : bool ) -> CollectResult {
353375 if !force && !self . is_enabled ( ) {
354- return ( 0 , 0 ) ;
376+ return CollectResult :: default ( ) ;
355377 }
356378
357379 // Try to acquire the collecting lock
358380 let Some ( _guard) = self . collecting . try_lock ( ) else {
359- return ( 0 , 0 ) ;
381+ return CollectResult :: default ( ) ;
360382 } ;
361383
384+ let start_time = std:: time:: Instant :: now ( ) ;
385+
362386 // Memory barrier to ensure visibility of all reference count updates
363387 // from other threads before we start analyzing the object graph.
364388 core:: sync:: atomic:: fence ( Ordering :: SeqCst ) ;
@@ -386,11 +410,24 @@ impl GcState {
386410 }
387411
388412 if collecting. is_empty ( ) {
389- self . generations [ 0 ] . count . store ( 0 , Ordering :: SeqCst ) ;
390- self . generations [ generation] . update_stats ( 0 , 0 ) ;
391- return ( 0 , 0 ) ;
413+ // Reset counts for generations whose objects were promoted away.
414+ // For gen2 (oldest), survivors stay in-place so don't reset gen2 count.
415+ let reset_end = if generation >= 2 { 2 } else { generation + 1 } ;
416+ for i in 0 ..reset_end {
417+ self . generations [ i] . count . store ( 0 , Ordering :: SeqCst ) ;
418+ }
419+ let duration = start_time. elapsed ( ) . as_secs_f64 ( ) ;
420+ self . generations [ generation] . update_stats ( 0 , 0 , 0 , duration) ;
421+ return CollectResult {
422+ collected : 0 ,
423+ uncollectable : 0 ,
424+ candidates : 0 ,
425+ duration,
426+ } ;
392427 }
393428
429+ let candidates = collecting. len ( ) ;
430+
394431 if debug. contains ( GcDebugFlags :: STATS ) {
395432 eprintln ! (
396433 "gc: collecting {} objects from generations 0..={}" ,
@@ -486,9 +523,17 @@ impl GcState {
486523 if unreachable. is_empty ( ) {
487524 drop ( gen_locks) ;
488525 self . promote_survivors ( generation, & survivor_refs) ;
489- self . generations [ 0 ] . count . store ( 0 , Ordering :: SeqCst ) ;
490- self . generations [ generation] . update_stats ( 0 , 0 ) ;
491- return ( 0 , 0 ) ;
526+ for i in 0 ..generation {
527+ self . generations [ i] . count . store ( 0 , Ordering :: SeqCst ) ;
528+ }
529+ let duration = start_time. elapsed ( ) . as_secs_f64 ( ) ;
530+ self . generations [ generation] . update_stats ( 0 , 0 , candidates, duration) ;
531+ return CollectResult {
532+ collected : 0 ,
533+ uncollectable : 0 ,
534+ candidates,
535+ duration,
536+ } ;
492537 }
493538
494539 // Release read locks before finalization phase.
@@ -498,9 +543,17 @@ impl GcState {
498543
499544 if unreachable_refs. is_empty ( ) {
500545 self . promote_survivors ( generation, & survivor_refs) ;
501- self . generations [ 0 ] . count . store ( 0 , Ordering :: SeqCst ) ;
502- self . generations [ generation] . update_stats ( 0 , 0 ) ;
503- return ( 0 , 0 ) ;
546+ for i in 0 ..generation {
547+ self . generations [ i] . count . store ( 0 , Ordering :: SeqCst ) ;
548+ }
549+ let duration = start_time. elapsed ( ) . as_secs_f64 ( ) ;
550+ self . generations [ generation] . update_stats ( 0 , 0 , candidates, duration) ;
551+ return CollectResult {
552+ collected : 0 ,
553+ uncollectable : 0 ,
554+ candidates,
555+ duration,
556+ } ;
504557 }
505558
506559 // 6b: Record initial strong counts (for resurrection detection)
@@ -594,15 +647,25 @@ impl GcState {
594647 } ;
595648
596649 // Promote survivors to next generation BEFORE tp_clear.
597- // This matches CPython's order ( move_legacy_finalizer_reachable → delete_garbage)
598- // and ensures survivor_refs are dropped before tp_clear, so reachable objects
599- // (e.g. LateFin) aren't kept alive beyond the deferred-drop phase.
650+ // move_legacy_finalizer_reachable → delete_garbage order ensures
651+ // survivor_refs are dropped before tp_clear, so reachable objects
652+ // aren't kept alive beyond the deferred-drop phase.
600653 self . promote_survivors ( generation, & survivor_refs) ;
601654 drop ( survivor_refs) ;
602655
603656 // Resurrected objects stay tracked — just drop our references
604657 drop ( resurrected) ;
605658
659+ if debug. contains ( GcDebugFlags :: COLLECTABLE ) {
660+ for obj in & truly_dead {
661+ eprintln ! (
662+ "gc: collectable <{} {:p}>" ,
663+ obj. class( ) . name( ) ,
664+ obj. as_ref( )
665+ ) ;
666+ }
667+ }
668+
606669 if debug. contains ( GcDebugFlags :: SAVEALL ) {
607670 let mut garbage_guard = self . garbage . lock ( ) ;
608671 for obj_ref in truly_dead. iter ( ) {
@@ -624,12 +687,22 @@ impl GcState {
624687 } ) ;
625688 }
626689
627- // Reset gen0 count
628- self . generations [ 0 ] . count . store ( 0 , Ordering :: SeqCst ) ;
690+ // Reset counts for generations whose objects were promoted away.
691+ // For gen2 (oldest), survivors stay in-place so don't reset gen2 count.
692+ let reset_end = if generation >= 2 { 2 } else { generation + 1 } ;
693+ for i in 0 ..reset_end {
694+ self . generations [ i] . count . store ( 0 , Ordering :: SeqCst ) ;
695+ }
629696
630- self . generations [ generation] . update_stats ( collected, 0 ) ;
697+ let duration = start_time. elapsed ( ) . as_secs_f64 ( ) ;
698+ self . generations [ generation] . update_stats ( collected, 0 , candidates, duration) ;
631699
632- ( collected, 0 )
700+ CollectResult {
701+ collected,
702+ uncollectable : 0 ,
703+ candidates,
704+ duration,
705+ }
633706 }
634707
635708 /// Promote surviving objects to the next generation.
0 commit comments