From add910a504e75521dbaf8e76cfbbcaa2cab31547 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Tue, 6 Oct 2026 23:32:28 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20GC=20mark=20=E6=92=AD=E7=A7=8D=E6=8C=89?= =?UTF-8?q?=2024=20=E6=A0=B9=E7=BB=84=E8=AE=A1=E6=95=B0=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3=20roots=20=E6=97=A5=E5=BF=97=E5=8F=A3=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 36 ++++---- Cargo.toml | 2 +- crates/oxide_vm/src/promise/jobs.rs | 22 +++-- crates/oxide_vm/src/session_gc.rs | 46 +++++++++- crates/oxide_vm/src/session_gc/tests.rs | 43 ++++++++-- crates/oxide_vm/src/vm/gc_hooks.rs | 109 ++++++++++++++++-------- crates/oxide_vm/src/vm/mod.rs | 1 + 7 files changed, 190 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index de06984f..a544a710 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1160,7 +1160,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.8.45" +version = "0.8.46" dependencies = [ "chrono", "fast-float", @@ -1185,7 +1185,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.8.45" +version = "0.8.46" dependencies = [ "num-bigint", "oxide_log", @@ -1193,7 +1193,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.8.45" +version = "0.8.46" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1204,7 +1204,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.8.45" +version = "0.8.46" dependencies = [ "ansi_term", "bumpalo", @@ -1225,7 +1225,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.8.45" +version = "0.8.46" dependencies = [ "lru", "oxide_bytecode", @@ -1234,7 +1234,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.8.45" +version = "0.8.46" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1253,7 +1253,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.8.45" +version = "0.8.46" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1267,7 +1267,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.8.45" +version = "0.8.46" dependencies = [ "num-bigint", "oxc_allocator", @@ -1282,7 +1282,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.8.45" +version = "0.8.46" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1291,7 +1291,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.8.45" +version = "0.8.46" dependencies = [ "bumpalo", "dashmap", @@ -1306,7 +1306,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.8.45" +version = "0.8.46" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1318,7 +1318,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.8.45" +version = "0.8.46" dependencies = [ "tracing", "tracing-appender", @@ -1327,7 +1327,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.8.45" +version = "0.8.46" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1342,7 +1342,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.8.45" +version = "0.8.46" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1358,7 +1358,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.8.45" +version = "0.8.46" dependencies = [ "num-bigint", "num-traits", @@ -1370,7 +1370,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.8.45" +version = "0.8.46" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1387,7 +1387,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.8.45" +version = "0.8.46" dependencies = [ "bumpalo", "hashbrown 0.14.5", @@ -1399,7 +1399,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.8.45" +version = "0.8.46" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index c9adedb7..71fdd8c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ members = [ ] [workspace.package] -version = "0.8.45" +version = "0.8.46" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_vm/src/promise/jobs.rs b/crates/oxide_vm/src/promise/jobs.rs index b19cd955..2ed4ec55 100644 --- a/crates/oxide_vm/src/promise/jobs.rs +++ b/crates/oxide_vm/src/promise/jobs.rs @@ -2,6 +2,7 @@ use oxide_types::value::JsValue; +use crate::vm::RootGroup; use crate::vm::Vm; use crate::vm_warn; @@ -80,7 +81,10 @@ impl Vm { } /// 供外部遍历微任务队列(GC mark/rewrite 用)。 -pub(crate) fn for_each_job_value(job: &Microtask, mut f: impl FnMut(JsValue)) { +/// +/// `group` 是调用方传入的根组来源(微任务队列属 `JobQueue` 组),原样透传给 +/// 内层闭包。 +pub(crate) fn for_each_job_value(job: &Microtask, group: RootGroup, mut f: impl FnMut(RootGroup, JsValue)) { match job { Microtask::Reaction { handler, @@ -89,10 +93,10 @@ pub(crate) fn for_each_job_value(job: &Microtask, mut f: impl FnMut(JsValue)) { reject, .. } => { - f(*handler); - f(*argument); - f(*resolve); - f(*reject); + f(group, *handler); + f(group, *argument); + f(group, *resolve); + f(group, *reject); } Microtask::Thenable { thenable, @@ -100,10 +104,10 @@ pub(crate) fn for_each_job_value(job: &Microtask, mut f: impl FnMut(JsValue)) { resolve, reject, } => { - f(*thenable); - f(*then); - f(*resolve); - f(*reject); + f(group, *thenable); + f(group, *then); + f(group, *resolve); + f(group, *reject); } } } diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index b008e09c..394a750c 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -8,6 +8,7 @@ use oxide_types::value::JsValue; use rustc_hash::FxBuildHasher; use crate::native_box_dispatch; +use crate::vm::RootGroup; use crate::vm::Vm; use oxide_builtins::{array_buffer, data_view, disposable_stack, map, module, regexp, set, typed_array, weak_map}; @@ -30,6 +31,8 @@ pub struct SessionGc { pub last_collection_duration_us: u64, pub max_collection_duration_us: u64, pub min_collection_duration_us: u64, + /// 最近一次 mark 播种的逐组根值计数(下标为 `RootGroup` 变体下标,和为根值总数)。 + pub root_counts: [u64; RootGroup::COUNT], pub(crate) mark_stack: Vec<*mut JsObject>, pub(crate) live_strings: HashSet<*mut JsString, FxBuildHasher>, pub(crate) live_bigints: HashSet<*mut num_bigint::BigInt, FxBuildHasher>, @@ -375,12 +378,19 @@ impl SessionGc { /// 字符串边走 rope 闭包传播(Cons 子节点与扁平化产物),BigInt 边直接入 /// 存活集。roots 由 `Vm::for_each_root` 枚举。只置位不搬移,调用前须先 /// `clear_all_marks` 清位。 + /// + /// # 副作用 + /// - `root_counts` 在播种开头重置、按根组累加(和为根值总数)。 pub(crate) fn mark(&mut self, vm: &Vm) { - vm_debug!("[GC] mark phase: {} roots", vm.gc_state.session_object_ptrs.len()); + // 播种开头重置逐组计数:计数器恒反映最近一次 mark 的播种画像。 + self.root_counts = [0; RootGroup::COUNT]; + let mut seeds = Vec::new(); let mut string_seeds: Vec<*mut JsString> = Vec::new(); let mut bigint_seeds: Vec<*mut num_bigint::BigInt> = Vec::new(); - vm.for_each_root(|root| { + let counts = &mut self.root_counts; + vm.for_each_root(|group, root| { + counts[group as usize] += 1; if root.is_object() { seeds.push(root.as_js_object_ptr()); } else if root.is_string() { @@ -390,6 +400,37 @@ impl SessionGc { } }); + // 根数口径是 `for_each_root` 枚举出的根值总数(24 组之和)。 + let root_total: u64 = self.root_counts.iter().sum(); + vm_debug!("[GC] mark phase: {} roots", root_total); + vm_debug!( + "[GC] mark phase root groups: regs={}, immutables={}, frames={}, save_stack={}, spill_stack={}, cell_stack={}, exception_value={}, pending_exception={}, last_uncaught={}, pending_length_exception={}, pending_completion={}, generator_suspended={}, delegated_iterator={}, async_context={}, async_gen_context={}, pending_async_escape={}, inline_callee={}, template_objects={}, number_to_string_cache={}, for_of_iters={}, job_queue={}, atomics_waiters={}, for_in_iters={}, global={}", + self.root_counts[RootGroup::Regs as usize], + self.root_counts[RootGroup::Immutables as usize], + self.root_counts[RootGroup::Frames as usize], + self.root_counts[RootGroup::SaveStack as usize], + self.root_counts[RootGroup::SpillStack as usize], + self.root_counts[RootGroup::CellStack as usize], + self.root_counts[RootGroup::ExceptionValue as usize], + self.root_counts[RootGroup::PendingException as usize], + self.root_counts[RootGroup::LastUncaught as usize], + self.root_counts[RootGroup::PendingLengthException as usize], + self.root_counts[RootGroup::PendingCompletion as usize], + self.root_counts[RootGroup::GeneratorSuspended as usize], + self.root_counts[RootGroup::DelegatedIterator as usize], + self.root_counts[RootGroup::AsyncContext as usize], + self.root_counts[RootGroup::AsyncGenContext as usize], + self.root_counts[RootGroup::PendingAsyncEscape as usize], + self.root_counts[RootGroup::InlineCallee as usize], + self.root_counts[RootGroup::TemplateObjects as usize], + self.root_counts[RootGroup::NumberToStringCache as usize], + self.root_counts[RootGroup::ForOfIters as usize], + self.root_counts[RootGroup::JobQueue as usize], + self.root_counts[RootGroup::AtomicsWaiters as usize], + self.root_counts[RootGroup::ForInIters as usize], + self.root_counts[RootGroup::Global as usize], + ); + let Self { mark_stack: stack, live_strings, @@ -1052,6 +1093,7 @@ impl Default for SessionGc { last_collection_duration_us: 0, max_collection_duration_us: 0, min_collection_duration_us: u64::MAX, + root_counts: [0; RootGroup::COUNT], mark_stack: Vec::new(), live_strings: HashSet::default(), live_bigints: HashSet::default(), diff --git a/crates/oxide_vm/src/session_gc/tests.rs b/crates/oxide_vm/src/session_gc/tests.rs index 363a01ca..46ddb341 100644 --- a/crates/oxide_vm/src/session_gc/tests.rs +++ b/crates/oxide_vm/src/session_gc/tests.rs @@ -8,7 +8,7 @@ use oxide_types::value::JsValue; use std::sync::Arc; use super::*; -use crate::vm::{CallFrame, FrameContinuation}; +use crate::vm::{CallFrame, FrameContinuation, RootGroup}; use oxide_builtins::{array_buffer, data_view, disposable_stack, map, module, set, typed_array}; use oxide_emit::module::{ModuleKind, ModuleSourceLoader, ResolvedModule}; use oxide_runtime_api::NativeResult; @@ -34,7 +34,7 @@ fn uncaught_value_is_gc_root() { let session = obj; vm.last_uncaught_value = Some(JsValue::from_js_object(session)); let mut roots = Vec::new(); - vm.for_each_root(|v| roots.push(v)); + vm.for_each_root(|_g, v| roots.push(v)); assert!(has_ptr(&roots, session)); } @@ -45,7 +45,7 @@ fn pending_length_exception_is_gc_root() { let session = obj; vm.pending_length_exception = Some(JsValue::from_js_object(session)); let mut roots = Vec::new(); - vm.for_each_root(|v| roots.push(v)); + vm.for_each_root(|_g, v| roots.push(v)); assert!(has_ptr(&roots, session)); } @@ -62,7 +62,7 @@ fn suspended_signal_fields_are_roots() { vm.async_context = Some(JsValue::from_js_object(b_s)); vm.async_gen_context = Some(JsValue::from_js_object(c_s)); let mut roots = Vec::new(); - vm.for_each_root(|v| roots.push(v)); + vm.for_each_root(|_g, v| roots.push(v)); assert!(has_ptr(&roots, a_s)); assert!(has_ptr(&roots, b_s)); assert!(has_ptr(&roots, c_s)); @@ -122,7 +122,7 @@ fn gc_roots_contains_registers_frames_and_root_roots() { }); let mut roots = Vec::new(); - vm.for_each_root(|v| roots.push(v)); + vm.for_each_root(|_g, v| roots.push(v)); assert!(has_ptr(&roots, root_session)); assert!(has_ptr(&roots, frame_session)); assert!(has_ptr(&roots, this_session)); @@ -1892,3 +1892,36 @@ fn promise_promote_migrates_reject_reactions_to_clone() { "晋升前登记的拒绝消费者应在拒绝走克隆后被触发" ); } + +/// mark 播种逐组计数:新 VM 加 1 个寄存器根后,Regs 组 256(全部寄存器槽, +/// 含未定义槽)、Global 组 1(global 对象)、9 个单槽侧通道组各 1(空时经 +/// `unwrap_or(undefined)` 各产出一个未定义根值)、总和 266,其余组为 0。 +#[test] +fn mark_counts_roots_by_group() { + let mut vm = Vm::new(); + vm.regs[0] = JsValue::from_js_object(plain_object(&mut vm)); + let mut gc = std::mem::take(&mut vm.gc_state.session_gc); + gc.mark(&vm); + vm.gc_state.session_gc = gc; + + let counts = &vm.gc_state.session_gc.root_counts; + assert_eq!(counts[RootGroup::Regs as usize], 256, "寄存器组应计满 256 槽"); + assert_eq!(counts[RootGroup::Global as usize], 1, "global 组应计 global 对象"); + // 9 个单槽侧通道:空 VM 时各经 unwrap_or(undefined) 产出一个未定义根值。 + for group in [ + RootGroup::ExceptionValue, + RootGroup::PendingException, + RootGroup::LastUncaught, + RootGroup::PendingLengthException, + RootGroup::GeneratorSuspended, + RootGroup::DelegatedIterator, + RootGroup::AsyncContext, + RootGroup::AsyncGenContext, + RootGroup::InlineCallee, + ] { + assert_eq!(counts[group as usize], 1, "侧通道组 {group:?} 应各计 1"); + } + // 计数非负:11 个非零组之和恰为 266,总和断言同时钉死其余 13 组为 0。 + let total: u64 = counts.iter().sum(); + assert_eq!(total, 266, "24 组之和应为根值总数"); +} diff --git a/crates/oxide_vm/src/vm/gc_hooks.rs b/crates/oxide_vm/src/vm/gc_hooks.rs index 005c8dcf..ab9ce994 100644 --- a/crates/oxide_vm/src/vm/gc_hooks.rs +++ b/crates/oxide_vm/src/vm/gc_hooks.rs @@ -8,13 +8,54 @@ use super::frames::Completion; use super::Vm; use crate::session_gc::SessionGc; +/// GC 根组来源:`for_each_value` 枚举的 24 个根组,变体顺序与根清单 +/// 的枚举顺序一致。 +/// +/// 变体下标是逐组计数数组(`SessionGc::root_counts`)的下标;`COUNT` 与 +/// 变体数同源,新增根组须同步加变体并更新 `COUNT`,计数数组长度随之跟随。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum RootGroup { + Regs, + Immutables, + Frames, + SaveStack, + SpillStack, + CellStack, + ExceptionValue, + PendingException, + LastUncaught, + PendingLengthException, + PendingCompletion, + GeneratorSuspended, + DelegatedIterator, + AsyncContext, + AsyncGenContext, + PendingAsyncEscape, + InlineCallee, + TemplateObjects, + NumberToStringCache, + ForOfIters, + JobQueue, + AtomicsWaiters, + ForInIters, + Global, +} + +impl RootGroup { + /// 根组总数(与变体数同源,绑定逐组计数数组的长度)。 + pub const COUNT: usize = 24; +} + impl Vm { /// GC 根收集的统一遍历(对象与字符串都产出)。 /// 覆盖执行核心的全部 JsValue 持有点:regs/帧/各栈段/cell/在途异常与完成/ /// 挂起信号/迭代器/微任务/global。 - pub(crate) fn for_each_value(&self, mut f: impl FnMut(JsValue)) { + /// + /// 闭包接收根组来源(`RootGroup`)与根值:组是枚举顺序中的 24 根组之一, + /// 调用方按组计数(如 `SessionGc` 的逐组计数)。 + pub(crate) fn for_each_value(&self, mut f: impl FnMut(RootGroup, JsValue)) { for value in &self.regs { - f(*value); + f(RootGroup::Regs, *value); } // 各代际 immutables 缓存含 session BigInt(new_bigint 分配),未入根则被 // sweep 释放 → 常量池加载时悬垂。perm 字符串无害(不在 session 集合中)。 @@ -22,23 +63,23 @@ impl Vm { for once_lock in &table.immutables { if let Some(immutable_vec) = once_lock.get() { for &value in immutable_vec.iter() { - f(value); + f(RootGroup::Immutables, value); } } } } for frame in &self.frames { - f(frame.saved_this); - f(frame.saved_new_target); - f(frame.callee); - f(frame.constructed_this.unwrap_or(JsValue::undefined())); + f(RootGroup::Frames, frame.saved_this); + f(RootGroup::Frames, frame.saved_new_target); + f(RootGroup::Frames, frame.callee); + f(RootGroup::Frames, frame.constructed_this.unwrap_or(JsValue::undefined())); } for &v in &self.save_stack { - f(v); + f(RootGroup::SaveStack, v); } // spill 栈是 session GC 根(漏根 → 溢出值被回收 → use-after-free)。 for &v in &self.spill_stack { - f(v); + f(RootGroup::SpillStack, v); } for cell_vec in &self.cell_stack { for &cell_ptr in cell_vec { @@ -46,56 +87,56 @@ impl Vm { continue; } // SAFETY: cell 经 alloc_cell 独立堆分配,本 session 内指针有效。 - f(unsafe { &*cell_ptr }.value); + f(RootGroup::CellStack, unsafe { &*cell_ptr }.value); } } - f(self.exception_value.unwrap_or(JsValue::undefined())); - f(self.pending_exception.unwrap_or(JsValue::undefined())); - f(self.last_uncaught_value.unwrap_or(JsValue::undefined())); - f(self.pending_length_exception.unwrap_or(JsValue::undefined())); + f(RootGroup::ExceptionValue, self.exception_value.unwrap_or(JsValue::undefined())); + f(RootGroup::PendingException, self.pending_exception.unwrap_or(JsValue::undefined())); + f(RootGroup::LastUncaught, self.last_uncaught_value.unwrap_or(JsValue::undefined())); + f(RootGroup::PendingLengthException, self.pending_length_exception.unwrap_or(JsValue::undefined())); // 悬挂的 return 完成持有返回值,是 GC 根。 if let Some(Completion::Return { value, .. }) = self.pending_completion { - f(value); + f(RootGroup::PendingCompletion, value); } - f(self.generator_suspended.unwrap_or(JsValue::undefined())); - f(self.delegated_iterator.unwrap_or(JsValue::undefined())); - f(self.async_context.unwrap_or(JsValue::undefined())); - f(self.async_gen_context.unwrap_or(JsValue::undefined())); + f(RootGroup::GeneratorSuspended, self.generator_suspended.unwrap_or(JsValue::undefined())); + f(RootGroup::DelegatedIterator, self.delegated_iterator.unwrap_or(JsValue::undefined())); + f(RootGroup::AsyncContext, self.async_context.unwrap_or(JsValue::undefined())); + f(RootGroup::AsyncGenContext, self.async_gen_context.unwrap_or(JsValue::undefined())); // 在途异步逃出的 promise/完成值/剩余迭代器都是 GC 根。 if let Some(pend) = &self.pending_async_escape { - f(pend.close_promise); + f(RootGroup::PendingAsyncEscape, pend.close_promise); if let Completion::Return { value, .. } = pend.completion { - f(value); + f(RootGroup::PendingAsyncEscape, value); } for &v in &pend.remaining { - f(v); + f(RootGroup::PendingAsyncEscape, v); } } - f(self.inline_callee.unwrap_or(JsValue::undefined())); + f(RootGroup::InlineCallee, self.inline_callee.unwrap_or(JsValue::undefined())); // 标签模板对象缓存:命中的模板对象是 GC 根(未根 → sweep 搬移/回收悬垂)。 for &cached in self.template_objects.values() { - f(cached); + f(RootGroup::TemplateObjects, cached); } // f64→string 缓存槽值是 session 串,是 GC 根(漏根 → 串清扫释放 → 命中悬垂)。 for &v in &self.number_to_string_cache_vals { if v.is_string() { - f(v); + f(RootGroup::NumberToStringCache, v); } } for &entry in &self.iters.for_of_iters { - f(entry.iterator); - f(entry.last_result); - f(entry.fast_value); - f(entry.fast_inner); + f(RootGroup::ForOfIters, entry.iterator); + f(RootGroup::ForOfIters, entry.last_result); + f(RootGroup::ForOfIters, entry.fast_value); + f(RootGroup::ForOfIters, entry.fast_inner); } // 微任务队列中的处理器/能力/值都是 GC 根。 for job in &self.job_queue { - crate::promise::for_each_job_value(job, &mut f); + crate::promise::for_each_job_value(job, RootGroup::JobQueue, &mut f); } // Atomics waiter 表登记的 promise 是 GC 根(run 内跨调用存活)。 for promises in self.atomics_waiters.values() { for p in promises { - f(*p); + f(RootGroup::AtomicsWaiters, *p); } } for iter in &self.iters.for_in_iters { @@ -105,15 +146,15 @@ impl Vm { // SAFETY: for_in_iters 存放堆上迭代器体,VM 表独占持有。 unsafe { for (v, _si) in (*(*iter)).keys.iter() { - f(*v); + f(RootGroup::ForInIters, *v); } } } - f(JsValue::from_js_object(self.session.global_object().as_ptr() as *mut JsObject)); + f(RootGroup::Global, JsValue::from_js_object(self.session.global_object().as_ptr() as *mut JsObject)); } /// GC 根统一枚举入口:遍历的字段清单与 `for_each_value` 相同。 - pub(crate) fn for_each_root(&self, f: impl FnMut(JsValue)) { + pub(crate) fn for_each_root(&self, f: impl FnMut(RootGroup, JsValue)) { self.for_each_value(f); } diff --git a/crates/oxide_vm/src/vm/mod.rs b/crates/oxide_vm/src/vm/mod.rs index c6d97201..51fbecee 100644 --- a/crates/oxide_vm/src/vm/mod.rs +++ b/crates/oxide_vm/src/vm/mod.rs @@ -39,6 +39,7 @@ mod vmhost; pub(crate) use frames::FrameArgs; pub use frames::{CallFrame, Completion, ForInIter, FrameContinuation, TryHandler}; +pub(crate) use gc_hooks::RootGroup; pub(crate) use inline::InlineSyncState; pub(crate) use tables::TableGen;