From 505cc4ff471fc99d75d69fd343ead6bda94f492a Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 17 Sep 2026 13:54:31 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=A1=A5=E9=BD=90=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E5=9E=83=E5=9C=BE=E5=9B=9E=E6=94=B6=E6=B3=A8=E9=87=8A=E5=B9=B6?= =?UTF-8?q?=E5=8E=BB=E9=99=A4=E5=8E=86=E5=8F=B2=E6=8E=AA=E8=BE=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_vm/src/session_gc.rs | 30 +++++++++++++++++++++++++ crates/oxide_vm/src/session_gc/tests.rs | 23 ++++++++++--------- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 4d1f7184..f439be25 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -42,6 +42,9 @@ impl SessionGc { } impl SessionGc { + /// 清空 session 与 epoch 全部对象的 GC mark 位。epoch 臂须与 session 同清: + /// 置位不随 session 清位消失,残留位会让下一次 `mark` 的 DFS 在已标对象处 + /// 短路,漏扫其新增引用边。 pub(crate) fn clear_all_marks(&mut self, vm: &mut Vm) { vm_debug!("[GC] clear_all_marks: {} session objects", vm.gc_state.session_object_ptrs.len()); for &ptr in &vm.gc_state.session_object_ptrs { @@ -375,6 +378,12 @@ impl SessionGc { } } + /// 从 VM roots 标记存活的 session/epoch 对象、字符串与 BigInt,供 `sweep` 判定。 + /// + /// 对象 DFS 同时跟踪 session 与 epoch 两张表,两族 mark 位同字节,晋升收集按 + /// “标记 ∩ epoch 表”取活集;字符串边走 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(); @@ -444,6 +453,10 @@ 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 { if obj_ptr.is_null() { return 0; @@ -546,6 +559,12 @@ impl SessionGc { bytes } + /// 移动式清扫 session 对象:存活对象复制进新 arena,死对象原地释放。 + /// + /// 按 GC mark 位分流:存活对象经 `clone_for_session_epoch` 复制(native 盒随族深拷) + /// 并登记 forwarding 旧址→新址,死对象释放本体、堆区与独占 upvalue;随后按转发表重写 + /// 新对象的值边、native 边与 promise 结算链裸指针,转发查找使共享与环去重到同一克隆。 + /// 最后重写全部根引用、换表并以新 Bump 接管 session epoch,旧 arena 整体归还并清 mark 位。 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); @@ -749,6 +768,7 @@ impl SessionGc { freed } + /// 完整收集的触发判断:对象、字符串或 BigInt 表非空,且字节账目已达阈值。 pub(crate) fn should_collect(&self, vm: &Vm) -> bool { (!vm.gc_state.session_object_ptrs.is_empty() || !vm.gc_state.session_string_ptrs.is_empty() @@ -789,6 +809,10 @@ impl SessionGc { } } + /// 完整收集编排:`mark` → `sweep`(对象移动式搬移)→ `sweep_session_strings` → + /// `sweep_session_bigints`,累加三类释放字节;字符串与 BigInt 地址稳定、无需 + /// forwarding。更新各表、字节账目与 mark 位,累计收集次数与最近/最大/最小耗时, + /// 释放字节大于 0 或每满 100 次时输出统计摘要。 pub(crate) fn collect(&mut self, vm: &mut Vm) { let start = Instant::now(); @@ -891,12 +915,14 @@ impl SessionGc { ); } + /// 执行期字符串回收触发入口:`should_collect_strings` 命中时执行 strings-only 收集。 pub(crate) fn maybe_collect_strings_only(&mut self, vm: &mut Vm) { if self.should_collect_strings(vm) { self.collect_strings_only(vm); } } + /// 完整收集触发入口:`should_collect` 命中时执行完整收集。 pub(crate) fn maybe_collect(&mut self, vm: &mut Vm) { if self.should_collect(vm) { self.collect(vm); @@ -1115,6 +1141,7 @@ impl SessionGc { freed } + /// 单行 GC 统计摘要:收集次数、扫描/存活/死对象数、释放字节与最近耗时。 pub(crate) fn stats_summary(&self) -> String { format!( "[GC] collection #{}: {} scanned, {} live, {} dead, {} freed, {}μs", @@ -1162,6 +1189,9 @@ fn rewrite_forwarded_value( .unwrap_or(value) } +/// 按 forwarding 表把全部 VM 根引用重写到搬移后的新地址。与 `for_each_value` +/// 共用同一字段清单(经 `rewrite_values` 遍历)且须同步:遗漏字段会在搬移后 +/// 保留指向旧 arena 的悬垂指针。 pub(crate) fn rewrite_vm_roots(vm: &mut Vm, forwarding: &HashMap<*mut JsObject, *mut JsObject, FxBuildHasher>) { vm_debug!("[GC] rewrite_vm_roots: {} forwarded objects", forwarding.len()); // 统一遍历:与 for_each_value 共用同一字段清单。 diff --git a/crates/oxide_vm/src/session_gc/tests.rs b/crates/oxide_vm/src/session_gc/tests.rs index fa504785..a081b604 100644 --- a/crates/oxide_vm/src/session_gc/tests.rs +++ b/crates/oxide_vm/src/session_gc/tests.rs @@ -122,8 +122,8 @@ fn gc_roots_contains_registers_frames_and_root_roots() { #[test] fn rewrite_matches_roots_coverage() { // 指针重写必须覆盖根收集访问的同一组执行核心字段:session→marker 映射后, - // 每个持有 session 的字段都应被改写为 marker(与 Step 1 的 for_each 覆盖测试 - // 互补,共同构成孪生清单验证)。 + // 每个持有 session 的字段都应被改写为 marker(与上文基于 for_each 的覆盖 + // 测试同口径,共同构成孪生清单验证)。 let mut vm = Vm::new(); let obj = plain_object(&mut vm); let session = vm.promote_object(obj); @@ -426,7 +426,8 @@ fn map_native_storage_is_not_a_normal_object_edge() { assert!(map_obj.hash_props_vec().is_none()); assert!(!map_obj.native_data().is_null()); - // Verify native storage pointer is not traced as a normal object edge. + // Map 的 native 存储指针不得被当作普通对象边扫描:扫到会把 native 堆数据 + // 误拉进 mark 集合。 let mut stack = Vec::new(); let mut live = HashSet::with_hasher(FxBuildHasher); let mut live_bigints = HashSet::with_hasher(FxBuildHasher); @@ -912,9 +913,9 @@ fn global_prop_opt(vm: &Vm, name: &str) -> Option { vm.resolve_property(global, si) } -/// 闭包捕获变量跨对象 sweep 存活:reset 触发完整收集(session_epoch 替换), -/// 修复前 cell 分配于旧 arena、随 Bump drop 悬垂;修复后独立堆分配、 -/// 地址稳定,sweep 只重写 cell.value 中的对象引用,值跨搬移保留。 +/// 闭包捕获变量跨对象 sweep 存活:reset 触发完整收集(session_epoch 替换)。 +/// cell 独立堆分配、不入 session arena,arena 整体回收时不连带释放——否则跨 +/// arena 存活的 cell 指针悬垂;sweep 只重写 cell.value 中的对象引用,值跨搬移保留。 #[test] fn closure_cell_survives_object_sweep() { let mut vm = vm_with_threshold(1); @@ -940,8 +941,8 @@ fn closure_cell_survives_object_sweep() { } /// 私有字段类的 brand cell 跨对象 sweep 存活:`@@class_brand` upvalue cell -/// 存类 brand 对象,sweep 后其值经 forwarding 重写为搬移后的新对象地址, -/// cell 指针仍稳定(修复前 cell 悬垂、值被复用覆盖)。 +/// 存类 brand 对象,sweep 后其值经 forwarding 重写为搬移后的新对象地址。 +/// cell 独立堆分配,指针在周边 arena 回收后仍稳定,值不被内存复用覆盖。 #[test] fn private_brand_cell_survives_object_sweep() { let mut vm = vm_with_threshold(1); @@ -1108,7 +1109,8 @@ fn strings_only_then_full_collect_keeps_object_strings_live() { let obj_session = vm.promote_object(obj); vm.regs[0] = JsValue::from_js_object(obj_session); - // 第一轮 strings-only:对象被 mark 置位,修复前该位残留至完整收集。 + // 第一轮 strings-only:对象被 mark 置位。收尾清残留,否则该位残留至完整 + // 收集会使 mark DFS 在对象处短路、漏标其属性中的串;随后校验串跨完整收集存活。 vm.maybe_collect_session_strings(); assert!(vm.gc_state.session_string_ptrs.contains(&s_ptr)); @@ -1464,7 +1466,8 @@ fn promise_settle_via_original_propagates_to_clone_reaction() { /// 指令边界触发执行期收集(阈值 1),原件随 epoch 释放;随后结算入口是 /// 克隆一(闭包目标槽在晋升时已改写到它),反应在最新克隆上——结算须沿 /// 链触达全部迁移反应,消费者(同 run 末微任务触发,字节码子模块表仍有效) -/// 方能落值。修复前克隆反应为空、原件已释放,反应静默永久丢失。 +/// 方能落值。反应迁入克隆并接结算链后,原件与旧克隆的结算沿链传到最新克隆, +/// 反应不因原件释放而静默丢失。 #[test] fn promise_promote_migrates_pre_promote_reactions_to_clone() { let mut vm = vm_with_threshold(1);