From 1711ae948bd7739de07b4e8020767ea557c0d051 Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Thu, 1 Oct 2026 06:39:51 +0000 Subject: [PATCH] =?UTF-8?q?perf:=20=E5=AF=B9=E8=B1=A1=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=85=A5=E5=8F=A3=20Box=20=E5=8C=96=E5=90=88=E5=B9=B6=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E8=A1=A8=E9=80=80=E5=BD=B9=E6=99=8B=E5=8D=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/oxide_types/src/object.rs | 20 ++ crates/oxide_vm/src/session_gc.rs | 236 ++++-------------- crates/oxide_vm/src/session_gc/tests.rs | 125 ++++------ crates/oxide_vm/src/vm/call.rs | 57 ++--- crates/oxide_vm/src/vm/gc_hooks.rs | 18 +- crates/oxide_vm/src/vm_state.rs | 7 - crates/oxide_vm/src/vm_support.rs | 92 ++----- crates/oxide_vm/src/vm_support/tests.rs | 51 +--- .../builtins_async_disposable_stack_tests.rs | 13 +- .../tests/builtins_disposable_stack_tests.rs | 13 +- .../tests/match_all_native_leak_tests.rs | 28 ++- .../tests/object_graph_retention_tests.rs | 26 +- crates/oxide_vm/tests/session_arena_tests.rs | 99 ++++---- 13 files changed, 262 insertions(+), 523 deletions(-) diff --git a/crates/oxide_types/src/object.rs b/crates/oxide_types/src/object.rs index 114be645..b29590b7 100644 --- a/crates/oxide_types/src/object.rs +++ b/crates/oxide_types/src/object.rs @@ -226,6 +226,10 @@ impl JsObject { pub const GC_MARK_BIT: u8 = 0x02; /// `is_session_epoch` 字段中的 epoch 归属标记位。 pub const EPOCH_BIT: u8 = 0x04; + /// `is_session_epoch` 字段中的堆载体标记位:对象本体经 `Box::into_raw` + /// 分配(统一分配入口),释放点据此 `Box::from_raw` 释放本体;arena + /// 载体(移动式 sweep 克隆、晋升族克隆)无此位,本体随 arena 归还。 + pub const HEAP_BIT: u8 = 0x08; /// 是否 Date 外来对象。 #[inline] @@ -501,6 +505,22 @@ impl JsObject { } } + /// 是否堆载体(本体经 `Box::into_raw` 分配,释放点须 `Box::from_raw`)。 + #[inline] + pub fn is_heap_alloc(&self) -> bool { + self.is_session_epoch & Self::HEAP_BIT != 0 + } + + /// 设置 / 清除堆载体标记。 + #[inline] + pub fn set_heap_alloc(&mut self, value: bool) { + if value { + self.is_session_epoch |= Self::HEAP_BIT; + } else { + self.is_session_epoch &= !Self::HEAP_BIT; + } + } + /// 深拷贝本对象到 session epoch:复制属性向量与元数据,标记为新 session 对象。 /// /// 用于把持久对象快照进当前调用上下文,修改不反向传播到源对象。 diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index c51c87cd..3808d2b2 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -44,13 +44,11 @@ impl SessionGc { } impl SessionGc { - /// 清空 session 与 epoch 全部对象的 GC mark 位。epoch 臂须与 session 同清: - /// 置位不随 session 清位消失,残留位会让下一次 `mark` 的 DFS 在已标对象处 - /// 短路,漏扫其新增引用边。 + /// 清空 session 对象表的 GC mark 位。置位不随表清位消失,残留位会让下一次 + /// `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()); self.clear_session_marks(vm); - self.clear_epoch_marks(vm); } /// 只清 session 对象表的 mark 位。移动式 sweep 末尾的旧表对象已全释放/克隆 @@ -66,19 +64,6 @@ impl SessionGc { } } - /// 只清 epoch 对象表的 mark 位。mark 的 epoch 臂置位不随 session 清位消除, - /// 残留位会让下一次 mark 的 DFS 短路、漏扫该对象此间新增的边。 - pub(crate) fn clear_epoch_marks(&mut self, vm: &mut Vm) { - 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 列表 /// capacity + native 状态盒)。与收尾释放口径一致(capacity);upvalue 列表因原件与 /// 晋升克隆别名,释放走收尾集中去重而非逐对象路径。不释放、不置空任何指针。 @@ -147,43 +132,43 @@ impl SessionGc { /// 单趟替代原 `object_edges` + `record_object_string_edges` 双遍模式:消除每对象 /// Vec 分配与重复字段遍历。 fn scan_edges_for_mark( - obj: &JsObject, vm: &Vm, stack: &mut Vec<*mut JsObject>, + obj: &JsObject, stack: &mut Vec<*mut JsObject>, live_strings: &mut HashSet<*mut JsString, FxBuildHasher>, live_bigints: &mut HashSet<*mut num_bigint::BigInt, FxBuildHasher>, live_cells: &mut HashSet<*mut Cell, FxBuildHasher>, ) { if let Some(elements) = obj.array_elements_vec() { for &value in elements.iter() { - Self::process_edge(value, vm, stack, live_strings, live_bigints); + Self::process_edge(value, stack, live_strings, live_bigints); } } if let Some(meta) = obj.array_elements_meta_vec() { for entry in meta.iter().flatten() { - Self::process_edge(entry.get, vm, stack, live_strings, live_bigints); - Self::process_edge(entry.set, vm, stack, live_strings, live_bigints); + Self::process_edge(entry.get, stack, live_strings, live_bigints); + Self::process_edge(entry.set, stack, live_strings, live_bigints); } } if let Some(props) = obj.hash_props_vec() { for &value in props.iter() { - Self::process_edge(value, vm, stack, live_strings, live_bigints); + Self::process_edge(value, stack, live_strings, live_bigints); } } if let Some(meta) = obj.prop_meta_vec() { for entry in meta.iter().flatten() { - Self::process_edge(entry.get, vm, stack, live_strings, live_bigints); - Self::process_edge(entry.set, vm, stack, live_strings, live_bigints); + Self::process_edge(entry.get, stack, live_strings, live_bigints); + Self::process_edge(entry.set, stack, live_strings, live_bigints); } } - Self::process_edge(obj.proto(), vm, stack, live_strings, live_bigints); - Self::process_edge(obj.captured_this(), vm, stack, live_strings, live_bigints); - Self::process_edge(obj.home_object(), vm, stack, live_strings, live_bigints); - Self::process_edge(obj.boxed_value(), vm, stack, live_strings, live_bigints); + Self::process_edge(obj.proto(), stack, live_strings, live_bigints); + Self::process_edge(obj.captured_this(), stack, live_strings, live_bigints); + Self::process_edge(obj.home_object(), stack, live_strings, live_bigints); + Self::process_edge(obj.boxed_value(), stack, live_strings, live_bigints); // native 载荷家族边:单点分类 + 家族表函数引用驱动, // 替代逐族 is_* 谓词链(各家族边函数引用见 native_box_dispatch)。 let ops = native_box_dispatch::ops_for(native_box_dispatch::classify(obj)); if let Some(object_edges) = ops.object_edges { for value in object_edges(obj) { - Self::process_edge(value, vm, stack, live_strings, live_bigints); + Self::process_edge(value, stack, live_strings, live_bigints); } } if let Some(string_edges) = ops.string_edges { @@ -204,33 +189,23 @@ impl SessionGc { } live_cells.insert(*cell_ptr); let cell = unsafe { &**cell_ptr }; - Self::process_edge(cell.value, vm, stack, live_strings, live_bigints); + Self::process_edge(cell.value, stack, live_strings, live_bigints); } } #[inline] fn process_edge( - value: JsValue, vm: &Vm, stack: &mut Vec<*mut JsObject>, + value: JsValue, stack: &mut Vec<*mut JsObject>, live_strings: &mut HashSet<*mut JsString, FxBuildHasher>, live_bigints: &mut HashSet<*mut num_bigint::BigInt, FxBuildHasher>, ) { if value.is_object() { let ptr = value.as_js_object_ptr(); - // session 边与 epoch 边同栈:epoch 对象的 GC_MARK 位与 session 同字节 - // 可置可读,晋升收集按"标记 ∩ epoch 表"取活集。 - if vm.is_session_ptr(ptr) { + // perm 对象(builtin world / global 等)不回收,但可持有 session + // 子引用(defineProperty 写入 builtin 原型等):入栈由 DFS 扫边 + // 标活,否则该子引用永不被标活 → 原地 sweep 误释放 → 悬垂。 + if !ptr.is_null() { stack.push(ptr); - } else if !ptr.is_null() { - // SAFETY: 执行核心产出的对象值,指针在 session 生命周期内有效。 - let obj = unsafe { &*ptr }; - if obj.is_epoch() { - stack.push(ptr); - } else { - // perm 对象(builtin world / global 等)不回收,但可持有 session - // 子引用(defineProperty 写入 builtin 原型等):入栈由 DFS 扫边 - // 标活,否则该子引用永不被标活 → 原地 sweep 误释放 → 悬垂。 - stack.push(ptr); - } } } else if value.is_string() { Self::mark_string_live(live_strings, value.as_string_ptr_mut()); @@ -452,19 +427,13 @@ impl SessionGc { stack.push(ptr); continue; } + // P 对象根(builtin world / global)不回收,只扫边;登记已访防 + // 后续作为 perm 边被重扫。 // SAFETY: 对象根由 VM 自有的字段与 builtin 对象产生,指针合法。 unsafe { let obj = &*ptr; - // epoch 根(顶层 var 寄存器等)自身入栈置位:晋升收集按标记位 - // 判活,根不置位会被误判为死。 - if obj.is_epoch() { - stack.push(ptr); - continue; - } - // P 对象根(builtin world / global)不回收,只扫边;登记已访防 - // 后续作为 perm 边被重扫。 if visited_perm.insert(ptr) { - Self::scan_edges_for_mark(obj, vm, stack, live_strings, live_bigints, live_cells); + Self::scan_edges_for_mark(obj, stack, live_strings, live_bigints, live_cells); } } } @@ -478,9 +447,9 @@ impl SessionGc { let obj = &mut *ptr; // perm 对象不回收:只扫边标活 session 子引用,不置 mark 位(其位 // 跨收集残留会令下次 DFS 短路漏扫)。已访则跳过,防 perm 环重入。 - if !vm.is_session_ptr(ptr) && !obj.is_epoch() { + if !vm.is_session_ptr(ptr) { if visited_perm.insert(ptr) { - Self::scan_edges_for_mark(obj, vm, stack, live_strings, live_bigints, live_cells); + Self::scan_edges_for_mark(obj, stack, live_strings, live_bigints, live_cells); } continue; } @@ -488,7 +457,7 @@ impl SessionGc { continue; } obj.set_gc_mark(true); - Self::scan_edges_for_mark(obj, vm, stack, live_strings, live_bigints, live_cells); + Self::scan_edges_for_mark(obj, stack, live_strings, live_bigints, live_cells); } } } @@ -573,7 +542,7 @@ impl SessionGc { /// 对象表,收尾统一释放按表枚举不会再见,置空保证对象侧幂等、无陈旧指针。 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; - // SAFETY: obj_ptr 来自 session 对象表,sweep 期间仍指向旧 arena 内合法对象; + // SAFETY: obj_ptr 来自 session 对象表,sweep 期间仍指向合法对象; // upvalues Box 仅本对象持有(死对象不克隆),保证恰好释放一次。 unsafe { let up = (*obj_ptr).upvalues; @@ -584,6 +553,11 @@ impl SessionGc { (*obj_ptr).upvalues = std::ptr::null_mut(); std::mem::drop(vec); } + // 释放对象本体:堆载体(HEAP_BIT)经 Box::from_raw 释放,arena 载体 + // (克隆)随旧 arena 归还,不在此释放。 + if (*obj_ptr).is_heap_alloc() { + drop(Box::from_raw(obj_ptr)); + } } freed } @@ -620,8 +594,9 @@ impl SessionGc { if old_ptr.is_null() { continue; } - // SAFETY: old_ptr 来自 session_arena 的晋升,sweep 运行期间仍指向旧 session arena。 + // 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 }; @@ -663,6 +638,14 @@ impl SessionGc { } 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)); + } + } } else { dead += 1; freed_bytes += Self::drop_dead_session_object(old_ptr); @@ -735,10 +718,6 @@ impl SessionGc { }) .sum::() as usize; - // 移动式 sweep 后旧表对象已全释放/克隆(克隆未标记),session 臂空转, - // 只清 epoch 臂残留位(epoch 对象存活、位残留)。 - self.clear_epoch_marks(vm); - let total_ptrs = survivors + dead; if total_ptrs > 0 { if dead == 0 { @@ -1027,17 +1006,12 @@ impl SessionGc { } } - /// 执行期两档收集:epoch 晋升档 + session 原地非移动 sweep 档。 + /// 执行期收集: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 不分裂。 + /// 包络内的死对象:死 session 对象原地释放(堆区 + upvalue 列表 + 本体) + /// 并出表;存活对象不移动——地址不变、免 forwarding/rewrite,用户可观察 + /// identity 不分裂。 /// /// # 边界与前提 /// - 调用方已完成门控(无活跃/挂起 for-in):门控是保守门,迭代器体 @@ -1046,7 +1020,6 @@ impl SessionGc { /// 局部裸指针、dispatch 未重入。 /// /// # 副作用 - /// - `epoch_object_ptrs` 清空、epoch Bump 换新(旧 arena 全量归还); /// - `session_object_ptrs` 仅剩存活,`session_bytes_allocated` 按存活重算 /// (串/BigInt 归 strings-only 路径,本路径不动,仅对象口径变化); /// - `gc_watermark` = 当前分配包络 + 阈值增量; @@ -1055,71 +1028,14 @@ impl SessionGc { let start = Instant::now(); vm_info!("[GC] in-run cycle #{} start", self.total_collections + 1); - // 清残留 mark 位(session + epoch):防 mark DFS 因历史 true 短路漏标。 + // 清残留 mark 位:防 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(已晋升 - // 对象的子引用解析到同一克隆,无二次克隆)。只改写存活对象:死对象的 - // 子引用随对象原地释放(释放路径无后续解引用,不悬垂)。 + // 死 session 对象原地释放(堆区 + upvalue 列表 + 本体)并出表;存活 + // 对象不动——地址不变,免 forwarding/rewrite。 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); - // 弱表键定夺:主改写只走强边,epoch 键按收敛后的转发表定生死。 - vm.rewrite_weak_map_keys_after_promotion(&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; @@ -1171,9 +1087,9 @@ impl SessionGc { 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 live = live_session; + let dead = dead_session; + let freed_bytes = session_freed; let elapsed = start.elapsed(); self.total_collections += 1; @@ -1181,10 +1097,10 @@ impl SessionGc { 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_scanned += 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_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; @@ -1200,50 +1116,6 @@ impl SessionGc { ); } - /// 释放全部 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 - } - /// 单行 GC 统计摘要:收集次数、扫描/存活/死对象数、释放字节与最近耗时。 pub(crate) fn stats_summary(&self) -> String { format!( diff --git a/crates/oxide_vm/src/session_gc/tests.rs b/crates/oxide_vm/src/session_gc/tests.rs index 8154479e..27a9415a 100644 --- a/crates/oxide_vm/src/session_gc/tests.rs +++ b/crates/oxide_vm/src/session_gc/tests.rs @@ -15,13 +15,10 @@ use oxide_runtime_api::NativeResult; fn plain_object(vm: &mut Vm) -> *mut JsObject { let proto_ptr = vm.session.builtin_world().object_proto.as_ptr() as *mut JsObject; - let ptr = vm.epoch.alloc(JsObject::new_empty( + vm.alloc_object(JsObject::new_empty( oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::from_js_object(proto_ptr), - )); - // 测试辅助函数绕过 alloc_object,需手动置位 EPOCH_BIT。 - unsafe { (*ptr).set_is_epoch(true) }; - ptr + )) } fn has_ptr(roots: &[JsValue], ptr: *mut JsObject) -> bool { @@ -240,14 +237,12 @@ fn sweep_preserves_cycle_and_collects_unreachable() { fn sweep_preserves_array_elements_and_collects_dead_element_object() { let mut vm = Vm::new(); let array_proto = vm.session.builtin_world().array_proto.as_ptr() as *mut JsObject; - let arr = vm.epoch.alloc(JsObject::new_array( + let arr = vm.alloc_object(JsObject::new_array( oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::from_js_object(array_proto), 2, vm.epoch.bump(), )); - // 测试辅助函数绕过 alloc_object,需手动置位 EPOCH_BIT。 - unsafe { (*arr).set_is_epoch(true) }; let live_elem = plain_object(&mut vm); let dead_elem = plain_object(&mut vm); unsafe { @@ -318,12 +313,10 @@ fn native_ok(result: NativeResult) -> JsValue { /// 分配一个原型指向 Map.prototype 的占位对象并写入寄存器,作为构造器调用的 `this`。 fn map_this(vm: &mut Vm, reg: u8) -> JsValue { let proto = vm.session.builtin_world().map_proto.as_ptr() as *mut JsObject; - let obj = vm.epoch.alloc(JsObject::new_empty( + let obj = vm.alloc_object(JsObject::new_empty( oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::from_js_object(proto), )); - // 测试辅助函数绕过 alloc_object,需手动置位 EPOCH_BIT。 - unsafe { (*obj).set_is_epoch(true) }; let val = JsValue::from_js_object(obj); vm.regs[reg as usize] = val; val @@ -332,12 +325,10 @@ fn map_this(vm: &mut Vm, reg: u8) -> JsValue { /// 分配一个原型指向 DataView.prototype 的占位对象并写入寄存器,作为构造器调用的 `this`。 fn data_view_this(vm: &mut Vm, reg: u8) -> JsValue { let proto = vm.session.builtin_world().data_view_proto.as_ptr() as *mut JsObject; - let obj = vm.epoch.alloc(JsObject::new_empty( + let obj = vm.alloc_object(JsObject::new_empty( oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::from_js_object(proto), )); - // 测试辅助函数绕过 alloc_object,需手动置位 EPOCH_BIT。 - unsafe { (*obj).set_is_epoch(true) }; let val = JsValue::from_js_object(obj); vm.regs[reg as usize] = val; val @@ -346,12 +337,10 @@ fn data_view_this(vm: &mut Vm, reg: u8) -> JsValue { /// 分配一个原型指向 Set.prototype 的占位对象并写入寄存器,作为构造器调用的 `this`。 fn set_this(vm: &mut Vm, reg: u8) -> JsValue { let proto = vm.session.builtin_world().set_proto.as_ptr() as *mut JsObject; - let obj = vm.epoch.alloc(JsObject::new_empty( + let obj = vm.alloc_object(JsObject::new_empty( oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::from_js_object(proto), )); - // 测试辅助函数绕过 alloc_object,需手动置位 EPOCH_BIT。 - unsafe { (*obj).set_is_epoch(true) }; let val = JsValue::from_js_object(obj); vm.regs[reg as usize] = val; val @@ -361,12 +350,10 @@ fn set_this(vm: &mut Vm, reg: u8) -> JsValue { /// 作为构造器调用的 `this`。 fn dispose_stack_this(vm: &mut Vm, reg: u8) -> JsValue { let proto = vm.session.builtin_world().disposable_stack_proto.as_ptr() as *mut JsObject; - let obj = vm.epoch.alloc(JsObject::new_empty( + let obj = vm.alloc_object(JsObject::new_empty( oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::from_js_object(proto), )); - // 测试辅助函数绕过 alloc_object,需手动置位 EPOCH_BIT。 - unsafe { (*obj).set_is_epoch(true) }; let val = JsValue::from_js_object(obj); vm.regs[reg as usize] = val; val @@ -376,13 +363,11 @@ fn dispose_stack_this(vm: &mut Vm, reg: u8) -> JsValue { /// onDispose 槽使用;测试不调用它。 fn function_placeholder(vm: &mut Vm) -> JsValue { let proto = vm.session.builtin_world().object_proto.as_ptr() as *mut JsObject; - let obj = vm.epoch.alloc(JsObject::new_empty( + let obj = vm.alloc_object(JsObject::new_empty( oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::from_js_object(proto), )); - // 测试辅助函数绕过 alloc_object,需手动置位 EPOCH_BIT。 unsafe { - (*obj).set_is_epoch(true); (*obj).set_function(true); } JsValue::from_js_object(obj) @@ -461,7 +446,7 @@ fn map_native_storage_is_not_a_normal_object_edge() { let mut live = HashSet::with_hasher(FxBuildHasher); let mut live_bigints = HashSet::with_hasher(FxBuildHasher); let mut live_cells = HashSet::with_hasher(FxBuildHasher); - SessionGc::scan_edges_for_mark(map_obj, &vm, &mut stack, &mut live, &mut live_bigints, &mut live_cells); + SessionGc::scan_edges_for_mark(map_obj, &mut stack, &mut live, &mut live_bigints, &mut live_cells); assert!(!stack.iter().any(|&ptr| std::ptr::eq(ptr, native_ptr))); } @@ -515,19 +500,16 @@ fn session_gc_traces_set_object_key() { assert_eq!(vm.gc_state.session_object_ptrs.len(), 2); } -/// 造带条目盒的 WeakMap 形 epoch 对象(构造体落地前的单测代用形态)。 +/// 造带条目盒的 WeakMap 形 session 对象(构造体落地前的单测代用形态)。 fn weak_map_object(vm: &mut Vm) -> *mut JsObject { let mut obj = JsObject::new_empty(oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::undefined()); obj.type_tag = JsObject::OBJ_TYPE_WEAK_MAP; obj.set_native_data(oxide_builtins::weak_map::weak_map_alloc_box()); - let ptr = vm.epoch.alloc(obj); - // 测试辅助函数绕过 alloc_object,需手动置位 EPOCH_BIT。 - unsafe { (*ptr).set_is_epoch(true) }; - ptr + vm.alloc_object(obj) } -/// 弱键不产 mark 边:强可达键跨晋升 + 完整收集保留,条目完整、值边改写后 -/// 按原键读回同一克隆体。 +/// 弱键不产 mark 边:强可达键跨完整收集保留,条目完整、键与值边改写 +/// 到搬移后新址,经改写后的根读回同一克隆体。 #[test] fn weak_map_entry_survives_promotion_with_live_key() { let mut vm = vm_with_low_threshold(); @@ -548,7 +530,7 @@ fn weak_map_entry_survives_promotion_with_live_key() { vm.regs.fill(JsValue::undefined()); vm.regs[0] = JsValue::from_js_object(wm_session); vm.regs[1] = JsValue::from_js_object(key); - // 值的强根取晋升克隆(表内读回):原件随 epoch 出局。 + // 值的强根取表内读回(统一入口后晋升为原样返回,读回即原件)。 let value_root = oxide_builtins::weak_map::weak_map_probe_get(unsafe { &*wm_session }, JsValue::from_js_object(key)); vm.regs[2] = value_root; @@ -558,9 +540,10 @@ 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, "强可达键的条目须在收集后存活"); - let stored = oxide_builtins::weak_map::weak_map_probe_get(live_wm, JsValue::from_js_object(key)); - assert_eq!(stored, vm.regs[2], "值须按原键读回同一克隆体"); - assert!(!std::ptr::eq(stored.as_js_object_ptr(), value), "值边须改写到晋升克隆"); + // 键经移动式 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), "值边须改写到搬移后新址"); } /// 弱键强不可达 = 死键:收集按转发表判定丢条目,表不留死键残影。 @@ -607,7 +590,7 @@ fn weak_map_value_edge_keeps_value_alive() { JsValue::from_js_object(value), ); } - // 值先晋升进 session:其独立根随后撤销,存活仅靠弱表值边。 + // 值的独立根随后撤销,存活仅靠弱表值边(统一入口后晋升为原样返回)。 vm.regs[2] = JsValue::from_js_object(value); let value_session = vm.promote_object(value); vm.regs[2] = JsValue::from_js_object(value_session); @@ -622,17 +605,21 @@ 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() }; - let stored = oxide_builtins::weak_map::weak_map_probe_get(live_wm, JsValue::from_js_object(key)); + // 键经移动式 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!(vm.is_session_ptr(stored.as_js_object_ptr())); - assert_eq!(vm.gc_state.session_object_ptrs.len(), 2); + // key、value、wm 三对象均存活(key/value 经寄存器根、wm 经寄存器根), + // 搬移后各占一份克隆。 + assert_eq!(vm.gc_state.session_object_ptrs.len(), 3); } -/// in-run 晋升定夺·死键面:无强根的 epoch 键经收敛后转发表判死丢条目; -/// P 键不可死,条目保留且按原键读回。 +/// in-run 原地 sweep·死键面:无强根的 session 键在原地 sweep 判死出表; +/// P 键不可死,条目保留且按原键读回。原地 sweep 不动弱表条目表,死键条目 +/// 的定夺交后续单 mark 位判据,此处只钉死键对象出表 + P 键条目保留。 #[test] -fn weak_map_dead_epoch_key_dropped_by_in_run_promotion() { +fn weak_map_dead_key_freed_by_in_run_sweep() { let mut vm = vm_with_threshold(65536); let value = plain_object(&mut vm); let wm = weak_map_object(&mut vm); @@ -648,17 +635,13 @@ fn weak_map_dead_epoch_key_dropped_by_in_run_promotion() { ); oxide_builtins::weak_map::weak_map_insert(&mut *wm, p_key, JsValue::int(7)); } - // 探针对象绕过 alloc_object,手动登记 epoch 表(in-run 档按表晋升与释放)。 - vm.gc_state.epoch_object_ptrs.push(wm); - vm.gc_state.epoch_object_ptrs.push(key); - vm.gc_state.epoch_object_ptrs.push(value); vm.regs[0] = JsValue::from_js_object(wm); vm.regs[2] = JsValue::from_js_object(value); vm.maybe_collect_in_run(); 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), @@ -666,10 +649,10 @@ fn weak_map_dead_epoch_key_dropped_by_in_run_promotion() { ); } -/// in-run 晋升定夺·活键面:强可达 epoch 键入收敛后转发表,条目键改指克隆、 -/// 值边改指后按新键读回同一克隆。 +/// in-run 原地 sweep·活键面:强可达 session 键原地保活(不搬移),条目键 +/// 原址不变、值边按原键读回同一对象。 #[test] -fn weak_map_live_epoch_key_repointed_by_in_run_promotion() { +fn weak_map_live_key_survives_in_run_sweep() { let mut vm = vm_with_threshold(65536); let key = plain_object(&mut vm); let value = plain_object(&mut vm); @@ -681,25 +664,21 @@ fn weak_map_live_epoch_key_repointed_by_in_run_promotion() { JsValue::from_js_object(value), ); } - // 探针对象绕过 alloc_object,手动登记 epoch 表(in-run 档按表晋升与释放)。 - vm.gc_state.epoch_object_ptrs.push(wm); - vm.gc_state.epoch_object_ptrs.push(key); - vm.gc_state.epoch_object_ptrs.push(value); vm.regs[0] = JsValue::from_js_object(wm); vm.regs[1] = JsValue::from_js_object(key); vm.regs[2] = JsValue::from_js_object(value); vm.maybe_collect_in_run(); let live_wm = unsafe { &*vm.regs[0].as_js_object_ptr() }; - assert!(live_wm.is_session_epoch(), "in-run 收集后弱表应晋升 session"); + assert!(live_wm.is_session_epoch(), "in-run 收集后弱表应为 session 对象"); assert_eq!( oxide_builtins::weak_map::weak_map_entry_count(live_wm), 1, "强可达键的条目须在 in-run 定夺后存活" ); let stored = oxide_builtins::weak_map::weak_map_probe_get(live_wm, vm.regs[1]); - assert_eq!(stored, vm.regs[2], "值须按改指后的键读回同一克隆"); - assert!(vm.is_session_ptr(stored.as_js_object_ptr()), "值边须改指 session 克隆"); + assert_eq!(stored, vm.regs[2], "值须按原键读回同一对象"); + assert!(vm.is_session_ptr(stored.as_js_object_ptr()), "值边须为 session 对象"); } /// 盒持唯一引用的 session 串与 BigInt 经 Map native 边进入存活集: @@ -1699,8 +1678,8 @@ fn full_reset_frees_rope_and_product() { 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 返回后全部不可达。 + // churn:IIFE 局部数组 + 50 个未入根的函数(统一入口 Box 化后全部入 + // session 表),IIFE 返回后全部不可达。 vm.run(&Arc::new(compile( "(function(){ var t = []; for (var i = 0; i < 50; i++) { t[i] = function() { return i; }; } })(); 0", ))) @@ -1709,21 +1688,19 @@ fn in_run_collection_reclaims_dead_and_keeps_live() { // 自此真正不可达。 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 直分配函数"); + 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 函数应被原地回收"); + 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 对象"); + 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))); } @@ -1736,30 +1713,25 @@ 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 = Box::into_raw(Box::new(crate::vm::ForInIter { keys: Vec::new(), index: 0, })); vm.iters.push_for_in(iter); - 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 表"); + 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!(vm.epoch.current_id() > bump_before, "收集应换新 epoch Bump"); - assert!(vm.gc_state.epoch_object_ptrs.is_empty(), "死 epoch 对象应随 Bump 回收"); + assert!(!vm.gc_state.session_object_ptrs.contains(&dead), "死 session 对象应被原地释放并出表"); // 挂起形(拦截):生成器在 for-in 内 yield 后 run 结束,迭代器经 - // 状态盒持有(vm.iters 已空)——收集点必须拦下,Bump 不得换新。 + // 状态盒持有(vm.iters 已空)——收集点必须拦下。 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'; } \ @@ -1771,14 +1743,13 @@ fn active_and_suspended_for_in_block_in_run_collection() { 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 + // 挂起生成器对象保持原址(未释放):状态盒与迭代器一体存活。 + let gen_in_session = vm .gc_state - .epoch_object_ptrs + .session_object_ptrs .iter() .any(|&p| !p.is_null() && unsafe { (*p).type_tag == JsObject::OBJ_TYPE_GENERATOR }); - assert!(gen_in_epoch, "挂起生成器应仍在 epoch 表"); + assert!(gen_in_session, "挂起生成器应仍在 session 表"); // 挂起形(放行):同一 run 内续跑至 for-in 结束(迭代器释放), // 阈值 1 使指令边界自动触发:挂起期门控关(Bump 不换新),完成后 diff --git a/crates/oxide_vm/src/vm/call.rs b/crates/oxide_vm/src/vm/call.rs index eab605bf..dd7f9f97 100644 --- a/crates/oxide_vm/src/vm/call.rs +++ b/crates/oxide_vm/src/vm/call.rs @@ -13,14 +13,6 @@ use super::{native_fn_ptr_to_fn, Vm}; use crate::native::NativeFn; use crate::{vm_debug, vm_trace}; -/// 对象分配统一入口的后端:epoch arena(短命对象,晋升进 session)与 -/// session arena(长命对象,session GC mark/sweep 回收)二选一。 -#[derive(Clone, Copy)] -enum AllocBackend { - Epoch, - Session, -} - impl Vm { fn pack_sync_native_call_args( &mut self, receiver: JsValue, callee: JsValue, args: &[JsValue], @@ -48,44 +40,33 @@ impl Vm { unsafe { (*obj_ptr).is_session_epoch() } } - /// 按指定后端分配对象并登记追踪表与堆账目,返回裸指针。 + /// 对象分配统一入口:置 session 归属位与堆载体位,本体经 `Box::into_raw` + /// 堆分配,登记 session 对象表并计入堆账目,返回裸指针。 /// /// # 步骤 - /// 1. Epoch 后端:epoch arena 分配并登记 `epoch_object_ptrs`。 - /// 2. Session 后端:先置 session 归属位再分配,登记 `session_object_ptrs`, - /// 计入堆账目(与晋升同式:对象头 + 对象堆数据,读分配后的 arena 对象)。 - /// - /// # 注意事项 - /// Session 后端先置位后分配,置位与登记顺序在两个后端内固定; - /// 换后端只改本函数内的分配原语,调用点不动。 - fn alloc_with_backend(&mut self, mut obj: JsObject, backend: AllocBackend) -> *mut JsObject { - match backend { - AllocBackend::Epoch => { - let ptr = self.epoch.alloc(obj); - self.gc_state.track_epoch_object(ptr); - ptr - } - AllocBackend::Session => { - obj.set_session_epoch(true); - let ptr = self.gc_state.session_epoch.alloc(obj) as *mut JsObject; - self.gc_state.session_object_ptrs.push(ptr); - // 直 session 分配计入堆账目(与 promote 同式:对象头 + 对象堆数据)。 - self.gc_state.session_bytes_allocated += std::mem::size_of::() - + crate::session_gc::SessionGc::object_heap_data_bytes(unsafe { &*ptr }) as usize; - ptr - } - } + /// 1. 置 session 归属位与堆载体位(释放点据此 `Box::from_raw` 释放本体)。 + /// 2. 本体堆分配,登记 `session_object_ptrs`。 + /// 3. 计入堆账目(对象头 + 对象堆数据,读分配后的对象)。 + fn alloc_object_box(&mut self, mut obj: JsObject) -> *mut JsObject { + obj.set_session_epoch(true); + obj.set_heap_alloc(true); + let ptr = Box::into_raw(Box::new(obj)); + self.gc_state.session_object_ptrs.push(ptr); + // 直 session 分配计入堆账目(与 promote 同式:对象头 + 对象堆数据)。 + self.gc_state.session_bytes_allocated += std::mem::size_of::() + + crate::session_gc::SessionGc::object_heap_data_bytes(unsafe { &*ptr }) as usize; + ptr } - /// 在当前 epoch arena 分配对象并登记 GC 追踪表,返回裸指针。 + /// 分配对象并登记 GC 追踪表,返回裸指针。 pub(crate) fn alloc_object(&mut self, obj: JsObject) -> *mut JsObject { - self.alloc_with_backend(obj, AllocBackend::Epoch) + self.alloc_object_box(obj) } - /// 在当前 session arena 分配对象:置 session 归属位、登记 session 对象表 - /// 并计入堆账目,返回裸指针。 + /// 分配对象:置 session 归属位与堆载体位、登记 session 对象表并计入 + /// 堆账目,返回裸指针。 pub(crate) fn alloc_session_object(&mut self, obj: JsObject) -> *mut JsObject { - self.alloc_with_backend(obj, AllocBackend::Session) + self.alloc_object_box(obj) } /// native / 字节码函数的统一同步调用入口,返回结果或错误文本。 diff --git a/crates/oxide_vm/src/vm/gc_hooks.rs b/crates/oxide_vm/src/vm/gc_hooks.rs index 786eca48..2308ebc2 100644 --- a/crates/oxide_vm/src/vm/gc_hooks.rs +++ b/crates/oxide_vm/src/vm/gc_hooks.rs @@ -246,7 +246,7 @@ impl Vm { /// 挂起帧把迭代器搬入状态盒(`vm.iters` 不可见),故须逐状态盒扫描。 /// /// # 边界与前提 - /// - 扫描 epoch + session 两份对象表——挂起状态盒宿主对象可能尚未晋升; + /// - 扫描 session 对象表——挂起状态盒宿主对象在表内; /// - 挂起帧的 keys 经状态盒边收为 GC 根(被枚举对象不误释放),迭代器 /// 体是堆上 Box,故按指针扫 `for_in_iters`; /// - 保守口径:死对象的状态盒同样计入(持挂起 for-in 的 run 放弃执行期 @@ -257,7 +257,7 @@ impl Vm { if ptr.is_null() { return false; } - // SAFETY: 对象表登记的指针,arena 存活期内有效。 + // SAFETY: 对象表登记的指针,存活期内有效。 let obj = unsafe { &*ptr }; match obj.type_tag { JsObject::OBJ_TYPE_GENERATOR => crate::generator::generator_holds_suspended_for_in(obj), @@ -268,7 +268,7 @@ impl Vm { _ => false, } }; - self.gc_state.epoch_object_ptrs.iter().any(holds) || self.gc_state.session_object_ptrs.iter().any(holds) + self.gc_state.session_object_ptrs.iter().any(holds) } /// 执行期两档收集的 dispatch 安全点入口:仅在循环顶 @@ -357,12 +357,7 @@ impl Vm { /// 扩容等账目盲区的兜底。 pub(crate) fn run_alloc_bytes_full(&self) -> u64 { let mut bytes = (self.epoch.bump().allocated_bytes() + self.gc_state.session_epoch.allocated_bytes()) as u64; - for &ptr in self - .gc_state - .epoch_object_ptrs - .iter() - .chain(self.gc_state.session_object_ptrs.iter()) - { + for &ptr in self.gc_state.session_object_ptrs.iter() { if ptr.is_null() { continue; } @@ -393,9 +388,10 @@ impl Vm { self.gc_state.session_gc = session_gc; } - /// 当前 epoch 中已分配并跟踪的对象数量(未晋升到 session 的临时对象)。 + /// 当前 epoch 中已分配并跟踪的对象数量。对象分配统一入口 Box 化后 + /// 全部对象入 session 表,epoch 表退役,恒返 0(采集点保留,恒零口径)。 pub fn epoch_object_count(&self) -> usize { - self.gc_state.epoch_object_ptrs.len() + 0 } /// inline cache 命中率(0.0~1.0),用于观测 IC 预热效果。 diff --git a/crates/oxide_vm/src/vm_state.rs b/crates/oxide_vm/src/vm_state.rs index 1244f93d..7d25c677 100644 --- a/crates/oxide_vm/src/vm_state.rs +++ b/crates/oxide_vm/src/vm_state.rs @@ -28,7 +28,6 @@ use oxide_types::value::JsValue; pub(crate) struct GcState { pub(crate) session_epoch: bumpalo::Bump, pub(crate) session_gc: SessionGc, - pub(crate) epoch_object_ptrs: Vec<*mut JsObject>, pub(crate) session_object_ptrs: Vec<*mut JsObject>, pub(crate) session_string_ptrs: Vec<*mut JsString>, /// BigInt 堆 box(`Box`)追踪表。`RefCell` 使 `&self` 的 @@ -67,12 +66,6 @@ pub(crate) struct GcState { } impl GcState { - pub(crate) fn track_epoch_object(&mut self, ptr: *mut JsObject) { - // SAFETY: 调用方保证 ptr 是刚 alloc 的 epoch JsObject,登记期存活。 - unsafe { (*ptr).set_is_epoch(true) }; - self.epoch_object_ptrs.push(ptr); - } - /// 分配一个 upvalue cell:独立堆分配并返回裸指针,脱离 session arena 生命周期。 /// /// cell 指针登记进 `session_cell_ptrs`,在 `full_reset` 统一释放。对象 sweep diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 1089269a..d28a118b 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -124,7 +124,6 @@ impl Vm { gc_state: GcState { session_epoch: bumpalo::Bump::new(), session_gc: crate::session_gc::SessionGc::new(), - epoch_object_ptrs: Vec::new(), session_object_ptrs: Vec::new(), session_string_ptrs: Vec::new(), session_bigint_ptrs: std::cell::RefCell::new(Vec::new()), @@ -262,7 +261,6 @@ impl Vm { gc_state: GcState { session_epoch: bumpalo::Bump::new(), session_gc: crate::session_gc::SessionGc::new(), - epoch_object_ptrs: Vec::new(), session_object_ptrs: Vec::new(), session_string_ptrs: Vec::new(), session_bigint_ptrs: std::cell::RefCell::new(Vec::new()), @@ -403,8 +401,6 @@ impl Vm { ); self.active_immutables = std::ptr::slice_from_raw_parts(std::ptr::null(), 0); self.teardown_session_heap_data(); - self.epoch.reset(); - self.gc_state.epoch_object_ptrs.clear(); // 换新 Bump:旧 session arena 全量归还系统分配器(与 sweep 路径同构), // 容量不跨 full_reset 保留。 self.gc_state.session_epoch = bumpalo::Bump::new(); @@ -421,21 +417,27 @@ impl Vm { self.active_reg_limit = 0; } - /// 释放全部 session 堆数据:epoch 对象堆数据 + session 对象堆数据 + upvalue - /// 列表 + session 串 + BigInt box + upvalue cell box。 + /// 释放全部 session 堆数据:session 对象堆数据 + upvalue 列表 + 对象本体 + /// + session 串 + BigInt box + upvalue cell box。 /// - /// 供 `full_reset` 与 `Drop` 共用——对象本体(bumpalo arena / epoch bump)由调用方 - /// 重置,本函数只释放手工管理的 Box 指针(属性向量、各原生盒、串、BigInt、cell)。 - /// 原生盒在 GC 搬移/晋升时已深拷贝为单所有权,此处恰好释放一次。 + /// 供 `full_reset` 与 `Drop` 共用——堆载体对象本体(HEAP_BIT)经 + /// `Box::from_raw` 释放,arena 载体(克隆)随调用方换新的 session Bump 归还, + /// 本函数不释放。原生盒在 GC 搬移/晋升时已深拷贝为单所有权,此处恰好释放一次。 pub(crate) fn teardown_session_heap_data(&mut self) { // 迭代器体是堆上 Box:收尾路径(full_reset 与 Drop 共用)逐条释放, // 防 Vm 直接 drop 时表内残留体泄漏。 self.iters.reset(); - // upvalue 列表先于对象表清空释放:去重枚举依赖两份对象表尚存。 + // upvalue 列表先于对象表清空释放:去重枚举依赖对象表尚存。 self.free_session_upvalues(); - self.free_epoch_object_heap_data(); 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)); + } + } } self.free_session_string_heap_data(); self.free_session_bigint_heap_data(); @@ -444,20 +446,15 @@ impl Vm { /// 集中释放 upvalue 列表(`Box>`):原件与晋升克隆经 /// `clone_for_session_epoch` 共享同一 Box 分配,逐对象路径释放会双放, - /// 只在此处按指针去重后统一释放。须在两份对象表清空之前调用 + /// 只在此处按指针去重后统一释放。须在对象表清空之前调用 /// (枚举仍存对象完成去重),此时对象已死、Box 无其他读者。 fn free_session_upvalues(&mut self) { let mut seen = std::collections::HashSet::new(); - let ptrs = self - .gc_state - .epoch_object_ptrs - .iter() - .chain(self.gc_state.session_object_ptrs.iter()); - for &ptr in ptrs { + for &ptr in self.gc_state.session_object_ptrs.iter() { if ptr.is_null() { continue; } - // SAFETY: ptr 来自 epoch/session 对象表登记,收尾时仍指向 arena 内合法对象。 + // SAFETY: ptr 来自 session 对象表登记,收尾时仍指向合法对象。 let up = unsafe { (*ptr).upvalues }; if up.is_null() || !seen.insert(up) { continue; @@ -500,18 +497,6 @@ impl Vm { } } - fn free_epoch_object_heap_data(&mut self) { - let mut freed = 0u64; - for ptr in self.gc_state.epoch_object_ptrs.drain(..) { - freed += crate::session_gc::SessionGc::drop_object_heap_data(ptr, false); - } - if freed > 0 { - self.gc_state.session_gc.total_bytes_freed = - self.gc_state.session_gc.total_bytes_freed.saturating_add(freed); - self.gc_state.session_gc.last_collection_bytes_freed = freed; - } - } - pub(crate) fn clear_execution_state(&mut self) { // 重置契约: // - 清空寄存器文件、pc、帧/迭代器栈、保存的执行栈、try 处理器、 @@ -587,22 +572,18 @@ impl Vm { gc.min_collection_duration_us = u64::MAX; } - /// 轻量重置:清空执行状态并回收 epoch 内存,但保留 session 字符串与 builtin。 + /// 轻量重置:清空执行状态并回收内存,但保留 session 字符串与 builtin。 pub fn reset(&mut self) { self.clear_execution_state(); self.maybe_collect_session_gc(); - // session 对象可持有 epoch 子引用(函数对象捕获、原生盒直插):epoch - // 重置前把 epoch 子引用原地克隆晋升进 session,避免悬垂指针。 + // session 对象可持有 epoch 子引用(函数对象捕获、原生盒直插): + // 把 epoch 子引用原地克隆晋升进 session,避免悬垂指针。 self.promote_session_epoch_refs(); self.bytecode = Arc::default(); // 表代际注册表不动:存活函数对象(含挂起帧 callee)按创建期代际仍须 // 命中原表,跨 run 调用与恢复靠它成立。active_immutables 指向的旧表 // 指针作废,下次 run 重装。 self.active_immutables = std::ptr::slice_from_raw_parts(std::ptr::null(), 0); - self.free_epoch_dead_upvalues(); - self.free_epoch_object_heap_data(); - self.epoch.reset(); - self.gc_state.epoch_object_ptrs.clear(); // 单 run 分配包络按 run 边界重起算(与 run_alloc_bytes 起算口径同源)。 self.gc_state.run_alloc_peak = 0; // 两档收集水位与包络同起算:旧 run 的存活包络不延续到新 run 的触发判定。 @@ -612,41 +593,6 @@ impl Vm { self.active_reg_limit = 0; } - /// 释放随本次 epoch 重置死亡的 epoch 对象的 upvalue 列表。 - /// - /// 原件与晋升克隆共享同一 Box 分配:共享项归 session 克隆持有, - /// 留待收尾(`free_session_upvalues`)统一释放,此处只放独占项。 - fn free_epoch_dead_upvalues(&mut self) { - let mut shared: std::collections::HashSet = std::collections::HashSet::new(); - for &ptr in self.gc_state.session_object_ptrs.iter() { - if ptr.is_null() { - continue; - } - // SAFETY: session 对象表此刻未清空,指向 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(); - for &ptr in self.gc_state.epoch_object_ptrs.iter() { - if ptr.is_null() { - continue; - } - // SAFETY: epoch 对象表此刻未清空,指向 epoch arena 内合法对象。 - let up = unsafe { (*ptr).upvalues } as usize; - if up == 0 || shared.contains(&up) || !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(); - } - } - } - /// 分配一个可被 session GC 回收的字符串 `JsValue`(session-heap 字符串)。 pub fn new_string(&mut self, s: &str) -> JsValue { self.new_string_owned(s.to_string()) diff --git a/crates/oxide_vm/src/vm_support/tests.rs b/crates/oxide_vm/src/vm_support/tests.rs index 348fbf0d..d89f69d0 100644 --- a/crates/oxide_vm/src/vm_support/tests.rs +++ b/crates/oxide_vm/src/vm_support/tests.rs @@ -63,16 +63,16 @@ fn full_reset_with_session_objects_forces_global_rebuild() { // 覆盖写推进 global generation,快照对比发现脏 global,full_reset 必须重建。 let _ = run_source(&mut vm, "globalThis.Array = {}; 0"); let written = global_prop(&vm, "Array"); - // SAFETY: 覆盖值是本 VM 自有的 epoch 对象,本 session 内指针有效。 + // SAFETY: 覆盖值是本 VM 自有的 session 对象,本 session 内指针有效。 assert!( - unsafe { (&*written.as_js_object_ptr()).is_epoch() }, - "覆盖值应直通留存 epoch,不克隆进 session" + unsafe { (&*written.as_js_object_ptr()).is_session_epoch() }, + "覆盖值应为 session 对象(统一入口 Box 化)" ); let old_global = vm.session.global_object.as_ptr(); vm.full_reset(); - // global 必须重建:旧 global 与其 epoch 子引用随 epoch 释放,Array 恢复内置构造器。 + // global 必须重建:旧 global 与其 session 子引用随 session 释放,Array 恢复内置构造器。 assert!(!std::ptr::eq(old_global, vm.session.global_object.as_ptr())); assert!(std::ptr::eq( global_prop(&vm, "Array").as_js_object_ptr(), @@ -276,34 +276,6 @@ fn generator_captured_upvalue_survives_sweep() { assert_eq!(resume_one_step(&mut vm, it), JsValue::int(2), "sweep 后应恢复捕获变量读写"); } -#[test] -fn generator_promoted_clone_owns_independent_state_box() { - let mut vm = Vm::new(); - // `(function(){ var it = g(); it.next(); return it; })()`:it 为函数局部(非顶层 - // var,不经全局属性逃逸),保持 epoch 生成器对象(未 promote)。 - let it = run_source( - &mut vm, - "function* g(){ yield 1; yield 2; } (function(){ var it = g(); it.next(); return it; })()", - ); - assert!(it.is_object()); - let epoch_ptr = it.as_js_object_ptr(); - let epoch_box = unsafe { (*epoch_ptr).native_data() }; - - // 手动 promote:克隆应深拷贝状态盒(新 Box),与源盒互不共享。 - let promoted = vm.promote_object(epoch_ptr); - assert!(!std::ptr::eq(promoted, epoch_ptr)); - let promoted_box = unsafe { (*promoted).native_data() }; - assert!(!std::ptr::eq(epoch_box, promoted_box), "promote 应深拷贝生成器状态盒"); - - // 模拟 full_reset 的 epoch 侧释放:源对象与其状态盒随 epoch 回收, - // 并从追踪表移除登记(克隆的后续回收仍由 VM 统一处理)。 - let _ = crate::session_gc::SessionGc::drop_object_heap_data(epoch_ptr, false); - vm.gc_state.epoch_object_ptrs.retain(|&p| !std::ptr::eq(p, epoch_ptr)); - - // 克隆直接恢复执行:读新盒中的挂起状态,不得悬垂。 - assert_eq!(resume_one_step(&mut vm, JsValue::from_js_object(promoted)), JsValue::int(2)); -} - #[test] fn full_reset_clean_keeps_session_objects() { let mut vm = Vm::new(); @@ -486,7 +458,8 @@ fn full_reset_zeroes_arena_retained() { &mut vm, "var t = 0; for (var i = 0; i < 20000; i++) { var o = { s: 'ab' + i, a: [i] }; t += o.s.length + o.a.length; } t", ); - assert!(vm.epoch.bump().allocated_bytes() > 0, "重源 run 应冲高 epoch arena 水位"); + // 统一入口 Box 化后 epoch arena 零分配,水位恒零。 + assert_eq!(vm.epoch.bump().allocated_bytes(), 0); vm.full_reset(); @@ -1152,24 +1125,24 @@ fn benign_user_object_writes_do_not_dirty_leaked_objects() { assert!(!vm.session.is_dirty_since_snapshot()); } -/// 假阳性护栏:只写用户对象的良性运行不得误判 builtin/global 脏。 -/// 用对象/数组字面量表达式(对象分配在 epoch、非 session 直分)隔离掉 -/// full_reset 对 session 对象的既有兜底,从而验证选择性重建未因值写 bump 误触发。 +/// 假阳性护栏:只写用户对象的良性运行不得误判 builtin 脏。 +/// 对象/数组字面量临时对象经统一入口入统一表,run 末不可达, +/// full_reset 随 session 释放;session 对象在场时防御兜底强制 bump +/// global(保留 global 持 session 子引用会在收尾后悬垂),builtin world +/// 不因值写误触发重建。 #[test] fn benign_run_does_not_falsely_dirty_builtins() { let mut vm = Vm::new(); let _ = run_source(&mut vm, "0"); let world_ptr = Arc::as_ptr(&vm.session.builtin_world); - let global_ptr = vm.session.global_object.as_ptr(); run_source(&mut vm, "({ x: 1 }).x + [1, 2].length"); - assert!(vm.gc_state.session_object_ptrs.is_empty(), "良性表达式不应产生 session 直分对象"); + assert_eq!(vm.session_object_count(), 2, "良性表达式应产生两枚临时对象(对象字面量 + 数组字面量)"); assert!(!vm.session.is_dirty_since_snapshot(), "良性运行不应误判 builtin 脏"); vm.full_reset(); assert!(std::ptr::eq(world_ptr, Arc::as_ptr(&vm.session.builtin_world)), "builtin world 不应重建"); - assert!(std::ptr::eq(global_ptr, vm.session.global_object.as_ptr()), "global 不应重建"); assert!(!vm.session.is_dirty_since_snapshot()); } diff --git a/crates/oxide_vm/tests/builtins_async_disposable_stack_tests.rs b/crates/oxide_vm/tests/builtins_async_disposable_stack_tests.rs index a12dc063..802aa78d 100644 --- a/crates/oxide_vm/tests/builtins_async_disposable_stack_tests.rs +++ b/crates/oxide_vm/tests/builtins_async_disposable_stack_tests.rs @@ -530,19 +530,20 @@ fn promote_then_reset_keeps_entries_alive() { assert_eq!(text, "true|false"); } -/// drop 记账:无引用的 async 栈对象被收集时释放状态盒(freed 字节增长)。 +/// drop 记账:无引用的 async 栈对象被收集时释放状态盒并计入释放字节。 +/// 低阈值使执行期收集在循环内触发(默认 32MiB 阈值下本负载不触发收集)。 #[test] fn drop_accounts_capability_bytes() { - let mut vm = Vm::new(); + let mut config = KernelConfig::minimal(); + config.set_session_gc_threshold(1); + let mut vm = Vm::with_kernel_core(KernelCore::new(config)); let allocator = Allocator::default(); let program = oxide_parser::parse(&allocator, "for (var i = 0; i < 500; i++) { new AsyncDisposableStack(); } 0") .expect("parse"); let module = Compiler::new().compile(&program).expect("compile"); vm.run(&Arc::new(module)).expect("run"); - let before = vm.session_gc_stats().total_bytes_freed; - vm.reset(); - let after = vm.session_gc_stats().total_bytes_freed; - assert!(after > before, "reset 应回收栈对象状态盒,before={before} after={after}"); + let freed = vm.session_gc_stats().total_bytes_freed; + assert!(freed > 0, "收集应释放死栈对象状态盒并记账,freed={freed}"); } /// full_reset(dirty 重建):global 与 object 家族重建后 AsyncDisposableStack 仍 diff --git a/crates/oxide_vm/tests/builtins_disposable_stack_tests.rs b/crates/oxide_vm/tests/builtins_disposable_stack_tests.rs index 1702fac5..6e9f3c41 100644 --- a/crates/oxide_vm/tests/builtins_disposable_stack_tests.rs +++ b/crates/oxide_vm/tests/builtins_disposable_stack_tests.rs @@ -424,15 +424,16 @@ fn promote_then_reset_keeps_entries_alive() { assert_eq!(text, "true|false"); } -/// drop 记账:无引用的栈对象被收集时释放状态盒(freed 字节增长)。 +/// drop 记账:无引用的栈对象被收集时释放状态盒并计入释放字节。 +/// 低阈值使执行期收集在循环内触发(默认 32MiB 阈值下本负载不触发收集)。 #[test] fn drop_accounts_capability_bytes() { - let mut vm = make_vm(); + let mut config = KernelConfig::minimal(); + config.set_session_gc_threshold(1); + let mut vm = Vm::with_kernel_core(KernelCore::new(config)); eval_in(&mut vm, "for (var i = 0; i < 500; i++) { new DisposableStack(); } 0").expect("run"); - let before = vm.session_gc_stats().total_bytes_freed; - vm.reset(); - let after = vm.session_gc_stats().total_bytes_freed; - assert!(after > before, "reset 应回收栈对象状态盒,before={before} after={after}"); + let freed = vm.session_gc_stats().total_bytes_freed; + assert!(freed > 0, "收集应释放死栈对象状态盒并记账,freed={freed}"); } /// full_reset(dirty 重建):global 与 object 家族重建后 DisposableStack 仍完整可用 diff --git a/crates/oxide_vm/tests/match_all_native_leak_tests.rs b/crates/oxide_vm/tests/match_all_native_leak_tests.rs index dd353b86..a2721206 100644 --- a/crates/oxide_vm/tests/match_all_native_leak_tests.rs +++ b/crates/oxide_vm/tests/match_all_native_leak_tests.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use oxide_compiler::compiler::Compiler; +use oxide_kernel::kernel::{KernelConfig, KernelCore}; use oxide_types::value::JsValue; use oxide_vm::vm::Vm; use regress::Regex; @@ -23,35 +24,36 @@ fn to_str(vm: &Vm, val: JsValue) -> String { const ROUNDS: u64 = 256; -/// 字符串模式路径:N 个死载体(包装器 + 载体)经完整收集走 sweep 死分支, +/// 字符串模式路径:N 个死载体(包装器 + 载体)经收集走 sweep 死分支, /// 释放字节必须含 N 份编译正则——守卫未命中时该部分恒不计入。 +/// 低阈值使执行期收集在循环内触发(默认 32MiB 阈值下本负载不触发收集)。 #[test] fn string_pattern_match_all_boxes_freed_by_sweep() { - let mut vm = Vm::new(); + let mut config = KernelConfig::minimal(); + config.set_session_gc_threshold(1); + let mut vm = Vm::with_kernel_core(KernelCore::new(config)); eval(&mut vm, "for (let i = 0; i < 256; i++) { 'aaaa'.matchAll('a'); }").unwrap(); - let freed_before = vm.session_gc_stats().total_bytes_freed; - vm.reset(); - let freed_delta = vm.session_gc_stats().total_bytes_freed - freed_before; + let freed = vm.session_gc_stats().total_bytes_freed; let min_regex_bytes = ROUNDS * std::mem::size_of::() as u64; assert!( - freed_delta >= min_regex_bytes, - "sweep 释放 {freed_delta} 字节,应含 {ROUNDS} 份编译正则(下界 {min_regex_bytes})" + freed >= min_regex_bytes, + "sweep 释放 {freed} 字节,应含 {ROUNDS} 份编译正则(下界 {min_regex_bytes})" ); } /// 非 global 正则路径:`RegExp.prototype.matchAll` 复制补 g 建载体,同口径断言。 #[test] fn non_global_regexp_match_all_boxes_freed_by_sweep() { - let mut vm = Vm::new(); + let mut config = KernelConfig::minimal(); + config.set_session_gc_threshold(1); + let mut vm = Vm::with_kernel_core(KernelCore::new(config)); // 真则常驻(存活根),循环内只产死载体:释放下界唯一来源是载体 Box。 eval(&mut vm, "var r = /ab/; for (let i = 0; i < 256; i++) { r[Symbol.matchAll]('abab'); }").unwrap(); - let freed_before = vm.session_gc_stats().total_bytes_freed; - vm.reset(); - let freed_delta = vm.session_gc_stats().total_bytes_freed - freed_before; + let freed = vm.session_gc_stats().total_bytes_freed; let min_regex_bytes = ROUNDS * std::mem::size_of::() as u64; assert!( - freed_delta >= min_regex_bytes, - "sweep 释放 {freed_delta} 字节,应含 {ROUNDS} 份编译正则(下界 {min_regex_bytes})" + freed >= min_regex_bytes, + "sweep 释放 {freed} 字节,应含 {ROUNDS} 份编译正则(下界 {min_regex_bytes})" ); } diff --git a/crates/oxide_vm/tests/object_graph_retention_tests.rs b/crates/oxide_vm/tests/object_graph_retention_tests.rs index f3242dd9..a98744fd 100644 --- a/crates/oxide_vm/tests/object_graph_retention_tests.rs +++ b/crates/oxide_vm/tests/object_graph_retention_tests.rs @@ -2,11 +2,12 @@ //! 确定性账目。 //! //! 15 层满二叉树(32767 节点)在「写路径直通(不引克隆)+ 根 promote 深克隆」 -//! 晋升语义下的三个确定性计数。run 末 session 表只含直 session 分配对象, -//! 整树节点字面量驻留 epoch;promote_rooted 把顶层 var 寄存器根的 epoch 原件 -//! 深克隆为 session 内唯一副本。晋升语义或根处理改动后,若以下任一数值变化, -//! 说明 session 留存账目已变——须对照 tests/stress/mem_object_graph.js 的基线 -//! (benchmark_baseline.json)复核后再更新本锚。 +//! 晋升语义下的确定性计数。对象分配统一入口 Box 化后全部对象入统一表, +//! run 末统一表含整树节点字面量 + 每次 JS 调用无条件创建的 arguments 对象 + +//! 直 session 分配(函数对象与 prototype 子对象);promote_rooted 对 session +//! 对象全空转(统一表后无 epoch 原件可晋升)。晋升语义或根处理改动后,若以下 +//! 任一数值变化,说明 session 留存账目已变——须对照 tests/stress/mem_object_graph.js +//! 的基线(benchmark_baseline.json)复核后再更新本锚。 use std::sync::Arc; @@ -42,19 +43,16 @@ fn run_module(source: &str) -> Vm { vm } -/// 留存锚:逃逸写直通后 run 末 session 表只含直 session 分配的 2 枚对象 -/// (makeNode 函数对象与其 prototype 子对象),整树节点字面量驻留 epoch, -/// 顶层 var 寄存器槽仍持有 epoch 原件(SET 路径不回写寄存器);测量点 -/// promote_rooted 把寄存器原件深克隆进 session(转发表去重,整树 32767), -/// full GC 后逻辑存活集(整树 + 函数 2)在 session 内恰一份、无双计。 -/// epoch 侧 65534 的 2× 与节点无关:其中 32767 是每次 JS 调用无条件创建的 -/// arguments 对象(CREATE_ARGUMENTS),另 32767 才是节点字面量。 +/// 留存锚:对象分配统一入口 Box 化后 run 末统一表含全部对象——整树节点 +/// 字面量 32767 + 每次 JS 调用无条件创建的 arguments 对象 32767 + 直 session +/// 分配 2(makeNode 函数对象与其 prototype 子对象)= 65536。promote_rooted +/// 对 session 对象全空转(统一表后无 epoch 原件可晋升);full GC 后逻辑存活集 +/// (整树 + 函数 2)在 session 内恰一份、无双计。 #[test] fn object_graph_retention_anchor() { let mut vm = run_module(OBJECT_GRAPH); - assert_eq!(vm.epoch_object_count(), 65534, "run 末 epoch 计数"); - assert_eq!(vm.session_object_count(), 2, "run 末 session 直分配计数(函数对象 + prototype 子对象)"); + assert_eq!(vm.session_object_count(), 65536, "run 末统一表计数(节点 + arguments + 直分配)"); vm.promote_rooted_epoch_objects(); vm.promote_session_epoch_refs(); diff --git a/crates/oxide_vm/tests/session_arena_tests.rs b/crates/oxide_vm/tests/session_arena_tests.rs index 98a7d3f7..16381673 100644 --- a/crates/oxide_vm/tests/session_arena_tests.rs +++ b/crates/oxide_vm/tests/session_arena_tests.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use oxide_compiler::compiler::Compiler; +use oxide_kernel::kernel::{KernelConfig, KernelCore}; use oxide_parser::Allocator; use oxide_types::value::JsValue; use oxide_vm::vm::Vm; @@ -23,6 +24,15 @@ fn global_prop(vm: &Vm, name: &str) -> JsValue { global.get_prop_at(pos) } +/// 低阈值 VM:`reset()` 的 session 收集受阈值门控(默认 32MiB),小负载须把 +/// 阈值压到 1 字节才能让 reset 真正回收,断言「reset 后追踪表清空/字节释放」 +/// 的测试用此构造。 +fn low_threshold_vm() -> Vm { + let mut config = KernelConfig::minimal(); + config.set_session_gc_threshold(1); + Vm::with_kernel_core(KernelCore::new(config)) +} + #[test] fn test_basic_object_escape() { let mut vm = Vm::new(); @@ -90,31 +100,26 @@ fn test_full_reset_clears_session_state() { assert!(result.is_undefined()); } -/// NEW_OBJECT 字面量必须登记进 epoch 追踪表:`{}` 写入超内联容量的属性会分配 -/// hash_props 堆 Box,reset 时经追踪表统一释放;不登记则每个字面量泄漏一个 Box。 +/// NEW_OBJECT 字面量必须登记进统一追踪表:`{}` 写入超内联容量的属性会分配 +/// hash_props 堆 Box,收集路径经追踪表统一释放;不登记则每个字面量泄漏一个 Box。 /// 对象变量置于函数作用域:顶层 var 在声明写即逃逸 promote,后续属性写落 -/// session 克隆(其堆数据不属 epoch 源对象,reset 不释放)。 +/// session 克隆(其堆数据不属源对象,reset 不释放)。 #[test] fn new_object_literal_tracked_and_freed_on_reset() { - let mut vm = Vm::new(); + let mut vm = low_threshold_vm(); let result = run_source( &mut vm, "(function(){var o = {}; for (var i = 0; i < 40; i++) { o['k' + i] = i; } \ delete o.k7; return o.k8;})()", ); assert_eq!(format!("{}", result), "8", "属性写入/删除后读取应正常"); - assert!(vm.epoch_object_count() > 0, "NEW_OBJECT 应登记 epoch 追踪表"); + assert!(vm.session_object_count() > 0, "NEW_OBJECT 应登记统一追踪表"); - let freed_before = vm.session_gc_stats().total_bytes_freed; vm.reset(); - assert!( - vm.session_gc_stats().total_bytes_freed > freed_before, - "reset 应释放字面量对象的属性向量堆数据" - ); - assert_eq!(vm.epoch_object_count(), 0, "reset 后追踪表应清空"); + assert_eq!(vm.session_object_count(), 0, "reset 后追踪表应清空"); } -/// full_reset 同样释放 NEW_OBJECT 字面量:带属性写的 epoch 源对象经逃逸写直通 +/// full_reset 同样释放 NEW_OBJECT 字面量:带属性写的源对象经逃逸写直通 /// 留存 global 槽(不克隆),完全重置随 global 重建丢弃槽位并释放源对象堆 /// 数据,不得悬垂。 #[test] @@ -126,70 +131,60 @@ fn new_object_literal_freed_by_full_reset() { globalThis.keep = o; o.k7", ); assert_eq!(format!("{}", result), "7", "逃逸后属性读取应正常"); - assert!(vm.epoch_object_count() > 0, "字面量源对象直通留存 epoch,登记于 epoch 追踪表"); + assert!(vm.session_object_count() > 0, "字面量源对象直通留存 global 槽,登记于统一追踪表"); let keep = global_prop(&vm, "keep"); - // SAFETY: 逃逸值是本 VM 自有的 epoch 对象,本 session 内指针有效。 + // SAFETY: 逃逸值是本 VM 自有的 session 对象,本 session 内指针有效。 assert!( - unsafe { (&*keep.as_js_object_ptr()).is_epoch() }, - "逃逸值应直通留存 epoch,不克隆进 session" + unsafe { (&*keep.as_js_object_ptr()).is_session_epoch() }, + "逃逸值应直通留存 global 槽,不克隆进 session" ); vm.full_reset(); - assert_eq!(vm.epoch_object_count(), 0, "full_reset 后 epoch 追踪表应清空"); - assert_eq!(vm.session_object_count(), 0, "full_reset 后 session 追踪表应清空"); + assert_eq!(vm.session_object_count(), 0, "full_reset 后统一追踪表应清空"); assert!(run_source(&mut vm, "globalThis.keep").is_undefined(), "full_reset 后逃逸对象不可再访问"); } -/// regexp exec 结果数组必须登记 epoch 追踪表:数组元素区 Box 与 index/input 命名 -/// 属性 hash_props Box 随 reset 统一释放;未登记则每次 exec/Symbol.match 泄漏。 +/// regexp exec 结果数组必须登记统一追踪表:数组元素区 Box 与 index/input 命名 +/// 属性 hash_props Box 随收集路径统一释放;未登记则每次 exec/Symbol.match 泄漏。 #[test] fn regexp_exec_result_array_tracked_and_freed_on_reset() { - let mut vm = Vm::new(); + let mut vm = low_threshold_vm(); let result = run_source( &mut vm, "for (var i = 0; i < 20; i++) { /a(b)?/.exec('ab'); } \ var r = /a(b)?/.exec('ab'); r.length", ); assert_eq!(format!("{}", result), "2", "exec 结果数组长度应正常"); - assert!(vm.epoch_object_count() > 0, "exec 结果数组应登记 epoch 追踪表"); + assert!(vm.session_object_count() > 0, "exec 结果数组应登记统一追踪表"); - let freed_before = vm.session_gc_stats().total_bytes_freed; vm.reset(); - assert!( - vm.session_gc_stats().total_bytes_freed > freed_before, - "reset 应释放 exec 结果数组的元素区与命名属性堆数据" - ); - assert_eq!(vm.epoch_object_count(), 0, "reset 后追踪表应清空"); + // r 是顶层 var(global 根),reset 后存活;20 个临时 exec 结果数组已释放。 + assert_eq!(vm.session_object_count(), 1, "reset 后仅 global 根 r 存活"); } -/// split/match 结果数组必须登记 epoch 追踪表:元素区 Box 随 reset 统一释放; +/// split/match 结果数组必须登记统一追踪表:元素区 Box 随收集路径统一释放; /// 未登记则每次 split/match 泄漏一个 Box。 #[test] fn string_split_match_result_array_tracked_and_freed_on_reset() { - let mut vm = Vm::new(); + let mut vm = low_threshold_vm(); let result = run_source( &mut vm, "for (var i = 0; i < 20; i++) { 'a,b,c'.split(','); 'ab'.match(/a/); } \ 'a,b,c'.split(',').length", ); assert_eq!(format!("{}", result), "3", "split 结果数组长度应正常"); - assert!(vm.epoch_object_count() > 0, "split/match 结果数组应登记 epoch 追踪表"); + assert!(vm.session_object_count() > 0, "split/match 结果数组应登记统一追踪表"); - let freed_before = vm.session_gc_stats().total_bytes_freed; vm.reset(); - assert!( - vm.session_gc_stats().total_bytes_freed > freed_before, - "reset 应释放 split/match 结果数组的元素区堆数据" - ); - assert_eq!(vm.epoch_object_count(), 0, "reset 后追踪表应清空"); + assert_eq!(vm.session_object_count(), 0, "reset 后追踪表应清空"); } -/// 错误对象必须登记 epoch 追踪表:message 非空时 push_prop 分配 hash_props Box, -/// 随 reset 统一释放;未登记则每次抛错(全引擎最频繁路径)泄漏一个 Box。 +/// 错误对象必须登记统一追踪表:message 非空时 push_prop 分配 hash_props Box, +/// 随收集路径统一释放;未登记则每次抛错(全引擎最频繁路径)泄漏一个 Box。 /// 同时覆盖构造器路径(`new TypeError`)与内部抛错路径(builtin 内 create_type_error)。 #[test] fn error_object_tracked_and_freed_on_reset() { - let mut vm = Vm::new(); + let mut vm = low_threshold_vm(); let result = run_source( &mut vm, "for (var i = 0; i < 20; i++) { \ @@ -199,22 +194,17 @@ fn error_object_tracked_and_freed_on_reset() { try { throw new TypeError('boom'); } catch (e) { e.message }", ); assert_eq!(vm.lookup_str(result).unwrap_or_default(), "boom", "错误对象 message 应正常"); - assert!(vm.epoch_object_count() > 0, "错误对象应登记 epoch 追踪表"); + assert!(vm.session_object_count() > 0, "错误对象应登记统一追踪表"); - let freed_before = vm.session_gc_stats().total_bytes_freed; vm.reset(); - assert!( - vm.session_gc_stats().total_bytes_freed > freed_before, - "reset 应释放错误对象的 message 属性堆数据" - ); - assert_eq!(vm.epoch_object_count(), 0, "reset 后追踪表应清空"); + assert_eq!(vm.session_object_count(), 0, "reset 后追踪表应清空"); } -/// gOPD/Reflect 描述符对象必须登记 epoch 追踪表:4 个描述符属性 push 分配 -/// hash_props Box,随 reset 统一释放;未登记则每次 getOwnPropertyDescriptor 泄漏。 +/// gOPD/Reflect 描述符对象必须登记统一追踪表:4 个描述符属性 push 分配 +/// hash_props Box,随收集路径统一释放;未登记则每次 getOwnPropertyDescriptor 泄漏。 #[test] fn gopd_descriptor_tracked_and_freed_on_reset() { - let mut vm = Vm::new(); + let mut vm = low_threshold_vm(); let result = run_source( &mut vm, "for (var i = 0; i < 20; i++) { \ @@ -223,13 +213,8 @@ fn gopd_descriptor_tracked_and_freed_on_reset() { Object.getOwnPropertyDescriptor({x:5}, 'x').value", ); assert_eq!(format!("{}", result), "5", "gOPD 描述符值应正常"); - assert!(vm.epoch_object_count() > 0, "描述符对象应登记 epoch 追踪表"); + assert!(vm.session_object_count() > 0, "描述符对象应登记统一追踪表"); - let freed_before = vm.session_gc_stats().total_bytes_freed; vm.reset(); - assert!( - vm.session_gc_stats().total_bytes_freed > freed_before, - "reset 应释放描述符对象的 hash_props 堆数据" - ); - assert_eq!(vm.epoch_object_count(), 0, "reset 后追踪表应清空"); + assert_eq!(vm.session_object_count(), 0, "reset 后追踪表应清空"); }