fix: upvalue cell 参与 mark-sweep 回收,闭合长 run 死闭包 cell 泄漏

This commit is contained in:
2026-09-26 18:27:21 +00:00
parent f1cd2cd612
commit 78953e338b
8 changed files with 377 additions and 22 deletions
+24 -4
View File
@@ -151,8 +151,8 @@ fn push_entry(table: *mut ModuleNsTable, name_si: u32, origin: ModuleNsOrigin, s
/// - 返回 `Initialized(v)`:`Value` 已初始化,或 cell 已初始化。
///
/// # 注意事项
/// - `Cell` 读依赖「cell 由 `alloc_cell` 分配并登记,至 `full_reset` 才统一释放」
/// 的生命周期契约;同一 VM 世代内指针有效。
/// - `Cell` 读依赖「cell 由 `alloc_cell` 分配并登记,参与 mark-sweep 回收、
/// `full_reset` 为收尾兜底」的生命周期契约;同一 VM 世代内指针有效。
pub fn module_ns_export(obj: &JsObject, key_si: u32) -> Option<ModuleNsQuery> {
let table = ns_table_ptr(obj);
if table.is_null() {
@@ -177,7 +177,7 @@ pub fn module_ns_export(obj: &JsObject, key_si: u32) -> Option<ModuleNsQuery> {
if cell.is_null() {
return Some(ModuleNsQuery::Uninitialized);
}
// SAFETY: cell 由 alloc_cell 分配并登记,至 full_reset 才释放。
// SAFETY: cell 由 alloc_cell 分配并登记,参与 mark-sweep、full_reset 收尾兜底。
let cell_ref = unsafe { &**cell };
if cell_ref.is_initialized() {
Some(ModuleNsQuery::Initialized(cell_ref.value))
@@ -215,6 +215,26 @@ pub fn module_ns_native_edges(obj: &JsObject) -> Vec<JsValue> {
edges
}
/// 收集条目表持有的共享 cell 指针(`Cell` 条目),供 GC mark cell 边。
/// cell 指针不经对象图、须独立入存活集(与 `module_ns_native_edges` 的值边互补)。
pub fn module_ns_cell_edges(obj: &JsObject) -> Vec<*mut Cell> {
let table = ns_table_ptr(obj);
if table.is_null() {
return Vec::new();
}
// SAFETY: table 归本 ns 对象持有,生命周期见 `module_ns_export`。
let table_ref = unsafe { &*table };
let mut out = Vec::new();
for entry in &table_ref.entries {
if let ModuleNsState::Cell(cell) = &entry.state {
if !cell.is_null() {
out.push(*cell);
}
}
}
out
}
/// 原地重写条目表内的 JsValue 边(对象搬移/epoch 晋升后的引用重定位)。
///
/// # 副作用
@@ -554,7 +574,7 @@ pub fn module_set_cell<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let Some(cell) = vm.module_frame_cell(cell_idx) else {
return type_error(vm, "__moduleSetCell: cell is not available");
};
// SAFETY: cell 由 alloc_cell 分配并登记,至 full_reset 才释放。
// SAFETY: cell 由 alloc_cell 分配并登记,参与 mark-sweep、full_reset 收尾兜底。
let value = unsafe { (*cell).value };
// SAFETY: idx 来自刚完成的表内查找,仍在界内。
unsafe {
+2 -1
View File
@@ -1,6 +1,7 @@
//! 闭包共享 cell:JS 值 + 已初始化 / GC 标记位,定长 16 字节。
//!
//! cell 组成闭包的 upvalue 数组(对象 `upvalues` 裸指针区),随闭包释放;
//! cell 组成闭包的 upvalue 数组(对象 `upvalues` 裸指针区),随 mark-sweep
//! 释放(死 cell 随收集释放,full_reset 收尾兜底);
//! `flags` 为 1 字节位集加 7 字节固定填充,保证数组元素步长恒定。
use crate::value::JsValue;
+19 -1
View File
@@ -10,7 +10,7 @@
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_runtime_api::{to_boolean, NativeResult};
use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes};
use oxide_types::object::{Cell, JsObject, NativeFnPtr, PropAttributes};
use oxide_types::value::JsValue;
use crate::vm::Vm;
@@ -556,6 +556,24 @@ pub(crate) fn async_native_string_edges(obj: &JsObject) -> Vec<*mut oxide_types:
out
}
/// 异步挂起帧 cell_stack 的 cell 指针扁平列表(GC mark cell 边)。
/// 与 `async_native_string_edges` 经同一字段清单(`suspended.cell_stack`)
/// 产出,cell 指针不经对象图、须独立入存活集。
pub(crate) fn async_native_cell_edges(obj: &JsObject) -> Vec<*mut Cell> {
let Some(state) = async_state_mut(obj) else {
return Vec::new();
};
let mut out = Vec::new();
for cells in &state.suspended.cell_stack {
for &cell_ptr in cells {
if !cell_ptr.is_null() {
out.push(cell_ptr);
}
}
}
out
}
/// 用转发函数重写状态快照中的所有 JsValue(session GC 移动式清扫 / promote 用)。
pub(crate) fn rewrite_async_native(obj: &JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue) {
let Some(state) = async_state_mut(obj) else {
+19 -1
View File
@@ -17,7 +17,7 @@ use std::collections::VecDeque;
use oxide_builtins::iterator::make_iter_result;
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_runtime_api::{NativeResult, VmHost};
use oxide_types::object::{JsObject, NativeFnPtr, PropAttributes};
use oxide_types::object::{Cell, JsObject, NativeFnPtr, PropAttributes};
use oxide_types::value::JsValue;
use crate::generator::{DelegateOutcome, GeneratorResumeMode};
@@ -1118,6 +1118,24 @@ pub(crate) fn async_generator_native_string_edges(obj: &JsObject) -> Vec<*mut ox
out
}
/// 异步生成器挂起帧 cell_stack 的 cell 指针扁平列表(GC mark cell 边)。
/// 与 `async_generator_native_string_edges` 经同一字段清单(`suspended.cell_stack`)
/// 产出,cell 指针不经对象图、须独立入存活集。
pub(crate) fn async_generator_native_cell_edges(obj: &JsObject) -> Vec<*mut Cell> {
let Some(state) = async_gen_state_mut(obj) else {
return Vec::new();
};
let mut out = Vec::new();
for cells in &state.suspended.cell_stack {
for &cell_ptr in cells {
if !cell_ptr.is_null() {
out.push(cell_ptr);
}
}
}
out
}
/// 用转发函数重写状态快照中的所有 JsValue(session GC 移动式清扫 / promote 用)。
pub(crate) fn rewrite_async_generator_native(obj: &JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue) {
let Some(state) = async_gen_state_mut(obj) else {
+19 -1
View File
@@ -9,7 +9,7 @@
use oxide_builtins::iterator::{is_callable, make_iter_result};
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_runtime_api::{to_boolean, NativeResult};
use oxide_types::object::{JsObject, NativeFnPtr};
use oxide_types::object::{Cell, JsObject, NativeFnPtr};
use oxide_types::value::JsValue;
use crate::vm::Vm;
@@ -939,6 +939,24 @@ pub(crate) fn generator_native_string_edges(obj: &JsObject) -> Vec<*mut oxide_ty
out
}
/// 生成器挂起帧 cell_stack 的 cell 指针扁平列表(GC mark cell 边)。
/// 与 `generator_native_string_edges` 经同一字段清单(`suspended.cell_stack`)
/// 产出,cell 指针不经对象图、须独立入存活集。
pub(crate) fn generator_native_cell_edges(obj: &JsObject) -> Vec<*mut Cell> {
let Some(state) = generator_state_mut(obj) else {
return Vec::new();
};
let mut out = Vec::new();
for cells in &state.suspended.cell_stack {
for &cell_ptr in cells {
if !cell_ptr.is_null() {
out.push(cell_ptr);
}
}
}
out
}
/// 用转发函数重写状态快照中的所有 JsValue(session GC 移动式清扫 / promote 用)。
pub(crate) fn rewrite_generator_native(obj: &JsObject, mut rewrite: impl FnMut(JsValue) -> JsValue) {
let Some(state) = generator_state_mut(obj) else {
+86 -9
View File
@@ -3,7 +3,7 @@ use std::mem::size_of;
use std::time::Instant;
use crate::{vm_debug, vm_info};
use oxide_types::object::{JsObject, JsString, PropMetaEntry};
use oxide_types::object::{Cell, JsObject, JsString, PropMetaEntry};
use oxide_types::value::JsValue;
use rustc_hash::FxBuildHasher;
@@ -32,6 +32,7 @@ pub struct SessionGc {
pub(crate) mark_stack: Vec<*mut JsObject>,
pub(crate) live_strings: HashSet<*mut JsString, FxBuildHasher>,
pub(crate) live_bigints: HashSet<*mut num_bigint::BigInt, FxBuildHasher>,
pub(crate) live_cells: HashSet<*mut Cell, FxBuildHasher>,
}
impl SessionGc {
@@ -47,6 +48,13 @@ impl SessionGc {
/// 短路,漏扫其新增引用边。
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 末尾的旧表对象已全释放/克隆
/// (克隆未标记),清位须与表重写同步执行,独立成臂供调用点按需取用。
pub(crate) fn clear_session_marks(&mut self, vm: &mut Vm) {
for &ptr in &vm.gc_state.session_object_ptrs {
if ptr.is_null() {
continue;
@@ -55,8 +63,11 @@ impl SessionGc {
// session_epoch.alloc,在 arena 存活期间有效。
unsafe { (*ptr).set_gc_mark(false) };
}
// epoch 臂同清:mark 的 epoch 臂置位不随 session 清位消除,残留位会让
// 下一次 mark 的 DFS 短路、漏扫该对象此间新增的边。
}
/// 只清 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;
@@ -138,6 +149,7 @@ impl SessionGc {
obj: &JsObject, vm: &Vm, 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() {
@@ -200,6 +212,9 @@ impl SessionGc {
for value in module::module_ns_native_edges(obj) {
Self::process_edge(value, vm, stack, live_strings, live_bigints);
}
for ptr in module::module_ns_cell_edges(obj) {
live_cells.insert(ptr);
}
}
if obj.is_generator_obj() {
for value in crate::generator::generator_native_edges(obj) {
@@ -208,6 +223,9 @@ impl SessionGc {
for ptr in crate::generator::generator_native_string_edges(obj) {
Self::mark_string_live(live_strings, ptr);
}
for ptr in crate::generator::generator_native_cell_edges(obj) {
live_cells.insert(ptr);
}
}
if obj.is_promise_obj() {
for value in crate::promise::promise_native_edges(obj) {
@@ -221,6 +239,9 @@ impl SessionGc {
for ptr in crate::async_func::async_native_string_edges(obj) {
Self::mark_string_live(live_strings, ptr);
}
for ptr in crate::async_func::async_native_cell_edges(obj) {
live_cells.insert(ptr);
}
}
if obj.is_async_generator_obj() {
for value in crate::async_generator::async_generator_native_edges(obj) {
@@ -229,17 +250,22 @@ impl SessionGc {
for ptr in crate::async_generator::async_generator_native_string_edges(obj) {
Self::mark_string_live(live_strings, ptr);
}
for ptr in crate::async_generator::async_generator_native_cell_edges(obj) {
live_cells.insert(ptr);
}
}
// RegExp 实例 source/flags 字段持有字符串边。
if obj.is_regexp_obj() {
Self::process_edge(obj.get_regexp_source(), vm, stack, live_strings, live_bigints);
Self::process_edge(obj.get_regexp_flags(), vm, stack, live_strings, live_bigints);
}
// 遍历 upvalue cell 中的引用。
// 遍历 upvalue cell 中的引用:cell 指针本身入存活集(对象 upvalues 边
// 是 cell 的根面),值边照常走 process_edge。
for cell_ptr in obj.upvalues_slice() {
if cell_ptr.is_null() {
continue;
}
live_cells.insert(*cell_ptr);
let cell = unsafe { &**cell_ptr };
Self::process_edge(cell.value, vm, stack, live_strings, live_bigints);
}
@@ -449,11 +475,21 @@ impl SessionGc {
mark_stack: stack,
live_strings,
live_bigints,
live_cells,
..
} = self;
stack.clear();
live_strings.clear();
live_bigints.clear();
live_cells.clear();
// 活跃帧 cell_stack 是 cell 的根面(不经对象图):逐层种子入存活集。
for cell_vec in &vm.cell_stack {
for &cell_ptr in cell_vec {
if !cell_ptr.is_null() {
live_cells.insert(cell_ptr);
}
}
}
for ptr in string_seeds {
Self::mark_string_live(live_strings, ptr);
}
@@ -479,7 +515,7 @@ impl SessionGc {
continue;
}
// P 对象根(builtin world / global)不回收,只扫边。
Self::scan_edges_for_mark(obj, vm, stack, live_strings, live_bigints);
Self::scan_edges_for_mark(obj, vm, stack, live_strings, live_bigints, live_cells);
}
}
@@ -494,7 +530,7 @@ impl SessionGc {
continue;
}
obj.set_gc_mark(true);
Self::scan_edges_for_mark(obj, vm, stack, live_strings, live_bigints);
Self::scan_edges_for_mark(obj, vm, stack, live_strings, live_bigints, live_cells);
}
}
}
@@ -741,7 +777,9 @@ impl SessionGc {
})
.sum::<u64>() as usize;
self.clear_all_marks(vm);
// 移动式 sweep 后旧表对象已全释放/克隆(克隆未标记),session 臂空转,
// 只清 epoch 臂残留位(epoch 对象存活、位残留)。
self.clear_epoch_marks(vm);
let total_ptrs = survivors + dead;
if total_ptrs > 0 {
@@ -835,6 +873,39 @@ impl SessionGc {
freed
}
/// 清扫 session upvalue cell:保留 `mark()` 阶段记为存活的部分,其余经
/// `Box::from_raw` 释放。存活 cell 不被搬移——Box 地址稳定——因此无需
/// forwarding 表与根指针重写。在 BigInt 清扫之后运行。
///
/// cell 不在 `session_bytes_allocated` 账目口径内(与字符串/BigInt 补回口径
/// 不同),故只累计释放字节统计、不重算存活账目。
/// 返回释放的字节数。
pub(crate) fn sweep_session_cells(&mut self, vm: &mut Vm) -> u64 {
let old = vm.gc_state.session_cell_ptrs.borrow_mut().drain(..).collect::<Vec<_>>();
let mut freed = 0u64;
let mut live = Vec::with_capacity(old.len());
for ptr in old {
if ptr.is_null() {
continue;
}
if self.live_cells.contains(&ptr) {
// 存活——地址不变,无需重写。
live.push(ptr);
} else {
// SAFETY: ptr 在 session_cell_ptrs 中但不可达,恰好释放一次。
freed += size_of::<Cell>() as u64;
unsafe {
drop(Box::from_raw(ptr));
}
}
}
*vm.gc_state.session_cell_ptrs.borrow_mut() = live;
self.total_bytes_freed = self.total_bytes_freed.saturating_add(freed);
self.last_collection_bytes_freed = self.last_collection_bytes_freed.saturating_add(freed);
freed
}
/// 完整收集的触发判断:对象、字符串或 BigInt 表非空,且字节账目已达阈值。
pub(crate) fn should_collect(&self, vm: &Vm) -> bool {
(!vm.gc_state.session_object_ptrs.is_empty()
@@ -889,6 +960,7 @@ impl SessionGc {
let mut freed_bytes = self.sweep(vm);
freed_bytes += self.sweep_session_strings(vm);
freed_bytes += self.sweep_session_bigints(vm);
freed_bytes += self.sweep_session_cells(vm);
let elapsed = start.elapsed();
self.total_collections += 1;
@@ -957,6 +1029,7 @@ impl SessionGc {
vm.gc_state.session_bytes_allocated = object_bytes;
let mut freed_bytes = self.sweep_session_strings(vm);
freed_bytes += self.sweep_session_bigints(vm);
freed_bytes += self.sweep_session_cells(vm);
// 恢复 mark 位不变量:mark 之后必由清位收尾(与 sweep 末尾同点清位)。
// 残留 marked 对象会让下一次完整收集(reset 路径)的 mark DFS 短路漏标,
@@ -1098,6 +1171,9 @@ impl SessionGc {
}
// SAFETY: ptr 来自 session 对象表登记,arena 存活期内有效。
if unsafe { (*ptr).is_gc_marked() } {
// 原地 sweep 不搬移对象:存活对象带位保留,清位并入活分支
// (收集后无残留,残留 true 使下一次 mark DFS 短路漏标)。
unsafe { (*ptr).set_gc_mark(false) };
survivors.push(ptr);
} else {
session_freed += Self::drop_dead_session_object(ptr);
@@ -1107,8 +1183,8 @@ impl SessionGc {
let live_session = survivors.len() as u64;
vm.gc_state.session_object_ptrs = survivors;
// 恢复 mark 位不变量:收集后无残留(残留 true 使下一次 mark DFS 短路漏标)。
self.clear_all_marks(vm);
// 死 cell 随死对象出表:存活 cell 经 mark 种子 + 对象边入存活集,此处清扫。
self.sweep_session_cells(vm);
// 对象口径重算:存活对象 + 存活串 + BigInt(串/BigInt 本路径不动,
// 随公式整体重算保持与 strings-only 口径一致)。
@@ -1242,6 +1318,7 @@ impl Default for SessionGc {
mark_stack: Vec::new(),
live_strings: HashSet::default(),
live_bigints: HashSet::default(),
live_cells: HashSet::default(),
}
}
}
+203 -2
View File
@@ -9,7 +9,8 @@ use std::sync::Arc;
use super::*;
use crate::vm::{CallFrame, FrameContinuation};
use oxide_builtins::{array_buffer, data_view, disposable_stack, map, set, typed_array};
use oxide_builtins::{array_buffer, data_view, disposable_stack, map, module, set, typed_array};
use oxide_emit::module::{ModuleKind, ModuleSourceLoader, ResolvedModule};
use oxide_runtime_api::NativeResult;
fn plain_object(vm: &mut Vm) -> *mut JsObject {
@@ -456,7 +457,8 @@ fn map_native_storage_is_not_a_normal_object_edge() {
let mut stack = Vec::new();
let mut live = HashSet::with_hasher(FxBuildHasher);
let mut live_bigints = HashSet::with_hasher(FxBuildHasher);
SessionGc::scan_edges_for_mark(map_obj, &vm, &mut stack, &mut live, &mut live_bigints);
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);
assert!(!stack.iter().any(|&ptr| std::ptr::eq(ptr, native_ptr)));
}
@@ -1216,6 +1218,205 @@ fn cells_freed_by_full_reset_and_reallocatable() {
assert_eq!(format!("{result}"), "2", "重置后新 cell 应正常分配与读取");
}
// ── upvalue cell 参与 mark-sweep:死 cell 释放、活 cell 四根面存活 ─────────
/// 死闭包 cell 字节账目差分:两条同构 arrow 仅 upvalue 数不同(3 vs 0),各走
/// 独立 VM「创建 → 撤根 → 完整收集」。两次收集字节账目差 = 死 arrow 的 upvalue
/// 列表 Box + 3 个 cell(对象本体、属性区、函数名串在 A/B 恒等相消)。死分支不
/// 释放 cell(泄漏)时差分少 3×16B,本断言转红。
#[test]
fn dead_closure_cells_freed_by_sweep_byte_diff() {
let collect_freed = |src: &str| -> u64 {
let mut v = vm_with_threshold(4096);
v.run(&Arc::new(compile(src))).expect("run create");
v.run(&Arc::new(compile("globalThis.arrow = undefined; 0")))
.expect("run unroot");
v.collect_session_gc();
let stats = v.session_gc_stats();
assert!(stats.last_collection_objects_dead >= 1, "arrow 闭包应经 sweep 死分支");
stats.last_collection_bytes_freed
};
let freed_with_captures = collect_freed(
"(function(){var a = 1; var b = 2; var c = 3; \
globalThis.arrow = () => a + b + c; return 0;})()",
);
let freed_no_captures = collect_freed("globalThis.arrow = () => 7; 0");
let upvalue_box_min = (size_of::<Vec<*mut Cell>>() + 3 * size_of::<*mut Cell>()) as u64;
let cells_min = (3 * size_of::<Cell>()) as u64;
assert!(
freed_with_captures >= freed_no_captures + upvalue_box_min + cells_min,
"两次收集字节账目差应为死闭包 upvalue 列表 Box + 3 cell(≥{} B),实际差 {}",
upvalue_box_min + cells_min,
freed_with_captures - freed_no_captures
);
}
/// 活闭包 cell 跨完整收集存活:global 上的活闭包经对象 upvalues 边可达,其 cell
/// 入存活集;清寄存器仅留 global 根后完整收集,cell 仍登记在 session 表。
#[test]
fn live_closure_cell_survives_full_collect() {
let mut vm = vm_with_threshold(1);
vm.run(&Arc::new(compile(
"function outer() { var counter = 0; function inc() { return ++counter; } globalThis.inc = inc; return 0; } outer(); 0",
)))
.expect("run1");
let inc_val = global_prop_opt(&vm, "inc").expect("inc 应挂在 global 上");
let obj = unsafe { &*inc_val.as_js_object_ptr() };
let cells = obj.upvalues_slice();
assert_eq!(cells.len(), 1, "inc 应捕获 counter 一个 cell");
let cell_ptr = cells[0];
assert!(vm.gc_state.session_cell_ptrs.borrow().contains(&cell_ptr), "活 cell 应登记在表");
vm.regs.fill(JsValue::undefined());
collect(&mut vm);
// 只断言表成员——sweep 已释放的指针解引用即 UB。
assert!(vm.gc_state.session_cell_ptrs.borrow().contains(&cell_ptr), "活 cell 应跨完整收集存活");
}
/// 活跃帧 cell_stack 根:内层函数 upvalue cell 在活跃帧 cell_stack 上(低阈值使
/// 同 run 内触发执行期收集);函数返回后闭包读值正确(cell 未被误释放)。
#[test]
fn active_frame_cell_stack_root_survives_in_run_collect() {
let mut vm = vm_with_threshold(1);
vm.run(&Arc::new(compile(
"function outer() { \
var counter = 42; \
function inc() { return counter; } \
globalThis.inc = inc; \
var sink = []; \
for (var i = 0; i < 500; i++) { sink.push({ x: i }); } \
return 0; \
} outer()",
)))
.expect("run");
assert!(vm.session_gc_stats().total_collections >= 1, "执行期收集应触发");
let read = vm.run(&Arc::new(compile("globalThis.inc()"))).expect("read");
assert_eq!(read, JsValue::int(42), "闭包读值正确(活跃帧 cell_stack 上的 cell 未被误释放)");
}
/// 挂起状态盒根:生成器局部变量被嵌套闭包捕获的 cell 在挂起帧 cell_stack 上
/// (快照入状态盒),挂起态完整收集;恢复后读值正确(cell 未被误释放)。
#[test]
fn suspended_state_box_cell_root_survives_collect() {
let mut vm = vm_with_low_threshold();
vm.run(&Arc::new(compile(
"(function(){ \
function* gen() { \
var x = 100; \
function f() { return x; } \
yield f; \
return f(); \
} \
var g = gen(); g.next(); globalThis.g = g; })(); 0",
)))
.expect("run");
let g_val = global_prop_opt(&vm, "g").expect("g 应挂在 global 上");
let g_session = vm.promote_object(g_val.as_js_object_ptr());
let g_obj = unsafe { &*g_session };
// SAFETY: 生成器状态盒经 Box::into_raw 挂对象构造,生命周期与对象一致。
let state = g_obj.native_data() as *mut crate::generator::GeneratorState;
let cell_ptr = unsafe { &(*state).suspended }
.cell_stack
.iter()
.flatten()
.copied()
.find(|p| !p.is_null())
.expect("挂起帧应持有 x cell");
assert!(vm.gc_state.session_cell_ptrs.borrow().contains(&cell_ptr), "cell 应登记在表");
vm.regs.fill(JsValue::undefined());
vm.regs[0] = JsValue::from_js_object(g_session);
collect(&mut vm);
// 只断言表成员——sweep 已释放的指针解引用即 UB。
assert!(vm.gc_state.session_cell_ptrs.borrow().contains(&cell_ptr), "挂起帧 cell 应跨完整收集存活");
// 恢复后读值:cell 未被误释放。
let read = vm.run(&Arc::new(compile("globalThis.g.next().value"))).expect("read");
assert_eq!(read, JsValue::int(100), "恢复后读值正确(挂起帧 cell 未被误释放)");
}
/// 文件加载器:base_dir/specifier join 后读源(依赖模块经真实文件解析)。
struct NsFileLoader;
impl ModuleSourceLoader for NsFileLoader {
fn resolve(
&mut self, base_dir: &str, specifier: &str, _attributes: &[(&str, &str)],
) -> Result<ResolvedModule, String> {
let joined = std::path::Path::new(base_dir).join(specifier);
let full = std::fs::canonicalize(&joined).map_err(|e| format!("cannot read {specifier}: {e}"))?;
let source = std::fs::read_to_string(&full).map_err(|e| format!("cannot read {specifier}: {e}"))?;
Ok(ResolvedModule {
source,
path: full.to_string_lossy().into_owned(),
kind: ModuleKind::Js,
})
}
}
/// 测试结束删除临时目录,避免在 crate 树残留文件。
struct NsCleanup(std::path::PathBuf);
impl Drop for NsCleanup {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
/// 模块 ns cell 根:依赖模块可重赋导出挂共享 cell(条目表 `Cell` 面),ns 对象
/// 经 global 可达;清寄存器仅留 ns 根后完整收集,cell 跨移动式 sweep 存活(深拷
/// 表保留同一 cell 指针),`module_ns_export` 读回正确。
#[test]
fn module_ns_cell_root_survives_collect() {
let cwd = std::env::current_dir().expect("cwd");
let dir = cwd.join("__module_ns_cell_gc__");
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("dep.mjs"), "var x = 42; export { x }; globalThis.f = function(){ x = 100; };")
.expect("write dep");
std::fs::write(dir.join("main.mjs"), "import * as ns from './dep.mjs'; globalThis.__ns = ns;").expect("write main");
let _cleanup = NsCleanup(dir.clone());
let allocator = oxide_parser::Allocator::default();
let source = std::fs::read_to_string(dir.join("main.mjs")).expect("read main");
let program = oxide_parser::parse_module(&allocator, &source).expect("parse module");
let module = Compiler::new()
.compile_module(&program, dir.join("main.mjs").to_string_lossy().as_ref(), &mut NsFileLoader)
.expect("compile module");
let mut vm = vm_with_low_threshold();
vm.run(&Arc::new(module)).expect("module run");
let ns_val = global_prop_opt(&vm, "__ns").expect("__ns 应挂在 global 上");
let ns_obj = unsafe { &*ns_val.as_js_object_ptr() };
let cell_ptr = module::module_ns_cell_edges(ns_obj)
.into_iter()
.next()
.expect("依赖 ns 应持有 x 的 cell");
assert!(vm.gc_state.session_cell_ptrs.borrow().contains(&cell_ptr), "cell 应登记在表");
vm.regs.fill(JsValue::undefined());
vm.regs[0] = ns_val;
collect(&mut vm);
// 只断言表成员——sweep 已释放的指针解引用即 UB。
assert!(
vm.gc_state.session_cell_ptrs.borrow().contains(&cell_ptr),
"模块 ns cell 应跨完整收集存活"
);
// 移动式 sweep 后 ns 晋升 session(原件出表),从 global 重取克隆读回。
let ns_val2 = global_prop_opt(&vm, "__ns").expect("__ns 应挂在 global 上");
let ns_obj2 = unsafe { &*ns_val2.as_js_object_ptr() };
assert!(module::module_ns_cell_edges(ns_obj2).contains(&cell_ptr), "克隆应共享同一 cell 指针");
let name_si = vm.kernel_core().perm_interner().intern("x").0;
let query = module::module_ns_export(ns_obj2, name_si).expect("x 应可导出");
match query {
module::ModuleNsQuery::Initialized(v) => {
assert_eq!(v, JsValue::int(42), "模块 ns cell 读回正确");
}
module::ModuleNsQuery::Uninitialized => panic!("x 应已初始化,实得 Uninitialized"),
}
}
#[test]
fn allocation_does_not_trigger_gc_before_safe_point() {
let mut vm = vm_with_threshold(1);
+5 -3
View File
@@ -36,7 +36,8 @@ pub(crate) struct GcState {
pub(crate) session_bigint_ptrs: RefCell<Vec<*mut num_bigint::BigInt>>,
/// upvalue cell(`Box<Cell>`)追踪表。`RefCell` 使 `&self` 的分配入口也能
/// 登记新 box。cell 独立堆分配、地址稳定,不参与对象搬移(sweep 只重写
/// `cell.value` 中的对象引用),只在 full_reset 统一释放。
/// `cell.value` 中的对象引用);参与 mark-sweep 回收(死 cell 随收集释放),
/// full_reset 为收尾兜底(表已空时 no-op)。
pub(crate) session_cell_ptrs: RefCell<Vec<*mut UpvalueCell>>,
pub(crate) session_bytes_allocated: usize,
/// 执行期 session 堆账目的峰值高水位(`session_bytes_allocated` 的采样上界),
@@ -82,8 +83,9 @@ impl GcState {
ptr
}
/// 释放全部 session 堆 upvalue cell box。仅在完全隔离重置(`full_reset`)时调用,
/// 此时没有存活的 cell_stack / 函数对象 upvalues 会引用它们。
/// 释放全部 session 堆 upvalue cell box(收尾兜底,幂等)。仅在完全隔离重置
/// (`full_reset`)时调用,此时没有存活的 cell_stack / 函数对象 upvalues 会引用
/// 它们;mark-sweep 已释放的死 cell 已出表,表内残留为收集后仍登记的存活 cell。
pub(crate) fn free_cells(&mut self) {
for ptr in self.session_cell_ptrs.borrow_mut().drain(..) {
// SAFETY: 每个指针来自 alloc_cell 的 Box::into_raw(Box::new(Cell)),