mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
933 lines
39 KiB
Rust
933 lines
39 KiB
Rust
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<LeakVerdict> {
|
||
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<u64> {
|
||
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<KernelCore>, pool: &Arc<VmPool>) -> 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<MetricCollection> = 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<KernelCore>) -> 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<KernelCore>) -> 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<KernelCore>) -> 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<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)
|
||
}
|
||
|
||
/// 校准用例:重源单次 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<KernelCore>) -> 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<KernelCore>) -> 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<f64> = 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::<f64>()
|
||
);
|
||
}
|
||
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::<f64>()
|
||
);
|
||
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);
|
||
}
|
||
}
|