diff --git a/crates/oxide_cli/src/bench/leak_detect.rs b/crates/oxide_cli/src/bench/leak_detect.rs index 08056def..f1864108 100644 --- a/crates/oxide_cli/src/bench/leak_detect.rs +++ b/crates/oxide_cli/src/bench/leak_detect.rs @@ -306,6 +306,74 @@ pub fn run_mem_dirty_rebuild_leak(kernel: &Arc) -> ExitCode { 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) -> 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 样本)总 /// 增量;斜率显著为正(R² > 0.9)返回失败,表示泄漏签名。 fn report_series(case: &str, unit: &str, series: &[(usize, f64)]) -> ExitCode { diff --git a/crates/oxide_cli/src/bench/mod.rs b/crates/oxide_cli/src/bench/mod.rs index 9d0bb608..2bad556b 100644 --- a/crates/oxide_cli/src/bench/mod.rs +++ b/crates/oxide_cli/src/bench/mod.rs @@ -40,6 +40,7 @@ pub fn run_benchmarks(config: BenchConfig, kernel: Arc, pool: Arc match config.filter.as_deref() { Some("vm_creation") => leak_detect::run_mem_vm_creation_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), }, _ => { diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index e9a66284..d2f5d31d 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -484,8 +484,28 @@ impl SessionGc { 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 { - Self::drop_session_object_heap_data(obj_ptr) + size_of::() as u64 + let mut freed = Self::drop_session_object_heap_data(obj_ptr) + size_of::() 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::>() 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` diff --git a/crates/oxide_vm/tests/gc_runtime_tests.rs b/crates/oxide_vm/tests/gc_runtime_tests.rs index fe102a2c..7bc40b63 100644 --- a/crates/oxide_vm/tests/gc_runtime_tests.rs +++ b/crates/oxide_vm/tests/gc_runtime_tests.rs @@ -259,3 +259,47 @@ fn closure_cells_freed_by_full_reset_without_double_free() { let text = vm.lookup_str(result).expect("拼接结果应为字符串").to_string(); 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"); +}