X Tutup
Skip to content

Commit d100dc2

Browse files
committed
gc: add CollectResult, stats fields, get_referrers, and fix count reset
- Add CollectResult struct with collected/uncollectable/candidates/duration - Add candidates and duration fields to GcStats and gc.get_stats() - Pass CollectResult to gc.callbacks info dict - Reset generation counts for all collected generations (0..=N) - Return 0 for third value in gc.get_threshold() (3.13+) - Implement gc.get_referrers() by scanning all tracked objects - Add DEBUG_COLLECTABLE output for collectable objects - Update test_gc.py to expect candidates/duration in stats
1 parent 9a0511b commit d100dc2

File tree

3 files changed

+129
-40
lines changed

3 files changed

+129
-40
lines changed

Lib/test/test_gc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,7 +822,7 @@ def test_get_stats(self):
822822
for st in stats:
823823
self.assertIsInstance(st, dict)
824824
self.assertEqual(set(st),
825-
{"collected", "collections", "uncollectable"})
825+
{"collected", "collections", "uncollectable", "candidates", "duration"})
826826
self.assertGreaterEqual(st["collected"], 0)
827827
self.assertGreaterEqual(st["collections"], 0)
828828
self.assertGreaterEqual(st["uncollectable"], 0)

crates/vm/src/gc_state.rs

Lines changed: 88 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,23 @@ bitflags::bitflags! {
2828
}
2929
}
3030

31+
/// Result from a single collection run
32+
#[derive(Debug, Default)]
33+
pub struct CollectResult {
34+
pub collected: usize,
35+
pub uncollectable: usize,
36+
pub candidates: usize,
37+
pub duration: f64,
38+
}
39+
3140
/// Statistics for a single generation (gc_generation_stats)
3241
#[derive(Debug, Default)]
3342
pub struct GcStats {
3443
pub collections: usize,
3544
pub collected: usize,
3645
pub uncollectable: usize,
46+
pub candidates: usize,
47+
pub duration: f64,
3748
}
3849

3950
/// A single GC generation with intrusive linked list
@@ -55,6 +66,8 @@ impl GcGeneration {
5566
collections: 0,
5667
collected: 0,
5768
uncollectable: 0,
69+
candidates: 0,
70+
duration: 0.0,
5871
}),
5972
}
6073
}
@@ -77,14 +90,24 @@ impl GcGeneration {
7790
collections: guard.collections,
7891
collected: guard.collected,
7992
uncollectable: guard.uncollectable,
93+
candidates: guard.candidates,
94+
duration: guard.duration,
8095
}
8196
}
8297

