mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
gc: add CollectResult, stats fields, get_referrers, and fix count reset (#7354)
* 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 * Update test_gc from v3.14.3 * Update test_gc.py from CPython v3.15.0a5 Taken from v3.15 (not v3.14.3) because get_stats() candidates/duration fields were added in 3.13+ and the corresponding test assertions only exist in 3.15. * Fix gc_state build on wasm32: skip Instant timing * Add candidates/duration to gc callback info, mark v3.15 test failures * Fix gc.get_referrers to exclude executing frames, fix Future cancelled exc leak - get_referrers: skip frame objects on the execution stack, since they are not GC-tracked in CPython (_PyInterpreterFrame) - _asyncio Future/Task make_cancelled_error_impl: clear the stored cancelled exception after returning it, matching the Python _make_cancelled_error behavior * Fix gc.get_threshold to return actual gen2 threshold value * Fix inconsistent GC count reset in early-return paths Use the same reset_end formula in unreachable-empty early returns as in the main collection path and collecting-empty path. * Accept keyword arguments in socket.__init__ Use a FromArgs struct instead of a positional-only tuple so that family, type, proto, and fileno can be passed as keyword arguments. * Disable comp_inlined in symbol table to match compiler The compiler does not yet implement PEP 709 inlined comprehensions (is_inlined_comprehension_context always returns false), but the symbol table was marking comprehensions as inlined. This mismatch could cause comprehension-local symbols to be merged into the parent scope while the compiler still looks them up in a separate scope. --------- Co-authored-by: CPython Developers <>
This commit is contained in:
1 parent
2853cca4c8
commit
23c8eb80fb
8 files changed
+349
-94
No files matched your search
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
# Run by test_gc.
|
||||
from test import support
|
||||
import _testinternalcapi
|
||||
import gc
|
||||
import unittest
|
||||
|
||||
class IncrementalGCTests(unittest.TestCase):
|
||||
|
||||
# Use small increments to emulate longer running process in a shorter time
|
||||
@support.gc_threshold(200, 10)
|
||||
def test_incremental_gc_handles_fast_cycle_creation(self):
|
||||
|
||||
class LinkedList:
|
||||
|
||||
#Use slots to reduce number of implicit objects
|
||||
__slots__ = "next", "prev", "surprise"
|
||||
|
||||
def __init__(self, next=None, prev=None):
|
||||
self.next = next
|
||||
if next is not None:
|
||||
next.prev = self
|
||||
self.prev = prev
|
||||
if prev is not None:
|
||||
prev.next = self
|
||||
|
||||
def make_ll(depth):
|
||||
head = LinkedList()
|
||||
for i in range(depth):
|
||||
head = LinkedList(head, head.prev)
|
||||
return head
|
||||
|
||||
head = make_ll(1000)
|
||||
|
||||
assert(gc.isenabled())
|
||||
olds = []
|
||||
initial_heap_size = _testinternalcapi.get_tracked_heap_size()
|
||||
for i in range(20_000):
|
||||
newhead = make_ll(20)
|
||||
newhead.surprise = head
|
||||
olds.append(newhead)
|
||||
if len(olds) == 20:
|
||||
new_objects = _testinternalcapi.get_tracked_heap_size() - initial_heap_size
|
||||
self.assertLess(new_objects, 27_000, f"Heap growing. Reached limit after {i} iterations")
|
||||
del olds[:]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Vendored
+98
-17
@@ -236,6 +236,8 @@ class GCTests(unittest.TestCase):
|
||||
# is 3 because it includes f's code object.
|
||||
self.assertIn(gc.collect(), (2, 3))
|
||||
|
||||
# TODO: RUSTPYTHON - weakref clear ordering differs from 3.15+
|
||||
@unittest.expectedFailure
|
||||
def test_function_tp_clear_leaves_consistent_state(self):
|
||||
# https://github.com/python/cpython/issues/91636
|
||||
code = """if 1:
|
||||
@@ -262,9 +264,11 @@ class GCTests(unittest.TestCase):
|
||||
# finalizer.
|
||||
def __del__(self):
|
||||
|
||||
# 5. Create a weakref to `func` now. If we had created
|
||||
# it earlier, it would have been cleared by the
|
||||
# garbage collector before calling the finalizers.
|
||||
# 5. Create a weakref to `func` now. In previous
|
||||
# versions of Python, this would avoid having it
|
||||
# cleared by the garbage collector before calling
|
||||
# the finalizers. Now, weakrefs get cleared after
|
||||
# calling finalizers.
|
||||
self[1].ref = weakref.ref(self[0])
|
||||
|
||||
# 6. Drop the global reference to `latefin`. The only
|
||||
@@ -293,16 +297,42 @@ class GCTests(unittest.TestCase):
|
||||
# which will find `cyc` and `func` as garbage.
|
||||
gc.collect()
|
||||
|
||||
# 9. Previously, this would crash because `func_qualname`
|
||||
# had been NULL-ed out by func_clear().
|
||||
# 9. Previously, this would crash because the weakref
|
||||
# created in the finalizer revealed the function after
|
||||
# `tp_clear` was called and `func_qualname`
|
||||
# had been NULL-ed out by func_clear(). Now, we clear
|
||||
# weakrefs to unreachable objects before calling `tp_clear`
|
||||
# but after calling finalizers.
|
||||
print(f"{func=}")
|
||||
"""
|
||||
# We're mostly just checking that this doesn't crash.
|
||||
rc, stdout, stderr = assert_python_ok("-c", code)
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertRegex(stdout, rb"""\A\s*func=<function at \S+>\s*\z""")
|
||||
# The `func` global is None because the weakref was cleared.
|
||||
self.assertRegex(stdout, rb"""\A\s*func=None""")
|
||||
self.assertFalse(stderr)
|
||||
|
||||
# TODO: RUSTPYTHON - _datetime module not available
|
||||
@unittest.expectedFailure
|
||||
def test_datetime_weakref_cycle(self):
|
||||
# https://github.com/python/cpython/issues/132413
|
||||
# If the weakref used by the datetime extension gets cleared by the GC (due to being
|
||||
# in an unreachable cycle) then datetime functions would crash (get_module_state()
|
||||
# was returning a NULL pointer). This bug is fixed by clearing weakrefs without
|
||||
# callbacks *after* running finalizers.
|
||||
code = """if 1:
|
||||
import _datetime
|
||||
class C:
|
||||
def __del__(self):
|
||||
print('__del__ called')
|
||||
_datetime.timedelta(days=1) # crash?
|
||||
|
||||
l = [C()]
|
||||
l.append(l)
|
||||
"""
|
||||
rc, stdout, stderr = assert_python_ok("-c", code)
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertEqual(stdout.strip(), b'__del__ called')
|
||||
|
||||
@refcount_test
|
||||
def test_frame(self):
|
||||
def f():
|
||||
@@ -652,9 +682,8 @@ class GCTests(unittest.TestCase):
|
||||
gc.collect()
|
||||
self.assertEqual(len(ouch), 2) # else the callbacks didn't run
|
||||
for x in ouch:
|
||||
# If the callback resurrected one of these guys, the instance
|
||||
# would be damaged, with an empty __dict__.
|
||||
self.assertEqual(x, None)
|
||||
# The weakref should be cleared before executing the callback.
|
||||
self.assertIsNone(x)
|
||||
|
||||
def test_bug21435(self):
|
||||
# This is a poor test - its only virtue is that it happened to
|
||||
@@ -821,11 +850,15 @@ class GCTests(unittest.TestCase):
|
||||
self.assertEqual(len(stats), 3)
|
||||
for st in stats:
|
||||
self.assertIsInstance(st, dict)
|
||||
self.assertEqual(set(st),
|
||||
{"collected", "collections", "uncollectable"})
|
||||
self.assertEqual(
|
||||
set(st),
|
||||
{"collected", "collections", "uncollectable", "candidates", "duration"}
|
||||
)
|
||||
self.assertGreaterEqual(st["collected"], 0)
|
||||
self.assertGreaterEqual(st["collections"], 0)
|
||||
self.assertGreaterEqual(st["uncollectable"], 0)
|
||||
self.assertGreaterEqual(st["candidates"], 0)
|
||||
self.assertGreaterEqual(st["duration"], 0)
|
||||
# Check that collection counts are incremented correctly
|
||||
if gc.isenabled():
|
||||
self.addCleanup(gc.enable)
|
||||
@@ -836,11 +869,25 @@ class GCTests(unittest.TestCase):
|
||||
self.assertEqual(new[0]["collections"], old[0]["collections"] + 1)
|
||||
self.assertEqual(new[1]["collections"], old[1]["collections"])
|
||||
self.assertEqual(new[2]["collections"], old[2]["collections"])
|
||||
self.assertGreater(new[0]["duration"], old[0]["duration"])
|
||||
self.assertEqual(new[1]["duration"], old[1]["duration"])
|
||||
self.assertEqual(new[2]["duration"], old[2]["duration"])
|
||||
for stat in ["collected", "uncollectable", "candidates"]:
|
||||
self.assertGreaterEqual(new[0][stat], old[0][stat])
|
||||
self.assertEqual(new[1][stat], old[1][stat])
|
||||
self.assertEqual(new[2][stat], old[2][stat])
|
||||
gc.collect(2)
|
||||
new = gc.get_stats()
|
||||
self.assertEqual(new[0]["collections"], old[0]["collections"] + 1)
|
||||
old, new = new, gc.get_stats()
|
||||
self.assertEqual(new[0]["collections"], old[0]["collections"])
|
||||
self.assertEqual(new[1]["collections"], old[1]["collections"])
|
||||
self.assertEqual(new[2]["collections"], old[2]["collections"] + 1)
|
||||
self.assertEqual(new[0]["duration"], old[0]["duration"])
|
||||
self.assertEqual(new[1]["duration"], old[1]["duration"])
|
||||
self.assertGreater(new[2]["duration"], old[2]["duration"])
|
||||
for stat in ["collected", "uncollectable", "candidates"]:
|
||||
self.assertEqual(new[0][stat], old[0][stat])
|
||||
self.assertEqual(new[1][stat], old[1][stat])
|
||||
self.assertGreaterEqual(new[2][stat], old[2][stat])
|
||||
|
||||
def test_freeze(self):
|
||||
gc.freeze()
|
||||
@@ -1156,6 +1203,37 @@ class GCTests(unittest.TestCase):
|
||||
""")
|
||||
assert_python_ok("-c", source)
|
||||
|
||||
def test_do_not_cleanup_type_subclasses_before_finalization(self):
|
||||
# See https://github.com/python/cpython/issues/135552
|
||||
# If we cleanup weakrefs for tp_subclasses before calling
|
||||
# the finalizer (__del__) then the line `fail = BaseNode.next.next`
|
||||
# should fail because we are trying to access a subclass
|
||||
# attribute. But subclass type cache was not properly invalidated.
|
||||
code = """
|
||||
class BaseNode:
|
||||
def __del__(self):
|
||||
BaseNode.next = BaseNode.next.next
|
||||
fail = BaseNode.next.next
|
||||
|
||||
class Node(BaseNode):
|
||||
pass
|
||||
|
||||
BaseNode.next = Node()
|
||||
BaseNode.next.next = Node()
|
||||
"""
|
||||
# this test checks garbage collection while interp
|
||||
# finalization
|
||||
assert_python_ok("-c", textwrap.dedent(code))
|
||||
|
||||
code_inside_function = textwrap.dedent(F"""
|
||||
def test():
|
||||
{textwrap.indent(code, ' ')}
|
||||
|
||||
test()
|
||||
""")
|
||||
# this test checks regular garbage collection
|
||||
assert_python_ok("-c", code_inside_function)
|
||||
|
||||
|
||||
@unittest.skipUnless(Py_GIL_DISABLED, "requires free-threaded GC")
|
||||
@unittest.skipIf(_testinternalcapi is None, "requires _testinternalcapi")
|
||||
@@ -1260,9 +1338,11 @@ class GCCallbackTests(unittest.TestCase):
|
||||
# Check that we got the right info dict for all callbacks
|
||||
for v in self.visit:
|
||||
info = v[2]
|
||||
self.assertTrue("generation" in info)
|
||||
self.assertTrue("collected" in info)
|
||||
self.assertTrue("uncollectable" in info)
|
||||
self.assertIn("generation", info)
|
||||
self.assertIn("collected", info)
|
||||
self.assertIn("uncollectable", info)
|
||||
self.assertIn("candidates", info)
|
||||
self.assertIn("duration", info)
|
||||
|
||||
def test_collect_generation(self):
|
||||
self.preclean()
|
||||
@@ -1450,6 +1530,7 @@ class GCTogglingTests(unittest.TestCase):
|
||||
self.assertEqual(x, None)
|
||||
|
||||
@gc_threshold(1000, 0, 0)
|
||||
@unittest.skipIf(Py_GIL_DISABLED, "requires GC generations or increments")
|
||||
def test_bug1055820d(self):
|
||||
# Corresponds to temp2d.py in the bug report. This is very much like
|
||||
# test_bug1055820c, but uses a __del__ method instead of a weakref
|
||||
|
||||
Vendored
-1
@@ -561,7 +561,6 @@ class ComprehensionTests(unittest.TestCase):
|
||||
for ch in st.get_children():
|
||||
self.get_identifiers_recursive(ch, res)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: 2 != 1
|
||||
def test_loopvar_in_only_one_scope(self):
|
||||
# ensure that the loop variable appears only once in the symtable
|
||||
comps = [
|
||||
|
||||
@@ -2037,20 +2037,13 @@ impl SymbolTableBuilder {
|
||||
self.line_index_start(range),
|
||||
);
|
||||
|
||||
// Mark non-generator comprehensions as inlined (PEP 709)
|
||||
// inline_comp = entry->ste_comprehension && !entry->ste_generator && !ste->ste_can_see_class_scope
|
||||
// We check is_generator and can_see_class_scope of parent
|
||||
let parent_can_see_class = self
|
||||
.tables
|
||||
.get(self.tables.len().saturating_sub(2))
|
||||
.map(|t| t.can_see_class_scope)
|
||||
.unwrap_or(false);
|
||||
if !is_generator
|
||||
&& !parent_can_see_class
|
||||
&& let Some(table) = self.tables.last_mut()
|
||||
{
|
||||
table.comp_inlined = true;
|
||||
}
|
||||
// PEP 709: inlined comprehensions are not yet implemented in the
|
||||
// compiler (is_inlined_comprehension_context always returns false),
|
||||
// so do NOT mark comp_inlined here. Setting it would cause the
|
||||
// symbol-table analyzer to merge comprehension-local symbols into
|
||||
// the parent scope, while the compiler still emits a separate code
|
||||
// object — leading to the merged symbols being missing from the
|
||||
// comprehension's own symbol table lookup.
|
||||
|
||||
// Register the passed argument to the generator function as the name ".0"
|
||||
self.register_name(".0", SymbolUsage::Parameter, range)?;
|
||||
|
||||
@@ -499,7 +499,8 @@ pub(crate) mod _asyncio {
|
||||
}
|
||||
|
||||
fn make_cancelled_error_impl(&self, vm: &VirtualMachine) -> PyBaseExceptionRef {
|
||||
if let Some(exc) = self.fut_cancelled_exc.read().clone()
|
||||
// If a saved CancelledError exists, take it (clearing the stored reference)
|
||||
if let Some(exc) = self.fut_cancelled_exc.write().take()
|
||||
&& let Ok(exc) = exc.downcast::<PyBaseException>()
|
||||
{
|
||||
return exc;
|
||||
@@ -508,12 +509,10 @@ pub(crate) mod _asyncio {
|
||||
let msg = self.fut_cancel_msg.read().clone();
|
||||
let args = if let Some(m) = msg { vec![m] } else { vec![] };
|
||||
|
||||
let exc = match get_cancelled_error_type(vm) {
|
||||
match get_cancelled_error_type(vm) {
|
||||
Ok(cancelled_error) => vm.new_exception(cancelled_error, args),
|
||||
Err(_) => vm.new_runtime_error("cancelled"),
|
||||
};
|
||||
*self.fut_cancelled_exc.write() = Some(exc.clone().into());
|
||||
exc
|
||||
}
|
||||
}
|
||||
|
||||
fn schedule_callbacks(zelf: &PyRef<Self>, vm: &VirtualMachine) -> PyResult<()> {
|
||||
@@ -1309,7 +1308,8 @@ pub(crate) mod _asyncio {
|
||||
}
|
||||
|
||||
fn make_cancelled_error_impl(&self, vm: &VirtualMachine) -> PyBaseExceptionRef {
|
||||
if let Some(exc) = self.base.fut_cancelled_exc.read().clone()
|
||||
// If a saved CancelledError exists, take it (clearing the stored reference)
|
||||
if let Some(exc) = self.base.fut_cancelled_exc.write().take()
|
||||
&& let Ok(exc) = exc.downcast::<PyBaseException>()
|
||||
{
|
||||
return exc;
|
||||
@@ -1318,12 +1318,10 @@ pub(crate) mod _asyncio {
|
||||
let msg = self.base.fut_cancel_msg.read().clone();
|
||||
let args = if let Some(m) = msg { vec![m] } else { vec![] };
|
||||
|
||||
let exc = match get_cancelled_error_type(vm) {
|
||||
match get_cancelled_error_type(vm) {
|
||||
Ok(cancelled_error) => vm.new_exception(cancelled_error, args),
|
||||
Err(_) => vm.new_runtime_error("cancelled"),
|
||||
};
|
||||
*self.base.fut_cancelled_exc.write() = Some(exc.clone().into());
|
||||
exc
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethod]
|
||||
|
||||
+18
-10
@@ -1384,13 +1384,20 @@ mod _socket {
|
||||
|
||||
impl DefaultConstructor for PySocket {}
|
||||
|
||||
#[derive(FromArgs)]
|
||||
pub struct SocketInitArgs {
|
||||
#[pyarg(any, optional)]
|
||||
family: OptionalArg<i32>,
|
||||
#[pyarg(any, optional)]
|
||||
r#type: OptionalArg<i32>,
|
||||
#[pyarg(any, optional)]
|
||||
proto: OptionalArg<i32>,
|
||||
#[pyarg(any, optional)]
|
||||
fileno: OptionalOption<PyObjectRef>,
|
||||
}
|
||||
|
||||
impl Initializer for PySocket {
|
||||
type Args = (
|
||||
OptionalArg<i32>,
|
||||
OptionalArg<i32>,
|
||||
OptionalArg<i32>,
|
||||
OptionalOption<PyObjectRef>,
|
||||
);
|
||||
type Args = SocketInitArgs;
|
||||
|
||||
fn init(zelf: PyRef<Self>, args: Self::Args, vm: &VirtualMachine) -> PyResult<()> {
|
||||
Self::_init(zelf, args, vm).map_err(|e| e.into_pyexception(vm))
|
||||
@@ -1414,13 +1421,14 @@ mod _socket {
|
||||
impl PySocket {
|
||||
fn _init(
|
||||
zelf: PyRef<Self>,
|
||||
(family, socket_kind, proto, fileno): <Self as Initializer>::Args,
|
||||
args: <Self as Initializer>::Args,
|
||||
vm: &VirtualMachine,
|
||||
) -> Result<(), IoOrPyException> {
|
||||
let mut family = family.unwrap_or(-1);
|
||||
let mut socket_kind = socket_kind.unwrap_or(-1);
|
||||
let mut proto = proto.unwrap_or(-1);
|
||||
let mut family = args.family.unwrap_or(-1);
|
||||
let mut socket_kind = args.r#type.unwrap_or(-1);
|
||||
let mut proto = args.proto.unwrap_or(-1);
|
||||
|
||||
let fileno = args.fileno;
|
||||
let sock;
|
||||
|
||||
// On Windows, fileno can be bytes from socket.share() for fromshare()
|
||||
|
||||
+112
-24
@@ -1,7 +1,6 @@
|
||||
//! Garbage Collection State and Algorithm
|
||||
//!
|
||||
//! This module implements CPython-compatible generational garbage collection
|
||||
//! for RustPython, using an intrusive doubly-linked list approach.
|
||||
//! Generational garbage collection using an intrusive doubly-linked list.
|
||||
|
||||
use crate::common::linked_list::LinkedList;
|
||||
use crate::common::lock::{PyMutex, PyRwLock};
|
||||
@@ -11,6 +10,16 @@ use core::ptr::NonNull;
|
||||
use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn elapsed_secs(start: &std::time::Instant) -> f64 {
|
||||
start.elapsed().as_secs_f64()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn elapsed_secs(_start: &()) -> f64 {
|
||||
0.0
|
||||
}
|
||||
|
||||
bitflags::bitflags! {
|
||||
/// GC debug flags (see Include/internal/pycore_gc.h)
|
||||
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
||||
@@ -28,12 +37,23 @@ bitflags::bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
/// Result from a single collection run
|
||||
#[derive(Debug, Default)]
|
||||
pub struct CollectResult {
|
||||
pub collected: usize,
|
||||
pub uncollectable: usize,
|
||||
pub candidates: usize,
|
||||
pub duration: f64,
|
||||
}
|
||||
|
||||
/// Statistics for a single generation (gc_generation_stats)
|
||||
#[derive(Debug, Default)]
|
||||
pub struct GcStats {
|
||||
pub collections: usize,
|
||||
pub collected: usize,
|
||||
pub uncollectable: usize,
|
||||
pub candidates: usize,
|
||||
pub duration: f64,
|
||||
}
|
||||
|
||||
/// A single GC generation with intrusive linked list
|
||||
@@ -55,6 +75,8 @@ impl GcGeneration {
|
||||
collections: 0,
|
||||
collected: 0,
|
||||
uncollectable: 0,
|
||||
candidates: 0,
|
||||
duration: 0.0,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -77,14 +99,24 @@ impl GcGeneration {
|
||||
collections: guard.collections,
|
||||
collected: guard.collected,
|
||||
uncollectable: guard.uncollectable,
|
||||
candidates: guard.candidates,
|
||||
duration: guard.duration,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_stats(&self, collected: usize, uncollectable: usize) {
|
||||
pub fn update_stats(
|
||||
&self,
|
||||
collected: usize,
|
||||
uncollectable: usize,
|
||||
candidates: usize,
|
||||
duration: f64,
|
||||
) {
|
||||
let mut guard = self.stats.lock();
|
||||
guard.collections += 1;
|
||||
guard.collected += collected;
|
||||
guard.uncollectable += uncollectable;
|
||||
guard.candidates += candidates;
|
||||
guard.duration += duration;
|
||||
}
|
||||
|
||||
/// Reset the stats mutex to unlocked state after fork().
|
||||
@@ -340,25 +372,30 @@ impl GcState {
|
||||
}
|
||||
|
||||
/// Perform garbage collection on the given generation
|
||||
pub fn collect(&self, generation: usize) -> (usize, usize) {
|
||||
pub fn collect(&self, generation: usize) -> CollectResult {
|
||||
self.collect_inner(generation, false)
|
||||
}
|
||||
|
||||
/// Force collection even if GC is disabled (for manual gc.collect() calls)
|
||||
pub fn collect_force(&self, generation: usize) -> (usize, usize) {
|
||||
pub fn collect_force(&self, generation: usize) -> CollectResult {
|
||||
self.collect_inner(generation, true)
|
||||
}
|
||||
|
||||
fn collect_inner(&self, generation: usize, force: bool) -> (usize, usize) {
|
||||
fn collect_inner(&self, generation: usize, force: bool) -> CollectResult {
|
||||
if !force && !self.is_enabled() {
|
||||
return (0, 0);
|
||||
return CollectResult::default();
|
||||
}
|
||||
|
||||
// Try to acquire the collecting lock
|
||||
let Some(_guard) = self.collecting.try_lock() else {
|
||||
return (0, 0);
|
||||
return CollectResult::default();
|
||||
};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let start_time = std::time::Instant::now();
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let start_time = ();
|
||||
|
||||
// Memory barrier to ensure visibility of all reference count updates
|
||||
// from other threads before we start analyzing the object graph.
|
||||
core::sync::atomic::fence(Ordering::SeqCst);
|
||||
@@ -386,11 +423,24 @@ impl GcState {
|
||||
}
|
||||
|
||||
if collecting.is_empty() {
|
||||
self.generations[0].count.store(0, Ordering::SeqCst);
|
||||
self.generations[generation].update_stats(0, 0);
|
||||
return (0, 0);
|
||||
// Reset counts for generations whose objects were promoted away.
|
||||
// For gen2 (oldest), survivors stay in-place so don't reset gen2 count.
|
||||
let reset_end = if generation >= 2 { 2 } else { generation + 1 };
|
||||
for i in 0..reset_end {
|
||||
self.generations[i].count.store(0, Ordering::SeqCst);
|
||||
}
|
||||
let duration = elapsed_secs(&start_time);
|
||||
self.generations[generation].update_stats(0, 0, 0, duration);
|
||||
return CollectResult {
|
||||
collected: 0,
|
||||
uncollectable: 0,
|
||||
candidates: 0,
|
||||
duration,
|
||||
};
|
||||
}
|
||||
|
||||
let candidates = collecting.len();
|
||||
|
||||
if debug.contains(GcDebugFlags::STATS) {
|
||||
eprintln!(
|
||||
"gc: collecting {} objects from generations 0..={}",
|
||||
@@ -486,9 +536,18 @@ impl GcState {
|
||||
if unreachable.is_empty() {
|
||||
drop(gen_locks);
|
||||
self.promote_survivors(generation, &survivor_refs);
|
||||
self.generations[0].count.store(0, Ordering::SeqCst);
|
||||
self.generations[generation].update_stats(0, 0);
|
||||
return (0, 0);
|
||||
let reset_end = if generation >= 2 { 2 } else { generation + 1 };
|
||||
for i in 0..reset_end {
|
||||
self.generations[i].count.store(0, Ordering::SeqCst);
|
||||
}
|
||||
let duration = elapsed_secs(&start_time);
|
||||
self.generations[generation].update_stats(0, 0, candidates, duration);
|
||||
return CollectResult {
|
||||
collected: 0,
|
||||
uncollectable: 0,
|
||||
candidates,
|
||||
duration,
|
||||
};
|
||||
}
|
||||
|
||||
// Release read locks before finalization phase.
|
||||
@@ -498,9 +557,18 @@ impl GcState {
|
||||
|
||||
if unreachable_refs.is_empty() {
|
||||
self.promote_survivors(generation, &survivor_refs);
|
||||
self.generations[0].count.store(0, Ordering::SeqCst);
|
||||
self.generations[generation].update_stats(0, 0);
|
||||
return (0, 0);
|
||||
let reset_end = if generation >= 2 { 2 } else { generation + 1 };
|
||||
for i in 0..reset_end {
|
||||
self.generations[i].count.store(0, Ordering::SeqCst);
|
||||
}
|
||||
let duration = elapsed_secs(&start_time);
|
||||
self.generations[generation].update_stats(0, 0, candidates, duration);
|
||||
return CollectResult {
|
||||
collected: 0,
|
||||
uncollectable: 0,
|
||||
candidates,
|
||||
duration,
|
||||
};
|
||||
}
|
||||
|
||||
// 6b: Record initial strong counts (for resurrection detection)
|
||||
@@ -594,15 +662,25 @@ impl GcState {
|
||||
};
|
||||
|
||||
// Promote survivors to next generation BEFORE tp_clear.
|
||||
// This matches CPython's order (move_legacy_finalizer_reachable → delete_garbage)
|
||||
// and ensures survivor_refs are dropped before tp_clear, so reachable objects
|
||||
// (e.g. LateFin) aren't kept alive beyond the deferred-drop phase.
|
||||
// move_legacy_finalizer_reachable → delete_garbage order ensures
|
||||
// survivor_refs are dropped before tp_clear, so reachable objects
|
||||
// aren't kept alive beyond the deferred-drop phase.
|
||||
self.promote_survivors(generation, &survivor_refs);
|
||||
drop(survivor_refs);
|
||||
|
||||
// Resurrected objects stay tracked — just drop our references
|
||||
drop(resurrected);
|
||||
|
||||
if debug.contains(GcDebugFlags::COLLECTABLE) {
|
||||
for obj in &truly_dead {
|
||||
eprintln!(
|
||||
"gc: collectable <{} {:p}>",
|
||||
obj.class().name(),
|
||||
obj.as_ref()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if debug.contains(GcDebugFlags::SAVEALL) {
|
||||
let mut garbage_guard = self.garbage.lock();
|
||||
for obj_ref in truly_dead.iter() {
|
||||
@@ -624,12 +702,22 @@ impl GcState {
|
||||
});
|
||||
}
|
||||
|
||||
// Reset gen0 count
|
||||
self.generations[0].count.store(0, Ordering::SeqCst);
|
||||
// Reset counts for generations whose objects were promoted away.
|
||||
// For gen2 (oldest), survivors stay in-place so don't reset gen2 count.
|
||||
let reset_end = if generation >= 2 { 2 } else { generation + 1 };
|
||||
for i in 0..reset_end {
|
||||
self.generations[i].count.store(0, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
self.generations[generation].update_stats(collected, 0);
|
||||
let duration = elapsed_secs(&start_time);
|
||||
self.generations[generation].update_stats(collected, 0, candidates, duration);
|
||||
|
||||
(collected, 0)
|
||||
CollectResult {
|
||||
collected,
|
||||
uncollectable: 0,
|
||||
candidates,
|
||||
duration,
|
||||
}
|
||||
}
|
||||
|
||||
/// Promote surviving objects to the next generation.
|
||||
|
||||
+58
-18
@@ -55,11 +55,11 @@ mod gc {
|
||||
}
|
||||
|
||||
// Invoke callbacks with "start" phase
|
||||
invoke_callbacks(vm, "start", generation_num as usize, 0, 0);
|
||||
invoke_callbacks(vm, "start", generation_num as usize, &Default::default());
|
||||
|
||||
// Manual gc.collect() should run even if GC is disabled
|
||||
let gc = gc_state::gc_state();
|
||||
let (collected, uncollectable) = gc.collect_force(generation_num as usize);
|
||||
let result = gc.collect_force(generation_num as usize);
|
||||
|
||||
// Move objects from gc_state.garbage to vm.ctx.gc_garbage (for DEBUG_SAVEALL)
|
||||
{
|
||||
@@ -74,15 +74,9 @@ mod gc {
|
||||
}
|
||||
|
||||
// Invoke callbacks with "stop" phase
|
||||
invoke_callbacks(
|
||||
vm,
|
||||
"stop",
|
||||
generation_num as usize,
|
||||
collected,
|
||||
uncollectable,
|
||||
);
|
||||
invoke_callbacks(vm, "stop", generation_num as usize, &result);
|
||||
|
||||
Ok(collected as i32)
|
||||
Ok((result.collected + result.uncollectable) as i32)
|
||||
}
|
||||
|
||||
/// Return the current collection thresholds as a tuple.
|
||||
@@ -148,6 +142,8 @@ mod gc {
|
||||
vm.ctx.new_int(stat.uncollectable).into(),
|
||||
vm,
|
||||
)?;
|
||||
dict.set_item("candidates", vm.ctx.new_int(stat.candidates).into(), vm)?;
|
||||
dict.set_item("duration", vm.ctx.new_float(stat.duration).into(), vm)?;
|
||||
result.push(dict.into());
|
||||
}
|
||||
|
||||
@@ -189,10 +185,49 @@ mod gc {
|
||||
/// Return the list of objects that directly refer to any of the arguments.
|
||||
#[pyfunction]
|
||||
fn get_referrers(args: FuncArgs, vm: &VirtualMachine) -> PyListRef {
|
||||
// This is expensive: we need to scan all tracked objects
|
||||
// For now, return an empty list (would need full object tracking to implement)
|
||||
let _ = args;
|
||||
vm.ctx.new_list(vec![])
|
||||
use std::collections::HashSet;
|
||||
|
||||
// Build a set of target object pointers for fast lookup
|
||||
let targets: HashSet<usize> = args
|
||||
.args
|
||||
.iter()
|
||||
.map(|obj| obj.as_ref() as *const crate::PyObject as usize)
|
||||
.collect();
|
||||
|
||||
// Collect pointers of frames currently on the execution stack.
|
||||
// In CPython, executing frames (_PyInterpreterFrame) are not GC-tracked
|
||||
// PyObjects, so they never appear in get_referrers results. Since
|
||||
// RustPython materializes every frame as a PyObject, we must exclude
|
||||
// them manually to match the expected behavior.
|
||||
let stack_frames: HashSet<usize> = vm
|
||||
.frames
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|fp| {
|
||||
let frame: &crate::PyObject = unsafe { fp.as_ref() }.as_ref();
|
||||
frame as *const crate::PyObject as usize
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut result = Vec::new();
|
||||
|
||||
// Scan all tracked objects across all generations
|
||||
let all_objects = gc_state::gc_state().get_objects(None);
|
||||
for obj in all_objects {
|
||||
let obj_ptr = obj.as_ref() as *const crate::PyObject as usize;
|
||||
if stack_frames.contains(&obj_ptr) {
|
||||
continue;
|
||||
}
|
||||
let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
|
||||
for child_ptr in referent_ptrs {
|
||||
if targets.contains(&(child_ptr.as_ptr() as usize)) {
|
||||
result.push(obj.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vm.ctx.new_list(result)
|
||||
}
|
||||
|
||||
/// Return True if the object is tracked by the garbage collector.
|
||||
@@ -243,8 +278,7 @@ mod gc {
|
||||
vm: &VirtualMachine,
|
||||
phase: &str,
|
||||
generation: usize,
|
||||
collected: usize,
|
||||
uncollectable: usize,
|
||||
result: &gc_state::CollectResult,
|
||||
) {
|
||||
let callbacks_list = &vm.ctx.gc_callbacks;
|
||||
let callbacks: Vec<PyObjectRef> = callbacks_list.borrow_vec().to_vec();
|
||||
@@ -255,8 +289,14 @@ mod gc {
|
||||
let phase_str: PyObjectRef = vm.ctx.new_str(phase).into();
|
||||
let info = vm.ctx.new_dict();
|
||||
let _ = info.set_item("generation", vm.ctx.new_int(generation).into(), vm);
|
||||
let _ = info.set_item("collected", vm.ctx.new_int(collected).into(), vm);
|
||||
let _ = info.set_item("uncollectable", vm.ctx.new_int(uncollectable).into(), vm);
|
||||
let _ = info.set_item("collected", vm.ctx.new_int(result.collected).into(), vm);
|
||||
let _ = info.set_item(
|
||||
"uncollectable",
|
||||
vm.ctx.new_int(result.uncollectable).into(),
|
||||
vm,
|
||||
);
|
||||
let _ = info.set_item("candidates", vm.ctx.new_int(result.candidates).into(), vm);
|
||||
let _ = info.set_item("duration", vm.ctx.new_float(result.duration).into(), vm);
|
||||
|
||||
for callback in callbacks {
|
||||
let _ = callback.call((phase_str.clone(), info.clone()), vm);
|
||||
|
||||
Reference in new issue
Block a user