mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
perf: 对象统一入口 Box 化合并对象表退役晋升档
This commit is contained in:
@@ -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 对象。
|
||||
///
|
||||
/// 用于把持久对象快照进当前调用上下文,修改不反向传播到源对象。
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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 不换新),完成后
|
||||
|
||||
@@ -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 / 字节码函数的统一同步调用入口,返回结果或错误文本。
|
||||
|
||||
@@ -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 预热效果。
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 后追踪表应清空");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user