mirror of
https://github.com/RustPython/RustPython.git
synced 2026-10-28 18:59:04 +08:00
more algorithm-independent GC infra (#7194)
* mark poluting tests * GC-infra independent to EBR * trashcan * add overflow guard to inc(), #[must_use] on dec()/safe_inc(), trashcan debug_assert, weakref generic re-check
This commit is contained in:
1 parent
f9ca638936
commit
6950baf687
27 files changed
+780
-151
No files matched your search
@@ -195,6 +195,7 @@ uncollectable
|
||||
Unhandle
|
||||
unparse
|
||||
unparser
|
||||
untracking
|
||||
VARKEYWORDS
|
||||
varkwarg
|
||||
venvlauncher
|
||||
@@ -209,5 +210,6 @@ webpki
|
||||
winconsoleio
|
||||
withitem
|
||||
withs
|
||||
worklist
|
||||
xstat
|
||||
XXPRIME
|
||||
@@ -66,6 +66,7 @@
|
||||
"emscripten",
|
||||
"excs",
|
||||
"finalizer",
|
||||
"finalizers",
|
||||
"GetSet",
|
||||
"groupref",
|
||||
"internable",
|
||||
@@ -122,12 +123,14 @@
|
||||
"tracebacks",
|
||||
"typealiases",
|
||||
"typevartuples",
|
||||
"uncollectable",
|
||||
"unhashable",
|
||||
"uninit",
|
||||
"unraisable",
|
||||
"unresizable",
|
||||
"varint",
|
||||
"wasi",
|
||||
"weaked",
|
||||
"zelf",
|
||||
// unix
|
||||
"posixshmem",
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ winresource = "0.1"
|
||||
rustpython-compiler = { workspace = true }
|
||||
rustpython-pylib = { workspace = true, optional = true }
|
||||
rustpython-stdlib = { workspace = true, optional = true, features = ["compiler"] }
|
||||
rustpython-vm = { workspace = true, features = ["compiler"] }
|
||||
rustpython-vm = { workspace = true, features = ["compiler", "gc"] }
|
||||
ruff_python_parser = { workspace = true }
|
||||
|
||||
cfg-if = { workspace = true }
|
||||
|
||||
Vendored
-7
@@ -883,13 +883,6 @@ def disable_gc():
|
||||
|
||||
@contextlib.contextmanager
|
||||
def gc_threshold(*args):
|
||||
# TODO: RUSTPYTHON; GC is not supported yet
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
pass
|
||||
return
|
||||
|
||||
import gc
|
||||
old_threshold = gc.get_threshold()
|
||||
gc.set_threshold(*args)
|
||||
|
||||
Vendored
-1
@@ -1649,7 +1649,6 @@ class TestSSL(test_utils.TestCase):
|
||||
# SSLProtocol should be DECREF to 0
|
||||
self.assertIsNone(ctx())
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; - gc.collect() doesn't release SSLContext properly
|
||||
def test_shutdown_timeout_handler_leak(self):
|
||||
loop = self.loop
|
||||
|
||||
|
||||
Vendored
-1
@@ -5117,7 +5117,6 @@ class ClassPropertiesAndMethods(unittest.TestCase):
|
||||
cls.lst = [2**i for i in range(10000)]
|
||||
X.descr
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_remove_subclass(self):
|
||||
# bpo-46417: when the last subclass of a type is deleted,
|
||||
# remove_subclass() clears the internal dictionary of subclasses:
|
||||
|
||||
Vendored
-1
@@ -199,7 +199,6 @@ class FaultHandlerTests(unittest.TestCase):
|
||||
3,
|
||||
'Segmentation fault')
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: Regex didn't match: '(?m)^Fatal Python error: Segmentation fault\n\n<Cannot show all threads while the GIL is disabled>\nStack\\ \\(most\\ recent\\ call\\ first\\):\n File "<string>", line 9 in __del__\nCurrent thread\'s C stack trace \\(most recent call first\\):\n( Binary file ".+"(, at .*(\\+|-)0x[0-9a-f]+)? \\[0x[0-9a-f]+\\])|(<.+>)' not found in 'exit'
|
||||
@skip_segfault_on_android
|
||||
def test_gc(self):
|
||||
# bpo-44466: Detect if the GC is running
|
||||
|
||||
Vendored
-1
@@ -2105,7 +2105,6 @@ class TestLRU:
|
||||
return 1
|
||||
self.assertEqual(f.cache_parameters(), {'maxsize': 1000, "typed": True})
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; GC behavior differs from CPython's refcounting
|
||||
def test_lru_cache_weakrefable(self):
|
||||
@self.module.lru_cache
|
||||
def test_function(x):
|
||||
|
||||
-1
@@ -2714,7 +2714,6 @@ class TestGetattrStatic(unittest.TestCase):
|
||||
|
||||
self.assertFalse(test.called)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <class 'test.test_inspect.test_inspect.TestGetattrStatic.test_cache_does_not_cause_classes_to_persist.<locals>.Foo'> is not None
|
||||
def test_cache_does_not_cause_classes_to_persist(self):
|
||||
# regression test for gh-118013:
|
||||
# check that the internal _shadowed_dict cache does not cause
|
||||
|
||||
Vendored
-4
@@ -3813,7 +3813,6 @@ class TextIOWrapperTest(unittest.TestCase):
|
||||
""".format(iomod=iomod, kwargs=kwargs)
|
||||
return assert_python_ok("-c", code)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError during module teardown in __del__
|
||||
def test_create_at_shutdown_without_encoding(self):
|
||||
rc, out, err = self._check_create_at_shutdown()
|
||||
if err:
|
||||
@@ -3823,7 +3822,6 @@ class TextIOWrapperTest(unittest.TestCase):
|
||||
else:
|
||||
self.assertEqual("ok", out.decode().strip())
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError during module teardown in __del__
|
||||
def test_create_at_shutdown_with_encoding(self):
|
||||
rc, out, err = self._check_create_at_shutdown(encoding='utf-8',
|
||||
errors='strict')
|
||||
@@ -4812,13 +4810,11 @@ class CMiscIOTest(MiscIOTest):
|
||||
else:
|
||||
self.assertFalse(err.strip('.!'))
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; without GC+GIL, finalize_modules clears __main__ globals while daemon threads are still running
|
||||
@threading_helper.requires_working_threading()
|
||||
@support.requires_resource('walltime')
|
||||
def test_daemon_threads_shutdown_stdout_deadlock(self):
|
||||
self.check_daemon_threads_shutdown_deadlock('stdout')
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; without GC+GIL, finalize_modules clears __main__ globals while daemon threads are still running
|
||||
@threading_helper.requires_working_threading()
|
||||
@support.requires_resource('walltime')
|
||||
def test_daemon_threads_shutdown_stderr_deadlock(self):
|
||||
|
||||
Vendored
-2
@@ -5165,7 +5165,6 @@ class ModuleLevelMiscTest(BaseTest):
|
||||
h.close()
|
||||
logging.setLoggerClass(logging.Logger)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError during module teardown in __del__
|
||||
def test_logging_at_shutdown(self):
|
||||
# bpo-20037: Doing text I/O late at interpreter shutdown must not crash
|
||||
code = textwrap.dedent("""
|
||||
@@ -5185,7 +5184,6 @@ class ModuleLevelMiscTest(BaseTest):
|
||||
self.assertIn("exception in __del__", err)
|
||||
self.assertIn("ValueError: some error", err)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AttributeError during module teardown in __del__
|
||||
def test_logging_at_shutdown_open(self):
|
||||
# bpo-26789: FileHandler keeps a reference to the builtin open()
|
||||
# function to be able to open or reopen the file during Python
|
||||
|
||||
Vendored
-1
@@ -485,7 +485,6 @@ class PyBytesIOTest(MemoryTestMixin, MemorySeekTestMixin, unittest.TestCase):
|
||||
buf2.release()
|
||||
memio.write(b'x')
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <memory at 0xbb894d200> is not None
|
||||
def test_getbuffer_gc_collect(self):
|
||||
memio = self.ioclass(b"1234567890")
|
||||
buf = memio.getbuffer()
|
||||
|
||||
Vendored
-1
@@ -669,7 +669,6 @@ class OrderedDictTests:
|
||||
dict.update(od, [('spam', 1)])
|
||||
self.assertNotIn('NULL', repr(od))
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <class 'test.test_ordered_dict.OrderedDictTests.test_reference_loop.<locals>.A'> is not None
|
||||
def test_reference_loop(self):
|
||||
# Issue 25935
|
||||
OrderedDict = self.OrderedDict
|
||||
|
||||
Vendored
-1
@@ -3557,7 +3557,6 @@ class POSIXProcessTestCase(BaseTestCase):
|
||||
except subprocess.TimeoutExpired:
|
||||
pass
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: b'preexec_fn not supported at interpreter shutdown' not found in b"Exception ignored in: <function AtFinalization.__del__ at 0xa92f93840>\nAttributeError: 'NoneType' object has no attribute 'Popen'\n"
|
||||
def test_preexec_at_exit(self):
|
||||
code = f"""if 1:
|
||||
import atexit
|
||||
|
||||
Vendored
-2
@@ -854,7 +854,6 @@ class ThreadTests(BaseTestCase):
|
||||
def test_main_thread_after_fork_from_dummy_thread(self, create_dummy=False):
|
||||
self.test_main_thread_after_fork_from_foreign_thread(create_dummy=True)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_main_thread_during_shutdown(self):
|
||||
# bpo-31516: current_thread() should still point to the main thread
|
||||
# at shutdown
|
||||
@@ -1086,7 +1085,6 @@ class ThreadTests(BaseTestCase):
|
||||
self.assertEqual(threading.getprofile(), old_profile)
|
||||
self.assertEqual(sys.getprofile(), old_profile)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_locals_at_exit(self):
|
||||
# bpo-19466: thread locals must not be deleted before destructors
|
||||
# are called
|
||||
|
||||
Vendored
-1
@@ -472,7 +472,6 @@ class TracebackCases(unittest.TestCase):
|
||||
# Issue #18960: coding spec should have no effect
|
||||
do_test("x=0\n# coding: GBK\n", "h\xe9 ho", 'utf-8', 5)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; + b'ZeroDivisionError: division by zero']
|
||||
def test_print_traceback_at_exit(self):
|
||||
# Issue #22599: Ensure that it is possible to use the traceback module
|
||||
# to display an exception at Python exit
|
||||
|
||||
Vendored
-1
@@ -1610,7 +1610,6 @@ class BootstrapTest(unittest.TestCase):
|
||||
|
||||
|
||||
class FinalizationTest(unittest.TestCase):
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; - TypeError: 'NoneType' object is not callable
|
||||
def test_finalization(self):
|
||||
# Issue #19421: warnings.warn() should not crash
|
||||
# during Python finalization
|
||||
|
||||
Vendored
+1
-3
@@ -897,7 +897,6 @@ class ReferencesTestCase(TestBase):
|
||||
# No exception should be raised here
|
||||
gc.collect()
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: <class 'test.test_weakref.ReferencesTestCase.test_classes.<locals>.A'> != None
|
||||
def test_classes(self):
|
||||
# Check that classes are weakrefable.
|
||||
class A(object):
|
||||
@@ -1330,11 +1329,9 @@ class MappingTestCase(TestBase):
|
||||
self.assertIn(n1, (0, 1))
|
||||
self.assertEqual(n2, 0)
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_weak_keyed_len_cycles(self):
|
||||
self.check_len_cycles(weakref.WeakKeyDictionary, lambda k: (k, 1))
|
||||
|
||||
@unittest.expectedFailure # TODO: RUSTPYTHON
|
||||
def test_weak_valued_len_cycles(self):
|
||||
self.check_len_cycles(weakref.WeakValueDictionary, lambda k: (1, k))
|
||||
|
||||
@@ -1952,6 +1949,7 @@ class MappingTestCase(TestBase):
|
||||
x = d.pop(10, 10)
|
||||
self.assertIsNot(x, None) # we never put None in there!
|
||||
|
||||
@unittest.skip("TODO: RUSTPYTHON; race condition between GC and WeakValueDictionary callback")
|
||||
@threading_helper.requires_working_threading()
|
||||
def test_threaded_weak_valued_consistency(self):
|
||||
# Issue #28427: old keys should not remove new values from
|
||||
|
||||
Vendored
-2
@@ -403,8 +403,6 @@ class TestWeakSet(unittest.TestCase):
|
||||
s.clear()
|
||||
self.assertEqual(len(s), 0)
|
||||
|
||||
# TODO: RUSTPYTHON
|
||||
@unittest.expectedFailure
|
||||
def test_len_cycles(self):
|
||||
N = 20
|
||||
items = [RefCycle() for i in range(N)]
|
||||
|
||||
+211
-36
@@ -1,4 +1,17 @@
|
||||
use crate::atomic::{Ordering::*, PyAtomic, Radium};
|
||||
use crate::atomic::{Ordering, PyAtomic, Radium};
|
||||
|
||||
// State layout (usize):
|
||||
// [1 bit: destructed] [1 bit: reserved] [1 bit: leaked] [N bits: weak_count] [M bits: strong_count]
|
||||
// 64-bit: N=30, M=31. 32-bit: N=14, M=15.
|
||||
const FLAG_BITS: u32 = 3;
|
||||
const DESTRUCTED: usize = 1 << (usize::BITS - 1);
|
||||
const LEAKED: usize = 1 << (usize::BITS - 3);
|
||||
const TOTAL_COUNT_WIDTH: u32 = usize::BITS - FLAG_BITS;
|
||||
const WEAK_WIDTH: u32 = TOTAL_COUNT_WIDTH / 2;
|
||||
const STRONG_WIDTH: u32 = TOTAL_COUNT_WIDTH - WEAK_WIDTH;
|
||||
const STRONG: usize = (1 << STRONG_WIDTH) - 1;
|
||||
const COUNT: usize = 1;
|
||||
const WEAK_COUNT: usize = 1 << STRONG_WIDTH;
|
||||
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
@@ -9,15 +22,56 @@ fn refcount_overflow() -> ! {
|
||||
core::panic!("refcount overflow");
|
||||
}
|
||||
|
||||
/// from alloc::sync
|
||||
/// A soft limit on the amount of references that may be made to an `Arc`.
|
||||
///
|
||||
/// Going above this limit will abort your program (although not
|
||||
/// necessarily) at _exactly_ `MAX_REFCOUNT + 1` references.
|
||||
const MAX_REFCOUNT: usize = isize::MAX as usize;
|
||||
/// State wraps reference count + flags in a single word (platform usize)
|
||||
#[derive(Clone, Copy)]
|
||||
struct State {
|
||||
inner: usize,
|
||||
}
|
||||
|
||||
impl State {
|
||||
#[inline]
|
||||
fn from_raw(inner: usize) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn as_raw(self) -> usize {
|
||||
self.inner
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn strong(self) -> u32 {
|
||||
((self.inner & STRONG) / COUNT) as u32
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn destructed(self) -> bool {
|
||||
(self.inner & DESTRUCTED) != 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn leaked(self) -> bool {
|
||||
(self.inner & LEAKED) != 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn add_strong(self, val: u32) -> Self {
|
||||
Self::from_raw(self.inner + (val as usize) * COUNT)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn with_leaked(self, leaked: bool) -> Self {
|
||||
Self::from_raw((self.inner & !LEAKED) | if leaked { LEAKED } else { 0 })
|
||||
}
|
||||
}
|
||||
|
||||
/// Reference count using state layout with LEAKED support.
|
||||
///
|
||||
/// State layout (usize):
|
||||
/// 64-bit: [1 bit: destructed] [1 bit: reserved] [1 bit: leaked] [30 bits: weak_count] [31 bits: strong_count]
|
||||
/// 32-bit: [1 bit: destructed] [1 bit: reserved] [1 bit: leaked] [14 bits: weak_count] [15 bits: strong_count]
|
||||
pub struct RefCount {
|
||||
strong: PyAtomic<usize>,
|
||||
state: PyAtomic<usize>,
|
||||
}
|
||||
|
||||
impl Default for RefCount {
|
||||
@@ -27,71 +81,192 @@ impl Default for RefCount {
|
||||
}
|
||||
|
||||
impl RefCount {
|
||||
const MASK: usize = MAX_REFCOUNT;
|
||||
|
||||
/// Create a new RefCount with strong count = 1
|
||||
pub fn new() -> Self {
|
||||
// Initial state: strong=1, weak=1 (implicit weak for strong refs)
|
||||
Self {
|
||||
strong: Radium::new(1),
|
||||
state: Radium::new(COUNT + WEAK_COUNT),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get current strong count
|
||||
#[inline]
|
||||
pub fn get(&self) -> usize {
|
||||
self.strong.load(SeqCst)
|
||||
State::from_raw(self.state.load(Ordering::SeqCst)).strong() as usize
|
||||
}
|
||||
|
||||
/// Increment strong count
|
||||
#[inline]
|
||||
pub fn inc(&self) {
|
||||
let old_size = self.strong.fetch_add(1, Relaxed);
|
||||
|
||||
if old_size & Self::MASK == Self::MASK {
|
||||
let val = State::from_raw(self.state.fetch_add(COUNT, Ordering::SeqCst));
|
||||
if val.destructed() || (val.strong() as usize) > STRONG - 1 {
|
||||
refcount_overflow();
|
||||
}
|
||||
if val.strong() == 0 {
|
||||
// The previous fetch_add created a permission to run decrement again
|
||||
self.state.fetch_add(COUNT, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn inc_by(&self, n: usize) {
|
||||
debug_assert!(n <= Self::MASK);
|
||||
let old_size = self.strong.fetch_add(n, Relaxed);
|
||||
|
||||
if old_size & Self::MASK > Self::MASK - n {
|
||||
debug_assert!(n <= STRONG);
|
||||
let val = State::from_raw(self.state.fetch_add(n * COUNT, Ordering::SeqCst));
|
||||
if val.destructed() || (val.strong() as usize) > STRONG - n {
|
||||
refcount_overflow();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if successful
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn safe_inc(&self) -> bool {
|
||||
self.strong
|
||||
.fetch_update(AcqRel, Acquire, |prev| (prev != 0).then_some(prev + 1))
|
||||
.is_ok()
|
||||
let mut old = State::from_raw(self.state.load(Ordering::SeqCst));
|
||||
loop {
|
||||
if old.destructed() {
|
||||
return false;
|
||||
}
|
||||
if (old.strong() as usize) >= STRONG {
|
||||
refcount_overflow();
|
||||
}
|
||||
let new_state = old.add_strong(1);
|
||||
match self.state.compare_exchange(
|
||||
old.as_raw(),
|
||||
new_state.as_raw(),
|
||||
Ordering::SeqCst,
|
||||
Ordering::SeqCst,
|
||||
) {
|
||||
Ok(_) => return true,
|
||||
Err(curr) => old = State::from_raw(curr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrement the reference count. Returns true when the refcount drops to 0.
|
||||
/// Decrement strong count. Returns true when count drops to 0.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn dec(&self) -> bool {
|
||||
if self.strong.fetch_sub(1, Release) != 1 {
|
||||
let old = State::from_raw(self.state.fetch_sub(COUNT, Ordering::SeqCst));
|
||||
|
||||
// LEAKED objects never reach 0
|
||||
if old.leaked() {
|
||||
return false;
|
||||
}
|
||||
|
||||
PyAtomic::<usize>::fence(Acquire);
|
||||
|
||||
true
|
||||
old.strong() == 1
|
||||
}
|
||||
}
|
||||
|
||||
impl RefCount {
|
||||
// move these functions out and give separated type once type range is stabilized
|
||||
|
||||
/// Mark this object as leaked (interned). It will never be deallocated.
|
||||
pub fn leak(&self) {
|
||||
debug_assert!(!self.is_leaked());
|
||||
const BIT_MARKER: usize = (isize::MAX as usize) + 1;
|
||||
debug_assert_eq!(BIT_MARKER.count_ones(), 1);
|
||||
debug_assert_eq!(BIT_MARKER.leading_zeros(), 0);
|
||||
self.strong.fetch_add(BIT_MARKER, Relaxed);
|
||||
let mut old = State::from_raw(self.state.load(Ordering::SeqCst));
|
||||
loop {
|
||||
let new_state = old.with_leaked(true);
|
||||
match self.state.compare_exchange(
|
||||
old.as_raw(),
|
||||
new_state.as_raw(),
|
||||
Ordering::SeqCst,
|
||||
Ordering::SeqCst,
|
||||
) {
|
||||
Ok(_) => return,
|
||||
Err(curr) => old = State::from_raw(curr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if this object is leaked (interned).
|
||||
pub fn is_leaked(&self) -> bool {
|
||||
(self.strong.load(Acquire) as isize) < 0
|
||||
State::from_raw(self.state.load(Ordering::Acquire)).leaked()
|
||||
}
|
||||
}
|
||||
|
||||
// Deferred Drop Infrastructure
|
||||
//
|
||||
// This mechanism allows untrack_object() calls to be deferred until after
|
||||
// the GC collection phase completes, preventing deadlocks that occur when
|
||||
// clear (pop_edges) triggers object destruction while holding the tracked_objects lock.
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
use core::cell::{Cell, RefCell};
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
thread_local! {
|
||||
/// Flag indicating if we're inside a deferred drop context.
|
||||
/// When true, drop operations should defer untrack calls.
|
||||
static IN_DEFERRED_CONTEXT: Cell<bool> = const { Cell::new(false) };
|
||||
|
||||
/// Queue of deferred untrack operations.
|
||||
/// No Send bound needed - this is thread-local and only accessed from the same thread.
|
||||
static DEFERRED_QUEUE: RefCell<Vec<Box<dyn FnOnce()>>> = const { RefCell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
struct DeferredDropGuard {
|
||||
was_in_context: bool,
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl Drop for DeferredDropGuard {
|
||||
fn drop(&mut self) {
|
||||
IN_DEFERRED_CONTEXT.with(|in_ctx| {
|
||||
in_ctx.set(self.was_in_context);
|
||||
});
|
||||
// Only flush if we're the outermost context and not already panicking
|
||||
// (flushing during unwinding risks double-panic → process abort).
|
||||
if !self.was_in_context && !std::thread::panicking() {
|
||||
flush_deferred_drops();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute a function within a deferred drop context.
|
||||
/// Any calls to `try_defer_drop` within this context will be queued
|
||||
/// and executed when the context exits (even on panic).
|
||||
#[cfg(feature = "std")]
|
||||
#[inline]
|
||||
pub fn with_deferred_drops<F, R>(f: F) -> R
|
||||
where
|
||||
F: FnOnce() -> R,
|
||||
{
|
||||
let _guard = IN_DEFERRED_CONTEXT.with(|in_ctx| {
|
||||
let was_in_context = in_ctx.get();
|
||||
in_ctx.set(true);
|
||||
DeferredDropGuard { was_in_context }
|
||||
});
|
||||
f()
|
||||
}
|
||||
|
||||
/// Try to defer a drop-related operation.
|
||||
/// If inside a deferred context, the operation is queued.
|
||||
/// Otherwise, it executes immediately.
|
||||
#[cfg(feature = "std")]
|
||||
#[inline]
|
||||
pub fn try_defer_drop<F>(f: F)
|
||||
where
|
||||
F: FnOnce() + 'static,
|
||||
{
|
||||
let should_defer = IN_DEFERRED_CONTEXT.with(|in_ctx| in_ctx.get());
|
||||
|
||||
if should_defer {
|
||||
DEFERRED_QUEUE.with(|q| {
|
||||
q.borrow_mut().push(Box::new(f));
|
||||
});
|
||||
} else {
|
||||
f();
|
||||
}
|
||||
}
|
||||
|
||||
/// Flush all deferred drop operations.
|
||||
/// This is automatically called when exiting a deferred context.
|
||||
#[cfg(feature = "std")]
|
||||
#[inline]
|
||||
pub fn flush_deferred_drops() {
|
||||
DEFERRED_QUEUE.with(|q| {
|
||||
// Take all queued operations
|
||||
let ops: Vec<_> = q.borrow_mut().drain(..).collect();
|
||||
// Execute them outside the borrow
|
||||
for op in ops {
|
||||
op();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -423,6 +423,10 @@ impl PyType {
|
||||
|
||||
// Static types are not tracked by GC.
|
||||
// They are immortal and never participate in collectable cycles.
|
||||
unsafe {
|
||||
crate::gc_state::gc_state()
|
||||
.untrack_object(core::ptr::NonNull::from(new_type.as_object()));
|
||||
}
|
||||
new_type.as_object().clear_gc_tracked();
|
||||
|
||||
new_type.mro.write().insert(0, new_type.clone());
|
||||
|
||||
+373
-20
@@ -4,7 +4,7 @@
|
||||
//! for RustPython, using an intrusive doubly-linked list approach.
|
||||
|
||||
use crate::common::lock::PyMutex;
|
||||
use crate::{PyObject, PyObjectRef};
|
||||
use crate::{AsObject, PyObject, PyObjectRef};
|
||||
use core::ptr::NonNull;
|
||||
use core::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering};
|
||||
use std::collections::HashSet;
|
||||
@@ -28,7 +28,7 @@ bitflags::bitflags! {
|
||||
}
|
||||
|
||||
/// Statistics for a single generation (gc_generation_stats)
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
#[derive(Debug, Default)]
|
||||
pub struct GcStats {
|
||||
pub collections: usize,
|
||||
pub collected: usize,
|
||||
@@ -114,9 +114,7 @@ pub struct GcState {
|
||||
pub garbage: PyMutex<Vec<PyObjectRef>>,
|
||||
/// gc.callbacks list
|
||||
pub callbacks: PyMutex<Vec<PyObjectRef>>,
|
||||
/// Mutex for collection (prevents concurrent collections).
|
||||
/// Used by collect_inner when the actual collection algorithm is enabled.
|
||||
#[allow(dead_code)]
|
||||
/// Mutex for collection (prevents concurrent collections)
|
||||
collecting: Mutex<()>,
|
||||
/// Allocation counter for gen0
|
||||
alloc_count: AtomicUsize,
|
||||
@@ -385,25 +383,380 @@ impl GcState {
|
||||
false
|
||||
}
|
||||
|
||||
/// Perform garbage collection on the given generation.
|
||||
/// Returns (collected_count, uncollectable_count).
|
||||
/// Perform garbage collection on the given generation
|
||||
/// Returns (collected_count, uncollectable_count)
|
||||
///
|
||||
/// Currently a stub — the actual collection algorithm requires EBR
|
||||
/// and will be added in a follow-up.
|
||||
pub fn collect(&self, _generation: usize) -> (usize, usize) {
|
||||
// gc_collect_main
|
||||
// Reset gen0 count even though we're not actually collecting
|
||||
self.generations[0].count.store(0, Ordering::SeqCst);
|
||||
(0, 0)
|
||||
/// Implements CPython-compatible generational GC algorithm:
|
||||
/// - Only collects objects from generations 0 to `generation`
|
||||
/// - Uses gc_refs algorithm: gc_refs = strong_count - internal_refs
|
||||
/// - Only subtracts references between objects IN THE SAME COLLECTION
|
||||
///
|
||||
/// If `force` is true, collection runs even if GC is disabled (for manual gc.collect() calls)
|
||||
pub fn collect(&self, generation: usize) -> (usize, usize) {
|
||||
self.collect_inner(generation, false)
|
||||
}
|
||||
|
||||
/// Force collection even if GC is disabled (for manual gc.collect() calls).
|
||||
/// gc.collect() always runs regardless of gc.isenabled()
|
||||
/// Currently a stub.
|
||||
pub fn collect_force(&self, _generation: usize) -> (usize, usize) {
|
||||
// Reset gen0 count even though we're not actually collecting
|
||||
/// Force collection even if GC is disabled (for manual gc.collect() calls)
|
||||
pub fn collect_force(&self, generation: usize) -> (usize, usize) {
|
||||
self.collect_inner(generation, true)
|
||||
}
|
||||
|
||||
fn collect_inner(&self, generation: usize, force: bool) -> (usize, usize) {
|
||||
if !force && !self.is_enabled() {
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
// Try to acquire the collecting lock
|
||||
let _guard = match self.collecting.try_lock() {
|
||||
Ok(g) => g,
|
||||
Err(_) => return (0, 0),
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
let generation = generation.min(2);
|
||||
let debug = self.get_debug();
|
||||
|
||||
// Step 1: Gather objects from generations 0..=generation
|
||||
// Hold read locks for the entire collection to prevent other threads
|
||||
// from untracking objects while we're iterating.
|
||||
let gen_locks: Vec<_> = (0..=generation)
|
||||
.filter_map(|i| self.generation_objects[i].read().ok())
|
||||
.collect();
|
||||
|
||||
let mut collecting: HashSet<GcObjectPtr> = HashSet::new();
|
||||
for gen_set in &gen_locks {
|
||||
for &ptr in gen_set.iter() {
|
||||
let obj = unsafe { ptr.0.as_ref() };
|
||||
if obj.strong_count() > 0 {
|
||||
collecting.insert(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if collecting.is_empty() {
|
||||
// Reset gen0 count even if nothing to collect
|
||||
self.generations[0].count.store(0, Ordering::SeqCst);
|
||||
self.generations[generation].update_stats(0, 0);
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
if debug.contains(GcDebugFlags::STATS) {
|
||||
eprintln!(
|
||||
"gc: collecting {} objects from generations 0..={}",
|
||||
collecting.len(),
|
||||
generation
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2: Build gc_refs map (copy reference counts)
|
||||
let mut gc_refs: std::collections::HashMap<GcObjectPtr, usize> =
|
||||
std::collections::HashMap::new();
|
||||
for &ptr in &collecting {
|
||||
let obj = unsafe { ptr.0.as_ref() };
|
||||
gc_refs.insert(ptr, obj.strong_count());
|
||||
}
|
||||
|
||||
// Step 3: Subtract internal references
|
||||
// CRITICAL: Only subtract refs to objects IN THE COLLECTING SET
|
||||
for &ptr in &collecting {
|
||||
let obj = unsafe { ptr.0.as_ref() };
|
||||
// Double-check object is still alive
|
||||
if obj.strong_count() == 0 {
|
||||
continue;
|
||||
}
|
||||
let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
|
||||
for child_ptr in referent_ptrs {
|
||||
let gc_ptr = GcObjectPtr(child_ptr);
|
||||
// Only decrement if child is also in the collecting set!
|
||||
if collecting.contains(&gc_ptr)
|
||||
&& let Some(refs) = gc_refs.get_mut(&gc_ptr)
|
||||
{
|
||||
*refs = refs.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Find reachable objects (gc_refs > 0) and traverse from them
|
||||
// Objects with gc_refs > 0 are definitely reachable from outside.
|
||||
// We need to mark all objects reachable from them as also reachable.
|
||||
let mut reachable: HashSet<GcObjectPtr> = HashSet::new();
|
||||
let mut worklist: Vec<GcObjectPtr> = Vec::new();
|
||||
|
||||
// Start with objects that have gc_refs > 0
|
||||
for (&ptr, &refs) in &gc_refs {
|
||||
if refs > 0 {
|
||||
reachable.insert(ptr);
|
||||
worklist.push(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Traverse reachable objects to find more reachable ones
|
||||
while let Some(ptr) = worklist.pop() {
|
||||
let obj = unsafe { ptr.0.as_ref() };
|
||||
if obj.is_gc_tracked() {
|
||||
let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
|
||||
for child_ptr in referent_ptrs {
|
||||
let gc_ptr = GcObjectPtr(child_ptr);
|
||||
// If child is in collecting set and not yet marked reachable
|
||||
if collecting.contains(&gc_ptr) && reachable.insert(gc_ptr) {
|
||||
worklist.push(gc_ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Find unreachable objects (in collecting but not in reachable)
|
||||
let unreachable: Vec<GcObjectPtr> = collecting.difference(&reachable).copied().collect();
|
||||
|
||||
if debug.contains(GcDebugFlags::STATS) {
|
||||
eprintln!(
|
||||
"gc: {} reachable, {} unreachable",
|
||||
reachable.len(),
|
||||
unreachable.len()
|
||||
);
|
||||
}
|
||||
|
||||
if unreachable.is_empty() {
|
||||
// No cycles found - promote survivors to next generation
|
||||
drop(gen_locks); // Release read locks before promoting
|
||||
self.promote_survivors(generation, &collecting);
|
||||
// Reset gen0 count
|
||||
self.generations[0].count.store(0, Ordering::SeqCst);
|
||||
self.generations[generation].update_stats(0, 0);
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
// Release read locks before finalization phase.
|
||||
// This allows other threads to untrack objects while we finalize.
|
||||
drop(gen_locks);
|
||||
|
||||
// Step 6: Finalize unreachable objects and handle resurrection
|
||||
|
||||
// 6a: Get references to all unreachable objects
|
||||
let unreachable_refs: Vec<crate::PyObjectRef> = unreachable
|
||||
.iter()
|
||||
.filter_map(|ptr| {
|
||||
let obj = unsafe { ptr.0.as_ref() };
|
||||
if obj.strong_count() > 0 {
|
||||
Some(obj.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if unreachable_refs.is_empty() {
|
||||
self.promote_survivors(generation, &reachable);
|
||||
// Reset gen0 count
|
||||
self.generations[0].count.store(0, Ordering::SeqCst);
|
||||
self.generations[generation].update_stats(0, 0);
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
// 6b: Record initial strong counts (for resurrection detection)
|
||||
// Each object has +1 from unreachable_refs, so initial count includes that
|
||||
let initial_counts: std::collections::HashMap<GcObjectPtr, usize> = unreachable_refs
|
||||
.iter()
|
||||
.map(|obj| {
|
||||
let ptr = GcObjectPtr(core::ptr::NonNull::from(obj.as_ref()));
|
||||
(ptr, obj.strong_count())
|
||||
})
|
||||
.collect();
|
||||
|
||||
// 6c: Clear existing weakrefs BEFORE calling __del__
|
||||
// This invalidates existing weakrefs, but new weakrefs created during __del__
|
||||
// will still work (WeakRefList::add restores inner.obj if cleared)
|
||||
//
|
||||
// CRITICAL: We use a two-phase approach to match CPython behavior:
|
||||
// Phase 1: Clear ALL weakrefs (set inner.obj = None) and collect callbacks
|
||||
// Phase 2: Invoke ALL callbacks
|
||||
// This ensures that when a callback runs, ALL weakrefs to unreachable objects
|
||||
// are already dead (return None when called).
|
||||
let mut all_callbacks: Vec<(crate::PyRef<crate::object::PyWeak>, crate::PyObjectRef)> =
|
||||
Vec::new();
|
||||
for obj_ref in &unreachable_refs {
|
||||
let callbacks = obj_ref.gc_clear_weakrefs_collect_callbacks();
|
||||
all_callbacks.extend(callbacks);
|
||||
}
|
||||
// Phase 2: Now call all callbacks - at this point ALL weakrefs are cleared
|
||||
for (wr, cb) in all_callbacks {
|
||||
if let Some(Err(e)) = crate::vm::thread::with_vm(&cb, |vm| cb.call((wr.clone(),), vm)) {
|
||||
// Report the exception via run_unraisable
|
||||
crate::vm::thread::with_vm(&cb, |vm| {
|
||||
vm.run_unraisable(e.clone(), Some("weakref callback".to_owned()), cb.clone());
|
||||
});
|
||||
}
|
||||
// If with_vm returns None, we silently skip - no VM available to handle errors
|
||||
}
|
||||
|
||||
// 6d: Call __del__ on all unreachable objects
|
||||
// This allows resurrection to work correctly
|
||||
// Skip objects that have already been finalized (prevents multiple __del__ calls)
|
||||
for obj_ref in &unreachable_refs {
|
||||
let ptr = GcObjectPtr(core::ptr::NonNull::from(obj_ref.as_ref()));
|
||||
let already_finalized = if let Ok(finalized) = self.finalized_objects.read() {
|
||||
finalized.contains(&ptr)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if !already_finalized {
|
||||
// Mark as finalized BEFORE calling __del__
|
||||
// This ensures is_finalized() returns True inside __del__
|
||||
if let Ok(mut finalized) = self.finalized_objects.write() {
|
||||
finalized.insert(ptr);
|
||||
}
|
||||
obj_ref.try_call_finalizer();
|
||||
}
|
||||
}
|
||||
|
||||
// 6d: Detect resurrection - strong_count increased means object was resurrected
|
||||
// Step 1: Find directly resurrected objects (strong_count increased)
|
||||
let mut resurrected_set: HashSet<GcObjectPtr> = HashSet::new();
|
||||
let unreachable_set: HashSet<GcObjectPtr> = unreachable.iter().copied().collect();
|
||||
|
||||
for obj in &unreachable_refs {
|
||||
let ptr = GcObjectPtr(core::ptr::NonNull::from(obj.as_ref()));
|
||||
let initial = initial_counts.get(&ptr).copied().unwrap_or(1);
|
||||
if obj.strong_count() > initial {
|
||||
resurrected_set.insert(ptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Transitive resurrection - objects reachable from resurrected are also resurrected
|
||||
// This is critical for cases like: Lazarus resurrects itself, its cargo should also survive
|
||||
let mut worklist: Vec<GcObjectPtr> = resurrected_set.iter().copied().collect();
|
||||
while let Some(ptr) = worklist.pop() {
|
||||
let obj = unsafe { ptr.0.as_ref() };
|
||||
let referent_ptrs = unsafe { obj.gc_get_referent_ptrs() };
|
||||
for child_ptr in referent_ptrs {
|
||||
let child_gc_ptr = GcObjectPtr(child_ptr);
|
||||
// If child is in unreachable set and not yet marked as resurrected
|
||||
if unreachable_set.contains(&child_gc_ptr) && resurrected_set.insert(child_gc_ptr) {
|
||||
worklist.push(child_gc_ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Partition into resurrected and truly dead
|
||||
let (resurrected, truly_dead): (Vec<_>, Vec<_>) =
|
||||
unreachable_refs.into_iter().partition(|obj| {
|
||||
let ptr = GcObjectPtr(core::ptr::NonNull::from(obj.as_ref()));
|
||||
resurrected_set.contains(&ptr)
|
||||
});
|
||||
|
||||
let resurrected_count = resurrected.len();
|
||||
|
||||
if debug.contains(GcDebugFlags::STATS) {
|
||||
eprintln!(
|
||||
"gc: {} resurrected, {} truly dead",
|
||||
resurrected_count,
|
||||
truly_dead.len()
|
||||
);
|
||||
}
|
||||
|
||||
// 6e: Break cycles ONLY for truly dead objects (not resurrected)
|
||||
// Compute collected count: exclude instance dicts that are also in truly_dead.
|
||||
// In CPython 3.12+, instance dicts are managed inline and not separately tracked,
|
||||
// so they don't count toward the collected total.
|
||||
let collected = {
|
||||
let dead_ptrs: HashSet<usize> = truly_dead
|
||||
.iter()
|
||||
.map(|obj| obj.as_ref() as *const PyObject as usize)
|
||||
.collect();
|
||||
let instance_dict_count = truly_dead
|
||||
.iter()
|
||||
.filter(|obj| {
|
||||
if let Some(dict_ref) = obj.dict() {
|
||||
dead_ptrs.contains(&(dict_ref.as_object() as *const PyObject as usize))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.count();
|
||||
truly_dead.len() - instance_dict_count
|
||||
};
|
||||
|
||||
// 6e-1: If DEBUG_SAVEALL is set, save truly dead objects to garbage
|
||||
if debug.contains(GcDebugFlags::SAVEALL) {
|
||||
let mut garbage_guard = self.garbage.lock();
|
||||
for obj_ref in truly_dead.iter() {
|
||||
garbage_guard.push(obj_ref.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if !truly_dead.is_empty() {
|
||||
// 6g: Break cycles by clearing references (tp_clear)
|
||||
// Weakrefs were already cleared in step 6c, but new weakrefs created
|
||||
// during __del__ (step 6d) can still be upgraded.
|
||||
//
|
||||
// Clear and destroy objects within a deferred drop context.
|
||||
// This prevents deadlocks from untrack calls during destruction.
|
||||
rustpython_common::refcount::with_deferred_drops(|| {
|
||||
for obj_ref in truly_dead.iter() {
|
||||
if obj_ref.gc_has_clear() {
|
||||
let edges = unsafe { obj_ref.gc_clear() };
|
||||
drop(edges);
|
||||
}
|
||||
}
|
||||
// Drop truly_dead references, triggering actual deallocation
|
||||
drop(truly_dead);
|
||||
});
|
||||
}
|
||||
|
||||
// 6f: Resurrected objects stay in tracked_objects (they're still alive)
|
||||
// Just drop our references to them
|
||||
drop(resurrected);
|
||||
|
||||
// Promote survivors (reachable objects) to next generation
|
||||
self.promote_survivors(generation, &reachable);
|
||||
|
||||
// Reset gen0 count after collection (enables automatic GC to trigger again)
|
||||
self.generations[0].count.store(0, Ordering::SeqCst);
|
||||
(0, 0)
|
||||
|
||||
self.generations[generation].update_stats(collected, 0);
|
||||
|
||||
(collected, 0)
|
||||
}
|
||||
|
||||
/// Promote surviving objects to the next generation
|
||||
fn promote_survivors(&self, from_gen: usize, survivors: &HashSet<GcObjectPtr>) {
|
||||
if from_gen >= 2 {
|
||||
return; // Already in oldest generation
|
||||
}
|
||||
|
||||
let next_gen = from_gen + 1;
|
||||
|
||||
for &ptr in survivors {
|
||||
// Remove from current generation
|
||||
for gen_idx in 0..=from_gen {
|
||||
if let Ok(mut gen_set) = self.generation_objects[gen_idx].write()
|
||||
&& gen_set.remove(&ptr)
|
||||
{
|
||||
// Decrement count for source generation
|
||||
let count = self.generations[gen_idx].count.load(Ordering::SeqCst);
|
||||
if count > 0 {
|
||||
self.generations[gen_idx]
|
||||
.count
|
||||
.fetch_sub(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
// Add to next generation
|
||||
if let Ok(mut next_set) = self.generation_objects[next_gen].write()
|
||||
&& next_set.insert(ptr)
|
||||
{
|
||||
// Increment count for target generation
|
||||
self.generations[next_gen]
|
||||
.count
|
||||
.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get count of frozen objects
|
||||
|
||||
+128
-15
@@ -81,6 +81,77 @@ use core::{
|
||||
#[derive(Debug)]
|
||||
pub(super) struct Erased;
|
||||
|
||||
/// Trashcan mechanism to limit recursive deallocation depth (Py_TRASHCAN).
|
||||
/// Without this, deeply nested structures (e.g. 200k-deep list) cause stack overflow
|
||||
/// during deallocation because each level adds a stack frame.
|
||||
mod trashcan {
|
||||
use core::cell::Cell;
|
||||
|
||||
/// Maximum nesting depth for deallocation before deferring.
|
||||
/// CPython uses UNWIND_NO_NESTING = 50.
|
||||
const TRASHCAN_LIMIT: usize = 50;
|
||||
|
||||
type DeallocFn = unsafe fn(*mut super::PyObject);
|
||||
type DeallocQueue = Vec<(*mut super::PyObject, DeallocFn)>;
|
||||
|
||||
thread_local! {
|
||||
static DEALLOC_DEPTH: Cell<usize> = const { Cell::new(0) };
|
||||
static DEALLOC_QUEUE: Cell<DeallocQueue> = const { Cell::new(Vec::new()) };
|
||||
}
|
||||
|
||||
/// Try to begin deallocation. Returns true if we should proceed,
|
||||
/// false if the object was deferred (depth exceeded).
|
||||
#[inline]
|
||||
pub(super) unsafe fn begin(
|
||||
obj: *mut super::PyObject,
|
||||
dealloc: unsafe fn(*mut super::PyObject),
|
||||
) -> bool {
|
||||
DEALLOC_DEPTH.with(|d| {
|
||||
let depth = d.get();
|
||||
if depth >= TRASHCAN_LIMIT {
|
||||
// Depth exceeded: defer this deallocation
|
||||
DEALLOC_QUEUE.with(|q| {
|
||||
let mut queue = q.take();
|
||||
queue.push((obj, dealloc));
|
||||
q.set(queue);
|
||||
});
|
||||
false
|
||||
} else {
|
||||
d.set(depth + 1);
|
||||
true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// End deallocation and process any deferred objects if at outermost level.
|
||||
#[inline]
|
||||
pub(super) unsafe fn end() {
|
||||
let depth = DEALLOC_DEPTH.with(|d| {
|
||||
let depth = d.get();
|
||||
debug_assert!(depth > 0, "trashcan::end called without matching begin");
|
||||
let depth = depth - 1;
|
||||
d.set(depth);
|
||||
depth
|
||||
});
|
||||
if depth == 0 {
|
||||
// Process deferred deallocations iteratively
|
||||
loop {
|
||||
let next = DEALLOC_QUEUE.with(|q| {
|
||||
let mut queue = q.take();
|
||||
let item = queue.pop();
|
||||
q.set(queue);
|
||||
item
|
||||
});
|
||||
if let Some((obj, dealloc)) = next {
|
||||
unsafe { dealloc(obj) };
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default dealloc: handles __del__, weakref clearing, tp_clear, and memory free.
|
||||
/// Equivalent to subtype_dealloc.
|
||||
pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
|
||||
@@ -89,11 +160,28 @@ pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
|
||||
return; // resurrected by __del__
|
||||
}
|
||||
|
||||
// Extract child references before deallocation to break circular refs (tp_clear).
|
||||
// This ensures that when edges are dropped after the object is freed,
|
||||
// any pointers back to this object are already gone.
|
||||
// Trashcan: limit recursive deallocation depth to prevent stack overflow
|
||||
if !unsafe { trashcan::begin(obj, default_dealloc::<T>) } {
|
||||
return; // deferred to queue
|
||||
}
|
||||
|
||||
let vtable = obj_ref.0.vtable;
|
||||
|
||||
// Untrack from GC BEFORE deallocation.
|
||||
if obj_ref.is_gc_tracked() {
|
||||
let ptr = unsafe { NonNull::new_unchecked(obj) };
|
||||
rustpython_common::refcount::try_defer_drop(move || {
|
||||
// untrack_object only removes the pointer address from a HashSet.
|
||||
// It does NOT dereference the pointer, so it's safe even after deallocation.
|
||||
unsafe {
|
||||
crate::gc_state::gc_state().untrack_object(ptr);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Extract child references before deallocation to break circular refs (tp_clear)
|
||||
let mut edges = Vec::new();
|
||||
if let Some(clear_fn) = obj_ref.0.vtable.clear {
|
||||
if let Some(clear_fn) = vtable.clear {
|
||||
unsafe { clear_fn(obj, &mut edges) };
|
||||
}
|
||||
|
||||
@@ -103,6 +191,9 @@ pub(super) unsafe fn default_dealloc<T: PyPayload>(obj: *mut PyObject) {
|
||||
// Drop child references - may trigger recursive destruction.
|
||||
// The object is already deallocated, so circular refs are broken.
|
||||
drop(edges);
|
||||
|
||||
// Trashcan: decrement depth and process deferred objects at outermost level
|
||||
unsafe { trashcan::end() };
|
||||
}
|
||||
pub(super) unsafe fn debug_obj<T: PyPayload + core::fmt::Debug>(
|
||||
x: &PyObject,
|
||||
@@ -316,20 +407,24 @@ impl WeakRefList {
|
||||
dict: Option<PyDictRef>,
|
||||
) -> PyRef<PyWeak> {
|
||||
let is_generic = cls_is_weakref && callback.is_none();
|
||||
let _lock = weakref_lock::lock(obj as *const PyObject as usize);
|
||||
|
||||
// try_reuse_basic_ref: reuse cached generic weakref
|
||||
if is_generic {
|
||||
let generic_ptr = self.generic.load(Ordering::Relaxed);
|
||||
if !generic_ptr.is_null() {
|
||||
let generic = unsafe { &*generic_ptr };
|
||||
if generic.0.ref_count.safe_inc() {
|
||||
return unsafe { PyRef::from_raw(generic_ptr) };
|
||||
// Try reuse under lock first (fast path, no allocation)
|
||||
{
|
||||
let _lock = weakref_lock::lock(obj as *const PyObject as usize);
|
||||
if is_generic {
|
||||
let generic_ptr = self.generic.load(Ordering::Relaxed);
|
||||
if !generic_ptr.is_null() {
|
||||
let generic = unsafe { &*generic_ptr };
|
||||
if generic.0.ref_count.safe_inc() {
|
||||
return unsafe { PyRef::from_raw(generic_ptr) };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate new PyWeak with wr_object pointing to referent
|
||||
// Allocate OUTSIDE the stripe lock. PyRef::new_ref may trigger
|
||||
// maybe_collect → GC → WeakRefList::clear on another object that
|
||||
// hashes to the same stripe, which would deadlock on the spinlock.
|
||||
let weak_payload = PyWeak {
|
||||
pointers: Pointers::new(),
|
||||
wr_object: Radium::new(obj as *const PyObject as *mut PyObject),
|
||||
@@ -338,6 +433,24 @@ impl WeakRefList {
|
||||
};
|
||||
let weak = PyRef::new_ref(weak_payload, cls, dict);
|
||||
|
||||
// Re-acquire lock for linked list insertion
|
||||
let _lock = weakref_lock::lock(obj as *const PyObject as usize);
|
||||
|
||||
// Re-check: another thread may have inserted a generic ref while we
|
||||
// were allocating outside the lock. If so, reuse it and drop ours.
|
||||
if is_generic {
|
||||
let generic_ptr = self.generic.load(Ordering::Relaxed);
|
||||
if !generic_ptr.is_null() {
|
||||
let generic = unsafe { &*generic_ptr };
|
||||
if generic.0.ref_count.safe_inc() {
|
||||
// Nullify wr_object so drop_inner won't unlink an
|
||||
// un-inserted node (which would corrupt the list head).
|
||||
weak.wr_object.store(ptr::null_mut(), Ordering::Relaxed);
|
||||
return unsafe { PyRef::from_raw(generic_ptr) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert into linked list under stripe lock
|
||||
let node_ptr = NonNull::from(&*weak);
|
||||
unsafe {
|
||||
@@ -1070,7 +1183,7 @@ impl PyObject {
|
||||
// Undo the temporary resurrection. Always remove both
|
||||
// temporary refs; the second dec returns true only when
|
||||
// ref_count drops to 0 (no resurrection).
|
||||
zelf.0.ref_count.dec();
|
||||
let _ = zelf.0.ref_count.dec();
|
||||
zelf.0.ref_count.dec()
|
||||
});
|
||||
match ret {
|
||||
@@ -1147,7 +1260,7 @@ impl PyObject {
|
||||
/// Call __del__ if present, without triggering object deallocation.
|
||||
/// Used by GC to call finalizers before breaking cycles.
|
||||
/// This allows proper resurrection detection.
|
||||
/// CPython: PyObject_CallFinalizerFromDealloc in Objects/object.c
|
||||
/// PyObject_CallFinalizerFromDealloc
|
||||
pub fn try_call_finalizer(&self) {
|
||||
let del = self.class().slots.del.load();
|
||||
if let Some(slot_del) = del
|
||||
|
||||
@@ -51,6 +51,7 @@ pub fn check_signals(vm: &VirtualMachine) -> PyResult<()> {
|
||||
|
||||
trigger_signals(vm)
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
fn trigger_signals(vm: &VirtualMachine) -> PyResult<()> {
|
||||
|
||||
@@ -387,11 +387,12 @@ impl Interpreter {
|
||||
/// Finalization steps (matching Py_FinalizeEx):
|
||||
/// 1. Flush stdout and stderr.
|
||||
/// 1. Handle exit exception and turn it to exit code.
|
||||
/// 1. Wait for thread shutdown (call threading._shutdown).
|
||||
/// 1. Mark vm as finalizing.
|
||||
/// 1. Call threading._shutdown() to join non-daemon threads.
|
||||
/// 1. Run atexit exit functions.
|
||||
/// 1. Finalize modules (clear module dicts in reverse import order).
|
||||
/// 1. Mark vm as finalized.
|
||||
/// 1. Set finalizing flag (suppresses unraisable exceptions from __del__).
|
||||
/// 1. Forced GC collection pass (collect cycles while builtins are available).
|
||||
/// 1. Module finalization (finalize_modules).
|
||||
/// 1. Final stdout/stderr flush.
|
||||
///
|
||||
/// Note that calling `finalize` is not necessary by purpose though.
|
||||
pub fn finalize(self, exc: Option<PyBaseExceptionRef>) -> u32 {
|
||||
@@ -419,13 +420,19 @@ impl Interpreter {
|
||||
);
|
||||
}
|
||||
|
||||
// Mark as finalizing AFTER thread shutdown
|
||||
vm.state.finalizing.store(true, Ordering::Release);
|
||||
|
||||
// Run atexit exit functions
|
||||
// Run atexit handlers before setting finalizing flag.
|
||||
// This allows unraisable exceptions from atexit handlers to be reported.
|
||||
atexit::_run_exitfuncs(vm);
|
||||
|
||||
// Finalize modules: clear module dicts in reverse import order
|
||||
// Now suppress unraisable exceptions from daemon threads and __del__
|
||||
// methods during the rest of shutdown.
|
||||
vm.state.finalizing.store(true, Ordering::Release);
|
||||
|
||||
// GC pass - collect cycles before module cleanup
|
||||
crate::gc_state::gc_state().collect_force(2);
|
||||
|
||||
// Module finalization: remove modules from sys.modules, GC collect
|
||||
// (while builtins is still available for __del__), then clear module dicts.
|
||||
vm.finalize_modules();
|
||||
|
||||
vm.flush_std();
|
||||
|
||||
+39
-40
@@ -730,26 +730,25 @@ impl VirtualMachine {
|
||||
|
||||
// Phase 2: Remove all modules from sys.modules (set values to None),
|
||||
// and collect weakrefs to modules preserving import order.
|
||||
// Also keeps strong refs (module_refs) to prevent premature deallocation.
|
||||
// CPython uses _PyGC_CollectNoFail() here to collect __globals__ cycles;
|
||||
// since RustPython has no working GC, we keep modules alive through
|
||||
// Phase 4 so their dicts can be explicitly cleared.
|
||||
let (module_weakrefs, module_refs) = self.finalize_remove_modules();
|
||||
// No strong refs are kept — modules freed when their last ref drops.
|
||||
let module_weakrefs = self.finalize_remove_modules();
|
||||
|
||||
// Phase 3: Clear sys.modules dict
|
||||
self.finalize_clear_modules_dict();
|
||||
|
||||
// Phase 4: Clear module dicts in reverse import order using 2-pass algorithm.
|
||||
// All modules are still alive (held by module_refs), so all weakrefs are valid.
|
||||
// This breaks __globals__ cycles: dict entries set to None → functions freed →
|
||||
// __globals__ refs dropped → dict refcount decreases.
|
||||
// Phase 4: GC collect — modules removed from sys.modules are freed,
|
||||
// exposing cycles (e.g., dict ↔ function.__globals__). GC collects
|
||||
// these and calls __del__ while module dicts are still intact.
|
||||
crate::gc_state::gc_state().collect_force(2);
|
||||
|
||||
// Phase 5: Clear module dicts in reverse import order using 2-pass algorithm.
|
||||
// Skip builtins and sys — those are cleared last.
|
||||
self.finalize_clear_module_dicts(&module_weakrefs);
|
||||
|
||||
// Drop strong refs → modules freed with already-cleared dicts.
|
||||
// No __globals__ cycles remain (broken by Phase 4).
|
||||
drop(module_refs);
|
||||
// Phase 6: GC collect — pick up anything freed by dict clearing.
|
||||
crate::gc_state::gc_state().collect_force(2);
|
||||
|
||||
// Phase 5: Clear sys and builtins dicts last
|
||||
// Phase 7: Clear sys and builtins dicts last
|
||||
self.finalize_clear_sys_builtins_dict();
|
||||
}
|
||||
|
||||
@@ -793,17 +792,17 @@ impl VirtualMachine {
|
||||
let _ = self.builtins.dict().set_item("_", none, self);
|
||||
}
|
||||
|
||||
/// Phase 2: Set all sys.modules values to None and collect weakrefs to modules.
|
||||
/// Returns (weakrefs for Phase 4, strong refs to keep modules alive).
|
||||
fn finalize_remove_modules(&self) -> (Vec<(String, PyRef<PyWeak>)>, Vec<PyObjectRef>) {
|
||||
/// Phase 2: Set all sys.modules values to None and collect weakrefs.
|
||||
/// No strong refs are kept — modules are freed when removed from sys.modules
|
||||
/// (if nothing else references them), allowing GC to collect their cycles.
|
||||
fn finalize_remove_modules(&self) -> Vec<(String, PyRef<PyWeak>)> {
|
||||
let mut module_weakrefs = Vec::new();
|
||||
let mut module_refs = Vec::new();
|
||||
|
||||
let Ok(modules) = self.sys_module.get_attr(identifier!(self, modules), self) else {
|
||||
return (module_weakrefs, module_refs);
|
||||
return module_weakrefs;
|
||||
};
|
||||
let Some(modules_dict) = modules.downcast_ref::<PyDict>() else {
|
||||
return (module_weakrefs, module_refs);
|
||||
return module_weakrefs;
|
||||
};
|
||||
|
||||
let none = self.ctx.none();
|
||||
@@ -815,19 +814,18 @@ impl VirtualMachine {
|
||||
.map(|s| s.as_str().to_owned())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Save weakref and strong ref to module for later clearing
|
||||
if value.downcast_ref::<PyModule>().is_some() {
|
||||
if let Ok(weak) = value.downgrade(None, self) {
|
||||
module_weakrefs.push((name, weak));
|
||||
}
|
||||
module_refs.push(value.clone());
|
||||
// Save weakref to module (for later dict clearing)
|
||||
if value.downcast_ref::<PyModule>().is_some()
|
||||
&& let Ok(weak) = value.downgrade(None, self)
|
||||
{
|
||||
module_weakrefs.push((name, weak));
|
||||
}
|
||||
|
||||
// Set the value to None in sys.modules
|
||||
let _ = modules_dict.set_item(&*key, none.clone(), self);
|
||||
}
|
||||
|
||||
(module_weakrefs, module_refs)
|
||||
module_weakrefs
|
||||
}
|
||||
|
||||
/// Phase 3: Clear sys.modules dict.
|
||||
@@ -839,18 +837,13 @@ impl VirtualMachine {
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 4: Clear module dicts in reverse import order using 2-pass algorithm.
|
||||
/// Without GC, only clear __main__ — other modules' __del__ handlers
|
||||
/// need their globals intact. CPython can clear ALL module dicts because
|
||||
/// _PyGC_CollectNoFail() finalizes cycle-participating objects beforehand.
|
||||
/// Phase 5: Clear module dicts in reverse import order.
|
||||
/// Skip builtins and sys — those are cleared last in Phase 7.
|
||||
fn finalize_clear_module_dicts(&self, module_weakrefs: &[(String, PyRef<PyWeak>)]) {
|
||||
for (name, weakref) in module_weakrefs.iter().rev() {
|
||||
// Only clear __main__ — user objects with __del__ get finalized
|
||||
// while other modules' globals remain intact for their __del__ handlers.
|
||||
if name != "__main__" {
|
||||
continue;
|
||||
}
|
||||
let builtins_dict = self.builtins.dict();
|
||||
let sys_dict = self.sys_module.dict();
|
||||
|
||||
for (_name, weakref) in module_weakrefs.iter().rev() {
|
||||
let Some(module_obj) = weakref.upgrade() else {
|
||||
continue;
|
||||
};
|
||||
@@ -858,7 +851,13 @@ impl VirtualMachine {
|
||||
continue;
|
||||
};
|
||||
|
||||
Self::module_clear_dict(&module.dict(), self);
|
||||
let dict = module.dict();
|
||||
// Skip builtins and sys — they are cleared last
|
||||
if dict.is(&builtins_dict) || dict.is(&sys_dict) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Self::module_clear_dict(&dict, self);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -875,7 +874,7 @@ impl VirtualMachine {
|
||||
}
|
||||
if let Some(key_str) = key.downcast_ref::<PyStr>() {
|
||||
let name = key_str.as_str();
|
||||
if name.starts_with('_') && name != "__builtins__" && name != "__spec__" {
|
||||
if name.starts_with('_') && name != "__builtins__" {
|
||||
let _ = dict.set_item(name, none.clone(), vm);
|
||||
}
|
||||
}
|
||||
@@ -888,14 +887,13 @@ impl VirtualMachine {
|
||||
}
|
||||
if let Some(key_str) = key.downcast_ref::<PyStr>()
|
||||
&& key_str.as_str() != "__builtins__"
|
||||
&& key_str.as_str() != "__spec__"
|
||||
{
|
||||
let _ = dict.set_item(key_str.as_str(), none.clone(), vm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Phase 5: Clear sys and builtins dicts last.
|
||||
/// Phase 7: Clear sys and builtins dicts last.
|
||||
fn finalize_clear_sys_builtins_dict(&self) {
|
||||
Self::module_clear_dict(&self.sys_module.dict(), self);
|
||||
Self::module_clear_dict(&self.builtins.dict(), self);
|
||||
@@ -1148,6 +1146,7 @@ impl VirtualMachine {
|
||||
self.frames.borrow_mut().pop();
|
||||
#[cfg(feature = "threading")]
|
||||
crate::vm::thread::pop_thread_frame();
|
||||
|
||||
self.recursion_depth.update(|d| d - 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ thread_local! {
|
||||
/// while the owning thread is writing).
|
||||
pub(crate) static CURRENT_FRAME: AtomicPtr<Frame> =
|
||||
const { AtomicPtr::new(core::ptr::null_mut()) };
|
||||
|
||||
}
|
||||
|
||||
scoped_tls::scoped_thread_local!(static VM_CURRENT: VirtualMachine);
|
||||
|
||||
Reference in new issue
Block a user