test: 新增 VmRSS 斜率采样工具与 VM 创建/选择性重建两条泄漏校准用例(bench leak 按 case 过滤分派)

This commit is contained in:
2026-09-11 11:35:39 +00:00
parent 6268275689
commit cb0168faa1
2 changed files with 226 additions and 16 deletions
+220 -15
View File
@@ -6,6 +6,7 @@ use std::time::Instant;
use oxide_compiler::compiler::{compiled_module_hash, Compiler};
use oxide_kernel::kernel::KernelCore;
use oxide_parser::Allocator;
use oxide_vm::vm::Vm;
use oxide_vm::vm_pool::VmPool;
use crate::bench::metrics::MetricCollection;
@@ -37,21 +38,8 @@ impl LeakSampler {
if self.window.len() < 10 {
return None;
}
let n = self.window.len() as f64;
let sum_x: f64 = self.window.iter().map(|(i, _)| *i as f64).sum();
let sum_y: f64 = self.window.iter().map(|(_, v)| v).sum();
let sum_xy: f64 = self.window.iter().map(|(i, v)| *i as f64 * v).sum();
let sum_x2: f64 = self.window.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 = self
.window
.iter()
.map(|(i, v)| (v - (slope * *i as f64 + intercept)).powi(2))
.sum();
let ss_tot: f64 = self.window.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 };
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 {
@@ -66,6 +54,44 @@ pub struct LeakVerdict {
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
}
/// 反复运行固定 JS 脚本,跟踪 session/code-forge/symbol 等内存指标,
/// 用 `LeakSampler` 回归检测随迭代次数增长的内存占用。
pub fn run_leak_detect(config: &BenchConfig, kernel: &Arc<KernelCore>, pool: &Arc<VmPool>) -> ExitCode {
@@ -149,3 +175,182 @@ pub fn run_leak_detect(config: &BenchConfig, kernel: &Arc<KernelCore>, pool: &Ar
println!("{}", json);
ExitCode::SUCCESS
}
/// S1 校准用例:共享 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)
}
/// S2 校准用例:单 VM 每轮「新键写脏 object/array/string 三家族原型 →
/// full_reset」,共 3000 轮,每 10 轮采一次 VmRSS,输出选择性重建释放
/// 路径的每轮内存增量(斜率 + 窗口总增量)。
///
/// # 注意事项
/// 键名逐轮递增保证走新键写入路径(既有槽位写不 bump 世代、不脏家族);
/// 修复释放路径后期望走平,残留本体钉在页粒度噪声级。
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();
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(&module) {
eprintln!("[dirty_rebuild] round {round} run failed: {e}");
return ExitCode::FAILURE;
}
vm.full_reset();
if round % SAMPLE_EVERY == 0 {
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
);
}
}
}
}
report_series("dirty_rebuild", "round", &series)
}
/// 全序列回归报告:打印首末 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);
}
/// 窗口采样器:正斜率序列出判定且斜率还原正确。
#[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);
}
}
+6 -1
View File
@@ -36,7 +36,12 @@ pub fn run_benchmarks(config: BenchConfig, kernel: Arc<KernelCore>, pool: Arc<Vm
match config.mode.as_str() {
"js" => js_stress::run_js_stress_bench(&config, &kernel, &pool),
"rust" => rust_bench::run_rust_bench(config.filter.as_deref()),
"leak" => leak_detect::run_leak_detect(&config, &kernel, &pool),
// leak 模式内按 case 过滤参数分派校准用例;缺省保持原泄漏检测行为。
"leak" => 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),
_ => leak_detect::run_leak_detect(&config, &kernel, &pool),
},
_ => {
eprintln!("Unknown bench mode: {}. Use: js, rust, or leak", config.mode);
ExitCode::FAILURE