mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-25 19:21:11 +08:00
fix: Vm 增加 Drop 收尾释放全部 session 堆数据消除逐实例泄漏
test262 每测试新建 Vm 直接 drop,此前对象堆数据/串/BigInt/cell 全部 泄漏。新增 teardown 收尾函数(epoch+session 对象堆数据、串、BigInt、 cell 统一释放)供 full_reset 与 Drop 共用。连带补齐 promote 分支的 ArrayBuffer 字节缓冲深拷贝(与 sweep 对齐,消除共享所有权 double-free), 并修正依赖旧泄漏行为跨 Vm 解析字符串的 error 测试。
This commit is contained in:
@@ -6,7 +6,7 @@ use oxide_types::value::JsValue;
|
|||||||
use rustc_hash::FxBuildHasher;
|
use rustc_hash::FxBuildHasher;
|
||||||
|
|
||||||
use crate::vm::Vm;
|
use crate::vm::Vm;
|
||||||
use oxide_builtins::{data_view, map, set, typed_array};
|
use oxide_builtins::{array_buffer, data_view, map, set, typed_array};
|
||||||
|
|
||||||
impl Vm {
|
impl Vm {
|
||||||
pub(crate) fn is_session_escape_root_ptr(&self, target_ptr: *mut JsObject) -> bool {
|
pub(crate) fn is_session_escape_root_ptr(&self, target_ptr: *mut JsObject) -> bool {
|
||||||
@@ -65,6 +65,9 @@ impl Vm {
|
|||||||
data_view::clone_data_view_native_with_rewrite(src_ref, dst_ref, |value| {
|
data_view::clone_data_view_native_with_rewrite(src_ref, dst_ref, |value| {
|
||||||
self.promote_value_if_epoch_object(value, forwarding)
|
self.promote_value_if_epoch_object(value, forwarding)
|
||||||
});
|
});
|
||||||
|
} else if src_ref.is_array_buffer_obj() {
|
||||||
|
// 字节缓冲深拷贝到新对象:源 Vec 随 epoch 释放,互不共享。
|
||||||
|
array_buffer::clone_array_buffer_native(src_ref, dst_ref);
|
||||||
} else if src_ref.is_generator_obj() {
|
} else if src_ref.is_generator_obj() {
|
||||||
// 生成器状态盒深拷贝到新对象:源盒随 epoch 释放,互不共享。
|
// 生成器状态盒深拷贝到新对象:源盒随 epoch 释放,互不共享。
|
||||||
crate::generator::clone_generator_native_with_rewrite(src_ref, dst_ref, |value| {
|
crate::generator::clone_generator_native_with_rewrite(src_ref, dst_ref, |value| {
|
||||||
|
|||||||
@@ -578,10 +578,7 @@ impl SessionGc {
|
|||||||
live.clear();
|
live.clear();
|
||||||
Self::record_object_string_edges(&mut live, obj);
|
Self::record_object_string_edges(&mut live, obj);
|
||||||
for s_ptr in live.drain() {
|
for s_ptr in live.drain() {
|
||||||
assert!(
|
assert!(self.live_strings.contains(&s_ptr), "存活对象持有未登记的 session 字符串边");
|
||||||
self.live_strings.contains(&s_ptr),
|
|
||||||
"存活对象持有未登记的 session 字符串边"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -451,6 +451,14 @@ pub struct Vm {
|
|||||||
pub(crate) cell_stack: Vec<Vec<*mut Cell>>,
|
pub(crate) cell_stack: Vec<Vec<*mut Cell>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for Vm {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// 直接 drop(test262 每测试新建即弃)不经 reset/full_reset 路径:
|
||||||
|
// 统一收尾释放全部 session 堆数据,防逐测试累积泄漏。
|
||||||
|
self.teardown_session_heap_data();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Vm {
|
impl Vm {
|
||||||
const SYNC_NATIVE_ARG_BASE: usize = 0;
|
const SYNC_NATIVE_ARG_BASE: usize = 0;
|
||||||
const SYNC_NATIVE_ARG_LIMIT: usize = 253;
|
const SYNC_NATIVE_ARG_LIMIT: usize = 253;
|
||||||
|
|||||||
@@ -299,32 +299,34 @@ impl Vm {
|
|||||||
self.bytecode = Arc::default();
|
self.bytecode = Arc::default();
|
||||||
self.immutables_cache.clear();
|
self.immutables_cache.clear();
|
||||||
self.active_immutables = std::ptr::slice_from_raw_parts(std::ptr::null(), 0);
|
self.active_immutables = std::ptr::slice_from_raw_parts(std::ptr::null(), 0);
|
||||||
self.free_epoch_object_heap_data();
|
self.teardown_session_heap_data();
|
||||||
self.epoch.reset();
|
self.epoch.reset();
|
||||||
self.gc_state.epoch_object_ptrs.clear();
|
self.gc_state.epoch_object_ptrs.clear();
|
||||||
// 先释放 session 对象堆数据(属性向量 + 各原生盒),再重置 arena:
|
|
||||||
// 原生盒在 GC 搬移/晋升时已深拷贝为单所有权,此处恰好释放一次。
|
|
||||||
let mut freed = 0u64;
|
|
||||||
for ptr in self.gc_state.session_object_ptrs.drain(..) {
|
|
||||||
freed += crate::session_gc::SessionGc::drop_object_heap_data(ptr, true);
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
self.gc_state.session_epoch.reset();
|
self.gc_state.session_epoch.reset();
|
||||||
self.gc_state.session_bytes_allocated = 0;
|
self.gc_state.session_bytes_allocated = 0;
|
||||||
self.gc_state.string_gc_watermark = self.kernel_core.config().session_gc_threshold;
|
self.gc_state.string_gc_watermark = self.kernel_core.config().session_gc_threshold;
|
||||||
self.gc_state.session_gc = crate::session_gc::SessionGc::new();
|
self.gc_state.session_gc = crate::session_gc::SessionGc::new();
|
||||||
self.free_session_string_heap_data();
|
|
||||||
self.free_session_bigint_heap_data();
|
|
||||||
self.gc_state.free_cells();
|
|
||||||
self.symbols.reset();
|
self.symbols.reset();
|
||||||
self.root_reg_limit = 0;
|
self.root_reg_limit = 0;
|
||||||
self.active_reg_limit = 0;
|
self.active_reg_limit = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 释放全部 session 堆数据:epoch 对象堆数据 + session 对象堆数据 + session 串 +
|
||||||
|
/// BigInt box + upvalue cell box。
|
||||||
|
///
|
||||||
|
/// 供 `full_reset` 与 `Drop` 共用——对象本体(bumpalo arena / epoch bump)由调用方
|
||||||
|
/// 重置,本函数只释放手工管理的 Box 指针(属性向量、各原生盒、串、BigInt、cell)。
|
||||||
|
/// 原生盒在 GC 搬移/晋升时已深拷贝为单所有权,此处恰好释放一次。
|
||||||
|
pub(crate) fn teardown_session_heap_data(&mut self) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
self.free_session_string_heap_data();
|
||||||
|
self.free_session_bigint_heap_data();
|
||||||
|
self.gc_state.free_cells();
|
||||||
|
}
|
||||||
|
|
||||||
fn free_epoch_object_heap_data(&mut self) {
|
fn free_epoch_object_heap_data(&mut self) {
|
||||||
let mut freed = 0u64;
|
let mut freed = 0u64;
|
||||||
for ptr in self.gc_state.epoch_object_ptrs.drain(..) {
|
for ptr in self.gc_state.epoch_object_ptrs.drain(..) {
|
||||||
@@ -682,11 +684,14 @@ mod tests {
|
|||||||
match vm.resume_generator(gen, crate::generator::GeneratorResumeMode::Next(JsValue::undefined())) {
|
match vm.resume_generator(gen, crate::generator::GeneratorResumeMode::Next(JsValue::undefined())) {
|
||||||
Ok(crate::generator::GeneratorStep::Suspended { value }) => value,
|
Ok(crate::generator::GeneratorStep::Suspended { value }) => value,
|
||||||
Ok(crate::generator::GeneratorStep::Completed { value }) => value,
|
Ok(crate::generator::GeneratorStep::Completed { value }) => value,
|
||||||
Ok(other) => panic!("unexpected step: {:?}", match other {
|
Ok(other) => panic!(
|
||||||
crate::generator::GeneratorStep::Thrown { value } => format!("Thrown({value})"),
|
"unexpected step: {:?}",
|
||||||
crate::generator::GeneratorStep::SuspendedRaw { value } => format!("SuspendedRaw({value})"),
|
match other {
|
||||||
_ => String::new(),
|
crate::generator::GeneratorStep::Thrown { value } => format!("Thrown({value})"),
|
||||||
}),
|
crate::generator::GeneratorStep::SuspendedRaw { value } => format!("SuspendedRaw({value})"),
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
),
|
||||||
Err(e) => panic!("resume failed: {e}"),
|
Err(e) => panic!("resume failed: {e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -694,10 +699,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn generator_survives_object_sweep_and_resumes() {
|
fn generator_survives_object_sweep_and_resumes() {
|
||||||
let mut vm = vm_with_low_threshold();
|
let mut vm = vm_with_low_threshold();
|
||||||
let _ = run_source(
|
let _ = run_source(&mut vm, "function* g(){ yield 1; yield 2; } globalThis.it = g(); globalThis.it.next(); 0");
|
||||||
&mut vm,
|
|
||||||
"function* g(){ yield 1; yield 2; } globalThis.it = g(); globalThis.it.next(); 0",
|
|
||||||
);
|
|
||||||
|
|
||||||
// 直接触发完整收集(保留执行上下文):存活生成器克隆进新 arena,
|
// 直接触发完整收集(保留执行上下文):存活生成器克隆进新 arena,
|
||||||
// 状态盒深拷贝为新 Box(reset 会清空模块表使恢复不可行,走收集入口等价验证)。
|
// 状态盒深拷贝为新 Box(reset 会清空模块表使恢复不可行,走收集入口等价验证)。
|
||||||
@@ -1069,4 +1071,3 @@ mod tests {
|
|||||||
assert_eq!(vm.lookup_str(te).as_deref(), Some("TypeError"));
|
assert_eq!(vm.lookup_str(te).as_deref(), Some("TypeError"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ fn eval(source: &str) -> Result<JsValue, String> {
|
|||||||
vm.run(&module)
|
vm.run(&module)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 在既有 Vm 内编译执行:结果字符串须在同一 Vm 上下文解析(session 串随 Vm 释放)。
|
||||||
|
fn eval_in(vm: &mut Vm, source: &str) -> Result<JsValue, String> {
|
||||||
|
let allocator = oxide_parser::Allocator::default();
|
||||||
|
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse: {:?}", e))?;
|
||||||
|
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile: {}", e))?;
|
||||||
|
vm.run(&module)
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn error_constructor_creates_object() {
|
fn error_constructor_creates_object() {
|
||||||
let mut vm = make_vm();
|
let mut vm = make_vm();
|
||||||
@@ -286,24 +294,24 @@ fn error_to_string_basic() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn error_to_string_empty_message() {
|
fn error_to_string_empty_message() {
|
||||||
let result = eval("new Error().toString()").unwrap();
|
let mut vm = make_vm();
|
||||||
let vm = make_vm();
|
let result = eval_in(&mut vm, "new Error().toString()").unwrap();
|
||||||
let s = vm.lookup_str(result);
|
let s = vm.lookup_str(result);
|
||||||
assert_eq!(s, Some("Error".to_string()));
|
assert_eq!(s, Some("Error".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn error_to_string_type_error() {
|
fn error_to_string_type_error() {
|
||||||
let result = eval("new TypeError('bad').toString()").unwrap();
|
let mut vm = make_vm();
|
||||||
let vm = make_vm();
|
let result = eval_in(&mut vm, "new TypeError('bad').toString()").unwrap();
|
||||||
let s = vm.lookup_str(result);
|
let s = vm.lookup_str(result);
|
||||||
assert_eq!(s, Some("TypeError: bad".to_string()));
|
assert_eq!(s, Some("TypeError: bad".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn error_to_string_name_only() {
|
fn error_to_string_name_only() {
|
||||||
let result = eval("Error.prototype.toString.call({name: 'E'})").unwrap();
|
let mut vm = make_vm();
|
||||||
let vm = make_vm();
|
let result = eval_in(&mut vm, "Error.prototype.toString.call({name: 'E'})").unwrap();
|
||||||
let s = vm.lookup_str(result);
|
let s = vm.lookup_str(result);
|
||||||
assert_eq!(s, Some("E".to_string()));
|
assert_eq!(s, Some("E".to_string()));
|
||||||
}
|
}
|
||||||
@@ -355,16 +363,16 @@ fn error_stack_is_string() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn error_stack_starts_with_header() {
|
fn error_stack_starts_with_header() {
|
||||||
let result = eval("new Error().stack()").unwrap();
|
let mut vm = make_vm();
|
||||||
let vm = make_vm();
|
let result = eval_in(&mut vm, "new Error().stack()").unwrap();
|
||||||
let s = vm.lookup_str(result).unwrap();
|
let s = vm.lookup_str(result).unwrap();
|
||||||
assert!(s.starts_with("Error"), "stack should start with 'Error', got: {}", s);
|
assert!(s.starts_with("Error"), "stack should start with 'Error', got: {}", s);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn error_stack_frame_format() {
|
fn error_stack_frame_format() {
|
||||||
let result = eval("(function foo() { return new Error('boom').stack(); })()").unwrap();
|
let mut vm = make_vm();
|
||||||
let vm = make_vm();
|
let result = eval_in(&mut vm, "(function foo() { return new Error('boom').stack(); })()").unwrap();
|
||||||
let s = vm.lookup_str(result).unwrap();
|
let s = vm.lookup_str(result).unwrap();
|
||||||
assert!(s.contains(" at "), "stack should have 4-space indent, got: {}", s);
|
assert!(s.contains(" at "), "stack should have 4-space indent, got: {}", s);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ fn compile(source: &str) -> oxide_bytecode::module::CompiledModule {
|
|||||||
#[test]
|
#[test]
|
||||||
fn generator_promoted_to_global_survives_full_reset_cleanly() {
|
fn generator_promoted_to_global_survives_full_reset_cleanly() {
|
||||||
let mut vm = Vm::new();
|
let mut vm = Vm::new();
|
||||||
vm.run(&compile("function* g(){ yield 1; yield 2; } globalThis.it = g(); globalThis.it.next(); 0"))
|
vm.run(&compile(
|
||||||
.expect("run1");
|
"function* g(){ yield 1; yield 2; } globalThis.it = g(); globalThis.it.next(); 0",
|
||||||
|
))
|
||||||
|
.expect("run1");
|
||||||
assert!(vm.session_object_count() > 0, "生成器挂 global 应被 promote 进 session");
|
assert!(vm.session_object_count() > 0, "生成器挂 global 应被 promote 进 session");
|
||||||
|
|
||||||
// global 含 session 对象即强制重建:旧 global 丢弃、it 槽不存在,全程无悬垂访问
|
// global 含 session 对象即强制重建:旧 global 丢弃、it 槽不存在,全程无悬垂访问
|
||||||
@@ -31,6 +33,8 @@ fn generator_promoted_to_global_survives_full_reset_cleanly() {
|
|||||||
let text = vm.lookup_str(result).expect("typeof 应返回字符串").to_string();
|
let text = vm.lookup_str(result).expect("typeof 应返回字符串").to_string();
|
||||||
assert_eq!(text, "undefined");
|
assert_eq!(text, "undefined");
|
||||||
// 新会话可正常创建并推进生成器。
|
// 新会话可正常创建并推进生成器。
|
||||||
let ok = vm.run(&compile("function* h(){ yield 9; } var it2 = h(); it2.next().value")).expect("run3");
|
let ok = vm
|
||||||
|
.run(&compile("function* h(){ yield 9; } var it2 = h(); it2.next().value"))
|
||||||
|
.expect("run3");
|
||||||
assert_eq!(format!("{ok}"), "9");
|
assert_eq!(format!("{ok}"), "9");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user