mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-27 04:01:11 +08:00
fix: sweep 死对象分支释放死闭包 upvalue 列表 Box(死对象不克隆、Box 独占,恰好一次释放后置空防收尾双放),配 mem_closure_dead_leak RSS 斜率校准用例
This commit is contained in:
@@ -306,6 +306,74 @@ pub fn run_mem_dirty_rebuild_leak(kernel: &Arc<KernelCore>) -> ExitCode {
|
|||||||
report_series("dirty_rebuild", "round", &series)
|
report_series("dirty_rebuild", "round", &series)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 校准用例:循环「创建 3-upvalue 闭包 → 写 global 临时槽(晋升 session)→
|
||||||
|
/// 撤根引用 → 完整收集(死分支)」,每轮产生一个死闭包,每 500 轮采一次
|
||||||
|
/// VmRSS,度量死闭包 upvalue 列表的释放路径,期望斜率走平。
|
||||||
|
///
|
||||||
|
/// # 注意事项
|
||||||
|
/// 死对象的 upvalue 列表 Box 无其他持有者(仅存活对象被克隆并共享同一 Box
|
||||||
|
/// 与原件):死分支不释放则死亡即永久泄漏——账目/peak/retained 全程无感
|
||||||
|
/// (死对象死亡即出表),唯一可观测量是进程 RSS。upvalue cell 的寿命绑定
|
||||||
|
/// full_reset,逐轮累积会掩盖斜率,故每窗口换新 VM,窗口边界随 teardown
|
||||||
|
/// 统一释放(兼覆盖收尾统一释放的不双放守卫)。
|
||||||
|
pub fn run_mem_closure_dead_leak(kernel: &Arc<KernelCore>) -> ExitCode {
|
||||||
|
const ROUNDS_PER_WINDOW: usize = 5000;
|
||||||
|
const WINDOWS: usize = 20;
|
||||||
|
const SAMPLE_EVERY: usize = 500;
|
||||||
|
|
||||||
|
let allocator = Allocator::default();
|
||||||
|
let mut modules = Vec::new();
|
||||||
|
for js in [
|
||||||
|
"globalThis.c = (function() { var a = 1; var b = 2; var c = 3; return function() { return a + b + c; }; })(); 0",
|
||||||
|
"globalThis.c = undefined; 0",
|
||||||
|
] {
|
||||||
|
let program = match oxide_parser::parse(&allocator, js) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(_) => return ExitCode::FAILURE,
|
||||||
|
};
|
||||||
|
let compiler = Compiler::new();
|
||||||
|
let hash = compiled_module_hash(&program);
|
||||||
|
let module = match kernel.code_forge().get_or_insert_with(hash, || compiler.compile(&program)) {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => return ExitCode::FAILURE,
|
||||||
|
};
|
||||||
|
modules.push(module);
|
||||||
|
}
|
||||||
|
let (create, unroot) = (&modules[0], &modules[1]);
|
||||||
|
|
||||||
|
let mut sampler = LeakSampler::new(20);
|
||||||
|
let mut series: Vec<(usize, f64)> = Vec::new();
|
||||||
|
for window in 0..WINDOWS {
|
||||||
|
let mut vm = Vm::with_kernel_core(Arc::clone(kernel));
|
||||||
|
for round in 0..ROUNDS_PER_WINDOW {
|
||||||
|
if let Err(e) = vm.run(create) {
|
||||||
|
eprintln!("[closure_dead_leak] window {window} round {round} create run failed: {e}");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
vm.collect_session_gc();
|
||||||
|
if let Err(e) = vm.run(unroot) {
|
||||||
|
eprintln!("[closure_dead_leak] window {window} round {round} unroot run failed: {e}");
|
||||||
|
return ExitCode::FAILURE;
|
||||||
|
}
|
||||||
|
vm.collect_session_gc();
|
||||||
|
let i = window * ROUNDS_PER_WINDOW + round;
|
||||||
|
if i % SAMPLE_EVERY == 0 {
|
||||||
|
if let Some(kb) = read_vmrss_kb() {
|
||||||
|
let v = kb as f64;
|
||||||
|
series.push((i, v));
|
||||||
|
if let Some(verdict) = sampler.add_sample(i, v) {
|
||||||
|
eprintln!(
|
||||||
|
"[LEAK] closure_dead_leak rss_kb: slope={:.6} R²={:.4} over {} samples",
|
||||||
|
verdict.slope, verdict.r2, 20
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
report_series("closure_dead_leak", "iter", &series)
|
||||||
|
}
|
||||||
|
|
||||||
/// 全序列回归报告:打印首末 RSS、全序列斜率/R² 与窗口(末 20 样本)总
|
/// 全序列回归报告:打印首末 RSS、全序列斜率/R² 与窗口(末 20 样本)总
|
||||||
/// 增量;斜率显著为正(R² > 0.9)返回失败,表示泄漏签名。
|
/// 增量;斜率显著为正(R² > 0.9)返回失败,表示泄漏签名。
|
||||||
fn report_series(case: &str, unit: &str, series: &[(usize, f64)]) -> ExitCode {
|
fn report_series(case: &str, unit: &str, series: &[(usize, f64)]) -> ExitCode {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ pub fn run_benchmarks(config: BenchConfig, kernel: Arc<KernelCore>, pool: Arc<Vm
|
|||||||
"leak" => match config.filter.as_deref() {
|
"leak" => match config.filter.as_deref() {
|
||||||
Some("vm_creation") => leak_detect::run_mem_vm_creation_leak(&kernel),
|
Some("vm_creation") => leak_detect::run_mem_vm_creation_leak(&kernel),
|
||||||
Some("dirty_rebuild") => leak_detect::run_mem_dirty_rebuild_leak(&kernel),
|
Some("dirty_rebuild") => leak_detect::run_mem_dirty_rebuild_leak(&kernel),
|
||||||
|
Some("closure_dead_leak") => leak_detect::run_mem_closure_dead_leak(&kernel),
|
||||||
_ => leak_detect::run_leak_detect(&config, &kernel, &pool),
|
_ => leak_detect::run_leak_detect(&config, &kernel, &pool),
|
||||||
},
|
},
|
||||||
_ => {
|
_ => {
|
||||||
|
|||||||
@@ -484,8 +484,28 @@ impl SessionGc {
|
|||||||
Self::drop_object_heap_data(obj_ptr, true)
|
Self::drop_object_heap_data(obj_ptr, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 释放一个死 session 对象:对象本体 + 堆数据 + upvalue 列表,返回释放字节数。
|
||||||
|
///
|
||||||
|
/// # 注意事项
|
||||||
|
/// 仅 sweep 死分支到达此处:只有存活对象被克隆、克隆与原件共享同一 upvalues
|
||||||
|
/// Box,死对象从不被克隆,其 upvalue 列表 Box 无其他持有者,在此恰好释放
|
||||||
|
/// 一次;存活分支绝不释放(克隆仍引用同一 Box)。释放后置空:死对象已移出
|
||||||
|
/// 对象表,收尾统一释放按表枚举不会再见,置空保证对象侧幂等、无陈旧指针。
|
||||||
fn drop_dead_session_object(obj_ptr: *mut JsObject) -> u64 {
|
fn drop_dead_session_object(obj_ptr: *mut JsObject) -> u64 {
|
||||||
Self::drop_session_object_heap_data(obj_ptr) + size_of::<JsObject>() as u64
|
let mut freed = Self::drop_session_object_heap_data(obj_ptr) + size_of::<JsObject>() as u64;
|
||||||
|
// SAFETY: obj_ptr 来自 session 对象表,sweep 期间仍指向旧 arena 内合法对象;
|
||||||
|
// upvalues Box 仅本对象持有(死对象不克隆),保证恰好释放一次。
|
||||||
|
unsafe {
|
||||||
|
let up = (*obj_ptr).upvalues;
|
||||||
|
if !up.is_null() {
|
||||||
|
let vec = Box::from_raw(up as *mut Vec<*mut oxide_types::object::Cell>);
|
||||||
|
freed += size_of::<Vec<*mut oxide_types::object::Cell>>() as u64
|
||||||
|
+ (vec.capacity() * size_of::<*mut oxide_types::object::Cell>()) as u64;
|
||||||
|
(*obj_ptr).upvalues = std::ptr::null_mut();
|
||||||
|
std::mem::drop(vec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
freed
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 释放一个已死 session `JsString`(由 `Vm::new_string`/`new_cons_string`
|
/// 释放一个已死 session `JsString`(由 `Vm::new_string`/`new_cons_string`
|
||||||
|
|||||||
@@ -259,3 +259,47 @@ fn closure_cells_freed_by_full_reset_without_double_free() {
|
|||||||
let text = vm.lookup_str(result).expect("拼接结果应为字符串").to_string();
|
let text = vm.lookup_str(result).expect("拼接结果应为字符串").to_string();
|
||||||
assert_eq!(text, "ok!");
|
assert_eq!(text, "ok!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 死闭包的 upvalue 列表在对象 sweep 死分支释放(死对象不克隆、Box 无其他
|
||||||
|
/// 持有者,恰好一次)后,收尾统一释放见已置空字段不双放:full_reset 后引擎
|
||||||
|
/// 可继续运行并重建新闭包;存活闭包走存活分支(克隆与原件共享 Box),
|
||||||
|
/// 不受死分支影响。
|
||||||
|
#[test]
|
||||||
|
fn dead_closure_upvalues_freed_by_sweep_without_double_free() {
|
||||||
|
let mut vm = vm_with_threshold(4096);
|
||||||
|
// run1:两个 3-upvalue 闭包逃逸 global(成 session 对象),同 run 内调用
|
||||||
|
// 存活闭包确认 upvalue 语义完好。
|
||||||
|
let first = compile(
|
||||||
|
"var a = 1; var b = 2; var c = 3; \
|
||||||
|
globalThis.dead = function() { return a + b + c; }; \
|
||||||
|
globalThis.live = function() { return a + b + c; }; globalThis.live()",
|
||||||
|
);
|
||||||
|
let result = vm.run(&first).expect("run1");
|
||||||
|
assert_eq!(format!("{}", result), "6");
|
||||||
|
|
||||||
|
// run2:撤销死闭包的根引用(不可达)。
|
||||||
|
let second = compile("globalThis.dead = undefined; 0");
|
||||||
|
vm.run(&second).expect("run2");
|
||||||
|
|
||||||
|
// 完整收集:死闭包走死分支(释放 upvalue 列表),存活闭包走存活分支
|
||||||
|
// (克隆与原件共享 Box,不在死分支释放)。
|
||||||
|
vm.collect_session_gc();
|
||||||
|
assert!(vm.session_gc_stats().last_collection_objects_dead >= 1, "死闭包应经 sweep 死分支");
|
||||||
|
|
||||||
|
// 跨 run 存活核(只读属性;跨 run 调用受 sub_module_index 缺口限制):
|
||||||
|
// sweep 后存活闭包克隆仍挂在 global 上。
|
||||||
|
let third = compile("typeof globalThis.live === 'function'");
|
||||||
|
let result = vm.run(&third).expect("run3");
|
||||||
|
assert!(result.is_bool() && result.as_bool());
|
||||||
|
|
||||||
|
// 收尾统一 upvalue 释放与死分支不双放(字段已置空、死对象已出表)。
|
||||||
|
vm.full_reset();
|
||||||
|
|
||||||
|
// 重置后引擎健康:重建新 3-upvalue 闭包,同 run 调用语义正确。
|
||||||
|
let fourth = compile(
|
||||||
|
"var x = 10; var y = 20; var z = 30; \
|
||||||
|
globalThis.f = function() { return x + y + z; }; globalThis.f()",
|
||||||
|
);
|
||||||
|
let result = vm.run(&fourth).expect("run4");
|
||||||
|
assert_eq!(format!("{}", result), "60");
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user