mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-24 02:31:13 +08:00
1b87e212f9
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 条目。
228 lines
9.4 KiB
Rust
228 lines
9.4 KiB
Rust
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 ®ressions {
|
||
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,
|
||
}
|
||
}
|