Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cspell.dict/rust-more.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ bstr
byteorder
byteset
caseless
cdpt
chrono
consts
cranelift
Expand Down
66 changes: 66 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
flamescope = { version = "0.1.2", optional = true }

rustls = { workspace = true, optional = true }
rustls-graviola = { workspace = true, optional = true }

Check warning on line 52 in Cargo.toml

View workflow job for this annotation

GitHub Actions / cargo shear

shear/misplaced_optional_dependency

misplaced optional dependency `rustls-graviola` (remove the `optional` flag and move to `[dev-dependencies]`)

[target.'cfg(windows)'.dependencies]
libc = { workspace = true }
Expand Down Expand Up @@ -201,6 +201,7 @@
bstr = "1"
bzip2 = "0.6"
chrono = { version = "0.4.44", default-features = false, features = ["clock", "std"] }
cdpt = "0.1.0"
console_error_panic_hook = "0.1"
constant_time_eq = "0.5"
cranelift = "0.132.0"
Expand Down
2 changes: 0 additions & 2 deletions Lib/test/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,6 @@ def test_function(self):
# is 3 because it includes f's code object.
self.assertIn(gc.collect(), (2, 3))

@unittest.expectedFailure # TODO: RUSTPYTHON; - weakref clear ordering differs from 3.15+
def test_function_tp_clear_leaves_consistent_state(self):
# https://github.com/python/cpython/issues/91636
code = """if 1:
Expand Down Expand Up @@ -831,7 +830,6 @@ def __del__(self):
rc, out, err = assert_python_ok(TESTFN)
self.assertEqual(out.strip(), b'__del__ called')

@unittest.expectedFailure # TODO: RUSTPYTHON
def test_get_stats(self):
stats = gc.get_stats()
self.assertEqual(len(stats), 3)
Expand Down
1 change: 1 addition & 0 deletions crates/vm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ writeable = { workspace = true }
exitcode = { workspace = true }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
cdpt = { workspace = true }
rustyline = { workspace = true }
which = { workspace = true }
widestring = { workspace = true }
Expand Down
43 changes: 29 additions & 14 deletions crates/vm/src/gc_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,21 @@ impl GcGeneration {
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct GcPtr(NonNull<PyObject>);

fn clear_weakrefs_and_invoke_callbacks(objects: &[PyObjectRef]) {
let mut all_callbacks = Vec::new();
for obj_ref in objects {
let callbacks = obj_ref.gc_clear_weakrefs_collect_callbacks();
all_callbacks.extend(callbacks);
}
for (wr, cb) in all_callbacks {
if let Some(Err(e)) = crate::vm::thread::with_vm(&cb, |vm| cb.call((wr.clone(),), vm)) {
crate::vm::thread::with_vm(&cb, |vm| {
vm.run_unraisable(e.clone(), Some("weakref callback".to_owned()), cb.clone());
});
}
}
}

/// Global GC state
pub struct GcState {
/// 3 generations (0 = youngest, 2 = oldest)
Expand Down Expand Up @@ -398,6 +413,11 @@ impl GcState {
_ => std::time::Instant::now(),
};

// Keep a CDPT guard during collection, following origin/gc's
// coarse-grained guarded collection experiment.
#[cfg(not(target_arch = "wasm32"))]
let _cdpt_guard = cdpt::pin();

// 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);
Expand Down Expand Up @@ -602,20 +622,10 @@ impl GcState {
})
.collect();

// 6c: Clear existing weakrefs BEFORE calling __del__
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);
}
for (wr, cb) in all_callbacks {
if let Some(Err(e)) = crate::vm::thread::with_vm(&cb, |vm| cb.call((wr.clone(),), vm)) {
crate::vm::thread::with_vm(&cb, |vm| {
vm.run_unraisable(e.clone(), Some("weakref callback".to_owned()), cb.clone());
});
}
}
// 6c: Clear weakrefs that existed before finalizers. This prevents a
// later tp_clear side effect from observing callback-free weakrefs to
// garbage in this generation.
clear_weakrefs_and_invoke_callbacks(&unreachable_refs);

// 6d: Call __del__ on unreachable objects (skip already-finalized).
// try_call_finalizer() internally checks gc_finalized() and sets it,
Expand All @@ -624,6 +634,11 @@ impl GcState {
obj_ref.try_call_finalizer();
}

// 6e: Clear weakrefs after finalizers, but before tp_clear. Finalizers
// can create weakrefs to other unreachable objects, and those must not
// reveal an object while its clear function is running.
clear_weakrefs_and_invoke_callbacks(&unreachable_refs);

// Detect resurrection
let mut resurrected_set: HashSet<GcPtr> = HashSet::new();
let unreachable_set: HashSet<GcPtr> = unreachable.iter().copied().collect();
Expand Down
3 changes: 2 additions & 1 deletion crates/vm/src/object/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1770,7 +1770,8 @@ impl PyObject {
}

/// Clear weakrefs but collect callbacks instead of calling them.
/// This is used by GC to ensure ALL weakrefs are cleared BEFORE any callbacks run.
/// GC uses this while handling a garbage set so callbacks run only after all
/// weakrefs in the current pass have been invalidated.
/// Returns collected callbacks as (PyRef<PyWeak>, callback) pairs.
// = handle_weakrefs
pub fn gc_clear_weakrefs_collect_callbacks(&self) -> Vec<(PyRef<PyWeak>, PyObjectRef)> {
Expand Down
2 changes: 0 additions & 2 deletions crates/vm/src/stdlib/gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,6 @@ 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());
}

Expand Down
Loading