mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
fix: upvalue cell 参与 mark-sweep 回收,闭合长 run 死闭包 cell 泄漏
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user