use std::collections::VecDeque; use std::process::ExitCode; use std::sync::Arc; use std::time::Instant; use oxide_compiler::compiler::{compiled_module_hash, Compiler}; use oxide_kernel::kernel::{KernelConfig, KernelCore}; use oxide_parser::Allocator; use oxide_types::value::JsValue; use oxide_vm::vm::Vm; use oxide_vm::vm_pool::VmPool; use crate::bench::metrics::MetricCollection; use crate::bench::output::format_json; use crate::bench::BenchConfig; /// 滑动窗口采样器:对 `(iteration, value)` 序列做线性回归, /// 当斜率明显为正且拟合度 R² 高时判定为内存泄漏。 pub struct LeakSampler { window: VecDeque<(usize, f64)>, window_size: usize, } impl LeakSampler { /// 以给定窗口大小创建采样器。 pub fn new(window_size: usize) -> Self { Self { window: VecDeque::new(), window_size, } } /// 加入一次采样;样本足够且回归显著时返回泄漏判定。 pub fn add_sample(&mut self, iteration: usize, value: f64) -> Option { self.window.push_back((iteration, value)); if self.window.len() >= self.window_size { self.window.pop_front(); } if self.window.len() < 10 { return None; } let points: Vec<(usize, f64)> = self.window.iter().copied().collect(); let (slope, r2) = linreg(&points); if r2 > 0.9 && slope > 0.0 { Some(LeakVerdict { slope, r2 }) } else { None } } } /// 泄漏判定的回归结果:线性斜率和拟合优度 R²。 pub struct LeakVerdict { pub slope: f64, pub r2: f64, } /// 对 `(iteration, value)` 序列做普通最小二乘,返回 `(slope, R²)`。 /// /// # 边界与前提 /// - 序列零方差时 R² 记 1.0(退化为完美拟合,斜率 0 不触发泄漏判定); /// - 少于 2 个样本返回 `(0, 0)`。 fn linreg(points: &[(usize, f64)]) -> (f64, f64) { let n = points.len() as f64; if n < 2.0 { return (0.0, 0.0); } let sum_x: f64 = points.iter().map(|(i, _)| *i as f64).sum(); let sum_y: f64 = points.iter().map(|(_, v)| *v).sum(); let sum_xy: f64 = points.iter().map(|(i, v)| *i as f64 * v).sum(); let sum_x2: f64 = points.iter().map(|(i, _)| (*i as f64).powi(2)).sum(); let slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x * sum_x); let intercept = (sum_y - slope * sum_x) / n; let y_mean = sum_y / n; let ss_res: f64 = points.iter().map(|(i, v)| (v - (slope * *i as f64 + intercept)).powi(2)).sum(); let ss_tot: f64 = points.iter().map(|(_, v)| (v - y_mean).powi(2)).sum(); let r2 = if ss_tot == 0.0 { 1.0 } else { 1.0 - ss_res / ss_tot }; (slope, r2) } /// 读取当前进程驻留集大小(`/proc/self/status` 的 VmRSS 行,单位 KB)。 /// /// # 边界与前提 /// 读取或解析失败(非 Linux 宿主、status 文件异常)返回 `None`。 pub fn read_vmrss_kb() -> Option { let status = std::fs::read_to_string("/proc/self/status").ok()?; for line in status.lines() { if let Some(rest) = line.strip_prefix("VmRSS:") { let kb = rest.split_whitespace().next()?; return kb.parse().ok(); } } None } // glibc 堆归页入口:把堆顶连续空闲页全部还给 OS(碎片化的内部空闲块 // 不受影响);仅 Linux/glibc 宿主链接,其余平台空操作。 #[cfg(target_os = "linux")] extern "C" { fn malloc_trim(pad: usize) -> bool; } /// 把上轮校准释放的堆页归还 OS,消除同进程复跑时"上轮残留页被下轮 /// 增长相复用"导致的第二复跑 RSS 斜率结构性低估。 fn release_free_heap() { #[cfg(target_os = "linux")] { // SAFETY: 无参无指针状态的 libc 调用,无别名与生命周期问题。 unsafe { let _ = malloc_trim(0); } } } /// 反复运行固定 JS 脚本,跟踪 session/code-forge/symbol 等内存指标, /// 用 `LeakSampler` 回归检测随迭代次数增长的内存占用。 pub fn run_leak_detect(config: &BenchConfig, kernel: &Arc, pool: &Arc) -> ExitCode { let js = "var ITERATIONS = 1000; var obj = {}; for (var i = 0; i < ITERATIONS; i++) { obj['key' + i] = i; } obj['key0']"; let allocator = Allocator::default(); 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, }; let mut samplers: Vec<(&str, LeakSampler)> = vec![ ("session_objects", LeakSampler::new(20)), ("session_bytes", LeakSampler::new(20)), ("code_forge_entries", LeakSampler::new(20)), ("symbol_registry", LeakSampler::new(20)), ]; let mut results: Vec = Vec::new(); for i in 0..config.iterations as usize { let start = Instant::now(); let mut guard = pool.spawn(); let vm = guard.vm_mut(); let _ = vm.run(&module); let elapsed = start.elapsed(); for (name, sampler) in &mut samplers { let value: f64 = match *name { "session_objects" => vm.session_object_count() as f64, "session_bytes" => vm.session_bytes_allocated() as f64, "code_forge_entries" => kernel.code_forge().len() as f64, "symbol_registry" => vm.symbol_registry_len() as f64, _ => continue, }; if let Some(verdict) = sampler.add_sample(i, value) { eprintln!( "[LEAK] {}: slope={:.6} R²={:.4} over {} samples", name, verdict.slope, verdict.r2, sampler.window.len(), ); } } if i == config.iterations as usize - 1 { let guard = pool.spawn(); let vm = guard.vm(); results.push(MetricCollection { test_name: "leak_detect".to_string(), wall_time_us: elapsed.as_micros() as u64, session_objects: vm.session_object_count(), session_bytes: vm.session_bytes_allocated(), epoch_objects: vm.epoch_object_count(), epoch_bytes: 0, gc_trigger_count: vm.session_gc_stats().total_collections, gc_bytes_freed: vm.session_gc_stats().last_collection_bytes_freed, gc_objects_scanned: vm.session_gc_stats().last_collection_objects_scanned, gc_collection_us: vm.session_gc_stats().last_collection_duration_us, instruction_count: vm.instruction_count(), compile_time_us: 0, exec_time_us: 0, ic_hit_rate: vm.ic_hit_rate(), ic_hits: vm.ic_hit_count(), ic_misses: vm.ic_miss_count(), peak_bytes: vm.session_bytes_peak() as u64, retained_bytes: vm.session_bytes_allocated() as u64, retained_objects: vm.session_object_count() as u64, }); } } let json = format_json(&results); println!("{}", json); ExitCode::SUCCESS } /// vm_creation 校准用例:共享 kernel 下循环 3000 次「新建 VM → 跑微源 → drop」, /// 每 100 次采一次 VmRSS,度量 VM 创建/销毁路径(含 session 收尾统一 /// 释放)的内存增量,期望走平。 /// /// # 注意事项 /// 不走 VM 池:池的锁/condvar 噪音与本用例观测面无关;出斜率先查 harness 污染 /// (基准脚手架自身引入的测量噪声,如 allocator 归还 OS 延迟),再疑收尾释放不全。 pub fn run_mem_vm_creation_leak(kernel: &Arc) -> ExitCode { const ITERATIONS: usize = 3000; const SAMPLE_EVERY: usize = 100; let js = "1 + 1"; let allocator = Allocator::default(); 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, }; let mut sampler = LeakSampler::new(20); let mut series: Vec<(usize, f64)> = Vec::new(); for i in 0..ITERATIONS { { let mut vm = Vm::with_kernel_core(Arc::clone(kernel)); if let Err(e) = vm.run(&module) { eprintln!("[vm_creation] iteration {i} run failed: {e}"); return ExitCode::FAILURE; } } 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] vm_creation rss_kb: slope={:.6} R²={:.4} over {} samples", verdict.slope, verdict.r2, 20 ); } } } } report_series("vm_creation", "iter", &series) } /// 校准用例:每 VM BuiltinWorld 构造成本测量基座。口径覆盖完整 /// `with_kernel_core` 构造链(`KernelSession::new` + `init_kernel_builtins` /// 全量绑定 + 内建 intrinsic 重挂):构造-释放轮(N 次构造、用完即弃, /// 度量每次构造 wall time 均值)+ 保留段(逐个 spawn 并保活、每 spawn 采 /// 一次 VmRSS,斜率 = 每保留 VM 的边际常驻,池规模放大锚)。 /// /// # 注意事项 /// 保留段正斜率即预期签名(每保留 VM 钉住一份 world 副本),独立打印 /// 逻辑,不进 `report_series` 泄漏判据;保留段前 `release_free_heap` 把 /// 释放轮滞留页归还 OS,免复用导致斜率结构性低估。构造-释放轮均值为 /// 热态口径(共享 kernel intern 缓存命中占主导)。 pub fn run_mem_builtin_world_build(kernel: &Arc) -> ExitCode { const CONSTRUCTIONS: usize = 1000; const RETAINED: usize = 400; // 构造-释放轮:只构造不 run,仅计时不采 RSS(释放后分配器滞留页无观测意义)。 let t0 = Instant::now(); for _ in 0..CONSTRUCTIONS { let _vm = Vm::with_kernel_core(Arc::clone(kernel)); } let total = t0.elapsed(); eprintln!( "[builtin_world_build] construct-release: n={CONSTRUCTIONS} total={:.3}s mean={:.4} ms/VM", total.as_secs_f64(), total.as_secs_f64() * 1000.0 / CONSTRUCTIONS as f64 ); // 释放轮滞留页归还 OS,保留段增长相不得复用(斜率低估源)。 release_free_heap(); // 保留段:逐个 spawn 保活,每 spawn 采一次 VmRSS,斜率 = 每保留 VM 边际常驻。 let mut series: Vec<(usize, f64)> = Vec::new(); if let Some(kb) = read_vmrss_kb() { series.push((0, kb as f64)); } let mut retained = Vec::with_capacity(RETAINED); for i in 0..RETAINED { retained.push(Vm::with_kernel_core(Arc::clone(kernel))); if let Some(kb) = read_vmrss_kb() { series.push((i + 1, kb as f64)); } } let (slope, r2) = linreg(&series); let (rss_first, rss_last) = (series.first().map(|(_, v)| *v), series.last().map(|(_, v)| *v)); eprintln!( "[builtin_world_build] retained: n={RETAINED} rss_kb={}..{} (delta {:+.0}) slope={:.4} kB/VM (~{:.0} B/VM) R²={:.4} [info: 正斜率即预期签名,不进 report_series 判据]", rss_first.unwrap_or(0.0), rss_last.unwrap_or(0.0), rss_last.unwrap_or(0.0) - rss_first.unwrap_or(0.0), slope, slope * 1024.0, r2 ); drop(retained); ExitCode::SUCCESS } /// dirty_rebuild 校准用例:单 VM 每轮「新键写脏 object/array/string 三家族原型 → /// full_reset」,共 3000 轮,每 10 轮采一次 VmRSS,输出选择性重建释放 /// 路径的每轮内存增量(斜率 + 窗口总增量)。 /// /// # 注意事项 /// 键名逐轮递增保证走新键写入路径(既有槽位写不 bump 世代、不脏家族)。 /// 跨轮继承锚点(登记表对象数 / global 属性槽数)采样自每轮 full_reset /// 后:wrapper 复用与槽位原位更新生效时两者应持平,增长即泄漏签名。 /// RSS 残差模型:object/function 家族脏重建每轮钉住 4 件本体(重指遗漏 /// 兜底,不进门)+ 逐轮唯一键在共享内核 shape/perm 缓存的追加式增长 /// (append-only 缓存,非 session 数据泄漏)。 pub fn run_mem_dirty_rebuild_leak(kernel: &Arc) -> ExitCode { const ROUNDS: usize = 3000; const SAMPLE_EVERY: usize = 10; let mut vm = Vm::with_kernel_core(Arc::clone(kernel)); let compiler = Compiler::new(); let mut sampler = LeakSampler::new(20); let mut series: Vec<(usize, f64)> = Vec::new(); // 跨轮继承锚点(登记表对象数 / global 属性槽数):wrapper 复用与槽位 // 原位更新生效时两者应跨轮持平,增长即泄漏的直接签名(无页粒度噪音)。 let mut first_anchor: Option<(usize, usize)> = None; let mut last_anchor: (usize, usize) = (0, 0); for round in 0..ROUNDS { // 键名逐轮递增:新键写才 bump 原型世代,重建才有脏家族可换。 let source = format!( "Object.prototype['d{round}'] = 1; Array.prototype['d{round}'] = 2; String.prototype['d{round}'] = 3;" ); let allocator = Allocator::default(); let program = match oxide_parser::parse(&allocator, &source) { Ok(p) => p, Err(_) => return ExitCode::FAILURE, }; let module = match compiler.compile(&program) { Ok(m) => m, Err(e) => { eprintln!("[dirty_rebuild] round {round} compile failed: {e}"); return ExitCode::FAILURE; } }; if let Err(e) = vm.run(&Arc::new(module)) { eprintln!("[dirty_rebuild] round {round} run failed: {e}"); return ExitCode::FAILURE; } vm.full_reset(); if round % SAMPLE_EVERY == 0 { let registry = vm.session().builtin_world().leaked_object_count(); let global_slots = unsafe { (*vm.session().global_object().as_ptr()).prop_vec_len() }; if first_anchor.is_none() { first_anchor = Some((registry, global_slots)); } last_anchor = (registry, global_slots); if let Some(kb) = read_vmrss_kb() { let v = kb as f64; series.push((round, v)); if let Some(verdict) = sampler.add_sample(round, v) { eprintln!( "[LEAK] dirty_rebuild rss_kb: slope={:.6} R²={:.4} over {} samples", verdict.slope, verdict.r2, 20 ); } } } } if let Some((r0, g0)) = first_anchor { let (r1, g1) = last_anchor; eprintln!( "[dirty_rebuild] anchors: registry {} -> {} ({:+}) global_slots {} -> {} ({:+})", r0, r1, r1 as i64 - r0 as i64, g0, g1, g1 as i64 - g0 as i64 ); } 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) } /// 校准用例:重源单次 run 后 `full_reset()`,读 session 账目锚 /// (`session_bytes_allocated`,全量重置清表归零后恰 0),再 50 轮轻源 /// (每轮 `full_reset()`)采 VmRSS,度量 full_reset 归还路径的跨轮内存 /// 增量(斜率 + 窗口总增量),期望走平。 /// /// # 注意事项 /// 不走 VM 池:drop clean 路径与池内归还走同一 `full_reset`,池锁/condvar /// 噪音与本测面无关。重源瞬时对象不逃逸 global,全量重置后 session 账目 /// 清零;锚 == 0 为确定性主门槛(in-engine,免分配器噪音),RSS 序列为次锚 /// (进程级辅助指标,峰后无新增量——归还 chunk 可能滞留分配器自由链不回 /// OS,不要求回落峰前)。 pub fn run_mem_pool_high_water(kernel: &Arc) -> ExitCode { const LIGHT_ROUNDS: usize = 50; let heavy_js = "var t = 0; for (var i = 0; i < 200000; i++) { var o = { s: 'abcdefghij' + i, a: [i, i + 1, i + 2] }; t += o.s.length + o.a.length; } t"; let light_js = "1 + 1"; let compile_one = |js: &str| { let allocator = Allocator::default(); let program = oxide_parser::parse(&allocator, js).expect("parse failed"); let hash = compiled_module_hash(&program); kernel .code_forge() .get_or_insert_with(hash, || Compiler::new().compile(&program)) .expect("compile failed") }; let heavy = compile_one(heavy_js); let light = compile_one(light_js); let mut vm = Vm::with_kernel_core(Arc::clone(kernel)); if let Err(e) = vm.run(&heavy) { eprintln!("[pool_high_water] heavy run failed: {e}"); return ExitCode::FAILURE; } vm.full_reset(); let anchor = vm.session_bytes_allocated(); eprintln!("[pool_high_water] anchor after full_reset: {anchor} bytes (expect 0)"); if anchor != 0 { return ExitCode::FAILURE; } let mut sampler = LeakSampler::new(20); let mut series: Vec<(usize, f64)> = Vec::new(); for i in 0..LIGHT_ROUNDS { if let Err(e) = vm.run(&light) { eprintln!("[pool_high_water] round {i} run failed: {e}"); return ExitCode::FAILURE; } vm.full_reset(); 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] pool_high_water rss_kb: slope={:.6} R²={:.4} over {} samples", verdict.slope, verdict.r2, 20 ); } } } report_series("pool_high_water", "round", &series) } /// 校准用例:单次 run(无 full_reset)跑 N 轮「闭包 + 对象 + 数组」churn /// 源,度量执行期死对象滞留的峰值。主锚 = in-engine 分配包络高水位 /// (`run_alloc_peak`,dispatch 循环顶 O(1) 采样);次锚 = RSS 序列 /// (块边界采样,单 run 内上升即 churn 本体,仅记录形态不做泄漏判定); /// 参考 = session/epoch 对象计数与留存账目。 /// /// # 注意事项 /// 不走 VM 池、不接 full_reset:churn 峰值是 run 内量。源分 CHUNKS 块连续 /// run(块间无 reset,arena/对象表/账目跨块累积,等价单次 run),RSS 在 /// 块边界采样以获得序列形态。降幅验收走宿内 pre/post 对照(宿主安静 /// 时点),跨宿主绝对比较禁止。 pub fn run_mem_object_churn_peak(kernel: &Arc) -> ExitCode { const N: usize = 500_000; const CHUNKS: usize = 8; const M: usize = N / CHUNKS; // 每轮对 t 的贡献 = f() + o.a + o.b.length = 2i + 2;Σ(i=0..M) = M² + M。 const EXPECTED: u64 = (M as u64) * (M as u64) + (M as u64); let js = format!( "var t = 0; for (var i = 0; i < {M}; i++) {{ var f = function() {{ return i; }}; var o = {{ a: i, b: [i, i + 1] }}; t += f() + o.a + o.b.length; }} t === {EXPECTED}" ); let allocator = Allocator::default(); let program = match oxide_parser::parse(&allocator, &js) { Ok(p) => p, Err(_) => return ExitCode::FAILURE, }; let hash = compiled_module_hash(&program); let module = match kernel .code_forge() .get_or_insert_with(hash, || Compiler::new().compile(&program)) { Ok(m) => m, Err(_) => return ExitCode::FAILURE, }; let mut vm = Vm::with_kernel_core(Arc::clone(kernel)); let mut series: Vec<(usize, f64)> = Vec::new(); if let Some(kb) = read_vmrss_kb() { series.push((0, kb as f64)); } for c in 0..CHUNKS { let result = match vm.run(&module) { Ok(v) => v, Err(e) => { eprintln!("[object_churn_peak] chunk {c} run failed: {e}"); return ExitCode::FAILURE; } }; if result != JsValue::bool(true) { eprintln!("[object_churn_peak] chunk {c} result mismatch: {result:?}"); return ExitCode::FAILURE; } if let Some(kb) = read_vmrss_kb() { series.push((c + 1, kb as f64)); } } let (slope, r2) = linreg(&series); let (rss_first, rss_last) = (series.first().map(|(_, v)| *v), series.last().map(|(_, v)| *v)); eprintln!( "[object_churn_peak] N={} chunks={} run_alloc_peak={} bytes (主锚)", N, CHUNKS, vm.run_alloc_peak() ); eprintln!( "[object_churn_peak] session_bytes_allocated={} session_bytes_peak={} session_objects={} epoch_objects={} gc_cycles={}", vm.session_bytes_allocated(), vm.session_bytes_peak(), vm.session_object_count(), vm.epoch_object_count(), vm.session_gc_stats().total_collections ); eprintln!( "[object_churn_peak] rss_kb={}..{} (delta {:+.0}) slope={:.4} kB/chunk R²={:.4} (单 run 内上升 = churn 本体,不做泄漏判定)", rss_first.unwrap_or(0.0), rss_last.unwrap_or(0.0), rss_last.unwrap_or(0.0) - rss_first.unwrap_or(0.0), slope, r2 ); ExitCode::SUCCESS } /// 校准用例:长寿命 kernel 寿命旋钮端到端 + PermInterner 增长斜率首测 /// 基线。每复跑一轮:专用 kernel(重建阈值 `Some(200_000 / 2)`,不污染 /// bench 共享 kernel)→ JS 固定段 8192 对象(计算键写入,仅 intern 不 /// 物化)→ 191_704 个唯一 14 字节键 intern(每 4 次物化一次,模拟 /// `perm_string` = intern + 物化同式,物化比例 ≈1/4;键长 14 字节贴合 /// 生产字面量短串面)→ 11 采样点 [10k,15k,20k,30k,40k,60k,80k,100k, /// 131072,160k,200k] 采 VmRSS。增长相斜率只记录不判定(append-only /// 增长,正斜率即预期签名,"斜率>0 即 fail" 契约不适用,独立打印逻辑, /// 不进 `report_series`);重建相(drop 旧 kernel 重建新 kernel)验"无存活 VM"边界重建:键数超阈值 → /// `should_rebuild_perm` 返回 `Some(建议上限)`(阈值取 2 的幂后加倍)→ /// drop 旧 kernel → 新 kernel 严格主锚 `entry_count` 恰 0 + 旋钮 `None`。 /// /// # 注意事项 /// 不传 bench 共享 kernel:20 万条目会改变后续 case 的 CodeForge/harness /// 面;本 case 全程无存活 VM(JS 段 VM 出作用域即 drop),drop 旧 Arc 即 /// 整体释放。重建后 RSS 可能不回落至增长前之下(glibc 小对象 free 滞留 /// 分配器自由链不回 OS),RSS 为次锚仅参考、不进 exit 判据。exit 0/1 /// 仅由重建相确定性断言驱动。第二复跑的增长相可能复用第一复跑的滞留 /// 页(内部空闲块不随堆顶归页归还 OS),其斜率是热态下界——首测基线 /// 取第一(冷)复跑;复跑斜率差超阈先按此归因,再疑用例。 pub fn run_mem_kernel_lifetime() -> ExitCode { const JS_OBJECTS: u32 = 8_192; // JS 固定段对象数(每对象一个唯一计算键) const TOTAL_STRINGS: u32 = 191_704; // 增长循环唯一键数(与 JS 固定段键不相交) const RERUNS: usize = 2; const REBUILD_THRESHOLD: u32 = 200_000 / 2; const TARGETS: [u32; 11] = [10_000, 15_000, 20_000, 30_000, 40_000, 60_000, 80_000, 100_000, 131_072, 160_000, 200_000]; let mut config = KernelConfig::minimal(); config.perm_interner_max_entries = Some(REBUILD_THRESHOLD); let expected_suggestion = REBUILD_THRESHOLD.next_power_of_two().saturating_mul(2); let js_source = format!( "var arr = []; for (var i = 0; i < {JS_OBJECTS}; i++) {{ var o = {{}}; o['s_' + i] = i; arr.push(o); }} arr.length === {JS_OBJECTS}" ); let allocator = Allocator::default(); let program = match oxide_parser::parse(&allocator, &js_source) { Ok(p) => p, Err(_) => return ExitCode::FAILURE, }; let js_module = match Compiler::new().compile(&program) { Ok(m) => Arc::new(m), Err(e) => { eprintln!("[kernel_lifetime] js phase compile failed: {e}"); return ExitCode::FAILURE; } }; let mut slopes: Vec = Vec::new(); for run in 0..RERUNS { let kernel = KernelCore::new(config.clone()); // JS 固定段:建对象 + 计算键写入(仅 intern 不物化)。 let rss_pre = read_vmrss_kb().unwrap_or(0); { let mut vm = Vm::with_kernel_core(Arc::clone(&kernel)); let result = match vm.run(&js_module) { Ok(v) => v, Err(e) => { eprintln!("[kernel_lifetime] run {run} js phase failed: {e}"); return ExitCode::FAILURE; } }; if result != JsValue::bool(true) { eprintln!("[kernel_lifetime] run {run} js phase result mismatch: {result:?}"); return ExitCode::FAILURE; } } let baseline = kernel.perm_interner().entry_count(); // 增长相:唯一键 intern + 1/4 物化,11 采样点采 VmRSS。 let mut series: Vec<(usize, f64)> = Vec::new(); let mut next_target = 0usize; for k in 0..TOTAL_STRINGS { let key = format!("s_{k:012x}"); let (id, _) = kernel.perm_interner().intern(&key); if k % 4 == 0 { let _ = kernel.perm_interner().string_ptr(id); } let count = baseline + k + 1; while next_target < TARGETS.len() && count >= TARGETS[next_target] { if let Some(kb) = read_vmrss_kb() { series.push((count as usize, kb as f64)); } next_target += 1; } } // 尾部采样点(200k)可能未达:按最终唯一键数补采。 while next_target < TARGETS.len() { if let Some(kb) = read_vmrss_kb() { series.push((kernel.perm_interner().entry_count() as usize, kb as f64)); } next_target += 1; } let entries = kernel.perm_interner().entry_count(); let rss_post = read_vmrss_kb().unwrap_or(0); let (slope, r2) = linreg(&series); slopes.push(slope); eprintln!( "[kernel_lifetime] run={run} threshold={REBUILD_THRESHOLD} pre={rss_pre} post={rss_post} samples={} entries={entries} slope={:.4} kB/entry (~{:.1} B/entry) R²={:.4} materialization≈1/4 [info: 探索期,不进 report_series 判据]", series.len(), slope, slope * 1024.0, r2 ); // 确定性断言:增长相总量 + 旋钮端到端(Some = 建议上限)。 if entries != baseline + TOTAL_STRINGS { eprintln!("[kernel_lifetime] run {run} growth mismatch: baseline={baseline} entries={entries}"); return ExitCode::FAILURE; } let knob = kernel.should_rebuild_perm(); if knob != Some(expected_suggestion) { eprintln!("[kernel_lifetime] run {run} knob not armed: {knob:?} (expect Some({expected_suggestion}))"); return ExitCode::FAILURE; } // 重建相:drop 旧 Arc 整体释放,新 kernel 严格主锚。 drop(kernel); let rebuilt = KernelCore::new(config.clone()); let rebuilt_entries = rebuilt.perm_interner().entry_count(); let rebuilt_knob = rebuilt.should_rebuild_perm(); eprintln!( "[kernel_lifetime] rebuild run={run}: threshold={REBUILD_THRESHOLD} suggestion={expected_suggestion} entries {entries} -> {rebuilt_entries} knob Some({expected_suggestion}) -> {rebuilt_knob:?} rss_kb={:?}", read_vmrss_kb() ); if rebuilt_entries != 0 || rebuilt_knob.is_some() { return ExitCode::FAILURE; } // 复跑边界:本轮回放释放的堆页归还 OS,免残留页计入下轮增长相。 release_free_heap(); } // 复跑一致性(仅记录不判定):跨轮斜率差 >10% 先查 harness 污染 // (分配器归还 OS 延迟),再疑用例。 if slopes.len() == RERUNS { eprintln!( "[kernel_lifetime] rerun drift: {:.1}% (仅记录)", (slopes[0] - slopes[1]).abs() / slopes[0].max(1e-9) * 100.0 ); } ExitCode::SUCCESS } /// 进程级启动五段分解度量:专用 kernel(`KernelConfig::minimal()`,同 /// `kernel_lifetime` 口径,不污染 bench 共享 kernel),每轮重建 kernel 与池 /// 模拟新进程,连测五段——① `KernelCore::new`(kernel 初始化)② /// `VmPool::new`(池构造,含同步预热 min_size 个 Vm)③ 首次 `pool.spawn()` ///(首次 spawn,命中预热池近零成本)④ 固定微源 /// `oxide_parser::parse` 加 `Compiler::new().compile`(parse 加 compile)⑤ /// 首次 `vm.run`(执行),打印各段毫秒加合计,三轮取中位。 /// /// # 边界与前提 /// - 启动是单次事件非序列,不进 `report_series` 泄漏判据; /// - 池构造段含 min_size 个 Vm 的预热成本,可与 `builtin_world_build` 用例 /// 的每 Vm 构造成本锚对照(同 debug 口径);偏差大先查 harness 污染再疑用例。 /// /// # 副作用 /// - 不改引擎执行路径;每轮 kernel 与池在轮末 drop,无跨轮状态。 pub fn run_mem_startup_breakdown() -> ExitCode { const ROUNDS: usize = 3; const SOURCE: &str = "var x = 1 + 2; x * 3"; let mut rounds: Vec<[f64; 5]> = Vec::with_capacity(ROUNDS); for round in 0..ROUNDS { // kernel 初始化:每轮全新构造,模拟进程启动。 let t0 = Instant::now(); let config = KernelConfig::minimal(); let kernel = KernelCore::new(config.clone()); let t1 = Instant::now(); // 池构造:同步预热 min_size 个 Vm,预热成本计入本段。 let pool = VmPool::new(Arc::clone(&kernel), config.min_pool_size, config.max_pool_size); let t2 = Instant::now(); // 首次 spawn:命中预热池,近零成本。 let mut guard = pool.spawn(); let t3 = Instant::now(); // parse 加 compile:固定微源每轮重测。 let allocator = Allocator::default(); let program = match oxide_parser::parse(&allocator, SOURCE) { Ok(p) => p, Err(e) => { eprintln!("[startup_breakdown] round {round} parse failed: {e:?}"); return ExitCode::FAILURE; } }; let module = match Compiler::new().compile(&program) { Ok(m) => Arc::new(m), Err(e) => { eprintln!("[startup_breakdown] round {round} compile failed: {e}"); return ExitCode::FAILURE; } }; let t4 = Instant::now(); // 首次执行:微源结果必须为 9。 let result = match guard.vm_mut().run(&module) { Ok(v) => v, Err(e) => { eprintln!("[startup_breakdown] round {round} run failed: {e}"); return ExitCode::FAILURE; } }; let t5 = Instant::now(); if result != JsValue::float(9.0) { eprintln!("[startup_breakdown] round {round} result mismatch: {result:?}"); return ExitCode::FAILURE; } rounds.push([ t1.duration_since(t0).as_secs_f64() * 1e3, t2.duration_since(t1).as_secs_f64() * 1e3, t3.duration_since(t2).as_secs_f64() * 1e3, t4.duration_since(t3).as_secs_f64() * 1e3, t5.duration_since(t4).as_secs_f64() * 1e3, ]); } // 逐轮明细 + 三轮中位:中位抗单轮噪声,明细供查 harness 污染。 let mut medians = [0.0f64; 5]; for s in 0..5 { let mut values = [rounds[0][s], rounds[1][s], rounds[2][s]]; values.sort_by(f64::total_cmp); medians[s] = values[1]; } for (round, segs) in rounds.iter().enumerate() { eprintln!( "[startup_breakdown] round {round}: kernel_init={:.3} pool_new={:.3} first_vm={:.3} parse_compile={:.3} first_run={:.3} total={:.3} ms", segs[0], segs[1], segs[2], segs[3], segs[4], segs.iter().sum::() ); } eprintln!( "[startup_breakdown] median: kernel_init={:.3} pool_new={:.3} first_vm={:.3} parse_compile={:.3} first_run={:.3} total={:.3} ms", medians[0], medians[1], medians[2], medians[3], medians[4], medians.iter().sum::() ); ExitCode::SUCCESS } /// 全序列回归报告:打印首末 RSS、全序列斜率/R² 与窗口(末 20 样本)总 /// 增量;斜率显著为正(R² > 0.9)返回失败,表示泄漏签名。 fn report_series(case: &str, unit: &str, series: &[(usize, f64)]) -> ExitCode { let Some(&(first_i, first_kb)) = series.first() else { eprintln!("[{case}] no samples"); return ExitCode::FAILURE; }; let &(last_i, last_kb) = series.last().expect("首样本存在则末样本存在"); let (slope, r2) = linreg(series); let window_delta = series .get(series.len().saturating_sub(20)) .map(|(_, a)| last_kb - *a) .unwrap_or(0.0); eprintln!( "[{case}] samples={} {}={}..{} rss_kb={}..{} (delta {:+.0}) slope={:.4} kB/{unit} R²={:.4} window_delta={:+.0} kB", series.len(), unit, first_i, last_i, first_kb, last_kb, last_kb - first_kb, slope, r2, window_delta ); if slope > 0.0 && r2 > 0.9 { ExitCode::FAILURE } else { ExitCode::SUCCESS } } #[cfg(test)] mod tests { use super::*; /// Linux 宿主 VmRSS 可读且大于 0。 #[test] fn vmrss_readable() { assert!(matches!(read_vmrss_kb(), Some(kb) if kb > 0)); } /// 已知斜率直线:斜率精确、R² 完美拟合。 #[test] fn linreg_exact_line() { let pts: Vec<(usize, f64)> = (0..10).map(|i| (i, 2.0 * i as f64 + 1.0)).collect(); let (slope, r2) = linreg(&pts); assert!((slope - 2.0).abs() < 1e-9); assert!((r2 - 1.0).abs() < 1e-9); } /// 零方差序列:斜率 0、R² 记完美拟合,不触发泄漏判定。 #[test] fn linreg_constant_series() { let pts = vec![(0, 100.0), (1, 100.0), (2, 100.0)]; let (slope, r2) = linreg(&pts); assert_eq!(slope, 0.0); assert_eq!(r2, 1.0); } /// 全量重置后 session 账目锚恰 0:`mem_pool_high_water` 的确定性主门槛 /// (锚 == 0)依赖全量重置清表归零,重源 run 后账目非零作对照。 #[test] fn session_accounting_zero_after_full_reset() { let kernel = KernelCore::new(KernelConfig::standard()); let mut vm = Vm::with_kernel_core(Arc::clone(&kernel)); let allocator = Allocator::default(); let program = oxide_parser::parse(&allocator, "var o = { a: 1 }; o").expect("parse"); let hash = compiled_module_hash(&program); let module = kernel .code_forge() .get_or_insert_with(hash, || Compiler::new().compile(&program)) .expect("compile"); vm.run(&module).expect("run"); assert!(vm.session_bytes_allocated() > 0); vm.full_reset(); assert_eq!(vm.session_bytes_allocated(), 0); } /// 窗口采样器:正斜率序列出判定且斜率还原正确。 #[test] fn sampler_verdict_on_rising_series() { let mut s = LeakSampler::new(20); let mut verdict = None; for i in 0..30 { verdict = s.add_sample(i, 1000.0 + 10.0 * i as f64); } let v = verdict.expect("正斜率高拟合序列应出判定"); assert!((v.slope - 10.0).abs() < 1e-6); assert!(v.r2 > 0.9); } }