From 4535f70f80b003f9b0f1e98fc9ff9505bb7e7bd6 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Wed, 24 Jun 2026 15:59:55 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20GC=20sweep=20=E5=A4=8D=E7=94=A8=20Vm.for?= =?UTF-8?q?warding=EF=BC=8C=E6=B6=88=E9=99=A4=E6=AF=8F=E8=BD=AE=20HashMap?= =?UTF-8?q?=20=E5=88=86=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 1 + crates/oxide_vm/Cargo.toml | 1 + crates/oxide_vm/src/session_gc.rs | 33 ++++++++++++++++++++++++++++--- crates/oxide_vm/src/vm.rs | 5 +++++ crates/oxide_vm/src/vm_support.rs | 2 ++ 5 files changed, 39 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79804039..e56ce4b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1125,6 +1125,7 @@ dependencies = [ "oxide_types", "proptest", "regex", + "rustc-hash", "ryu", "serde_json", "smallvec", diff --git a/crates/oxide_vm/Cargo.toml b/crates/oxide_vm/Cargo.toml index 365a1cb2..9807b34d 100644 --- a/crates/oxide_vm/Cargo.toml +++ b/crates/oxide_vm/Cargo.toml @@ -21,6 +21,7 @@ indexmap.workspace = true smallvec.workspace = true unicode-normalization.workspace = true tracing.workspace = true +rustc-hash.workspace = true [dev-dependencies] oxide_compiler = { path = "../oxide_compiler" } diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 64b1b1ba..7e945fbd 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -5,6 +5,7 @@ use std::time::Instant; use oxide_kernel::vm_debug; use oxide_types::object::{JsObject, PropMetaEntry}; use oxide_types::value::JsValue; +use rustc_hash::FxBuildHasher; use crate::builtins::{array_buffer, data_view, map, regexp, set, typed_array}; use crate::vm::Vm; @@ -187,7 +188,7 @@ impl SessionGc { pub(crate) fn sweep(&mut self, vm: &mut Vm) -> u64 { let old_ptrs = std::mem::take(&mut vm.session_object_ptrs); - let mut forwarding: HashMap<*mut JsObject, *mut JsObject> = HashMap::new(); + let mut forwarding = std::mem::take(&mut vm.forwarding); let new_arena = bumpalo::Bump::new(); let mut survivors = 0u64; let mut dead = 0u64; @@ -251,6 +252,8 @@ impl SessionGc { rewrite_vm_roots(vm, &forwarding); vm.session_object_ptrs = forwarding.values().copied().collect(); + forwarding.clear(); + vm.forwarding = forwarding; vm.session_epoch = new_arena; vm.session_bytes_allocated = vm.session_object_ptrs.len() * size_of::(); @@ -342,7 +345,9 @@ impl Default for SessionGc { } } -fn rewrite_forwarded_value(value: JsValue, forwarding: &HashMap<*mut JsObject, *mut JsObject>) -> JsValue { +fn rewrite_forwarded_value( + value: JsValue, forwarding: &HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>, +) -> JsValue { if !value.is_object() { return value; } @@ -352,7 +357,7 @@ fn rewrite_forwarded_value(value: JsValue, forwarding: &HashMap<*mut JsObject, * .unwrap_or(value) } -fn rewrite_vm_roots(vm: &mut Vm, forwarding: &HashMap<*mut JsObject, *mut JsObject>) { +fn rewrite_vm_roots(vm: &mut Vm, forwarding: &HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>) { for value in &mut vm.regs { *value = rewrite_forwarded_value(*value, forwarding); } @@ -532,6 +537,28 @@ mod tests { assert!(!vm.session_object_ptrs.iter().any(|ptr| unsafe { (*(*ptr)).is_gc_marked() })); } + #[test] + fn forwarding_is_cleared_after_sweep() { + let mut vm = Vm::new(); + let root = plain_object(&mut vm); + let child = plain_object(&mut vm); + unsafe { + (*root).set_prop_at(0, JsValue::from_js_object(child)); + } + let root_session = vm.promote_object(root); + vm.regs[0] = JsValue::from_js_object(root_session); + let _ = vm.promote_object(child); + let roots = vm.gc_roots(); + let mut gc = std::mem::take(&mut vm.session_gc); + gc.mark(&vm, &roots); + let _ = gc.sweep(&mut vm); + vm.session_gc = gc; + + // The reused forwarding map MUST be cleared after sweep, otherwise + // promote would observe stale old->new entries pointing into the freed arena. + assert!(vm.forwarding.is_empty()); + } + fn vm_with_low_threshold() -> Vm { let mut cfg = KernelConfig::minimal(); cfg.set_session_gc_threshold(1); diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index f74a2cf5..49130033 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -155,6 +155,11 @@ pub struct Vm { pub epoch: Epoch, pub(crate) session_epoch: bumpalo::Bump, pub(crate) session_gc: SessionGc, + /// Long-lived forwarding map reused by GC sweep and promote. + /// `mem::take`'d out for use, `clear()`'d and put back — capacity retained + /// across collections, so no per-event `HashMap` allocation. `FxBuildHasher` + /// because the keys are raw pointers (collision resistance is irrelevant). + pub(crate) forwarding: HashMap<*mut JsObject, *mut JsObject, rustc_hash::FxBuildHasher>, pub(crate) epoch_object_ptrs: Vec<*mut JsObject>, pub(crate) session_object_ptrs: Vec<*mut JsObject>, pub(crate) session_bytes_allocated: usize, diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 2084b395..a74c66c9 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -32,6 +32,7 @@ impl Vm { epoch: Epoch::new(), session_epoch: bumpalo::Bump::new(), session_gc: crate::session_gc::SessionGc::new(), + forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher), epoch_object_ptrs: Vec::new(), session_object_ptrs: Vec::new(), session_bytes_allocated: 0, @@ -77,6 +78,7 @@ impl Vm { epoch: Epoch::new(), session_epoch: bumpalo::Bump::new(), session_gc: crate::session_gc::SessionGc::new(), + forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher), epoch_object_ptrs: Vec::new(), session_object_ptrs: Vec::new(), session_bytes_allocated: 0,