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, pool: &Arc) -> ExitCode { let mut results: Vec = 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::() / n) as u64, session_objects: (metrics.iter().map(|m| m.session_objects as f64).sum::() / n) as usize, session_bytes: (metrics.iter().map(|m| m.session_bytes as f64).sum::() / n) as usize, epoch_objects: (metrics.iter().map(|m| m.epoch_objects as f64).sum::() / n) as usize, epoch_bytes: (metrics.iter().map(|m| m.epoch_bytes as f64).sum::() / n) as u64, gc_trigger_count: (metrics.iter().map(|m| m.gc_trigger_count as f64).sum::() / n) as u64, gc_bytes_freed: (metrics.iter().map(|m| m.gc_bytes_freed as f64).sum::() / n) as u64, gc_objects_scanned: (metrics.iter().map(|m| m.gc_objects_scanned as f64).sum::() / n) as u64, gc_collection_us: (metrics.iter().map(|m| m.gc_collection_us as f64).sum::() / n) as u64, instruction_count: (metrics.iter().map(|m| m.instruction_count as f64).sum::() / n) as u64, compile_time_us: (metrics.iter().map(|m| m.compile_time_us as f64).sum::() / n) as u64, exec_time_us: (metrics.iter().map(|m| m.exec_time_us as f64).sum::() / n) as u64, ic_hit_rate: metrics.iter().map(|m| m.ic_hit_rate).sum::() / n, ic_hits: (metrics.iter().map(|m| m.ic_hits as f64).sum::() / n) as u64, ic_misses: (metrics.iter().map(|m| m.ic_misses as f64).sum::() / n) as u64, peak_bytes: (metrics.iter().map(|m| m.peak_bytes as f64).sum::() / n) as u64, retained_bytes: (metrics.iter().map(|m| m.retained_bytes as f64).sum::() / n) as u64, retained_objects: (metrics.iter().map(|m| m.retained_objects as f64).sum::() / n) as u64, } }