From 390b72e3bd54bf291b2610231582304f54f3d21e Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 12 Sep 2026 00:25:56 +0000 Subject: [PATCH] =?UTF-8?q?perf:=20=E6=89=A7=E8=A1=8C=E6=9C=9F=20GC=20?= =?UTF-8?q?=E6=94=B6=E9=9B=86=E2=80=94=E2=80=94=E6=89=A9=E5=B1=95=20mark?= =?UTF-8?q?=20=E8=B7=9F=E9=9A=8F=20epoch=20=E8=87=82=E3=80=81=E6=B4=BB=20e?= =?UTF-8?q?poch=20=E5=AF=B9=E8=B1=A1=E6=99=8B=E5=8D=87=20session=EF=BC=88?= =?UTF-8?q?=E5=85=8B=E9=9A=86=E4=BD=93=E7=BD=AE=E6=B4=BB=E9=98=B2=E5=90=8C?= =?UTF-8?q?=E8=BD=AE=E5=8E=9F=E5=9C=B0=20sweep=20=E8=AF=AF=E6=94=BE?= =?UTF-8?q?=EF=BC=89=E3=80=81=E6=AD=BB=20session=20=E5=AF=B9=E8=B1=A1?= =?UTF-8?q?=E5=8E=9F=E5=9C=B0=E9=87=8A=E6=94=BE=E5=87=BA=E8=A1=A8=EF=BC=8C?= =?UTF-8?q?for-in=20=E9=97=A8=E6=8E=A7=EF=BC=88=E6=B4=BB=E8=B7=83=E8=BF=AD?= =?UTF-8?q?=E4=BB=A3=E5=99=A8=20+=20=E6=8C=82=E8=B5=B7=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E7=9B=92=EF=BC=89=E4=B8=8E=E5=88=86=E9=85=8D=E5=8C=85=E7=BB=9C?= =?UTF-8?q?=E6=B0=B4=E4=BD=8D=E8=A7=A6=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_vm/src/async_func.rs | 11 +- crates/oxide_vm/src/async_generator.rs | 11 +- crates/oxide_vm/src/generator.rs | 11 +- crates/oxide_vm/src/session_arena.rs | 46 ++- crates/oxide_vm/src/session_gc.rs | 423 +++++++++++++++++++++++-- crates/oxide_vm/src/vm.rs | 66 +++- crates/oxide_vm/src/vm_state.rs | 12 +- crates/oxide_vm/src/vm_support.rs | 8 + 8 files changed, 536 insertions(+), 52 deletions(-) diff --git a/crates/oxide_vm/src/async_func.rs b/crates/oxide_vm/src/async_func.rs index b0b05e4e..b368bfb4 100644 --- a/crates/oxide_vm/src/async_func.rs +++ b/crates/oxide_vm/src/async_func.rs @@ -478,11 +478,20 @@ fn async_state_mut(obj: &JsObject) -> Option<&mut AsyncState> { None } else { // SAFETY: native_data 在 create_async_context 中由 Box::into_raw 分配, - // 生命周期与异步上下文对象一致;GC 只在 reset(无执行状态)时运行。 + // 生命周期与异步上下文对象一致;GC 路径(mark 边收集、执行期门控扫描)只读状态盒。 Some(unsafe { &mut *ptr }) } } +/// 异步挂起帧是否持有 for-in 迭代器:执行期收集门控按指针扫描 +/// (ForInIter body 分配于 epoch arena,换新 Bump 即时失效在表迭代器)。 +pub(crate) fn async_holds_suspended_for_in(obj: &JsObject) -> bool { + let Some(state) = async_state_mut(obj) else { + return false; + }; + !state.suspended.for_in_iters.is_empty() +} + /// 异步状态内所有对象引用的扁平列表(GC mark 边)。 pub(crate) fn async_native_edges(obj: &JsObject) -> Vec { let Some(state) = async_state_mut(obj) else { diff --git a/crates/oxide_vm/src/async_generator.rs b/crates/oxide_vm/src/async_generator.rs index 4201231d..b160aa36 100644 --- a/crates/oxide_vm/src/async_generator.rs +++ b/crates/oxide_vm/src/async_generator.rs @@ -864,11 +864,20 @@ fn async_gen_state_mut(obj: &JsObject) -> Option<&mut AsyncGeneratorState> { None } else { // SAFETY: native_data 在 create_async_generator_object 中由 Box::into_raw 分配, - // 生命周期与异步生成器对象一致;GC 只在 reset(无执行状态)时运行。 + // 生命周期与异步生成器对象一致;GC 路径(mark 边收集、执行期门控扫描)只读状态盒。 Some(unsafe { &mut *ptr }) } } +/// 异步生成器挂起帧是否持有 for-in 迭代器:执行期收集门控按指针扫描 +/// (ForInIter body 分配于 epoch arena,换新 Bump 即时失效在表迭代器)。 +pub(crate) fn async_generator_holds_suspended_for_in(obj: &JsObject) -> bool { + let Some(state) = async_gen_state_mut(obj) else { + return false; + }; + !state.suspended.for_in_iters.is_empty() +} + /// 异步生成器状态内所有对象引用的扁平列表(GC mark 边)。 pub(crate) fn async_generator_native_edges(obj: &JsObject) -> Vec { let Some(state) = async_gen_state_mut(obj) else { diff --git a/crates/oxide_vm/src/generator.rs b/crates/oxide_vm/src/generator.rs index ec537d94..2dd1dc27 100644 --- a/crates/oxide_vm/src/generator.rs +++ b/crates/oxide_vm/src/generator.rs @@ -859,11 +859,20 @@ fn generator_state_mut(obj: &JsObject) -> Option<&mut GeneratorState> { None } else { // SAFETY: native_data 在 create_generator_object 中由 Box::into_raw 分配, - // 生命周期与生成器对象一致;GC 只在 reset(无执行状态)时运行。 + // 生命周期与生成器对象一致;GC 路径(mark 边收集、执行期门控扫描)只读状态盒。 Some(unsafe { &mut *ptr }) } } +/// 生成器挂起帧是否持有 for-in 迭代器:执行期收集门控按指针扫描 +/// (ForInIter body 分配于 epoch arena,换新 Bump 即时失效在表迭代器)。 +pub(crate) fn generator_holds_suspended_for_in(obj: &JsObject) -> bool { + let Some(state) = generator_state_mut(obj) else { + return false; + }; + !state.suspended.for_in_iters.is_empty() +} + /// 生成器状态内所有对象引用的扁平列表(GC mark 边)。 pub(crate) fn generator_native_edges(obj: &JsObject) -> Vec { let Some(state) = generator_state_mut(obj) else { diff --git a/crates/oxide_vm/src/session_arena.rs b/crates/oxide_vm/src/session_arena.rs index e1944d07..bd3cadcc 100644 --- a/crates/oxide_vm/src/session_arena.rs +++ b/crates/oxide_vm/src/session_arena.rs @@ -141,53 +141,69 @@ impl Vm { return; } let mut forwarding = std::mem::take(&mut self.gc_state.forwarding); - for &ptr in &objects { + self.rewrite_session_epoch_refs(&objects, &mut forwarding); + forwarding.clear(); + self.gc_state.forwarding = forwarding; + // 改写期新克隆已随晋升推入 gc_state 侧的表(此刻仅含克隆体),把取出的 + // 旧对象按原相对序拼回同一表,表 = 克隆体 + 旧对象,各恰登记一次。 + self.gc_state.session_object_ptrs.extend(objects); + } + + /// 按调用方给定的转发表把对象列表的 epoch 子引用就地改写:未在上游晋升过的 + /// epoch 子对象经转发表克隆进 session(递归去重共享与环),已是 session/ + /// 非 epoch 的值原样保留。JS 边与 native 状态盒走同一改写闭包。 + /// + /// # 注意事项 + /// - 调用方持有转发表期间不得让其它晋升路径改动 `gc_state.forwarding`; + /// 晋升新克隆推入的对象表由调用方决定何时拼回。 + /// - 死对象(不可达)的 epoch 子引用改写会把死对象一并克隆进 session—— + /// 克隆体无根,下一轮收集按死对象出表,不泄漏也不悬垂。 + pub(crate) fn rewrite_session_epoch_refs( + &mut self, objects: &[*mut JsObject], forwarding: &mut HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>, + ) { + for &ptr in objects { if ptr.is_null() { continue; } // SAFETY: ptr 来自 session_epoch.alloc,arena 存活期内有效。 unsafe { let obj = &mut *ptr; - obj.rewrite_object_values(|value| self.promote_value_if_epoch_object(value, &mut forwarding)); + obj.rewrite_object_values(|value| self.promote_value_if_epoch_object(value, forwarding)); if obj.is_map() { - map::rewrite_map_native(obj, |value| self.promote_value_if_epoch_object(value, &mut forwarding)); + map::rewrite_map_native(obj, |value| self.promote_value_if_epoch_object(value, forwarding)); } else if obj.is_set() { - set::rewrite_set_native(obj, |value| self.promote_value_if_epoch_object(value, &mut forwarding)); + set::rewrite_set_native(obj, |value| self.promote_value_if_epoch_object(value, forwarding)); } else if obj.is_disposable_stack_obj() || obj.is_async_disposable_stack_obj() { disposable_stack::rewrite_dispose_native(obj, |value| { - self.promote_value_if_epoch_object(value, &mut forwarding) + self.promote_value_if_epoch_object(value, forwarding) }); } else if obj.is_typed_array_obj() { typed_array::rewrite_typed_array_native(obj, |value| { - self.promote_value_if_epoch_object(value, &mut forwarding) + self.promote_value_if_epoch_object(value, forwarding) }); } else if obj.is_data_view_obj() { data_view::rewrite_data_view_native(obj, |value| { - self.promote_value_if_epoch_object(value, &mut forwarding) + self.promote_value_if_epoch_object(value, forwarding) }); } else if obj.is_generator_obj() { crate::generator::rewrite_generator_native(obj, |value| { - self.promote_value_if_epoch_object(value, &mut forwarding) + self.promote_value_if_epoch_object(value, forwarding) }); } else if obj.is_promise_obj() { crate::promise::rewrite_promise_native(obj, |value| { - self.promote_value_if_epoch_object(value, &mut forwarding) + self.promote_value_if_epoch_object(value, forwarding) }); } else if obj.is_async_obj() { crate::async_func::rewrite_async_native(obj, |value| { - self.promote_value_if_epoch_object(value, &mut forwarding) + self.promote_value_if_epoch_object(value, forwarding) }); } else if obj.is_async_generator_obj() { crate::async_generator::rewrite_async_generator_native(obj, |value| { - self.promote_value_if_epoch_object(value, &mut forwarding) + self.promote_value_if_epoch_object(value, forwarding) }); } } } - forwarding.clear(); - self.gc_state.forwarding = forwarding; - // 晋升过程新克隆的对象已推入 gc_state 侧的表,拼回旧表保持原序在前。 - self.gc_state.session_object_ptrs.extend(objects); } /// 把根直接持有的 epoch 对象(顶层 var 寄存器、挂起句柄等)晋升进 session, diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 0aa67d6b..58457e4f 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -52,6 +52,16 @@ impl SessionGc { // session_epoch.alloc,在 arena 存活期间有效。 unsafe { (*ptr).set_gc_mark(false) }; } + // epoch 臂同清:mark 的 epoch 臂置位不随 session 清位消除,残留位会让 + // 下一次 mark 的 DFS 短路、漏扫该对象此间新增的边。 + for &ptr in &vm.gc_state.epoch_object_ptrs { + if ptr.is_null() { + continue; + } + // SAFETY: epoch_object_ptrs 中的指针只来自 alloc_object 的 epoch.alloc, + // 在 epoch 存活期间有效。 + unsafe { (*ptr).set_gc_mark(false) }; + } } /// 只读核算 session 对象的堆数据字节(属性/元素/meta Vec capacity + upvalue 列表 @@ -220,8 +230,15 @@ impl SessionGc { ) { if value.is_object() { let ptr = value.as_js_object_ptr(); + // session 边与 epoch 边同栈:epoch 对象的 GC_MARK 位与 session 同字节 + // 可置可读,晋升收集按"标记 ∩ epoch 表"取活集。 if vm.is_session_ptr(ptr) { stack.push(ptr); + } else if !ptr.is_null() { + // SAFETY: 执行核心产出的对象值,指针在 session 生命周期内有效。 + if unsafe { (&*ptr).is_epoch() } { + stack.push(ptr); + } } } else if value.is_string() { Self::mark_string_live(live_strings, value.as_string_ptr_mut()); @@ -397,9 +414,16 @@ impl SessionGc { stack.push(ptr); continue; } - // SAFETY: 对象根由 VM 自有的字段与 builtin 对象产生。 + // SAFETY: 对象根由 VM 自有的字段与 builtin 对象产生,指针合法。 unsafe { let obj = &*ptr; + // epoch 根(顶层 var 寄存器等)自身入栈置位:晋升收集按标记位 + // 判活,根不置位会被误判为死。 + if obj.is_epoch() { + stack.push(ptr); + continue; + } + // P 对象根(builtin world / global)不回收,只扫边。 Self::scan_edges_for_mark(obj, vm, stack, live_strings, live_bigints); } } @@ -727,18 +751,6 @@ impl SessionGc { && vm.gc_state.session_bytes_allocated >= vm.kernel_core().config().session_gc_threshold } - /// 执行期完整 GC 的触发判断:账目须超过水位(上次收集后的存活字节 + - /// 阈值增量)。与 `should_collect`(reset 路径用)不同,本方法走增量水位, - /// 防止存活对象超阈值时每指令重复触发无死对象可回收的白跑。 - /// 预留给 17.3b(对象侧执行期触发)安全点审计后使用。 - #[allow(dead_code)] - pub(crate) fn should_collect_gc(&self, vm: &Vm) -> bool { - (!vm.gc_state.session_object_ptrs.is_empty() - || !vm.gc_state.session_string_ptrs.is_empty() - || !vm.gc_state.session_bigint_ptrs.borrow().is_empty()) - && vm.gc_state.session_bytes_allocated >= vm.gc_state.gc_watermark - } - /// 执行期字符串回收的触发判断:账目须超过水位(上次收集后的存活字节 + /// 阈值增量)。完整收集(reset)仍用 [`Self::should_collect`] 的阈值直接比较, /// 保证死对象超阈值即被 reset 回收;执行期走增量水位,活串超阈值时不每指令 @@ -886,16 +898,216 @@ impl SessionGc { } } - /// 执行期完整 GC 入口:按水位判定,触发后抬高水位。 - /// 预留给 17.3b(对象侧执行期触发)安全点审计后使用。 - #[allow(dead_code)] - pub(crate) fn maybe_collect_gc(&mut self, vm: &mut Vm) { - if self.should_collect_gc(vm) { - self.collect(vm); - // 抬高下次触发水位:存活字节 + 阈值增量,避免活对象超阈值时每指令重复触发。 - let threshold = vm.gc_state.gc_threshold_cached; - vm.gc_state.gc_watermark = vm.gc_state.session_bytes_allocated.saturating_add(threshold); + /// 执行期两档收集:epoch 晋升档 + session 原地非移动 sweep 档。 + /// + /// 在 dispatch 安全点(循环顶 + `native_call_depth == 0`)回收单 run 分配 + /// 包络内的死对象: + /// - epoch 晋升档:扩展 mark(epoch 臂跟随)定活集,活 epoch 对象晋升 + /// session(递归克隆 + forwarding 去重共享与环 + native 盒深拷),根与 + /// session 对象的 epoch 子引用按转发表改写,全部 epoch 旧堆区与独占 + /// upvalue 列表释放,epoch 换新 Bump; + /// - session 原地 sweep 档:死 session 对象原地释放(堆区 + upvalue 列表) + /// 并出表;存活对象不移动——地址不变、免 forwarding/rewrite,用户可观察 + /// identity 不分裂。 + /// + /// # 边界与前提 + /// - 调用方已完成门控(无活跃/挂起 for-in):ForInIter body 分配于 epoch + /// arena,换新 Bump 即时失效在表迭代器; + /// - 调用点为 dispatch 安全点(`native_call_depth == 0`),无在途 builtin + /// 局部裸指针、dispatch 未重入。 + /// + /// # 副作用 + /// - `epoch_object_ptrs` 清空、epoch Bump 换新(旧 arena 全量归还); + /// - `session_object_ptrs` 仅剩存活,`session_bytes_allocated` 按存活重算 + /// (串/BigInt 归 strings-only 路径,本路径不动,仅对象口径变化); + /// - `gc_watermark` = 当前分配包络 + 阈值增量; + /// - 累计/时长统计更新;mark 位全清(收集后无残留)。 + pub(crate) fn collect_in_run(&mut self, vm: &mut Vm) { + let start = Instant::now(); + vm_info!("[GC] in-run cycle #{} start", self.total_collections + 1); + + // 清残留 mark 位(session + epoch):防 mark DFS 因历史 true 短路漏标。 + self.clear_all_marks(vm); + + // 扩展 mark:epoch 臂跟随,活 epoch 集 = 被标记 ∩ epoch 表。 + self.mark(vm); + + // ── epoch 晋升档 ── + + let mut forwarding = std::mem::take(&mut vm.gc_state.forwarding); + let epoch_ptrs = std::mem::take(&mut vm.gc_state.epoch_object_ptrs); + + // 活 epoch 对象逐个晋升:递归克隆携带 native 盒,共享/环经 forwarding + // 去重;死对象留在 epoch arena,换新 Bump 前统一释放。 + let mut live_epoch = 0u64; + for &ptr in &epoch_ptrs { + if ptr.is_null() { + continue; + } + // SAFETY: ptr 来自 epoch 对象表登记,arena 存活期内有效。 + if unsafe { (*ptr).is_gc_marked() } { + vm.promote_object_inner(ptr, &mut forwarding); + live_epoch += 1; + } } + + // 晋升克隆体自身即活:克隆不携带源对象标记位,而紧随其后的 session + // 原地 sweep 按标记位保活——不置位则克隆体在同一轮内被当作死对象 + // 原地释放,而根已改写指向克隆体(悬垂)。此点转发表值集恰为本轮 + // 晋升克隆体全集(session 原地改写路径的补晋升发生在其后、不入 + // 本轮 sweep 集,无需置位)。 + for &dst in forwarding.values() { + // SAFETY: dst 为本轮晋升克隆体,session arena 存活期内有效。 + unsafe { (*dst).set_gc_mark(true) }; + } + + // 根持有的 epoch 对象(顶层 var 寄存器等)按转发表解析到克隆体。 + rewrite_vm_roots(vm, &forwarding); + + // session 对象可持有绕过写屏障的 epoch 子引用(函数 captured_this/ + // home_object、native 盒直插):按转发表就地晋升进 session(已晋升 + // 对象的子引用解析到同一克隆,无二次克隆)。只改写存活对象:死对象的 + // 子引用随对象原地释放(释放路径无后续解引用,不悬垂)。 + let session_ptrs = std::mem::take(&mut vm.gc_state.session_object_ptrs); + let marked_session: Vec<*mut JsObject> = session_ptrs + .iter() + .copied() + .filter(|&ptr| !ptr.is_null() && unsafe { (*ptr).is_gc_marked() }) + .collect(); + vm.rewrite_session_epoch_refs(&marked_session, &mut forwarding); + forwarding.clear(); + vm.gc_state.forwarding = forwarding; + + // 全部 epoch 旧堆区释放:活对象的 vec/native 盒已被克隆深拷贝取代, + // 原件原地释放;死对象堆区随本体原地释放。 + let epoch_freed = Self::free_epoch_object_heap_data(&epoch_ptrs, &session_ptrs); + + // 换新 epoch Bump:旧 arena 全量归还,容量不保留。 + vm.epoch.reset(); + + // ── session 原地非移动 sweep 档 ── + + // 死 session 对象原地释放(堆区 + upvalue 列表)并出表;存活对象不动 + // arena 槽位——地址不变,免 forwarding/rewrite。 + let mut survivors = Vec::with_capacity(session_ptrs.len()); + let mut dead_session = 0u64; + let mut session_freed = 0u64; + for &ptr in &session_ptrs { + if ptr.is_null() { + continue; + } + // SAFETY: ptr 来自 session 对象表登记,arena 存活期内有效。 + if unsafe { (*ptr).is_gc_marked() } { + survivors.push(ptr); + } else { + session_freed += Self::drop_dead_session_object(ptr); + dead_session += 1; + } + } + let live_session = survivors.len() as u64; + vm.gc_state.session_object_ptrs = survivors; + + // 恢复 mark 位不变量:收集后无残留(残留 true 使下一次 mark DFS 短路漏标)。 + self.clear_all_marks(vm); + + // 对象口径重算:存活对象 + 存活串 + BigInt(串/BigInt 本路径不动, + // 随公式整体重算保持与 strings-only 口径一致)。 + let mut object_bytes: usize = 0; + for &ptr in &vm.gc_state.session_object_ptrs { + if ptr.is_null() { + continue; + } + // SAFETY: 存活对象在 session 表中,arena 存活期内有效。 + let obj = unsafe { &*ptr }; + object_bytes += size_of::() + Self::object_heap_data_bytes(obj) as usize; + } + let mut string_bytes: usize = 0; + for &ptr in &vm.gc_state.session_string_ptrs { + if ptr.is_null() { + continue; + } + // SAFETY: ptr 在字符串表登记,收尾前有效。 + string_bytes += size_of::() + unsafe { (*ptr).len() }; + } + let bigint_bytes = vm.gc_state.session_bigint_ptrs.borrow().len() * size_of::(); + vm.gc_state.session_bytes_allocated = object_bytes + string_bytes + bigint_bytes; + + // 抬高下次触发水位:当前分配包络 + 阈值增量——存活包络超阈值时不每指令 + // 重复触发无死对象可回收的白跑。 + vm.gc_state.gc_watermark = vm.run_alloc_bytes().saturating_add(vm.gc_state.gc_threshold_cached); + vm.gc_state.gc_gate_retry_alloc = 0; + + let live = live_epoch + live_session; + let dead = (epoch_ptrs.len() as u64 - live_epoch) + dead_session; + let freed_bytes = epoch_freed + session_freed; + + let elapsed = start.elapsed(); + self.total_collections += 1; + self.last_collection_duration_us = elapsed.as_micros() as u64; + self.max_collection_duration_us = self.max_collection_duration_us.max(self.last_collection_duration_us); + self.min_collection_duration_us = self.min_collection_duration_us.min(self.last_collection_duration_us); + self.total_bytes_freed = self.total_bytes_freed.saturating_add(freed_bytes); + self.total_objects_scanned += (epoch_ptrs.len() + session_ptrs.len()) as u64; + self.total_objects_live += live; + self.total_objects_dead += dead; + self.last_collection_objects_scanned = (epoch_ptrs.len() + session_ptrs.len()) as u64; + self.last_collection_objects_live = live; + self.last_collection_objects_dead = dead; + self.last_collection_bytes_freed = freed_bytes; + + vm_info!( + "[GC] in-run cycle #{} end: {} scanned, {} live, {} dead, {:.1}ms, {} bytes freed", + self.total_collections, + self.last_collection_objects_scanned, + self.last_collection_objects_live, + self.last_collection_objects_dead, + elapsed.as_secs_f64() * 1000.0, + freed_bytes, + ); + } + + /// 释放全部 epoch 对象的旧堆区(四处属性/元素向量 + native 状态盒)与独占 + /// upvalue 列表,返回释放字节数。 + /// + /// 调用前提:活 epoch 对象已晋升 session——其 vec/native 盒被克隆深拷贝取代, + /// 原件在此恰好释放一次;死对象堆区随本体原地释放。upvalue 列表原件与晋升 + /// 克隆共享同一 Box(`clone_for_session_epoch` 别名):共享项由 session 对象 + /// 持有、留待收尾统一释放,此处只放独占项,保证恰好一次。 + fn free_epoch_object_heap_data(epoch_ptrs: &[*mut JsObject], session_ptrs: &[*mut JsObject]) -> u64 { + // 共享集:session 对象表(存活与待死同表)仍持有的 upvalue Box—— + // 待死 session 对象的 Box 由本收集的原地 sweep 释放,交点恰好一处。 + let mut shared: std::collections::HashSet = std::collections::HashSet::new(); + for &ptr in session_ptrs { + if ptr.is_null() { + continue; + } + // SAFETY: ptr 来自 session 对象表登记,arena 存活期内有效。 + let up = unsafe { (*ptr).upvalues } as usize; + if up != 0 { + shared.insert(up); + } + } + + let mut seen: std::collections::HashSet = std::collections::HashSet::new(); + let mut freed = 0u64; + for &ptr in epoch_ptrs { + if ptr.is_null() { + continue; + } + // SAFETY: epoch 对象表未清空,指向 epoch arena 内合法对象。 + unsafe { + freed += Self::drop_object_heap_data(ptr, false); + let up = (*ptr).upvalues as usize; + if up == 0 || shared.contains(&up) || !seen.insert(up) { + continue; + } + // SAFETY: up 由 set_upvalues 的 Box::into_raw 分配,去重与共享集 + // 保证本路径恰好释放一次。 + drop(Box::from_raw(up as *mut Vec<*mut oxide_types::object::Cell>)); + (*ptr).upvalues = std::ptr::null_mut(); + } + } + freed } pub(crate) fn stats_summary(&self) -> String { @@ -1950,4 +2162,171 @@ mod tests { vm.full_reset(); assert!(vm.gc_state.session_string_ptrs.is_empty()); } + + // ── 执行期两档收集 ────────────────────────────────────────────────────── + + /// 执行期收集有效性:死 epoch 对象随换新 Bump 回收、死 session 对象原地 + /// 回收出表,活对象晋升 session 后值保持可读。 + #[test] + fn in_run_collection_reclaims_dead_and_keeps_live() { + let mut vm = vm_with_threshold(65536); + vm.run(&Arc::new(compile("globalThis.keep = { a: 1 }; 0"))).expect("run1"); + // churn:IIFE 局部数组 + 50 个未入根的函数(函数 session 直分配、 + // 数组 epoch 分配),IIFE 返回后全部不可达。 + vm.run(&Arc::new(compile( + "(function(){ var t = []; for (var i = 0; i < 50; i++) { t[i] = function() { return i; }; } })(); 0", + ))) + .expect("run2"); + // run 边界清空执行状态:寄存器文件中的陈旧值不再是执行根,churn 对象 + // 自此真正不可达。 + vm.run(&Arc::new(compile("0"))).expect("run3"); + + assert!(!vm.gc_state.epoch_object_ptrs.is_empty(), "churn 应留有 epoch 对象"); + let session_before = vm.session_object_count(); + assert!(session_before > 0, "churn 应产生 session 直分配函数"); + + vm.maybe_collect_in_run(); + + assert!(vm.session_gc_stats().total_collections >= 1, "执行期收集应跑一轮"); + assert!(vm.gc_state.epoch_object_ptrs.is_empty(), "epoch 对象应全部晋升或回收"); + assert!(vm.session_object_count() < session_before, "死 session 函数应被原地回收"); + + // 活对象晋升 session 后值可读。 + let keep = global_prop_opt(&vm, "keep").expect("keep 应挂在 global"); + assert!(keep.is_object()); + let keep_ptr = keep.as_js_object_ptr(); + assert!(unsafe { (*keep_ptr).is_session_epoch() }, "keep 晋升后应为 session 对象"); + let a = vm.resolve_property(unsafe { &*keep_ptr }, vm.kernel_core().perm_interner().intern("a").0); + assert_eq!(a, Some(JsValue::int(1))); + } + + /// for-in 门控双形钉住:活跃形(迭代器在 `vm.iters` 表内)与挂起形 + /// (生成器在 for-in 内 yield,迭代器搬入状态盒)都须拦下执行期收集—— + /// 换新 Bump 使 ForInIter body 即时失效;门控开后方可收集。 + #[test] + fn active_and_suspended_for_in_block_in_run_collection() { + // 活跃形:迭代器直接压在 vm.iters,O(1) 子句即拦。 + let mut vm = vm_with_threshold(65536); + let dead = plain_object(&mut vm); + vm.gc_state.epoch_object_ptrs.push(dead); + let iter = vm.epoch.alloc(crate::vm::ForInIter { + keys: bumpalo::collections::Vec::new_in(vm.epoch.bump()), + index: 0, + }); + vm.iters.push_for_in(iter.cast::>()); + let bump_before = vm.epoch.current_id(); + + vm.maybe_collect_in_run(); + assert_eq!(vm.session_gc_stats().total_collections, 0, "活跃 for-in:门控关闭,不收集"); + assert_eq!(vm.epoch.current_id(), bump_before, "门控关闭:epoch Bump 未换新"); + assert!(vm.gc_state.epoch_object_ptrs.contains(&dead), "死对象仍在 epoch 表"); + + // 迭代器出表后同一调用点即应收集。 + vm.iters.for_in_iters.pop(); + vm.maybe_collect_in_run(); + assert_eq!(vm.session_gc_stats().total_collections, 1, "门控开:应收集"); + assert!(vm.epoch.current_id() > bump_before, "收集应换新 epoch Bump"); + assert!(vm.gc_state.epoch_object_ptrs.is_empty(), "死 epoch 对象应随 Bump 回收"); + + // 挂起形(拦截):生成器在 for-in 内 yield 后 run 结束,迭代器经 + // 状态盒持有(vm.iters 已空)——收集点必须拦下,Bump 不得换新。 + let mut vm = vm_with_threshold(65536); + let bump_before = vm.epoch.current_id(); + vm.run(&Arc::new(compile( + "var o = { a: 1, b: 2 }; \ + function* gen() { for (var k in o) { yield k; } return 'done'; } \ + var g = gen(); globalThis.g = g; g.next(); 0", + ))) + .expect("run1"); + assert!(vm.iters.for_in_iters.is_empty(), "挂起时迭代器已搬入状态盒"); + assert!(vm.suspended_holds_for_in(), "生成器状态盒应持挂起 for-in 迭代器"); + + vm.maybe_collect_in_run(); + assert_eq!(vm.session_gc_stats().total_collections, 0, "挂起 for-in:门控关闭,不收集"); + assert!(vm.epoch.current_id() == bump_before, "门控关闭:epoch Bump 未换新"); + // 挂起生成器对象保持原址(未晋升、未释放):状态盒与迭代器一体存活。 + let gen_in_epoch = vm + .gc_state + .epoch_object_ptrs + .iter() + .any(|&p| !p.is_null() && unsafe { (*p).type_tag == JsObject::OBJ_TYPE_GENERATOR }); + assert!(gen_in_epoch, "挂起生成器应仍在 epoch 表"); + + // 挂起形(放行):同一 run 内续跑至 for-in 结束(迭代器释放), + // 阈值 1 使指令边界自动触发:挂起期门控关(Bump 不换新),完成后 + // 门控开、执行期收集放行;yield 值跨收集保持正确。 + let mut vm = vm_with_threshold(1); + vm.run(&Arc::new(compile( + "var o = { a: 1, b: 2 }; \ + function* gen() { for (var k in o) { yield k; } return 'done'; } \ + var g = gen(); globalThis.g = g; \ + globalThis.v1 = g.next().value; globalThis.v2 = g.next().value; g.next(); 0", + ))) + .expect("run2"); + assert_eq!(vm.lookup_str(global_prop_opt(&vm, "v1").expect("v1")), Some("a".to_string())); + assert_eq!(vm.lookup_str(global_prop_opt(&vm, "v2").expect("v2")), Some("b".to_string())); + assert!(!vm.suspended_holds_for_in(), "for-in 结束后状态盒不应再持迭代器"); + assert!(vm.session_gc_stats().total_collections >= 1, "完成后门控开:执行期收集应放行"); + + // 生成器对象跨收集仍为合法生成器(晋升 session 或保持 epoch 完好)。 + let g = global_prop_opt(&vm, "g").expect("g 应挂在 global"); + assert!(unsafe { (*g.as_js_object_ptr()).is_generator_obj() }, "收集后生成器对象应完好"); + } + + /// 非移动不变量:执行期收集的存活 session 对象地址不变(元素堆区亦不 + /// 换盒)——用户可观察 identity 不分裂,免 forwarding/rewrite。 + #[test] + fn in_run_collection_keeps_live_addresses_stable() { + let mut vm = vm_with_threshold(65536); + vm.run(&Arc::new(compile("globalThis.o = { a: [1, 2, 3], f: function() { return 1; } }; 0"))) + .expect("run1"); + + // 第一轮执行期收集:o 及其 epoch 子引用晋升 session。 + vm.maybe_collect_in_run(); + let o = global_prop_opt(&vm, "o").expect("o 应挂在 global"); + let o_ptr = o.as_js_object_ptr(); + assert!(unsafe { (*o_ptr).is_session_epoch() }, "o 晋升后应为 session 对象"); + let a_ptr = vm + .resolve_property(unsafe { &*o_ptr }, vm.kernel_core().perm_interner().intern("a").0) + .expect("o.a") + .as_js_object_ptr(); + let f_ptr = vm + .resolve_property(unsafe { &*o_ptr }, vm.kernel_core().perm_interner().intern("f").0) + .expect("o.f") + .as_js_object_ptr(); + let a_elems = unsafe { (*a_ptr).array_elements_raw() }; + + // 第二轮 churn + 收集:死对象回收,存活对象与堆区地址保持不变。 + vm.run(&Arc::new(compile( + "(function(){ var t = []; for (var i = 0; i < 50; i++) { t[i] = { x: i }; } })(); 0", + ))) + .expect("run2"); + // run 边界清空执行状态:churn 局部(t 数组与 50 个对象)自此不可达。 + vm.run(&Arc::new(compile("0"))).expect("run3"); + vm.maybe_collect_in_run(); + assert!(vm.session_gc_stats().total_collections >= 2, "两轮收集均应执行"); + + assert_eq!(global_prop_opt(&vm, "o").expect("o").as_js_object_ptr(), o_ptr, "存活对象地址不变"); + assert_eq!( + vm.resolve_property(unsafe { &*o_ptr }, vm.kernel_core().perm_interner().intern("a").0) + .expect("o.a") + .as_js_object_ptr(), + a_ptr, + "数组对象地址不变" + ); + assert_eq!( + vm.resolve_property(unsafe { &*o_ptr }, vm.kernel_core().perm_interner().intern("f").0) + .expect("o.f") + .as_js_object_ptr(), + f_ptr, + "函数对象地址不变" + ); + assert_eq!(unsafe { (*a_ptr).array_elements_raw() }, a_elems, "元素堆区不换盒"); + assert_eq!( + vm.resolve_property(unsafe { &*a_ptr }, vm.kernel_core().perm_interner().intern("1").0) + .unwrap_or(JsValue::int(-1)), + JsValue::int(2), + "元素值跨收集可读" + ); + } } diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 3914460b..be7d3ca4 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -964,13 +964,62 @@ impl Vm { self.gc_state.session_gc = session_gc; } - /// 执行期完整 GC(对象+字符串)的 dispatch 安全点入口:按水位判定触发。 - /// 仅在 native_call_depth == 0 的指令边界调用——此时无 builtin 局部裸指针, - /// 对象搬移安全。预留给 17.3b(对象侧执行期触发)安全点审计后使用。 - #[allow(dead_code)] - pub(crate) fn maybe_collect_session_gc_at_dispatch(&mut self) { + /// 门控子句二:三型状态盒(生成器/异步函数/异步生成器)的 + /// `suspended.for_in_iters` 是否非空。 + /// + /// ForInIter body 分配于 epoch arena,收集换新 Bump 即时失效在表迭代器; + /// 挂起帧把迭代器搬入状态盒(`vm.iters` 不可见),故须逐状态盒扫描。 + /// + /// # 边界与前提 + /// - 扫描 epoch + session 两份对象表——挂起状态盒宿主对象可能尚未晋升; + /// - 挂起帧的 keys 经状态盒边收为 GC 根(被枚举对象不误释放),悬的是 + /// 迭代器 body,故按指针扫 `for_in_iters`; + /// - 保守口径:死对象的状态盒同样计入(持挂起 for-in 的 run 放弃执行期 + /// 收集),正确性优先于收益; + /// - 新增挂起态持有者须在此登记。 + pub(crate) fn suspended_holds_for_in(&self) -> bool { + let holds = |&ptr: &*mut JsObject| { + if ptr.is_null() { + return false; + } + // SAFETY: 对象表登记的指针,arena 存活期内有效。 + let obj = unsafe { &*ptr }; + match obj.type_tag { + JsObject::OBJ_TYPE_GENERATOR => crate::generator::generator_holds_suspended_for_in(obj), + JsObject::OBJ_TYPE_ASYNC => crate::async_func::async_holds_suspended_for_in(obj), + JsObject::OBJ_TYPE_ASYNC_GENERATOR => { + crate::async_generator::async_generator_holds_suspended_for_in(obj) + } + _ => false, + } + }; + self.gc_state.epoch_object_ptrs.iter().any(holds) || self.gc_state.session_object_ptrs.iter().any(holds) + } + + /// 执行期两档收集的 dispatch 安全点入口:仅在循环顶 + /// (`native_call_depth == 0`,无 builtin 局部裸指针、dispatch 未重入) + /// 由触发块调用;先门控后收集。 + /// + /// # 边界与前提 + /// - 子句一(活跃 for-in,O(1))未过:免费重试,不设锚; + /// - 子句二(状态盒扫描,O(对象数))未过:重扫按包络增长一个阈值设锚, + /// 避免持挂起 for-in 的 run 每指令边界重扫; + /// - 门控过但无对象可回收时仍跑一轮(mark + 换新 Bump),水位同点抬高, + /// 触发间距由包络增量控制。 + pub(crate) fn maybe_collect_in_run(&mut self) { + if !self.iters.for_in_iters.is_empty() { + return; + } + if self.gc_state.gc_gate_retry_alloc > self.run_alloc_bytes() { + return; + } + if self.suspended_holds_for_in() { + self.gc_state.gc_gate_retry_alloc = + self.run_alloc_bytes().saturating_add(self.gc_state.gc_threshold_cached); + return; + } let mut session_gc = std::mem::take(&mut self.gc_state.session_gc); - session_gc.maybe_collect_gc(self); + session_gc.collect_in_run(self); self.gc_state.session_gc = session_gc; } @@ -1651,6 +1700,11 @@ impl Vm { if bytes >= self.gc_state.string_gc_watermark { self.maybe_collect_session_strings(); } + // 执行期两档收集:O(1) 包络超触发水位 → 门控(活跃/挂起 for-in) + // + epoch 晋升收集 + session 原地 sweep(边界契约见 maybe_collect_in_run)。 + if alloc >= self.gc_state.gc_watermark { + self.maybe_collect_in_run(); + } } if let Some(max_steps) = max_steps { if steps > max_steps { diff --git a/crates/oxide_vm/src/vm_state.rs b/crates/oxide_vm/src/vm_state.rs index 687819c8..63dbeda2 100644 --- a/crates/oxide_vm/src/vm_state.rs +++ b/crates/oxide_vm/src/vm_state.rs @@ -49,14 +49,14 @@ pub(crate) struct GcState { /// 触发无死串可回收的白跑,且保证触发点恒在无 builtin 局部活值的边界。 pub(crate) string_gc_watermark: usize, /// 缓存的 GC 阈值(从 config 读一次),热路径只做 usize 比较,免 Arc 解引用。 - /// 预留给 17.3b(对象侧执行期触发)安全点审计后使用。 - #[allow(dead_code)] pub(crate) gc_threshold_cached: usize, - /// 执行期完整 GC(对象+字符串)的触发水位:本次收集后的存活字节 + 阈值增量。 - /// 仅当账目超过水位才在指令边界触发完整回收——存活对象超阈值时不每指令重复 - /// 触发无死对象可回收的白跑。预留给 17.3b 安全点审计后使用。 - #[allow(dead_code)] + /// 执行期两档收集(epoch 晋升 + session 原地 sweep)的触发水位:本次 + /// 收集后的分配包络 + 阈值增量。仅当 `run_alloc_bytes` 超过水位才在指令 + /// 边界触发——存活包络超阈值时不每指令重复触发无死对象可回收的白跑。 pub(crate) gc_watermark: usize, + /// 门控未过(挂起态持有 for-in)后的重扫锚:包络再增长一个阈值才重扫 + /// 三型状态盒(O(对象数) 扫描),避免持挂起 for-in 的 run 每指令边界重扫。 + pub(crate) gc_gate_retry_alloc: usize, pub(crate) forwarding: HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>, } diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index fea27f88..d2f98d67 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -118,6 +118,7 @@ impl Vm { string_gc_watermark: gc_threshold, gc_threshold_cached: gc_threshold, gc_watermark: gc_threshold, + gc_gate_retry_alloc: 0, forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher), }, symbols: SymbolState { @@ -235,6 +236,7 @@ impl Vm { string_gc_watermark: gc_threshold, gc_threshold_cached: gc_threshold, gc_watermark: gc_threshold, + gc_gate_retry_alloc: 0, forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher), }, symbols: SymbolState { @@ -356,6 +358,9 @@ impl Vm { self.gc_state.session_bytes_peak = 0; self.gc_state.run_alloc_peak = 0; self.gc_state.string_gc_watermark = self.kernel_core.config().session_gc_threshold; + // 两档收集水位与门控重扫锚同点复位(同式:阈值增量起算)。 + self.gc_state.gc_watermark = self.gc_state.gc_threshold_cached; + self.gc_state.gc_gate_retry_alloc = 0; self.gc_state.session_gc = crate::session_gc::SessionGc::new(); self.symbols.reset(); self.root_reg_limit = 0; @@ -508,6 +513,9 @@ impl Vm { self.gc_state.epoch_object_ptrs.clear(); // 单 run 分配包络按 run 边界重起算(与 run_alloc_bytes 起算口径同源)。 self.gc_state.run_alloc_peak = 0; + // 两档收集水位与包络同起算:旧 run 的存活包络不延续到新 run 的触发判定。 + self.gc_state.gc_watermark = self.gc_state.gc_threshold_cached; + self.gc_state.gc_gate_retry_alloc = 0; self.root_reg_limit = 0; self.active_reg_limit = 0; }