83-
pub fn update_stats(&self, collected: usize, uncollectable: usize) {
98+
pub fn update_stats(
99+
&self,
100+
collected: usize,
101+
uncollectable: usize,
102+
candidates: usize,
103+
duration: f64,
104+
) {
84105
let mut guard = self.stats.lock();
85106
guard.collections += 1;
86107
guard.collected += collected;
87108
guard.uncollectable += uncollectable;
109+
guard.candidates += candidates;
110+
guard.duration += duration;
88111
}
89112

90113
/// Reset the stats mutex to unlocked state after fork().
@@ -346,25 +369,27 @@ impl GcState {
346369
}
347370

348371
/// Perform garbage collection on the given generation
349-
pub fn collect(&self, generation: usize) -> (usize, usize) {
372+
pub fn collect(&self, generation: usize) -> CollectResult {
350373
self.collect_inner(generation, false)
351374
}
352375

353376
/// Force collection even if GC is disabled (for manual gc.collect() calls)
354-
pub fn collect_force(&self, generation: usize) -> (usize, usize) {
377+
pub fn collect_force(&self, generation: usize) -> CollectResult {
355378
self.collect_inner(generation, true)
356379
}
357380

358-
fn collect_inner(&self, generation: usize, force: bool) -> (usize, usize) {
381+
fn collect_inner(&self, generation: usize, force: bool) -> CollectResult {
359382
if !force && !self.is_enabled() {
360-
return (0, 0);
383+
return CollectResult::default();
361384
}
362385

363386
// Try to acquire the collecting lock
364387
let Some(_guard) = self.collecting.try_lock() else {
365-
return (0, 0);
388+
return CollectResult::default();
366389
};
367390

391+
let start_time = std::time::Instant::now();
392+
368393
// Memory barrier to ensure visibility of all reference count updates
369394
// from other threads before we start analyzing the object graph.
370395
core::sync::atomic::fence(Ordering::SeqCst);
@@ -392,11 +417,21 @@ impl GcState {
392417
}
393418

394419
if collecting.is_empty() {
395-
self.generations[0].count.store(0, Ordering::SeqCst);
396-
self.generations[generation].update_stats(0, 0);
397-
return (0, 0);
420+
for i in 0..=generation {
421+
self.generations[i].count.store(0, Ordering::SeqCst);
422+
}
423+
let duration = start_time.elapsed().as_secs_f64();
424+
self.generations[generation].update_stats(0, 0, 0, duration);
425+
return CollectResult {
426+
collected: 0,
427+
uncollectable: 0,
428+
candidates: 0,
429+
duration,
430+
};
398431
}
399432

433+
let candidates = collecting.len();
434+
400435
if debug.contains(GcDebugFlags::STATS) {
401436
eprintln!(
402437
"gc: collecting {} objects from generations 0..={}",
@@ -495,9 +530,17 @@ impl GcState {
495530
if unreachable.is_empty() {
496531
drop(gen_locks);
497532
self.promote_survivors(generation, &survivor_refs);
498-
self.generations[0].count.store(0, Ordering::SeqCst);
499-
self.generations[generation].update_stats(0, 0);
500-
return (0, 0);
533+
for i in 0..=generation {
534+
self.generations[i].count.store(0, Ordering::SeqCst);
535+
}
536+
let duration = start_time.elapsed().as_secs_f64();
537+
self.generations[generation].update_stats(0, 0, candidates, duration);
538+
return CollectResult {
539+
collected: 0,
540+
uncollectable: 0,
541+
candidates,
542+
duration,
543+
};
501544
}
502545

503546
// Release read locks before finalization phase.
@@ -507,9 +550,17 @@ impl GcState {
507550

508551
if unreachable_refs.is_empty() {
509552
self.promote_survivors(generation, &survivor_refs);
510-
self.generations[0].count.store(0, Ordering::SeqCst);
511-
self.generations[generation].update_stats(0, 0);
512-
return (0, 0);
553+
for i in 0..=generation {
554+
self.generations[i].count.store(0, Ordering::SeqCst);
555+
}
556+
let duration = start_time.elapsed().as_secs_f64();
557+
self.generations[generation].update_stats(0, 0, candidates, duration);
558+
return CollectResult {
559+
collected: 0,
560+
uncollectable: 0,
561+
candidates,
562+
duration,
563+
};
513564
}
514565

515566
// 6b: Record initial strong counts (for resurrection detection)
@@ -612,6 +663,16 @@ impl GcState {
612663
// Resurrected objects stay tracked — just drop our references
613664
drop(resurrected);
614665

666+
if debug.contains(GcDebugFlags::COLLECTABLE) {
667+
for obj in &truly_dead {
668+
eprintln!(
669+
"gc: collectable <{} {:p}>",
670+
obj.class().name(),
671+
obj.as_ref()
672+
);
673+
}
674+
}
675+
615676
if debug.contains(GcDebugFlags::SAVEALL) {
616677
let mut garbage_guard = self.garbage.lock();
617678
for obj_ref in truly_dead.iter() {
@@ -633,12 +694,20 @@ impl GcState {
633694
});
634695
}
635696

636-
// Reset gen0 count
637-
self.generations[0].count.store(0, Ordering::SeqCst);
697+
// Reset counts for all collected generations
698+
for i in 0..=generation {
699+
self.generations[i].count.store(0, Ordering::SeqCst);
700+
}
638701

639-
self.generations[generation].update_stats(collected, 0);
702+
let duration = start_time.elapsed().as_secs_f64();
703+
self.generations[generation].update_stats(collected, 0, candidates, duration);
640704

641-
(collected, 0)
705+
CollectResult {
706+
collected,
707+
uncollectable: 0,
708+
candidates,
709+
duration,
710+
}
642711
}
643712

644713
/// Promote surviving objects to the next generation.

crates/vm/src/stdlib/gc.rs

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,11 @@ mod gc {
5555
}
5656

5757
// Invoke callbacks with "start" phase
58-
invoke_callbacks(vm, "start", generation_num as usize, 0, 0);
58+
invoke_callbacks(vm, "start", generation_num as usize, &Default::default());
5959

6060
// Manual gc.collect() should run even if GC is disabled
6161
let gc = gc_state::gc_state();
62-
let (collected, uncollectable) = gc.collect_force(generation_num as usize);
62+
let result = gc.collect_force(generation_num as usize);
6363

6464
// Move objects from gc_state.garbage to vm.ctx.gc_garbage (for DEBUG_SAVEALL)
6565
{
@@ -74,26 +74,21 @@ mod gc {
7474
}
7575

7676
// Invoke callbacks with "stop" phase
77-
invoke_callbacks(
78-
vm,
79-
"stop",
80-
generation_num as usize,
81-
collected,
82-
uncollectable,
83-
);
77+
invoke_callbacks(vm, "stop", generation_num as usize, &result);
8478

85-
Ok(collected as i32)
79+
Ok((result.collected + result.uncollectable) as i32)
8680
}
8781

8882
/// Return the current collection thresholds as a tuple.
83+
/// The third value is always 0 (matching 3.13+).
8984
#[pyfunction]
9085
fn get_threshold(vm: &VirtualMachine) -> PyObjectRef {
91-
let (t0, t1, t2) = gc_state::gc_state().get_threshold();
86+
let (t0, t1, _t2) = gc_state::gc_state().get_threshold();
9287
vm.ctx
9388
.new_tuple(vec![
9489
vm.ctx.new_int(t0).into(),
9590
vm.ctx.new_int(t1).into(),
96-
vm.ctx.new_int(t2).into(),
91+
vm.ctx.new_int(0).into(),
9792
])
9893
.into()
9994
}
@@ -148,6 +143,8 @@ mod gc {
148143
vm.ctx.new_int(stat.uncollectable).into(),
149144
vm,
150145
)?;
146+
dict.set_item("candidates", vm.ctx.new_int(stat.candidates).into(), vm)?;
147+
dict.set_item("duration", vm.ctx.new_float(stat.duration).into(), vm)?;
151148
result.push(dict.into());
152149
}
153150

@@ -189,10 +186,30 @@ mod gc {
189186
/// Return the list of objects that directly refer to any of the arguments.
190187
#[pyfunction]
191188
fn get_referrers(args: FuncArgs, vm: &VirtualMachine) -> PyListRef {
192-
// This is expensive: we need to scan all tracked objects
193-
// For now, return an empty list (would need full object tracking to implement)
194-
let _ = args;
195-
vm.ctx.new_list(vec![])
189+
use std::collections::HashSet;
190+
191+
// Build a set of target object pointers for fast lookup
192+
let targets: HashSet<usize> = args
193+
.args
194+
.iter()
195+
.map(|obj| obj.as_ref() as *const crate::PyObject as usize)
196+
.collect();
197+
198+
let mut result = Vec::new();
199+
200+
// Scan all tracked objects across all generations
201+
let all_objects = gc_state::gc_state().get_objects(None);
202+
for obj in all_objects {
203+
let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
204+
for child_ptr in referent_ptrs {
205+
if targets.contains(&(child_ptr.as_ptr() as usize)) {
206+
result.push(obj.clone());
207+
break;
208+
}
209+
}
210+
}
211+
212+
vm.ctx.new_list(result)
196213
}
197214

198215
/// Return True if the object is tracked by the garbage collector.
@@ -243,8 +260,7 @@ mod gc {
243260
vm: &VirtualMachine,
244261
phase: &str,
245262
generation: usize,
246-
collected: usize,
247-
uncollectable: usize,
263+
result: &gc_state::CollectResult,
248264
) {
249265
let callbacks_list = &vm.ctx.gc_callbacks;
250266
let callbacks: Vec<PyObjectRef> = callbacks_list.borrow_vec().to_vec();
@@ -255,8 +271,12 @@ mod gc {
255271
let phase_str: PyObjectRef = vm.ctx.new_str(phase).into();
256272
let info = vm.ctx.new_dict();
257273
let _ = info.set_item("generation", vm.ctx.new_int(generation).into(), vm);
258-
let _ = info.set_item("collected", vm.ctx.new_int(collected).into(), vm);
259-
let _ = info.set_item("uncollectable", vm.ctx.new_int(uncollectable).into(), vm);
274+
let _ = info.set_item("collected", vm.ctx.new_int(result.collected).into(), vm);
275+
let _ = info.set_item(
276+
"uncollectable",
277+
vm.ctx.new_int(result.uncollectable).into(),
278+
vm,
279+
);
260280

261281
for callback in callbacks {
262282
let _ = callback.call((phase_str.clone(), info.clone()), vm);

0 commit comments

Comments
 (0)
X Tutup