Files
OxideJS-New/crates/oxide_cli/src/bench/js_stress.rs
T
root 1b87e212f9 test: 新增内存占用基准用例与留存内存采集
bench 每轮采集在 workload 后 promote 根持有/会话引用的 epoch 对象并强制
一次 full GC,新增 peak_bytes / retained_bytes / retained_objects 三口径
(原 session_* 字段为执行期累计分配,不反映 workload 后留存);dispatch
循环增加 session 堆高水位采样,VM 暴露 session_bytes_peak 与
collect_session_gc。新增 8 个 mem_* 压力用例(对象图/字符串/闭包链/数组
增删/跨调用保留/Map/Set 保留/字符串拼接),基线扩至 76 条目。
2026-09-10 08:18:52 +00:00

228 lines
9.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use std::fs;
use std::process::ExitCode;
use std::sync::Arc;
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_pool::VmPool;
use crate::bench::baseline::{compare_baseline, save_baseline};
use crate::bench::metrics::MetricCollection;
use crate::bench::output::{format_json, format_text_table};
use crate::bench::BenchConfig;
/// 运行 `tests/stress/` 下全部(或 filter 匹配的)JS 压力测试:
/// 预热后迭代执行并采集指标,输出 JSON + 表格;必要时保存/对比基线。
pub fn run_js_stress_bench(config: &BenchConfig, kernel: &Arc<KernelCore>, pool: &Arc<VmPool>) -> ExitCode {
let mut results: Vec<MetricCollection> = Vec::new();
let compiler = Compiler::new();
let test_dir = "tests/stress";
let mut files: Vec<_> = match fs::read_dir(test_dir) {
Ok(entries) => entries
.filter_map(|e| e.ok())
.filter(|e| {
e.path().extension().is_some_and(|ext| ext == "js")
&& config
.filter
.as_ref()
.map_or(true, |f| e.file_name().to_string_lossy().contains(f.as_str()))
})
.map(|e| e.path())
.collect(),
Err(e) => {
eprintln!("Could not read {}: {}", test_dir, e);
return ExitCode::FAILURE;
}
};
files.sort();
if files.is_empty() {
eprintln!("No matching stress tests found in {}", test_dir);
return ExitCode::FAILURE;
}
for path in &files {
let test_name = path.file_stem().unwrap_or_default().to_string_lossy().to_string();
let js = match fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
eprintln!("Failed to read {:?}: {}", path, e);
continue;
}
};
let allocator = Allocator::default();
let program = match oxide_parser::parse(&allocator, &js) {
Ok(p) => p,
Err(errors) => {
eprintln!("Parse error in {}: {:?}", test_name, errors.first());
continue;
}
};
let hash = compiled_module_hash(&program);
let module = match kernel.code_forge().get_or_insert_with(hash, || compiler.compile(&program)) {
Ok(m) => m,
Err(e) => {
eprintln!("Compile error in {}: {}", test_name, e);
continue;
}
};
for _ in 0..config.warmup {
let mut guard = pool.spawn();
let _ = guard.vm_mut().run(&module);
}
let mut metrics = Vec::new();
for _ in 0..config.iterations {
let compile_start = Instant::now();
let mut guard = pool.spawn();
let vm = guard.vm_mut();
let compile_time = compile_start.elapsed();
let pre_session = vm.session_object_count();
let pre_epoch = vm.epoch_object_count();
let exec_start = Instant::now();
let _result = vm.run(&module);
let exec_time = exec_start.elapsed();
// 执行期指标须在强制 GC 前读取:GC 会搬移/清扫 session 对象,
// 其后读到的计数是存活集而非本执行期的累计量
let gc_stats = vm.session_gc_stats();
let gc_trigger_count = gc_stats.total_collections;
let gc_bytes_freed = gc_stats.last_collection_bytes_freed;
let gc_objects_scanned = gc_stats.last_collection_objects_scanned;
let gc_collection_us = gc_stats.last_collection_duration_us;
let session_objects = vm.session_object_count().saturating_sub(pre_session);
let session_bytes = vm.session_bytes_allocated();
let epoch_objects = vm.epoch_object_count().saturating_sub(pre_epoch);
let instruction_count = vm.instruction_count();
let ic_hit_rate = vm.ic_hit_rate();
let ic_hits = vm.ic_hit_count();
let ic_misses = vm.ic_miss_count();
// 留存口径:先让存活集对 session 完全可见——根直接持有的 epoch 对象
// (顶层 var 等)晋升进 session,session 对象持有的 epoch 子引用
// (闭包捕获、原生盒直插)同样晋升(均与 epoch 边界同机制);
// 再强制完整 GC 清掉不可达垃圾,清扫后账目即 workload 留存堆
vm.promote_rooted_epoch_objects();
vm.promote_session_epoch_refs();
vm.collect_session_gc();
let retained_bytes = vm.session_bytes_allocated();
let retained_objects = vm.session_object_count();
// 峰值高水位取执行期采样与 GC 后存活字节的较大者(存活集亦属高水位)
let peak_bytes = vm.session_bytes_peak().max(retained_bytes);
metrics.push(MetricCollection {
test_name: test_name.clone(),
wall_time_us: (compile_time + exec_time).as_micros() as u64,
session_objects,
session_bytes,
epoch_objects,
epoch_bytes: 0,
gc_trigger_count,
gc_bytes_freed,
gc_objects_scanned,
gc_collection_us,
instruction_count,
compile_time_us: compile_time.as_micros() as u64,
exec_time_us: exec_time.as_micros() as u64,
ic_hit_rate,
ic_hits,
ic_misses,
peak_bytes: peak_bytes as u64,
retained_bytes: retained_bytes as u64,
retained_objects: retained_objects as u64,
});
}
let avg = average_metrics(&metrics);
results.push(avg);
}
let json = format_json(&results);
let table = format_text_table(&results);
eprintln!("{}", table);
println!("{}", json);
if config.update_baseline {
if let Err(e) = save_baseline(&results) {
eprintln!("Failed to save baseline: {}", e);
return ExitCode::FAILURE;
}
eprintln!("Baseline saved to BENCHMARK_BASELINE.md + benchmark_baseline.json");
} else {
let regressions = compare_baseline(&results);
if !regressions.is_empty() {
eprintln!("\nRegression detected:");
for r in &regressions {
eprintln!(
" {}: {} baseline={} current={} ratio={:.2}% tolerance={:.0}%",
r.test_name,
r.metric,
r.baseline,
r.current,
(r.ratio - 1.0) * 100.0,
r.tolerance * 100.0,
);
}
return ExitCode::FAILURE;
}
}
ExitCode::SUCCESS
}
fn average_metrics(metrics: &[MetricCollection]) -> MetricCollection {
let n = metrics.len() as f64;
if n == 0.0 || metrics.is_empty() {
return metrics.first().cloned().unwrap_or(MetricCollection {
test_name: String::new(),
wall_time_us: 0,
session_objects: 0,
session_bytes: 0,
epoch_objects: 0,
epoch_bytes: 0,
gc_trigger_count: 0,
gc_bytes_freed: 0,
gc_objects_scanned: 0,
gc_collection_us: 0,
instruction_count: 0,
compile_time_us: 0,
exec_time_us: 0,
ic_hit_rate: 0.0,
ic_hits: 0,
ic_misses: 0,
peak_bytes: 0,
retained_bytes: 0,
retained_objects: 0,
});
}
MetricCollection {
test_name: metrics[0].test_name.clone(),
wall_time_us: (metrics.iter().map(|m| m.wall_time_us as f64).sum::<f64>() / n) as u64,
session_objects: (metrics.iter().map(|m| m.session_objects as f64).sum::<f64>() / n) as usize,
session_bytes: (metrics.iter().map(|m| m.session_bytes as f64).sum::<f64>() / n) as usize,
epoch_objects: (metrics.iter().map(|m| m.epoch_objects as f64).sum::<f64>() / n) as usize,
epoch_bytes: (metrics.iter().map(|m| m.epoch_bytes as f64).sum::<f64>() / n) as u64,
gc_trigger_count: (metrics.iter().map(|m| m.gc_trigger_count as f64).sum::<f64>() / n) as u64,
gc_bytes_freed: (metrics.iter().map(|m| m.gc_bytes_freed as f64).sum::<f64>() / n) as u64,
gc_objects_scanned: (metrics.iter().map(|m| m.gc_objects_scanned as f64).sum::<f64>() / n) as u64,
gc_collection_us: (metrics.iter().map(|m| m.gc_collection_us as f64).sum::<f64>() / n) as u64,
instruction_count: (metrics.iter().map(|m| m.instruction_count as f64).sum::<f64>() / n) as u64,
compile_time_us: (metrics.iter().map(|m| m.compile_time_us as f64).sum::<f64>() / n) as u64,
exec_time_us: (metrics.iter().map(|m| m.exec_time_us as f64).sum::<f64>() / n) as u64,
ic_hit_rate: metrics.iter().map(|m| m.ic_hit_rate).sum::<f64>() / n,
ic_hits: (metrics.iter().map(|m| m.ic_hits as f64).sum::<f64>() / n) as u64,
ic_misses: (metrics.iter().map(|m| m.ic_misses as f64).sum::<f64>() / n) as u64,
peak_bytes: (metrics.iter().map(|m| m.peak_bytes as f64).sum::<f64>() / n) as u64,
retained_bytes: (metrics.iter().map(|m| m.retained_bytes as f64).sum::<f64>() / n) as u64,
retained_objects: (metrics.iter().map(|m| m.retained_objects as f64).sum::<f64>() / n) as u64,
}
}