diff --git a/Cargo.lock b/Cargo.lock index 7980403..e56ce4b 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 365a1cb..9807b34 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 64b1b1b..7e945fb 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 f74a2cf..4913003 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 2084b39..a74c66c 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,