perf: 对象统一入口 Box 化合并对象表退役晋升档

This commit is contained in:
2026-10-01 06:39:51 +00:00
parent 0e3992722e
commit 1711ae948b
13 changed files with 262 additions and 523 deletions
+20
View File
@@ -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 对象。
///
/// 用于把持久对象快照进当前调用上下文,修改不反向传播到源对象。
+54 -182
View File
@@ -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::<JsObject>() 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::<JsObject>() 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::<u64>() 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<usize> = 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<usize> = 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!(
+48 -77
View File
@@ -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 不换新),完成后
+19 -38
View File
@@ -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::<JsObject>()
+ 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::<JsObject>()
+ 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 / 字节码函数的统一同步调用入口,返回结果或错误文本。
+7 -11
View File
@@ -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 预热效果。
-7
View File
@@ -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<i128>`)追踪表。`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
+19 -73
View File
@@ -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<Vec<*mut Cell>>`):原件与晋升克隆经
/// `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<usize> = 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<usize> = 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())
+12 -39
View File
@@ -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());
}
@@ -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 仍
@@ -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 仍完整可用
@@ -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::<Regex>() 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::<Regex>() 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})"
);
}
@@ -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();
+42 -57
View File
@@ -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 后追踪表应清空");
}