From 5c8699733d287fe5a33d6094da33437db3775e20 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 1 Oct 2026 09:06:23 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=94=B6=E9=9B=86=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E5=8E=9F=E5=9C=B0=E5=8C=96=E9=80=80=E5=BD=B9=E7=A7=BB?= =?UTF-8?q?=E5=8A=A8=E5=BC=8Fsweep=E4=B8=8E=E9=97=A8=E6=8E=A7?= 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 | 13 +- crates/oxide_vm/src/promise/gc_edges.rs | 24 +- crates/oxide_vm/src/promise/mod.rs | 2 +- crates/oxide_vm/src/session_arena.rs | 81 ++-- crates/oxide_vm/src/session_gc.rs | 426 ++++++++------------ crates/oxide_vm/src/session_gc/tests.rs | 136 +++---- crates/oxide_vm/src/vm/dispatch_loop.rs | 7 +- crates/oxide_vm/src/vm/gc_hooks.rs | 59 +-- crates/oxide_vm/src/vm_state.rs | 25 +- crates/oxide_vm/src/vm_support.rs | 64 +-- crates/oxide_vm/src/vm_support/tests.rs | 10 +- crates/oxide_vm/tests/generator_gc_tests.rs | 13 +- 14 files changed, 309 insertions(+), 573 deletions(-) diff --git a/crates/oxide_vm/src/async_func.rs b/crates/oxide_vm/src/async_func.rs index 4077ef60..a926f1ed 100644 --- a/crates/oxide_vm/src/async_func.rs +++ b/crates/oxide_vm/src/async_func.rs @@ -697,20 +697,11 @@ fn async_state_mut(obj: &JsObject) -> Option<&mut AsyncState> { None } else { // SAFETY: native_data 在 create_async_context 中由 Box::into_raw 分配, - // 生命周期与异步上下文对象一致;GC 路径(mark 边收集、执行期门控扫描)只读状态盒。 + // 生命周期与异步上下文对象一致;GC 路径(mark 边收集)只读状态盒。 Some(unsafe { &mut *ptr }) } } -/// 异步挂起帧是否持有 for-in 迭代器:执行期收集门控按指针扫描 -/// (迭代器体是堆上 Box,门控是保守门)。 -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 边):对象/字符串/BigInt 均产出, /// 消费侧按值类型分发到对象栈与存活集。 pub(crate) fn async_native_edges(obj: &JsObject) -> Vec { diff --git a/crates/oxide_vm/src/async_generator.rs b/crates/oxide_vm/src/async_generator.rs index cd4c5d0a..be769b18 100644 --- a/crates/oxide_vm/src/async_generator.rs +++ b/crates/oxide_vm/src/async_generator.rs @@ -1671,20 +1671,11 @@ fn async_gen_state_mut(obj: &JsObject) -> Option<&mut AsyncGeneratorState> { None } else { // SAFETY: native_data 在 create_async_generator_object 中由 Box::into_raw 分配, - // 生命周期与异步生成器对象一致;GC 路径(mark 边收集、执行期门控扫描)只读状态盒。 + // 生命周期与异步生成器对象一致;GC 路径(mark 边收集)只读状态盒。 Some(unsafe { &mut *ptr }) } } -/// 异步生成器挂起帧是否持有 for-in 迭代器:执行期收集门控按指针扫描 -/// (迭代器体是堆上 Box,门控是保守门)。 -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 边):对象/字符串/BigInt /// 均产出,消费侧按值类型分发到对象栈与存活集。 pub(crate) fn async_generator_native_edges(obj: &JsObject) -> Vec { diff --git a/crates/oxide_vm/src/generator.rs b/crates/oxide_vm/src/generator.rs index 36eee0be..8969124e 100644 --- a/crates/oxide_vm/src/generator.rs +++ b/crates/oxide_vm/src/generator.rs @@ -959,20 +959,11 @@ fn generator_state_mut(obj: &JsObject) -> Option<&mut GeneratorState> { None } else { // SAFETY: native_data 在 create_generator_object 中由 Box::into_raw 分配, - // 生命周期与生成器对象一致;GC 路径(mark 边收集、执行期门控扫描)只读状态盒。 + // 生命周期与生成器对象一致;GC 路径(mark 边收集)只读状态盒。 Some(unsafe { &mut *ptr }) } } -/// 生成器挂起帧是否持有 for-in 迭代器:执行期收集门控按指针扫描 -/// (迭代器体是堆上 Box,门控是保守门)。 -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 边):对象/字符串/BigInt 均产出, /// 消费侧按值类型分发到对象栈与存活集。 pub(crate) fn generator_native_edges(obj: &JsObject) -> Vec { @@ -1031,7 +1022,7 @@ pub(crate) fn generator_native_cell_edges(obj: &JsObject) -> Vec<*mut Cell> { out } -/// 用转发函数重写状态快照中的所有 JsValue(session GC 移动式清扫 / promote 用)。 +/// 用转发函数重写状态快照中的所有 JsValue(promote 用)。 pub(crate) fn rewrite_generator_native(obj: &JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue) { let Some(state) = generator_state_mut(obj) else { return; diff --git a/crates/oxide_vm/src/promise/gc_edges.rs b/crates/oxide_vm/src/promise/gc_edges.rs index 6a91d6b6..4611c43c 100644 --- a/crates/oxide_vm/src/promise/gc_edges.rs +++ b/crates/oxide_vm/src/promise/gc_edges.rs @@ -33,11 +33,11 @@ pub(crate) fn promise_native_edges(obj: &JsObject) -> Vec { edges } -/// 用转发函数重写状态盒中的所有 JsValue(session GC 移动式清扫 / promote 用)。 +/// 用转发函数重写状态盒中的所有 JsValue(promote 用)。 /// -/// 结算链指针是裸指针边、不在本函数改写面:晋升路径克隆新分配于 session -/// arena(原件地址不变),同 arena 改写场景指针天然有效;搬移换址场景由 -/// 调用方在重写完成后经 `repoint_promise_promoted_clone` 按转发表重定位。 +/// 结算链指针是裸指针边、不在本函数改写面:晋升克隆新分配于 session arena +/// (原件地址不变),改写场景指针天然有效;结算链接链由 +/// `migrate_settlement_to_newest_clone` 负责。 pub(crate) fn rewrite_promise_native(obj: &JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue) { let Some(state) = promise_state_mut(obj) else { return; @@ -53,12 +53,12 @@ pub(crate) fn rewrite_promise_native(obj: &JsObject, mut rewrite: impl FnMut(JsV } } -/// 深拷贝状态盒到新对象(promote / sweep 用):新对象持独立 Box,源盒可安全释放。 +/// 深拷贝状态盒到新对象(promote 用):新对象持独立 Box,源盒可安全释放。 /// /// 源的反应**迁移**(非复制)进新对象:源盒随 epoch 释放,留在源上的反应会 /// 永久丢失、原件侧消费者永不触发;每条反应的 JsValue 随状态盒其余字段同一 -/// pass 晋升/改写。结算链指针按原值保留(晋升场景由 -/// `migrate_settlement_to_newest_clone` 接链,搬移场景按转发表重定位)。 +/// pass 晋升/改写。结算链指针按原值保留(接链由 +/// `migrate_settlement_to_newest_clone` 负责)。 pub(crate) fn clone_promise_native_with_rewrite( old: &JsObject, new: &mut JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue, ) { @@ -144,16 +144,6 @@ pub(crate) fn migrate_settlement_to_newest_clone( } } -/// 按给定转发改写结算链指针(裸指针边、非 JsValue),session GC 搬移后调用。 -pub(crate) fn repoint_promise_promoted_clone(obj: &JsObject, forward: impl FnOnce(*mut JsObject) -> *mut JsObject) { - let Some(state) = promise_state_mut(obj) else { - return; - }; - if !state.promoted_clone.is_null() { - state.promoted_clone = forward(state.promoted_clone); - } -} - /// 改写单条反应的四条 JsValue 引用(派生 promise 能力与处理器),源反应迁移 /// 与再晋升合并共用。 fn rewrite_reaction JsValue>(r: PromiseReaction, rewrite: &mut F) -> PromiseReaction { diff --git a/crates/oxide_vm/src/promise/mod.rs b/crates/oxide_vm/src/promise/mod.rs index 126801de..2c62ae28 100644 --- a/crates/oxide_vm/src/promise/mod.rs +++ b/crates/oxide_vm/src/promise/mod.rs @@ -19,7 +19,7 @@ mod settlement; pub(crate) use gc_edges::{ clone_promise_native_with_rewrite, drop_promise_native, migrate_settlement_to_newest_clone, promise_native_edges, - promise_native_size, repoint_promise_promoted_clone, rewrite_promise_native, + promise_native_size, rewrite_promise_native, }; pub(crate) use jobs::{for_each_job_value, rewrite_job_values}; pub use reactions::promise_settled_value; diff --git a/crates/oxide_vm/src/session_arena.rs b/crates/oxide_vm/src/session_arena.rs index 14c5ab0e..bfca1a91 100644 --- a/crates/oxide_vm/src/session_arena.rs +++ b/crates/oxide_vm/src/session_arena.rs @@ -9,15 +9,15 @@ use crate::vm::Vm; use oxide_builtins::{array_buffer, data_view, disposable_stack, map, module, regexp, set, typed_array, weak_map}; impl Vm { - /// 单对象晋升测试入口:取/清共享转发表后调 `promote_object_inner`。 - /// 生产路径(边界修复/执行期晋升档/根晋升)各自持表管理,不经过此包装。 + /// 单对象晋升测试入口:局部转发表下调 `promote_object_inner`。 + /// 生产路径(边界修复/根晋升)各自持表管理,不经过此包装。 #[cfg(test)] pub(crate) fn promote_object(&mut self, src: *mut JsObject) -> *mut JsObject { vm_debug!("promote_object: src={:p}", src); - let mut forwarding = std::mem::take(&mut self.gc_state.forwarding); + let mut forwarding = HashMap::with_hasher(FxBuildHasher); let result = self.promote_object_inner(src, &mut forwarding); + // 局部表随作用域丢弃,晋升克隆的转发改写已在其上完成。 forwarding.clear(); - self.gc_state.forwarding = forwarding; result } @@ -53,7 +53,7 @@ impl Vm { }); } else if src_ref.is_weak_map_obj() { // 值边随克隆晋升(强边);弱键原样搬运,生死交晋升收敛后的 - // 弱键定夺路径按同一转发表判定。 + // 弱键定夺路径(现为原样保留)。 weak_map::clone_weak_map_native_with_rewrite(src_ref, dst_ref, |value| { self.promote_value_if_epoch_object(value, forwarding) }); @@ -145,7 +145,8 @@ impl Vm { /// global 对象的属性值(逃逸写直通后 epoch 值直落 global 槽,global 不入 /// session 对象表,由 `rewrite_session_epoch_refs` 同趟改写)。 /// `rewrite_object_values` 覆盖元素/meta/属性/proto/captured_this/home_object/ - /// cell 值;克隆子树经 forwarding 去重,环与共享引用各克隆一次。 + /// cell 值;克隆子树经局部转发表去重,环与共享引用各克隆一次。 + /// 统一入口 Box 化后生产路径不产 epoch 对象,本方法为晋升族空转保留面。 /// /// # 注意事项 /// - 须在执行外的安全点调用(无在途 builtin 局部裸指针、dispatch 未重入): @@ -154,14 +155,12 @@ impl Vm { /// - session 对象表为空时 global 的 epoch 子引用仍须修复,不做空表早退。 pub fn promote_session_epoch_refs(&mut self) { let objects = std::mem::take(&mut self.gc_state.session_object_ptrs); - let mut forwarding = std::mem::take(&mut self.gc_state.forwarding); + let mut forwarding = HashMap::with_hasher(FxBuildHasher); self.rewrite_session_epoch_refs(&objects, &mut forwarding); - // 弱键按收敛后的转发表定生死:未入表 epoch 键随 epoch 释放而亡。 + // 弱键定夺退化为原样保留(定夺路径无克隆可重指,见 + // resolve_weak_key_after_promotion);调用保留以收口族内流程。 self.rewrite_weak_map_keys_after_promotion(&objects, &mut forwarding); - forwarding.clear(); - self.gc_state.forwarding = forwarding; - // 改写期新克隆已随晋升推入 gc_state 侧的表(此刻仅含克隆体),把取出的 - // 旧对象按原相对序拼回同一表,表 = 克隆体 + 旧对象,各恰登记一次。 + // 局部转发表随作用域丢弃:晋升新克隆已推入对象表,旧对象按原序拼回。 self.gc_state.session_object_ptrs.extend(objects); } @@ -173,8 +172,8 @@ impl Vm { /// 与列表同趟改写,无 native 盒只覆盖 JS 边。 /// /// # 注意事项 - /// - 调用方持有转发表期间不得让其它晋升路径改动 `gc_state.forwarding`; - /// 晋升新克隆推入的对象表由调用方决定何时拼回。 + /// - 调用方持有局部转发表期间,晋升新克隆推入的对象表由调用方决定何时 + /// 拼回;本方法不改表。 /// - 死对象(不可达)的 epoch 子引用改写会把死对象一并克隆进 session—— /// 克隆体无根,下一轮收集按死对象出表,不泄漏也不悬垂。 pub(crate) fn rewrite_session_epoch_refs( @@ -244,15 +243,15 @@ impl Vm { } /// 晋升收敛后的弱键定夺:对候选集(session 对象列表 + 已晋升克隆)中的 - /// 每个弱表按转发表判定弱键——表内键改指新址,未入表 epoch 键为死键丢 - /// 条目,session/P 键保留。 + /// 每个弱表按定夺路径处理条目。统一入口 Box 化后定夺退化为原样保留 + /// (无克隆可重指、reset 边界 mark 位无本轮可达性信息),本路径不改表项。 /// /// # 副作用 - /// - 每个弱表原位重建条目表;无死键时条目与容量口径不变。 + /// - 每个弱表原位重建条目表(整表重建口径,条目内容不变)。 /// /// # 边界与前提 /// - 须在主改写路径(JS 边 + 值边)对转发表收敛完成后调用;转发表届时 - /// 即最终强可达集。调用方负责其后的清表。 + /// 即最终强可达集(统一入口树上恒空)。调用方负责其后的清表。 /// - global 上的弱表经其晋升克隆进入转发表值集,候选集无需单列 global。 pub(crate) fn rewrite_weak_map_keys_after_promotion( &mut self, objects: &[*mut JsObject], forwarding: &mut HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>, @@ -267,7 +266,7 @@ impl Vm { if obj.is_weak_map_obj() { weak_map::rewrite_weak_map_native( obj, - |key| crate::session_gc::resolve_weak_key_after_promotion(key, forwarding), + |key| crate::session_gc::resolve_weak_key_after_promotion(key), |value| value, ); } @@ -280,7 +279,7 @@ impl Vm { if obj.is_weak_map_obj() { weak_map::rewrite_weak_map_native( obj, - |key| crate::session_gc::resolve_weak_key_after_promotion(key, forwarding), + |key| crate::session_gc::resolve_weak_key_after_promotion(key), |value| value, ); } @@ -289,26 +288,19 @@ impl Vm { } /// 把根直接持有的 epoch 对象(顶层 var 寄存器、挂起句柄等)晋升进 session, - /// 并把根引用改写到克隆体。 + /// 并把根引用改写到克隆体。统一入口 Box 化后生产路径无 epoch 对象, + /// 本方法为晋升族空转保留面(基准留存测量与图留存测试仍经此入口)。 /// /// # 副作用 /// - 每个根 epoch 对象深克隆进 session(含 native 状态盒,子引用递归晋升), - /// 账目计入克隆字节;根引用按转发表重写。 + /// 账目计入克隆字节;根引用按局部转发表重写。 /// /// # 注意事项 /// - 须在执行外的安全点调用(无在途 builtin 局部裸指针、dispatch 未重入): /// 本方法改写全部根寄存器槽,执行期调用将使 builtin 局部裸指针失效 /// (与 `collect_session_gc` 同前提)。 - /// - 供 workload 后观测点(基准留存测量)使用:本方法之后存活集完全对 - /// session 可见,后续完整 GC 的留存账目不再漏"仅驻留 epoch arena 的对象"。 /// - 与 `promote_session_epoch_refs` 互补:本方法处理根直接持有的 epoch 对象, /// 后者处理 session 对象持有的 epoch 子引用(闭包捕获等绕过写屏障的来源)。 - /// - 弱键定夺(`rewrite_weak_map_keys_after_promotion`)只按根直接子树的 - /// 转发表为判据:经 session 对象(非根)强持的 epoch 键不在表内会被判死 - /// 丢条目,而本调用不释放 epoch——键强存活时条目即消失。配对调用 - /// (随后 `promote_session_epoch_refs`)由第二趟收敛转发表,但第一趟已丢 - /// 条目不可复原;独立调用须根直接子树已覆盖全部强可达 epoch 对象 - /// (即不存在经 session 对象强持的弱键)方为合法时机。 pub fn promote_rooted_epoch_objects(&mut self) { let mut epoch_roots = Vec::new(); self.for_each_root(|value| { @@ -322,23 +314,22 @@ impl Vm { } } }); - let mut forwarding = std::mem::take(&mut self.gc_state.forwarding); + // 局部转发表:晋升克隆去重共享与环,作用域结束即弃。 + let mut forwarding = HashMap::with_hasher(FxBuildHasher); for ptr in epoch_roots { self.promote_object_inner(ptr, &mut forwarding); } crate::session_gc::rewrite_vm_roots(self, &forwarding); - // 弱表键定夺:根直接持有的弱表克隆其弱键按转发表判生死。 + // 弱表键定夺退化为原样保留(定夺路径无克隆可重指,见 + // resolve_weak_key_after_promotion);调用保留以收口族内流程。 self.rewrite_weak_map_keys_after_promotion(&[], &mut forwarding); - forwarding.clear(); - self.gc_state.forwarding = forwarding; } /// 逃逸写屏障:写向全局/session 目标的对象值原样直通。 /// /// session 对象持有 epoch 子引用是 GC 设计的一等存活态:epoch 边界 reset - /// 经 `promote_session_epoch_refs` 统一克隆晋升(转发表去重共享与环), - /// 执行期收集晋升档按同一转发表改写根与存活 session 对象子引用,两条 - /// 路径覆盖全部跨界持有面;builtin 调用期内 epoch 不重置,帧局部裸指针 + /// 经 `promote_session_epoch_refs` 统一克隆晋升(转发表去重共享与环) + /// 覆盖全部跨界持有面;builtin 调用期内 epoch 不重置,帧局部裸指针 /// 全程有效。此处若克隆,同一逻辑对象分裂为原件与克隆两份,经克隆的写 /// 对方经原件的读不可见(builtin 帧指针与逃逸目标双引用并存时即暴露)。 pub(crate) fn promote_if_needed_for_write_ptr(&mut self, _target_ptr: *mut JsObject, value: JsValue) -> JsValue { @@ -530,20 +521,4 @@ mod tests { assert!(!is_epoch_object(&vm, meta.get)); assert!(!is_epoch_object(&vm, meta.set)); } - - #[test] - fn promote_clears_forwarding_map() { - let mut vm = Vm::new(); - let first = plain_object(&mut vm); - let promoted = vm.promote_object(first); - assert!(!promoted.is_null()); - // 共享 forwarding 表必须在每次 promote 后清空,使后续 promote(或 GC 清扫) - // 永不观察到过期的 old->new 映射。 - assert!(vm.gc_state.forwarding.is_empty()); - - let second = plain_object(&mut vm); - let promoted2 = vm.promote_object(second); - assert!(!promoted2.is_null()); - assert!(vm.gc_state.forwarding.is_empty()); - } } diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 3808d2b2..bdd2f720 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -13,10 +13,10 @@ use oxide_builtins::{array_buffer, data_view, disposable_stack, map, module, reg /// session 级 mark-sweep GC 的状态与统计。 /// -/// 回收 session arena 中不再可达的对象与 `Vm::new_string` 分配的 session 字符串: -/// `mark` 从 VM roots 标记存活对象,`sweep` 将存活对象复制进新 arena(移动式)、 -/// 更新所有引用,并释放死对象;`sweep_session_strings` 按存活标记回收字符串。 -/// 所有统计字段供外部观测 GC 行为。 +/// 回收 session 对象表中不再可达的对象与 `Vm::new_string` 分配的 session 字符串: +/// `mark` 从 VM roots 标记存活对象,`sweep` 原地清扫(存活对象带位保留、地址不变, +/// 死对象本体、堆区与独占 upvalue 列表原地释放出表);`sweep_session_strings` +/// 按存活标记回收字符串。所有统计字段供外部观测 GC 行为。 pub struct SessionGc { pub total_collections: u64, pub total_bytes_freed: u64, @@ -51,8 +51,10 @@ impl SessionGc { self.clear_session_marks(vm); } - /// 只清 session 对象表的 mark 位。移动式 sweep 末尾的旧表对象已全释放/克隆 - /// (克隆未标记),清位须与表重写同步执行,独立成臂供调用点按需取用。 + /// 只清 session 对象表的 mark 位。原地 sweep 后死对象已出表、存活对象 + /// 的位在活分支清掉,正常收集路径无残留位;此臂处理 strings-only 路径 + /// 与收集前历史残留——残留 true 会让下一次 `mark` 的 DFS 在已标对象处 + /// 短路,漏扫其新增引用边。 pub(crate) fn clear_session_marks(&mut self, vm: &mut Vm) { for &ptr in &vm.gc_state.session_object_ptrs { if ptr.is_null() { @@ -368,12 +370,13 @@ impl SessionGc { } } - /// 从 VM roots 标记存活的 session/epoch 对象、字符串与 BigInt,供 `sweep` 判定。 + /// 从 VM roots 标记存活 session 对象、字符串与 BigInt,供 `sweep` 判定。 /// - /// 对象 DFS 同时跟踪 session 与 epoch 两张表,两族 mark 位同字节,晋升收集按 - /// “标记 ∩ epoch 表”取活集;字符串边走 rope 闭包传播(Cons 子节点与扁平化产物), - /// BigInt 边直接入存活集。roots 由 `Vm::for_each_root` 枚举,字段清单与 - /// `rewrite_values` 一一对应、须同步。只置位不搬移,调用前须先 `clear_all_marks` 清位。 + /// 对象 DFS 跟踪 session 对象单表(统一入口 Box 化后无 epoch 表);perm + /// 对象不置位(其位跨收集残留会令 DFS 短路漏扫),走独立已访集防环。 + /// 字符串边走 rope 闭包传播(Cons 子节点与扁平化产物),BigInt 边直接入 + /// 存活集。roots 由 `Vm::for_each_root` 枚举,字段清单与 `rewrite_values` + /// 一一对应、须同步。只置位不搬移,调用前须先 `clear_all_marks` 清位。 pub(crate) fn mark(&mut self, vm: &Vm) { vm_debug!("[GC] mark phase: {} roots", vm.gc_state.session_object_ptrs.len()); let mut seeds = Vec::new(); @@ -464,20 +467,20 @@ impl SessionGc { /// 释放对象本体之外的堆外属性数据(元素区、元素 meta、hash 属性区、属性 meta /// 与各族 native 状态盒),按 capacity 经 `Box::from_raw` 各恰好释放一次,返回 - /// 字节数。`obj_ptr` 为空时返回 0;`require_session` 为 true 时断言对象属 - /// session epoch,epoch 原件走 false 分支。不含 upvalue 列表与对象本体,由调用方处理。 - pub(crate) fn drop_object_heap_data(obj_ptr: *mut JsObject, require_session: bool) -> u64 { + /// 字节数。`obj_ptr` 为空时返回 0。不含 upvalue 列表与对象本体,由调用方处理。 + /// + /// # 边界与前提 + /// - 统一对象表内全部对象为 session 对象(统一入口 Box 化后生产路径无 + /// epoch 原件),本函数只服务表内对象的释放路径,不做归属断言。 + pub(crate) fn drop_object_heap_data(obj_ptr: *mut JsObject) -> u64 { if obj_ptr.is_null() { return 0; } - // SAFETY: `obj_ptr` 在调用本辅助函数前已校验,指向 VM session arena 拥有的 - // session 对象。只重建 JsObject::ensure_hash_props/ensure_prop_meta 分配的 Box, + // SAFETY: `obj_ptr` 在调用本辅助函数前已校验,指向 VM 拥有的 session 对象。 + // 只重建 JsObject::ensure_hash_props/ensure_prop_meta 分配的 Box, // 且只在这里释放一次。 unsafe { let obj = &mut *obj_ptr; - if require_session { - debug_assert!(obj.is_session_epoch()); - } let mut freed_bytes = 0u64; let elems_ptr = obj.array_elements_raw() as *mut Vec; @@ -529,19 +532,23 @@ impl SessionGc { } } - fn drop_session_object_heap_data(obj_ptr: *mut JsObject) -> u64 { - Self::drop_object_heap_data(obj_ptr, true) - } - /// 释放一个死 session 对象:对象本体 + 堆数据 + upvalue 列表,返回释放字节数。 /// - /// # 注意事项 - /// 仅 sweep 死分支到达此处:只有存活对象被克隆、克隆与原件共享同一 upvalues - /// Box,死对象从不被克隆,其 upvalue 列表 Box 无其他持有者,在此恰好释放 - /// 一次;存活分支绝不释放(克隆仍引用同一 Box)。释放后置空:死对象已移出 - /// 对象表,收尾统一释放按表枚举不会再见,置空保证对象侧幂等、无陈旧指针。 - fn drop_dead_session_object(obj_ptr: *mut JsObject) -> u64 { - let mut freed = Self::drop_session_object_heap_data(obj_ptr) + size_of::() as u64; + /// # 步骤 + /// 1. 释放堆数据(元素区 / 属性区 / meta 区 / native 状态盒)。 + /// 2. 释放 upvalue 列表 Box 并置空对象侧字段。 + /// 3. 释放对象本体:堆载体(HEAP_BIT)经 `Box::from_raw` 释放; + /// Bump 载体(晋升族克隆,仅测试形态)随旧 Bump 换新归还,不在此释放。 + /// + /// # 边界与前提 + /// - `obj_ptr` 必须非空:空指针时第 1 步返回 0,但本体字节与位域读取 + /// 对空指针无效,调用点(sweep 死分支 / 收尾逐对象路径)先行跳过空位。 + /// - 仅 sweep 死分支与统一收尾到达此处:死对象从不被克隆,upvalue 列表 + /// Box 无其他持有者,在此恰好释放一次;存活分支绝不释放(若有克隆仍 + /// 引用同一 Box)。释放后死对象已移出对象表,收尾统一释放按表枚举 + /// 不会再见,置空保证对象侧幂等、无陈旧指针。 + pub(crate) fn drop_dead_session_object(obj_ptr: *mut JsObject) -> u64 { + let mut freed = Self::drop_object_heap_data(obj_ptr) + size_of::() as u64; // SAFETY: obj_ptr 来自 session 对象表,sweep 期间仍指向合法对象; // upvalues Box 仅本对象持有(死对象不克隆),保证恰好释放一次。 unsafe { @@ -553,8 +560,8 @@ impl SessionGc { (*obj_ptr).upvalues = std::ptr::null_mut(); std::mem::drop(vec); } - // 释放对象本体:堆载体(HEAP_BIT)经 Box::from_raw 释放,arena 载体 - // (克隆)随旧 arena 归还,不在此释放。 + // 释放对象本体:堆载体(HEAP_BIT)经 Box::from_raw 释放,Bump + // 载体(克隆)随旧 Bump 换新归还,不在此释放。 if (*obj_ptr).is_heap_alloc() { drop(Box::from_raw(obj_ptr)); } @@ -576,172 +583,116 @@ impl SessionGc { bytes } - /// 移动式清扫 session 对象:存活对象复制进新 arena,死对象原地释放。 + /// 原地清扫 session 对象:存活对象带位保留(清位并入活分支)、地址不变, + /// 死对象释放本体、堆区与独占 upvalue 列表并出表;弱表弱键在死对象释放前 + /// 按 mark 位定生死(死键条目丢弃)。免转发表与根重写。 /// - /// 按 GC mark 位分流:存活对象经 `clone_for_session_epoch` 复制(native 盒随族深拷) - /// 并登记 forwarding 旧址→新址,死对象释放本体、堆区与独占 upvalue;随后按转发表重写 - /// 新对象的值边、native 边与 promise 结算链裸指针,转发查找使共享与环去重到同一克隆。 - /// 最后重写全部根引用、换表并以新 Bump 接管 session epoch,旧 arena 整体归还并清 mark 位。 + /// # 步骤 + /// 1. 弱键定夺:对每个存活弱表按 mark 位重建条目表(此刻全部键对象 + /// 仍分配,位域可读,见 `resolve_weak_key_sweep` 时序前提)。 + /// 2. 分流:已标对象保留并清位(无残留位),未标对象原地释放出表。 + /// 3. 表回写存活集,按存活重算对象口径账目,累计扫描/存活/死统计。 + /// + /// # 副作用 + /// - `session_object_ptrs` 仅剩存活对象;`session_bytes_allocated` 重算为 + /// 存活对象字节(串/BigInt/cell 分量由各清扫路径自行补回); + /// - `total_*` 与 `last_collection_*` 统计按对象口径更新(绝对赋值, + /// 串/BigInt/cell 清扫在其上累加)。 + /// + /// # 边界与前提 + /// - 调用前须完成 `mark`(位域为本次收集的判定依据); + /// - 死对象从不被克隆,upvalue 列表 Box 无其他持有者,恰好一次释放成立。 pub(crate) fn sweep(&mut self, vm: &mut Vm) -> u64 { - let old_ptrs = std::mem::take(&mut vm.gc_state.session_object_ptrs); - let mut forwarding = std::mem::take(&mut vm.gc_state.forwarding); - let new_arena = bumpalo::Bump::new(); - let mut survivors = 0u64; - let mut dead = 0u64; - let mut freed_bytes = 0u64; + self.sweep_in_place(vm).2 + } - for old_ptr in old_ptrs { - if old_ptr.is_null() { + /// 原地 sweep 核心:弱键定夺、死对象释放与出表、存活重算、统计累计。 + /// `sweep`(完整收集)与 `collect_in_run`(执行期收集)共用,保证两条 + /// 收集路径的弱键定夺口径一致。返回 (存活数, 死数, 释放字节)。 + fn sweep_in_place(&mut self, vm: &mut Vm) -> (u64, u64, u64) { + let old_ptrs = std::mem::take(&mut vm.gc_state.session_object_ptrs); + let mut survivors = Vec::with_capacity(old_ptrs.len()); + + // 弱键定夺先于死对象释放:键对象(含死键)此刻全部仍分配, + // 对象头位域读取安全(时序前提见 resolve_weak_key_sweep)。 + for &ptr in &old_ptrs { + if ptr.is_null() { continue; } - // SAFETY: old_ptr 来自 session 对象表,sweep 运行期间仍指向合法对象。 - let is_live = unsafe { (*old_ptr).is_gc_marked() }; - let is_heap = unsafe { (*old_ptr).is_heap_alloc() }; - if is_live { - survivors += 1; - let old_ref = unsafe { &*old_ptr }; - let clone = old_ref.clone_for_session_epoch(); - let new_ptr = new_arena.alloc(clone) as *mut JsObject; - let new_ref = unsafe { &mut *new_ptr }; - if old_ref.is_map() { - map::clone_map_native_with_rewrite(old_ref, new_ref, |value| value); - } else if old_ref.is_set() { - set::clone_set_native_with_rewrite(old_ref, new_ref, |value| value); - } else if old_ref.is_weak_map_obj() { - // 弱键原样搬运:键生死按转发表收敛后的判定(phase 2 与 - // 晋升定夺路径同判据),此处只深拷贝盒与改写值边。 - weak_map::clone_weak_map_native_with_rewrite(old_ref, new_ref, |value| value); - } else if old_ref.is_disposable_stack_obj() || old_ref.is_async_disposable_stack_obj() { - disposable_stack::clone_dispose_native_with_rewrite(old_ref, new_ref, |value| value); - } else if old_ref.is_array_buffer_obj() { - array_buffer::clone_array_buffer_native(old_ref, new_ref); - } else if old_ref.is_shared_array_buffer_obj() { - array_buffer::clone_shared_array_buffer_native(old_ref, new_ref); - } else if old_ref.is_typed_array_obj() { - typed_array::clone_typed_array_native_with_rewrite(old_ref, new_ref, |value| value); - } else if old_ref.is_data_view_obj() { - data_view::clone_data_view_native_with_rewrite(old_ref, new_ref, |value| value); - } else if old_ref.is_promise_obj() { - crate::promise::clone_promise_native_with_rewrite(old_ref, new_ref, |value| value); - } else if old_ref.is_async_obj() { - crate::async_func::clone_async_native_with_rewrite(old_ref, new_ref, |value| value); - } else if old_ref.is_async_generator_obj() { - crate::async_generator::clone_async_generator_native_with_rewrite(old_ref, new_ref, |value| value); - } else if old_ref.is_generator_obj() { - crate::generator::clone_generator_native_with_rewrite(old_ref, new_ref, |value| value); - } else if old_ref.holds_compiled_regex() { - // 已编译正则是 Box 深拷贝到新对象:源 Box 由 drop 释放,互不共享。 - regexp::clone_regexp_native(old_ref, new_ref); - } else if old_ref.is_module_namespace() { - // 条目表深拷贝:`clone_for_session_epoch` 只浅拷贝 native_data 指针。 - module::clone_module_ns_native_with_rewrite(old_ref, new_ref, |value| value); - } - forwarding.insert(old_ptr, new_ptr); - freed_bytes += Self::drop_session_object_heap_data(old_ptr); - // 释放原件本体:堆载体(HEAP_BIT)经 Box::from_raw 释放,arena - // 载体(克隆)随旧 arena 归还,不在此释放。 - if is_heap { - freed_bytes += size_of::() as u64; - unsafe { - drop(Box::from_raw(old_ptr)); - } + // SAFETY: ptr 来自 session 对象表登记,sweep 运行期间有效。 + let obj = unsafe { &*ptr }; + if obj.is_gc_marked() && obj.is_weak_map_obj() { + // SAFETY: 弱表 native 盒独占,整表重建在定夺期间无并发读者。 + unsafe { + weak_map::rewrite_weak_map_native( + &mut *ptr, + |key| resolve_weak_key_sweep(key), + |value| value, + ); } + } + } + + // 按 mark 位分流:存活保留清位,死对象释放本体、堆区与 upvalue 出表。 + let mut dead = 0u64; + let mut freed_bytes = 0u64; + for &ptr in &old_ptrs { + if ptr.is_null() { + continue; + } + // SAFETY: ptr 来自 session 对象表登记,sweep 运行期间有效。 + if unsafe { (*ptr).is_gc_marked() } { + // 原地 sweep 不搬移对象:存活对象带位保留,清位并入活分支 + // (收集后无残留,残留 true 使下一次 mark DFS 短路漏标)。 + unsafe { (*ptr).set_gc_mark(false) }; + survivors.push(ptr); } else { + // SAFETY: 死对象从不被克隆,upvalue 列表 Box 无其他持有者。 + freed_bytes += Self::drop_dead_session_object(ptr); dead += 1; - freed_bytes += Self::drop_dead_session_object(old_ptr); } } + let live = survivors.len() as u64; + vm.gc_state.session_object_ptrs = survivors; - for &dst in forwarding.values() { - // SAFETY: forwarding 中的指针全是新分配且已初始化的对象。 - let obj = unsafe { &mut *dst }; - obj.rewrite_object_values(|value| { - if value.is_object() { - let ptr = value.as_js_object_ptr(); - if let Some(&fwd) = forwarding.get(&ptr) { - return JsValue::from_js_object(fwd); - } - } - value - }); - if obj.is_map() { - map::rewrite_map_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); - } else if obj.is_set() { - set::rewrite_set_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); - } else if obj.is_weak_map_obj() { - // 键按转发表 + mark 位定生死(死键条目丢弃),值走强边转发改写。 - weak_map::rewrite_weak_map_native( - obj, - |key| resolve_weak_key_sweep(key, &forwarding), - |value| rewrite_forwarded_value(value, &forwarding), - ); - } else if obj.is_disposable_stack_obj() || obj.is_async_disposable_stack_obj() { - disposable_stack::rewrite_dispose_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); - } else if obj.is_typed_array_obj() { - typed_array::rewrite_typed_array_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); - } else if obj.is_data_view_obj() { - data_view::rewrite_data_view_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); - } else if obj.is_generator_obj() { - crate::generator::rewrite_generator_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); - } else if obj.is_promise_obj() { - crate::promise::rewrite_promise_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); - // 结算链指针是裸指针边(非 JsValue):搬移换址后按转发表重定位, - // 旧克隆的结算传导仍指向后继克隆的新址。 - crate::promise::repoint_promise_promoted_clone(obj, |ptr| { - rewrite_forwarded_value(JsValue::from_js_object(ptr), &forwarding).as_js_object_ptr() - }); - } else if obj.is_async_obj() { - crate::async_func::rewrite_async_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); - } else if obj.is_async_generator_obj() { - crate::async_generator::rewrite_async_generator_native(obj, |value| { - rewrite_forwarded_value(value, &forwarding) - }); - } else if obj.is_module_namespace() { - module::rewrite_module_ns_native(obj, |value| rewrite_forwarded_value(value, &forwarding)); - } - } - - rewrite_vm_roots(vm, &forwarding); - - vm.gc_state.session_object_ptrs = forwarding.values().copied().collect(); - forwarding.clear(); - vm.gc_state.forwarding = forwarding; - vm.gc_state.session_epoch = new_arena; + // 对象口径重算:存活对象头 + 堆数据求和(串/BigInt/cell 分量不在此口径, + // 由各清扫路径按存活补回,与完整收集的最终账目一致)。 vm.gc_state.session_bytes_allocated = vm .gc_state .session_object_ptrs .iter() .filter(|&&ptr| !ptr.is_null()) .map(|&ptr| { + // SAFETY: 存活对象在 session 表中,sweep 运行期间有效。 let obj = unsafe { &*ptr }; size_of::() as u64 + Self::object_heap_data_bytes(obj) }) .sum::() as usize; - let total_ptrs = survivors + dead; - if total_ptrs > 0 { + // 统计按对象口径累计(串/BigInt/cell 清扫在其上累加释放字节)。 + self.total_bytes_freed = self.total_bytes_freed.saturating_add(freed_bytes); + self.total_objects_scanned += live + dead; + self.total_objects_live += live; + self.total_objects_dead += dead; + self.last_collection_objects_scanned = live + dead; + self.last_collection_objects_live = live; + self.last_collection_objects_dead = dead; + self.last_collection_bytes_freed = freed_bytes; + + if live + dead > 0 { if dead == 0 { - vm_debug!("[GC] sweep phase -> no objects collected ({} live, {} dead)", survivors, dead); + vm_debug!("[GC] sweep phase -> no objects collected ({} live, {} dead)", live, dead); } else { vm_debug!( "[GC] sweep phase: {} scanned, {} live, {} dead, {} bytes", - total_ptrs, - survivors, + live + dead, + live, dead, freed_bytes ); } } - - self.total_bytes_freed += freed_bytes; - self.total_objects_scanned += total_ptrs; - self.total_objects_live += survivors; - self.total_objects_dead += dead; - self.last_collection_objects_scanned = total_ptrs; - self.last_collection_objects_live = survivors; - self.last_collection_objects_dead = dead; - self.last_collection_bytes_freed = freed_bytes; - freed_bytes + (live, dead, freed_bytes) } /// 清扫 session `JsString`:保留 `mark()` 阶段记为存活的部分,其余经 @@ -884,10 +835,11 @@ impl SessionGc { } } - /// 完整收集编排:`mark` → `sweep`(对象移动式搬移)→ `sweep_session_strings` → - /// `sweep_session_bigints`,累加三类释放字节;字符串与 BigInt 地址稳定、无需 - /// forwarding。更新各表、字节账目与 mark 位,累计收集次数与最近/最大/最小耗时, - /// 释放字节大于 0 或每满 100 次时输出统计摘要。 + /// 完整收集编排:`mark` → `sweep`(对象原地清扫)→ `sweep_session_strings` → + /// `sweep_session_bigints` → `sweep_session_cells`,累加三类释放字节; + /// 对象与字符串、BigInt 地址均稳定、无需 forwarding。更新各表、字节账目与 + /// mark 位,累计收集次数与最近/最大/最小耗时,释放字节大于 0 或每满 100 次 + /// 时输出统计摘要。 pub(crate) fn collect(&mut self, vm: &mut Vm) { let start = Instant::now(); @@ -922,9 +874,9 @@ impl SessionGc { /// 仅回收 session 字符串:完整 mark(对象只置位不搬移)→ 按存活集清扫字符串。 /// - /// 刻意跳过对象 sweep 搬移——builtin/dispatch 层持有跨分配点的 session 对象 - /// 裸指针,移动式 sweep 会使其悬垂;对象执行期回收仍只在 reset 统一进行。 - /// 字符串每串独立 Box、地址稳定,清扫无需 forwarding 与根重写。 + /// 刻意跳过对象清扫——轻量档只走串表释放,对象口径的回收归完整收集与 + /// 执行期收集(dispatch 安全点)。字符串每串独立 Box、地址稳定,清扫 + /// 无需 forwarding 与根重写。 /// /// # 副作用 /// - 清空全部对象 mark 位;按存活集释放死串;累计 `total_collections` 与时长统计; @@ -1006,22 +958,22 @@ impl SessionGc { } } - /// 执行期收集:session 原地非移动 sweep。 + /// 执行期收集:session 原地 sweep(与完整收集共用清扫核心)。 /// /// 在 dispatch 安全点(循环顶 + `native_call_depth == 0`)回收单 run 分配 /// 包络内的死对象:死 session 对象原地释放(堆区 + upvalue 列表 + 本体) - /// 并出表;存活对象不移动——地址不变、免 forwarding/rewrite,用户可观察 - /// identity 不分裂。 + /// 并出表,弱键按 mark 位定夺;存活对象不移动——地址不变、免转发表与 + /// 根重写,用户可观察 identity 不分裂。 /// /// # 边界与前提 - /// - 调用方已完成门控(无活跃/挂起 for-in):门控是保守门,迭代器体 - /// 是堆上 Box,收集不搬移迭代器体; + /// - 无门控:for-in 迭代器体是堆上 Box,活跃形经 VM 表、挂起形经状态盒 + /// 边进入根收集,键引用被 mark 标活,原地清扫不误释放; /// - 调用点为 dispatch 安全点(`native_call_depth == 0`),无在途 builtin /// 局部裸指针、dispatch 未重入。 /// /// # 副作用 - /// - `session_object_ptrs` 仅剩存活,`session_bytes_allocated` 按存活重算 - /// (串/BigInt 归 strings-only 路径,本路径不动,仅对象口径变化); + /// - `session_object_ptrs` 仅剩存活,`session_bytes_allocated` 按存活整体 + /// 重算(对象 + 串 + BigInt;本路径不扫串/BigInt,串 GC 归 strings-only); /// - `gc_watermark` = 当前分配包络 + 阈值增量; /// - 累计/时长统计更新;mark 位全清(收集后无残留)。 pub(crate) fn collect_in_run(&mut self, vm: &mut Vm) { @@ -1033,44 +985,16 @@ impl SessionGc { self.mark(vm); - // 死 session 对象原地释放(堆区 + upvalue 列表 + 本体)并出表;存活 - // 对象不动——地址不变,免 forwarding/rewrite。 - let session_ptrs = std::mem::take(&mut vm.gc_state.session_object_ptrs); - 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() } { - // 原地 sweep 不搬移对象:存活对象带位保留,清位并入活分支 - // (收集后无残留,残留 true 使下一次 mark DFS 短路漏标)。 - unsafe { (*ptr).set_gc_mark(false) }; - 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; + // 死 session 对象原地释放(堆区 + upvalue 列表 + 本体)并出表,弱键 + // 按 mark 位定夺;存活对象不动——地址不变,免转发表与根重写。对象 + // 口径统计由清扫核心累计,此处只补集合与账目。 + self.sweep_in_place(vm); // 死 cell 随死对象出表:存活 cell 经 mark 种子 + 对象边入存活集,此处清扫。 self.sweep_session_cells(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; - } + // 整体口径重算:对象分量已由清扫核心写入,此处补串与 BigInt 分量 + // (本路径不扫串/BigInt,按当前表整体计入,与 strings-only 口径一致)。 let mut string_bytes: usize = 0; for &ptr in &vm.gc_state.session_string_ptrs { if ptr.is_null() { @@ -1080,30 +1004,18 @@ impl SessionGc { string_bytes += size_of::() + unsafe { (*ptr).payload_bytes() }; } 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.session_bytes_allocated = + vm.gc_state.session_bytes_allocated.saturating_add(string_bytes).saturating_add(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_session; - let dead = dead_session; - let freed_bytes = 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 += session_ptrs.len() as u64; - self.total_objects_live += live; - self.total_objects_dead += dead; - self.last_collection_objects_scanned = 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", @@ -1112,7 +1024,7 @@ impl SessionGc { self.last_collection_objects_live, self.last_collection_objects_dead, elapsed.as_secs_f64() * 1000.0, - freed_bytes, + self.last_collection_bytes_freed, ); } @@ -1165,56 +1077,38 @@ fn rewrite_forwarded_value( .unwrap_or(value) } -/// 移动式 sweep 改写期的弱键判定:转发表内键改指新址;未入表的 session/ -/// epoch 键按 mark 位定生死(已标 = 强可达、随后晋升或原地存活,未标 = -/// 死键丢弃);P 键不可死,恒保留。 +/// 原地 sweep 的弱键判定:session 键按 mark 位定生死(已标 = 强可达保留, +/// 未标 = 死键丢弃),非 session 键(perm 对象等)不可死、恒保留;symbol +/// 键按值恒等、恒保留。 /// /// # 边界与前提 -/// - 未入表键读归属位与 mark 位须解引用旧指针:调用点(sweep 改写相) -/// 旧 arena 尚未归还、清位发生在改写相之后,对象头位域可读。 -/// - 死键指针只作哈希键与位域读取,不 deref 其已释放的堆数据。 -pub(crate) fn resolve_weak_key_sweep( - key: weak_map::WeakKey, forwarding: &HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>, -) -> Option { +/// - 时序前提:定夺须先于死对象本体释放执行——判定要读键对象本体的 mark +/// 位域,调用点(sweep 弱键定夺相)位于死对象释放相之前,全部键对象(含 +/// 死键)此刻仍分配,位域读取安全;死键指针此后只作被丢弃的表项内容, +/// 不再被解引用。 +/// - 死键指针只读对象头位域,不解引用其堆数据(属性区等随释放销毁)。 +pub(crate) fn resolve_weak_key_sweep(key: weak_map::WeakKey) -> Option { let weak_map::WeakKey::Obj(ptr) = key else { return Some(key); }; let mut_ptr = ptr as *mut JsObject; - if let Some(&new) = forwarding.get(&mut_ptr) { - return Some(weak_map::WeakKey::Obj(new as *const JsObject)); - } - // SAFETY: 见函数边界与前提——sweep 改写相旧 arena 存活,对象头位域可读。 + // SAFETY: 见时序前提——定夺相键对象仍分配,对象头位域可读。 let old = unsafe { &*mut_ptr }; - if (old.is_session_epoch() || old.is_epoch()) && !old.is_gc_marked() { + if old.is_session_epoch() && !old.is_gc_marked() { return None; } Some(key) } -/// 晋升收敛后的弱键定夺:转发表内键改指新址;未入表 epoch 键 = 本轮晋升 -/// 未覆盖 = 死键丢弃;session 键不搬移、原样保留(惰性判定交下一轮 sweep); -/// P 键不可死,恒保留。 +/// 晋升收敛后的弱键定夺:统一入口 Box 化后晋升族在生产路径不产克隆 +/// (epoch 守卫恒假、无 epoch 对象),转发表恒空、无重指需求;reset 边界 +/// 的 mark 位已被前序收集清位,不携带本轮可达性信息。故定夺退化为原样 +/// 保留——弱键生死由原地 sweep 的 mark 位判定单一裁定,本路径不改表项。 /// /// # 边界与前提 -/// - 晋升定夺路径(原地晋升 / in-run 晋升档)的转发表已收敛为最终强可达集, -/// 判据不含 mark 位(reset 边界路径此前已清位)。 -/// - 解引用仅限未入表键的归属位读取,调用点旧 arena 存活(epoch 释放 / -/// 清表发生在定夺之后)。 -pub(crate) fn resolve_weak_key_after_promotion( - key: weak_map::WeakKey, forwarding: &HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>, -) -> Option { - let weak_map::WeakKey::Obj(ptr) = key else { - return Some(key); - }; - let mut_ptr = ptr as *mut JsObject; - if let Some(&new) = forwarding.get(&mut_ptr) { - return Some(weak_map::WeakKey::Obj(new as *const JsObject)); - } - // SAFETY: 见函数边界与前提——定夺点旧 arena 存活,对象头位域可读。 - let old = unsafe { &*mut_ptr }; - if old.is_epoch() { - return None; - } +/// - 不解引用任何对象本体(无位域读取),无定夺时序面; +/// - 晋升族整体为死代码保留面,弱表族本体退役时一并删除。 +pub(crate) fn resolve_weak_key_after_promotion(key: weak_map::WeakKey) -> Option { Some(key) } diff --git a/crates/oxide_vm/src/session_gc/tests.rs b/crates/oxide_vm/src/session_gc/tests.rs index 27a9415a..2be0b822 100644 --- a/crates/oxide_vm/src/session_gc/tests.rs +++ b/crates/oxide_vm/src/session_gc/tests.rs @@ -224,7 +224,8 @@ fn sweep_preserves_cycle_and_collects_unreachable() { vm.gc_state.session_gc = gc; assert_eq!(vm.gc_state.session_object_ptrs.len(), 2); - assert!(!vm.gc_state.session_object_ptrs.contains(&root_session)); + // 原地 sweep 不搬移存活对象:根与子保持原址(克隆体即原对象),死对象出表。 + assert!(vm.gc_state.session_object_ptrs.contains(&root_session)); assert!(!vm.gc_state.session_object_ptrs.contains(&dead_session)); assert!(!vm .gc_state @@ -262,39 +263,19 @@ fn sweep_preserves_array_elements_and_collects_dead_element_object() { let _ = gc.sweep(&mut vm); vm.gc_state.session_gc = gc; - // sweep 复制存活对象并把 VM 根改写为新 arena 指针:regs[0] 是数组的新地址, - // 旧指针已随旧 arena 释放,不可再用。 - let arr_new = vm.regs[0].as_js_object_ptr(); - let live_session = unsafe { (*arr_new).get_prop_at(0).as_js_object_ptr() }; + // 原地 sweep 不搬移存活对象:数组保持原址、根指针不变,死元素对象 + // 原地释放并出表,存活集合只剩数组与元素 0 的对象。 + let arr_addr = vm.regs[0].as_js_object_ptr(); + assert_eq!(arr_addr, arr_session, "存活数组原地保留,根指针不变"); + let live_session = unsafe { (*arr_addr).get_prop_at(0).as_js_object_ptr() }; assert_eq!(unsafe { (*live_session).prop_count() }, 0); - assert_eq!(unsafe { (*arr_new).prop_count() }, 2); - assert_eq!(unsafe { (*arr_new).get_prop_at(0).as_js_object_ptr() }, live_session); - // 元素 1 引用已断开:死元素对象被回收,存活集合只剩数组与元素 0 的对象。 - assert_eq!(unsafe { (*arr_new).get_prop_at(1) }, JsValue::undefined()); + assert_eq!(unsafe { (*arr_addr).prop_count() }, 2); + assert_eq!(unsafe { (*arr_addr).get_prop_at(0).as_js_object_ptr() }, live_session); + // 元素 1 引用已断开:死元素对象被回收。 + assert_eq!(unsafe { (*arr_addr).get_prop_at(1) }, JsValue::undefined()); assert_eq!(vm.gc_state.session_object_ptrs.len(), 2); } -#[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 mut gc = std::mem::take(&mut vm.gc_state.session_gc); - gc.mark(&vm); - let _ = gc.sweep(&mut vm); - vm.gc_state.session_gc = gc; - - // 复用的 forwarding 表必须在 sweep 后清空,否则 promote 会观察到指向已释放 - // arena 的过期 old->new 条目。 - assert!(vm.gc_state.forwarding.is_empty()); -} - fn vm_with_low_threshold() -> Vm { let mut cfg = KernelConfig::minimal(); cfg.set_session_gc_threshold(1); @@ -402,7 +383,7 @@ fn gc_stats_summary_includes_collection() { } #[test] -fn moving_sweep_rewrites_global_root_edges() { +fn full_collect_keeps_global_root_edges_in_place() { let mut vm = vm_with_low_threshold(); let obj = plain_object(&mut vm); unsafe { @@ -426,7 +407,10 @@ fn moving_sweep_rewrites_global_root_edges() { .expect("global slot"); let new_value = global.get_prop_at(pos); assert!(new_value.is_object()); - assert!(!std::ptr::eq(new_value.as_js_object_ptr(), old_ptr)); + assert!( + std::ptr::eq(new_value.as_js_object_ptr(), old_ptr), + "原地 sweep 不搬移对象:global 根边原址不变" + ); assert_eq!(unsafe { (*new_value.as_js_object_ptr()).get_prop_at(0) }, JsValue::int(42)); } @@ -508,8 +492,8 @@ fn weak_map_object(vm: &mut Vm) -> *mut JsObject { vm.alloc_object(obj) } -/// 弱键不产 mark 边:强可达键跨完整收集保留,条目完整、键与值边改写 -/// 到搬移后新址,经改写后的根读回同一克隆体。 +/// 弱键不产 mark 边:强可达键跨完整收集保留,条目完整、键与值边原地不变, +/// 经原键读回同一对象。 #[test] fn weak_map_entry_survives_promotion_with_live_key() { let mut vm = vm_with_low_threshold(); @@ -540,13 +524,13 @@ fn weak_map_entry_survives_promotion_with_live_key() { let live_wm = unsafe { &*vm.regs[0].as_js_object_ptr() }; assert_eq!(oxide_builtins::weak_map::weak_map_entry_count(live_wm), 1, "强可达键的条目须在收集后存活"); - // 键经移动式 sweep 搬移,条目键改指克隆体:按改写后的根读回。 + // 原地 sweep 不搬移对象:键与值边原地保留,按原键读回同一对象。 let stored = oxide_builtins::weak_map::weak_map_probe_get(live_wm, vm.regs[1]); - assert_eq!(stored, vm.regs[2], "值须按改写后的键读回同一克隆体"); - assert!(!std::ptr::eq(stored.as_js_object_ptr(), value), "值边须改写到搬移后新址"); + assert_eq!(stored, vm.regs[2], "值须按原键读回同一对象"); + assert!(std::ptr::eq(stored.as_js_object_ptr(), value), "值边原地不变"); } -/// 弱键强不可达 = 死键:收集按转发表判定丢条目,表不留死键残影。 +/// 弱键强不可达 = 死键:收集按 mark 位定夺丢条目,表不留死键残影。 #[test] fn weak_map_entry_dropped_when_key_unrooted() { let mut vm = vm_with_low_threshold(); @@ -575,8 +559,7 @@ fn weak_map_entry_dropped_when_key_unrooted() { assert_eq!(oxide_builtins::weak_map::weak_map_entry_count(live_wm), 0, "死键条目须在收集时丢弃"); } -/// 值边为强边:值对象唯一引用仅来自值边时仍入存活集,搬移后条目按转发表 -/// 改写指到新址。 +/// 值边为强边:值对象唯一引用仅来自值边时仍入存活集,原地保留原址。 #[test] fn weak_map_value_edge_keeps_value_alive() { let mut vm = vm_with_low_threshold(); @@ -605,19 +588,18 @@ fn weak_map_value_edge_keeps_value_alive() { vm.gc_state.session_gc = gc; let live_wm = unsafe { &*vm.regs[0].as_js_object_ptr() }; - // 键经移动式 sweep 搬移,条目键改指克隆体:按改写后的根读回。 + // 原地 sweep 不搬移对象:键与值边原地保留,按原键读回同一对象。 let stored = oxide_builtins::weak_map::weak_map_probe_get(live_wm, vm.regs[1]); assert!(stored.is_object(), "唯一经值边引用的值须存活"); - assert!(!std::ptr::eq(stored.as_js_object_ptr(), value_session), "值边须在搬移后改写到新址"); + assert!(std::ptr::eq(stored.as_js_object_ptr(), value_session), "值边原地不变"); assert!(vm.is_session_ptr(stored.as_js_object_ptr())); // key、value、wm 三对象均存活(key/value 经寄存器根、wm 经寄存器根), - // 搬移后各占一份克隆。 + // 原地保留原址。 assert_eq!(vm.gc_state.session_object_ptrs.len(), 3); } -/// in-run 原地 sweep·死键面:无强根的 session 键在原地 sweep 判死出表; -/// P 键不可死,条目保留且按原键读回。原地 sweep 不动弱表条目表,死键条目 -/// 的定夺交后续单 mark 位判据,此处只钉死键对象出表 + P 键条目保留。 +/// in-run 原地 sweep·死键面:无强根的 session 键在原地 sweep 按 mark 位判死, +/// 死键条目丢弃、死键对象释放出表;P 键不可死,条目保留且按原键读回。 #[test] fn weak_map_dead_key_freed_by_in_run_sweep() { let mut vm = vm_with_threshold(65536); @@ -642,6 +624,11 @@ fn weak_map_dead_key_freed_by_in_run_sweep() { assert_eq!(vm.session_gc_stats().total_collections, 1, "in-run 收集应跑一轮"); assert!(!vm.gc_state.session_object_ptrs.contains(&key), "死键对象须出表"); let live_wm = unsafe { &*vm.regs[0].as_js_object_ptr() }; + assert_eq!( + oxide_builtins::weak_map::weak_map_entry_count(live_wm), + 1, + "死键条目须丢弃,仅 P 键条目保留" + ); assert_eq!( oxide_builtins::weak_map::weak_map_probe_get(live_wm, p_key), JsValue::int(7), @@ -1122,8 +1109,8 @@ fn global_prop_opt(vm: &Vm, name: &str) -> Option { } /// 闭包捕获变量跨对象 sweep 存活:reset 触发完整收集(session_epoch 替换)。 -/// cell 独立堆分配、不入 session arena,arena 整体回收时不连带释放——否则跨 -/// arena 存活的 cell 指针悬垂;sweep 只重写 cell.value 中的对象引用,值跨搬移保留。 +/// cell 独立堆分配、地址稳定,原地 sweep 不搬移对象、不触碰 cell 结构—— +/// 跨收集的 cell 指针与值均保留。 #[test] fn closure_cell_survives_object_sweep() { let mut vm = vm_with_threshold(1); @@ -1134,11 +1121,11 @@ fn closure_cell_survives_object_sweep() { .expect("run1"); assert!(!vm.gc_state.session_cell_ptrs.borrow().is_empty(), "run1 应分配 upvalue cell"); - // reset 触发对象 sweep:存活对象搬到新 arena,旧 arena 释放。 + // reset 触发对象 sweep:存活对象原地保留,死对象原地释放。 vm.reset(); assert!(vm.session_gc_stats().total_collections > 0, "reset 应触发对象收集"); - // 跨搬移后从 global 重新取 inc 函数对象:upvalue cell 指针稳定、值保留。 + // 收集后从 global 重新取 inc 函数对象:upvalue cell 指针稳定、值保留。 let inc_val = global_prop_opt(&vm, "inc").expect("inc 应挂在 global 上"); let obj = unsafe { &*inc_val.as_js_object_ptr() }; let cells = obj.upvalues_slice(); @@ -1149,8 +1136,8 @@ fn closure_cell_survives_object_sweep() { } /// 私有字段类的 brand cell 跨对象 sweep 存活:`@@class_brand` upvalue cell -/// 存类 brand 对象,sweep 后其值经 forwarding 重写为搬移后的新对象地址。 -/// cell 独立堆分配,指针在周边 arena 回收后仍稳定,值不被内存复用覆盖。 +/// 存类 brand 对象,原地 sweep 后其值为原 brand 对象(地址不变)。cell 独立 +/// 堆分配、地址稳定,原地 sweep 不触碰 cell 结构体。 #[test] fn private_brand_cell_survives_object_sweep() { let mut vm = vm_with_threshold(1); @@ -1346,8 +1333,8 @@ impl Drop for NsCleanup { } /// 模块 ns cell 根:依赖模块可重赋导出挂共享 cell(条目表 `Cell` 面),ns 对象 -/// 经 global 可达;清寄存器仅留 ns 根后完整收集,cell 跨移动式 sweep 存活(深拷 -/// 表保留同一 cell 指针),`module_ns_export` 读回正确。 +/// 经 global 可达;清寄存器仅留 ns 根后完整收集,cell 跨原地 sweep 存活 +/// (ns 对象与条目表原地不变),`module_ns_export` 读回正确。 #[test] fn module_ns_cell_root_survives_collect() { let cwd = std::env::current_dir().expect("cwd"); @@ -1385,10 +1372,10 @@ fn module_ns_cell_root_survives_collect() { "模块 ns cell 应跨完整收集存活" ); - // 移动式 sweep 后 ns 晋升 session(原件出表),从 global 重取克隆读回。 + // 原地 sweep 不搬移对象:ns 与条目表原地保留,从 global 重取读回。 let ns_val2 = global_prop_opt(&vm, "__ns").expect("__ns 应挂在 global 上"); let ns_obj2 = unsafe { &*ns_val2.as_js_object_ptr() }; - assert!(module::module_ns_cell_edges(ns_obj2).contains(&cell_ptr), "克隆应共享同一 cell 指针"); + assert!(module::module_ns_cell_edges(ns_obj2).contains(&cell_ptr), "ns 条目表应保留同一 cell 指针"); let name_si = vm.kernel_core().perm_interner().intern("x").0; let query = module::module_ns_export(ns_obj2, name_si).expect("x 应可导出"); match query { @@ -1705,12 +1692,14 @@ fn in_run_collection_reclaims_dead_and_keeps_live() { assert_eq!(a, Some(JsValue::int(1))); } -/// for-in 门控双形钉住:活跃形(迭代器在 `vm.iters` 表内)与挂起形 -/// (生成器在 for-in 内 yield,迭代器搬入状态盒)都须拦下执行期收集—— -/// 门控是保守门,迭代器体为堆上 Box;门控开后方可收集。 +/// 无门控 for-in 双形钉住:活跃形(迭代器在 `vm.iters` 表内)与挂起形 +/// (生成器在 for-in 内 yield,迭代器搬入状态盒)存在时执行期收集照常执行 +/// ——迭代器键引用经根收集标活、原地清扫不搬移对象,无门控亦不误释放, +/// yield 值跨收集保持正确。 #[test] -fn active_and_suspended_for_in_block_in_run_collection() { - // 活跃形:迭代器直接压在 vm.iters,O(1) 子句即拦。 +fn active_and_suspended_for_in_do_not_block_in_run_collection() { + // 活跃形:迭代器直接压在 vm.iters,收集照常执行;死对象原地释放, + // 迭代器键经根收集标活。 let mut vm = vm_with_threshold(65536); let dead = plain_object(&mut vm); let iter = Box::into_raw(Box::new(crate::vm::ForInIter { @@ -1720,17 +1709,11 @@ fn active_and_suspended_for_in_block_in_run_collection() { vm.iters.push_for_in(iter); vm.maybe_collect_in_run(); - assert_eq!(vm.session_gc_stats().total_collections, 0, "活跃 for-in:门控关闭,不收集"); - assert!(vm.gc_state.session_object_ptrs.contains(&dead), "死对象仍在 session 表"); - - // 迭代器出表后同一调用点即应收集。 - vm.iters.pop_for_in(); - vm.maybe_collect_in_run(); - assert_eq!(vm.session_gc_stats().total_collections, 1, "门控开:应收集"); + assert_eq!(vm.session_gc_stats().total_collections, 1, "无门控:活跃 for-in 存在时收集照常执行"); assert!(!vm.gc_state.session_object_ptrs.contains(&dead), "死 session 对象应被原地释放并出表"); - // 挂起形(拦截):生成器在 for-in 内 yield 后 run 结束,迭代器经 - // 状态盒持有(vm.iters 已空)——收集点必须拦下。 + // 挂起形:生成器在 for-in 内 yield 后 run 结束,迭代器经状态盒持有 + // (vm.iters 已空)——键经状态盒边入根收集标活,收集照常执行。 let mut vm = vm_with_threshold(65536); vm.run(&Arc::new(compile( "var o = { a: 1, b: 2 }; \ @@ -1739,10 +1722,9 @@ fn active_and_suspended_for_in_block_in_run_collection() { ))) .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_eq!(vm.session_gc_stats().total_collections, 1, "挂起 for-in:收集照常执行"); // 挂起生成器对象保持原址(未释放):状态盒与迭代器一体存活。 let gen_in_session = vm .gc_state @@ -1751,9 +1733,8 @@ fn active_and_suspended_for_in_block_in_run_collection() { .any(|&p| !p.is_null() && unsafe { (*p).type_tag == JsObject::OBJ_TYPE_GENERATOR }); assert!(gen_in_session, "挂起生成器应仍在 session 表"); - // 挂起形(放行):同一 run 内续跑至 for-in 结束(迭代器释放), - // 阈值 1 使指令边界自动触发:挂起期门控关(Bump 不换新),完成后 - // 门控开、执行期收集放行;yield 值跨收集保持正确。 + // 同一 run 内续跑至 for-in 结束(迭代器释放),阈值 1 使指令边界自动触发 + // 收集:挂起期与完成期收集均照常放行,yield 值跨收集保持正确。 let mut vm = vm_with_threshold(1); vm.run(&Arc::new(compile( "var o = { a: 1, b: 2 }; \ @@ -1764,10 +1745,9 @@ fn active_and_suspended_for_in_block_in_run_collection() { .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, "完成后门控开:执行期收集应放行"); + assert!(vm.session_gc_stats().total_collections >= 1, "执行期收集应已放行"); - // 生成器对象跨收集仍为合法生成器(晋升 session 或保持 epoch 完好)。 + // 生成器对象跨收集仍为合法生成器(session 对象原地保留)。 let g = global_prop_opt(&vm, "g").expect("g 应挂在 global"); assert!(unsafe { (*g.as_js_object_ptr()).is_generator_obj() }, "收集后生成器对象应完好"); } diff --git a/crates/oxide_vm/src/vm/dispatch_loop.rs b/crates/oxide_vm/src/vm/dispatch_loop.rs index f692bab3..86a45763 100644 --- a/crates/oxide_vm/src/vm/dispatch_loop.rs +++ b/crates/oxide_vm/src/vm/dispatch_loop.rs @@ -123,7 +123,7 @@ impl Vm { // 返回顶层后检查恢复,存活值此时已回拷为执行根。账目未超水位时仅 // 少量字段比较。 if self.native_call_depth == 0 { - // 宿主 `$262.gc()` 置位的强制收集在此边界消费:移动式 sweep + // 宿主 `$262.gc()` 置位的强制收集在此边界消费:完整收集 // 唯一安全点(无在途 dispatch 局部值),与正常触发路径同一入口。 if self.gc_state.pending_forced_collect { self.gc_state.pending_forced_collect = false; @@ -142,8 +142,9 @@ 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)。 + // 执行期原地 sweep 收集:O(1) 包络超触发水位 → 收集(无门控: + // 迭代器键引用经根收集标活,原地释放不搬移存活对象; + // 边界契约见 maybe_collect_in_run)。 if alloc >= self.gc_state.gc_watermark { self.maybe_collect_in_run(); } diff --git a/crates/oxide_vm/src/vm/gc_hooks.rs b/crates/oxide_vm/src/vm/gc_hooks.rs index 2308ebc2..7102f313 100644 --- a/crates/oxide_vm/src/vm/gc_hooks.rs +++ b/crates/oxide_vm/src/vm/gc_hooks.rs @@ -239,60 +239,17 @@ impl Vm { self.gc_state.session_gc = session_gc; } - /// 门控子句二:三型状态盒(生成器/异步函数/异步生成器)的 - /// `suspended.for_in_iters` 是否非空。 - /// - /// 门控是保守门:for-in 迭代器活跃/挂起期间拦下执行期收集; - /// 挂起帧把迭代器搬入状态盒(`vm.iters` 不可见),故须逐状态盒扫描。 - /// - /// # 边界与前提 - /// - 扫描 session 对象表——挂起状态盒宿主对象在表内; - /// - 挂起帧的 keys 经状态盒边收为 GC 根(被枚举对象不误释放),迭代器 - /// 体是堆上 Box,故按指针扫 `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: 对象表登记的指针,存活期内有效。 - 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.session_object_ptrs.iter().any(holds) - } - - /// 执行期两档收集的 dispatch 安全点入口:仅在循环顶 + /// 执行期原地 sweep 收集的 dispatch 安全点入口:仅在循环顶 /// (`native_call_depth == 0`,无 builtin 局部裸指针、dispatch 未重入) - /// 由触发块调用;先门控后收集。 + /// 由触发块在水位命中后调用,无门控直接收集。 /// /// # 边界与前提 - /// - 子句一(活跃 for-in,O(1))未过:免费重试,不设锚; - /// - 子句二(状态盒扫描,O(对象数))未过:重扫按包络增长一个阈值设锚, - /// 避免持挂起 for-in 的 run 每指令边界重扫; - /// - 门控过但无对象可回收时仍跑一轮(mark + 换新 Bump),水位同点抬高, + /// - for-in 迭代器体是堆上 Box:活跃形经 VM 表、挂起形经状态盒边进入 + /// 根收集,键引用被 mark 标活,原地清扫不搬移对象、不误释放, + /// 无需拦截; + /// - 无对象可回收时仍跑一轮(mark + 原地清扫),水位同点抬高, /// 触发间距由包络增量控制。 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.collect_in_run(self); self.gc_state.session_gc = session_gc; @@ -373,10 +330,10 @@ impl Vm { bytes } - /// 无条件执行一次完整 session GC(mark + 移动式 sweep + 串/BigInt 清扫)。 + /// 无条件执行一次完整 session GC(mark + 对象原地清扫 + 串/BigInt 清扫)。 /// /// # 副作用 - /// - 存活对象复制进新 session arena,全部根与原生盒按转发表重写; + /// - 存活对象地址不变、根与原生盒免改写; /// `session_bytes_allocated` 重置为清扫后的存活字节。 /// /// # 注意事项 diff --git a/crates/oxide_vm/src/vm_state.rs b/crates/oxide_vm/src/vm_state.rs index 7d25c677..ce5b9ee8 100644 --- a/crates/oxide_vm/src/vm_state.rs +++ b/crates/oxide_vm/src/vm_state.rs @@ -11,8 +11,6 @@ use std::cell::{Cell, RefCell}; use std::collections::HashMap; -use rustc_hash::FxBuildHasher; - use crate::session_gc::SessionGc; use crate::vm::ForInIter; use oxide_builtins::iterator::BuiltinIterKind; @@ -35,9 +33,9 @@ pub(crate) struct GcState { /// mark/sweep 回收(值量少),只在 full_reset 统一释放。 pub(crate) session_bigint_ptrs: RefCell>, /// upvalue cell(`Box`)追踪表。`RefCell` 使 `&self` 的分配入口也能 - /// 登记新 box。cell 独立堆分配、地址稳定,不参与对象搬移(sweep 只重写 - /// `cell.value` 中的对象引用);参与 mark-sweep 回收(死 cell 随收集释放), - /// full_reset 为收尾兜底(表已空时 no-op)。 + /// 登记新 box。cell 独立堆分配、地址稳定,不参与对象搬移(原地 sweep + /// 不触碰 cell,也不重写 `cell.value`);参与 mark-sweep 回收(死 cell + /// 随收集释放),full_reset 为收尾兜底(表已空时 no-op)。 pub(crate) session_cell_ptrs: RefCell>, pub(crate) session_bytes_allocated: usize, /// 执行期 session 堆账目的峰值高水位(`session_bytes_allocated` 的采样上界), @@ -52,24 +50,21 @@ pub(crate) struct GcState { pub(crate) string_gc_watermark: usize, /// 缓存的 GC 阈值(从 config 读一次),热路径只做 usize 比较,免 Arc 解引用。 pub(crate) gc_threshold_cached: usize, - /// 执行期两档收集(epoch 晋升 + session 原地 sweep)的触发水位:本次 - /// 收集后的分配包络 + 阈值增量。仅当 `run_alloc_bytes` 超过水位才在指令 - /// 边界触发——存活包络超阈值时不每指令重复触发无死对象可回收的白跑。 + /// 执行期原地 sweep 收集的触发水位:本次收集后的分配包络 + 阈值增量。 + /// 仅当 `run_alloc_bytes` 超过水位才在指令边界触发——存活包络超阈值时 + /// 不每指令重复触发无死对象可回收的白跑。 pub(crate) gc_watermark: usize, - /// 门控未过(挂起态持有 for-in)后的重扫锚:包络再增长一个阈值才重扫 - /// 三型状态盒(O(对象数) 扫描),避免持挂起 for-in 的 run 每指令边界重扫。 - pub(crate) gc_gate_retry_alloc: usize, /// 宿主强制收集旗标(`$262.gc()`):native 重入中置位,由下一个顶层 - /// 指令边界(移动式 sweep 的唯一安全点)消费并执行完整收集。 + /// 指令边界(完整收集的唯一安全点:无在途 builtin 局部裸指针)消费。 pub(crate) pending_forced_collect: bool, - pub(crate) forwarding: HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>, } impl GcState { /// 分配一个 upvalue cell:独立堆分配并返回裸指针,脱离 session arena 生命周期。 /// - /// cell 指针登记进 `session_cell_ptrs`,在 `full_reset` 统一释放。对象 sweep - /// 搬移不触碰 cell 结构体(地址稳定),只重写 `cell.value` 中的对象引用。 + /// cell 指针登记进 `session_cell_ptrs`,在 `full_reset` 统一释放。对象 + /// sweep 原地化不搬移对象、不触碰 cell 结构体(地址稳定),cell 的 + /// 生死由 mark-sweep 自行裁定(死 cell 随收集释放)。 /// `&self` 使调用方可与 `cell_stack` 等其它字段的借用并存(分字段借用)。 pub(crate) fn alloc_cell(&self, value: JsValue, initialized: bool) -> *mut UpvalueCell { let ptr = Box::into_raw(Box::new(UpvalueCell::new(value, initialized))); diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index d28a118b..8e6ea50e 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -134,9 +134,7 @@ impl Vm { string_gc_watermark: gc_threshold, gc_threshold_cached: gc_threshold, gc_watermark: gc_threshold, - gc_gate_retry_alloc: 0, pending_forced_collect: false, - forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher), }, symbols: SymbolState { symbol_counter: 0, @@ -271,9 +269,7 @@ impl Vm { string_gc_watermark: gc_threshold, gc_threshold_cached: gc_threshold, gc_watermark: gc_threshold, - gc_gate_retry_alloc: 0, pending_forced_collect: false, - forwarding: std::collections::HashMap::with_hasher(rustc_hash::FxBuildHasher), }, symbols: SymbolState { symbol_counter: 0, @@ -408,65 +404,42 @@ 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; - // 两档收集水位与门控重扫锚同点复位(同式:阈值增量起算)。 + // 执行期收集水位同点复位(同式:阈值增量起算):旧 run 的存活包络 + // 不延续到新 run 的触发判定。 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; self.active_reg_limit = 0; } - /// 释放全部 session 堆数据:session 对象堆数据 + upvalue 列表 + 对象本体 + /// 释放全部 session 堆数据:session 对象(本体 + 堆数据 + upvalue 列表) /// + session 串 + BigInt box + upvalue cell box。 /// - /// 供 `full_reset` 与 `Drop` 共用——堆载体对象本体(HEAP_BIT)经 - /// `Box::from_raw` 释放,arena 载体(克隆)随调用方换新的 session Bump 归还, - /// 本函数不释放。原生盒在 GC 搬移/晋升时已深拷贝为单所有权,此处恰好释放一次。 + /// 供 `full_reset` 与 `Drop` 共用——统一入口路径上对象本体全部为堆载体 + /// (`Box::from_raw` 恰好释放一次;晋升族 Bump 克隆仅测试形态,本体随 + /// 换新 Bump 归还),堆数据与 upvalue 列表各恰好释放一次。独占所有权 + /// 免去重:死对象已出表不重复枚举,表内无共享 upvalue Box 的原件-克隆对 + /// (死对象从不被克隆,生产路径不产克隆)。 pub(crate) fn teardown_session_heap_data(&mut self) { // 迭代器体是堆上 Box:收尾路径(full_reset 与 Drop 共用)逐条释放, // 防 Vm 直接 drop 时表内残留体泄漏。 self.iters.reset(); - // upvalue 列表先于对象表清空释放:去重枚举依赖对象表尚存。 - self.free_session_upvalues(); + // 对象逐条独占释放:本体 + 堆数据 + upvalue 列表,顺序由 + // drop_dead_session_object 收口。 for ptr in self.gc_state.session_object_ptrs.drain(..) { - crate::session_gc::SessionGc::drop_object_heap_data(ptr, true); - // 释放对象本体:堆载体(HEAP_BIT)经 Box::from_raw 释放,arena - // 载体(克隆)随换新 Bump 归还,不在此释放。 - if !ptr.is_null() && unsafe { (*ptr).is_heap_alloc() } { - unsafe { - drop(Box::from_raw(ptr)); - } + if ptr.is_null() { + continue; } + // SAFETY: ptr 来自 session 对象表登记,收尾时仍指向合法对象; + // 表独占持有,无其他释放点。 + crate::session_gc::SessionGc::drop_dead_session_object(ptr); } self.free_session_string_heap_data(); self.free_session_bigint_heap_data(); self.gc_state.free_cells(); } - /// 集中释放 upvalue 列表(`Box>`):原件与晋升克隆经 - /// `clone_for_session_epoch` 共享同一 Box 分配,逐对象路径释放会双放, - /// 只在此处按指针去重后统一释放。须在对象表清空之前调用 - /// (枚举仍存对象完成去重),此时对象已死、Box 无其他读者。 - fn free_session_upvalues(&mut self) { - let mut seen = std::collections::HashSet::new(); - for &ptr in self.gc_state.session_object_ptrs.iter() { - if ptr.is_null() { - continue; - } - // SAFETY: ptr 来自 session 对象表登记,收尾时仍指向合法对象。 - let up = unsafe { (*ptr).upvalues }; - if up.is_null() || !seen.insert(up) { - continue; - } - // SAFETY: up 由 set_upvalues 的 Box::into_raw 分配,去重保证恰好释放一次。 - unsafe { - drop(Box::from_raw(up as *mut Vec<*mut oxide_types::object::Cell>)); - (*ptr).upvalues = std::ptr::null_mut(); - } - } - } - /// 释放 VM 内建原型 P 对象(生成器/Promise/异步族与 Object 原型)的堆外属性区。 /// /// # 注意事项 @@ -576,8 +549,8 @@ impl Vm { pub fn reset(&mut self) { self.clear_execution_state(); self.maybe_collect_session_gc(); - // session 对象可持有 epoch 子引用(函数对象捕获、原生盒直插): - // 把 epoch 子引用原地克隆晋升进 session,避免悬垂指针。 + // 晋升族空转调用保留:统一入口 Box 化后生产路径不产 epoch 对象, + // 本调用在统一表上恒无操作(随晋升族整体退役)。 self.promote_session_epoch_refs(); self.bytecode = Arc::default(); // 表代际注册表不动:存活函数对象(含挂起帧 callee)按创建期代际仍须 @@ -586,9 +559,8 @@ impl Vm { self.active_immutables = std::ptr::slice_from_raw_parts(std::ptr::null(), 0); // 单 run 分配包络按 run 边界重起算(与 run_alloc_bytes 起算口径同源)。 self.gc_state.run_alloc_peak = 0; - // 两档收集水位与包络同起算:旧 run 的存活包络不延续到新 run 的触发判定。 + // 执行期收集水位与包络同起算:旧 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; } diff --git a/crates/oxide_vm/src/vm_support/tests.rs b/crates/oxide_vm/src/vm_support/tests.rs index d89f69d0..b89764fb 100644 --- a/crates/oxide_vm/src/vm_support/tests.rs +++ b/crates/oxide_vm/src/vm_support/tests.rs @@ -225,8 +225,8 @@ fn reentry_pump_hits_alloc_cap() { assert!(err.contains("memory limit"), "unexpected error: {err}"); } -/// 直接恢复生成器一步:等价 `it.next()`——回归聚焦 GC 搬移后的状态盒 -/// 有效性(同 run 内恢复,不经 run 边界换表)。 +/// 直接恢复生成器一步:等价 `it.next()`——回归聚焦 GC 收集后的状态盒 +/// 有效性(原地 sweep 不搬移对象;同 run 内恢复,不经 run 边界换表)。 fn resume_one_step(vm: &mut Vm, gen: JsValue) -> JsValue { match vm.resume_generator(gen, crate::generator::GeneratorResumeMode::Next(JsValue::undefined())) { Ok(crate::generator::GeneratorStep::Suspended { value }) => value, @@ -248,12 +248,12 @@ fn generator_survives_object_sweep_and_resumes() { let mut vm = vm_with_low_threshold(); let _ = run_source(&mut vm, "function* g(){ yield 1; yield 2; } globalThis.it = g(); globalThis.it.next(); 0"); - // 直接触发完整收集(保留执行上下文):存活生成器克隆进新 arena, - // 状态盒深拷贝为新 Box(走收集入口而非 run 边界:聚焦 GC 搬移本身)。 + // 直接触发完整收集(保留执行上下文):存活生成器原地保留、 + // 状态盒不动(走收集入口而非 run 边界:聚焦原地收集本身)。 vm.maybe_collect_session_gc(); assert!(vm.session_gc_stats().total_collections > 0, "应触发对象收集"); - // 从 global 取 sweep 重写后的生成器(同 run,模块表未换发,可恢复)。 + // 从 global 取 sweep 后存活的生成器(同 run,模块表未换发,可恢复)。 let it = global_prop(&vm, "it"); assert_eq!(resume_one_step(&mut vm, it), JsValue::int(2), "sweep 后应恢复第二次 yield"); } diff --git a/crates/oxide_vm/tests/generator_gc_tests.rs b/crates/oxide_vm/tests/generator_gc_tests.rs index 6af204bd..60aa0605 100644 --- a/crates/oxide_vm/tests/generator_gc_tests.rs +++ b/crates/oxide_vm/tests/generator_gc_tests.rs @@ -1,5 +1,5 @@ -//! 生成器 × session GC 回归:promote 后 full_reset 释放 epoch 原对象与 session 克隆 -//! 的堆数据(各单所有权),不 double-free、不悬垂,VM 继续可用。 +//! 生成器 × session GC 回归:full_reset 逐对象释放 session 生成器(本体 + 堆数据 +//! + upvalue 列表,各单所有权),不 double-free、不悬垂,VM 继续可用。 //! sweep 变体与闭包捕获变体见 `vm_support.rs` 内部测试(需 crate 内 `resume_generator` //! 在模块表未重建时恢复执行,integration 层无法触达)。 @@ -15,8 +15,8 @@ fn compile(source: &str) -> oxide_bytecode::module::CompiledModule { Compiler::new().compile(&program).expect("compile") } -/// promote + full_reset 变体:global 根持有生成器克隆,full_reset 释放 epoch 原对象 -/// 与 session 克隆的堆数据(各单所有权),不 double-free、不悬垂,VM 继续可用。 +/// full_reset 变体:global 根持有生成器,full_reset 释放其本体、堆数据与 +/// upvalue 列表(各单所有权),不 double-free、不悬垂,VM 继续可用。 #[test] fn generator_promoted_to_global_survives_full_reset_cleanly() { let mut vm = Vm::new(); @@ -24,10 +24,9 @@ fn generator_promoted_to_global_survives_full_reset_cleanly() { "function* g(){ yield 1; yield 2; } globalThis.it = g(); globalThis.it.next(); 0", ))) .expect("run1"); - assert!(vm.session_object_count() > 0, "生成器挂 global 应被 promote 进 session"); + assert!(vm.session_object_count() > 0, "生成器挂 global 应为 session 对象"); - // global 含 session 对象即强制重建:旧 global 丢弃、it 槽不存在,全程无悬垂访问 - // (修复前 epoch 原对象释放共享状态盒 → 读已释放内存)。 + // global 含 session 对象即强制重建:旧 global 丢弃、it 槽不存在,全程无悬垂访问。 vm.full_reset(); assert_eq!(vm.session_object_count(), 0, "full_reset 应清空 session 对象");