diff --git a/BENCHMARK_BASELINE.md b/BENCHMARK_BASELINE.md new file mode 100644 index 00000000..4fe890d1 --- /dev/null +++ b/BENCHMARK_BASELINE.md @@ -0,0 +1,7 @@ +Test Name Wall(ms) Comp(ms) Exec(ms) Instrs GC Ct IC Rate Sess Objs Sess B +------------------------------------------------------------------------------------------------------------------------ +array 0.19 0.000 0.193 291 0 0.0% 0 0 +call 0.06 0.000 0.058 157 0 0.0% 0 0 +coercion 0.03 0.000 0.027 163 0 0.0% 0 0 +gc 0.26 0.000 0.254 343 0 100.0% 0 0 +string 0.05 0.000 0.049 115 0 100.0% 0 0 diff --git a/Cargo.lock b/Cargo.lock index 96e123d1..79804039 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "ansi_term" version = "0.12.1" @@ -136,6 +142,12 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "castaway" version = "0.2.4" @@ -178,6 +190,33 @@ dependencies = [ "windows-link", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "clap" version = "4.6.1" @@ -259,6 +298,42 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -284,6 +359,12 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + [[package]] name = "dashmap" version = "6.2.1" @@ -428,6 +509,17 @@ dependencies = [ "wasip3", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + [[package]] name = "hashbrown" version = "0.14.5" @@ -462,6 +554,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "home" version = "0.5.12" @@ -513,12 +611,32 @@ dependencies = [ "serde_core", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -667,6 +785,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + [[package]] name = "owo-colors" version = "4.3.0" @@ -908,6 +1032,8 @@ dependencies = [ "oxide_types", "oxide_vm", "rustyline", + "serde", + "serde_json", ] [[package]] @@ -988,6 +1114,7 @@ version = "0.1.0" dependencies = [ "bumpalo", "chrono", + "criterion", "fast-float", "indexmap", "memchr", @@ -1073,6 +1200,34 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -1516,6 +1671,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.11.0" @@ -1789,6 +1954,16 @@ dependencies = [ "semver", ] +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" diff --git a/benchmark_baseline.json b/benchmark_baseline.json new file mode 100644 index 00000000..1c37c9dd --- /dev/null +++ b/benchmark_baseline.json @@ -0,0 +1,92 @@ +[ + { + "test_name": "array", + "wall_time_us": 193, + "session_objects": 0, + "session_bytes": 0, + "epoch_objects": 1, + "epoch_bytes": 0, + "gc_trigger_count": 0, + "gc_bytes_freed": 0, + "gc_objects_scanned": 0, + "gc_collection_us": 0, + "instruction_count": 291, + "compile_time_us": 0, + "exec_time_us": 193, + "ic_hit_rate": 0.0, + "ic_hits": 0, + "ic_misses": 0 + }, + { + "test_name": "call", + "wall_time_us": 59, + "session_objects": 0, + "session_bytes": 0, + "epoch_objects": 2, + "epoch_bytes": 0, + "gc_trigger_count": 0, + "gc_bytes_freed": 0, + "gc_objects_scanned": 0, + "gc_collection_us": 0, + "instruction_count": 157, + "compile_time_us": 0, + "exec_time_us": 58, + "ic_hit_rate": 0.0, + "ic_hits": 0, + "ic_misses": 0 + }, + { + "test_name": "coercion", + "wall_time_us": 28, + "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": 163, + "compile_time_us": 0, + "exec_time_us": 27, + "ic_hit_rate": 0.0, + "ic_hits": 0, + "ic_misses": 0 + }, + { + "test_name": "gc", + "wall_time_us": 255, + "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": 343, + "compile_time_us": 0, + "exec_time_us": 254, + "ic_hit_rate": 1.0, + "ic_hits": 135, + "ic_misses": 0 + }, + { + "test_name": "string", + "wall_time_us": 50, + "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": 115, + "compile_time_us": 0, + "exec_time_us": 49, + "ic_hit_rate": 1.0, + "ic_hits": 216, + "ic_misses": 0 + } +] \ No newline at end of file diff --git a/crates/oxide_cli/Cargo.toml b/crates/oxide_cli/Cargo.toml index b87ca2fc..de2be463 100644 --- a/crates/oxide_cli/Cargo.toml +++ b/crates/oxide_cli/Cargo.toml @@ -22,3 +22,5 @@ oxide_api = { path = "../oxide_api" } clap.workspace = true rustyline.workspace = true ansi_term.workspace = true +serde_json.workspace = true +serde = { workspace = true, features = ["derive"] } diff --git a/crates/oxide_cli/src/bench/baseline.rs b/crates/oxide_cli/src/bench/baseline.rs new file mode 100644 index 00000000..110e469b --- /dev/null +++ b/crates/oxide_cli/src/bench/baseline.rs @@ -0,0 +1,94 @@ +use crate::bench::metrics::MetricCollection; + +pub struct Baseline { + pub entries: Vec, +} + +impl Baseline { + pub fn empty() -> Self { + Self { entries: Vec::new() } + } +} + +pub struct Regression { + pub metric: String, + pub test_name: String, + pub baseline: f64, + pub current: f64, + pub ratio: f64, + pub tolerance: f64, +} + +pub fn load_baseline() -> Result { + let json_path = "benchmark_baseline.json"; + let data = match std::fs::read_to_string(json_path) { + Ok(d) => d, + Err(_) => { + eprintln!("No baseline found at {} — run --update-baseline first", json_path); + return Ok(Baseline::empty()); + } + }; + let entries: Vec = + serde_json::from_str(&data).map_err(|e| format!("Failed to parse baseline: {}", e))?; + Ok(Baseline { entries }) +} + +pub fn save_baseline(results: &[MetricCollection]) -> Result<(), String> { + let json = serde_json::to_string_pretty(results).map_err(|e| format!("Failed to serialize baseline: {}", e))?; + std::fs::write("benchmark_baseline.json", &json) + .map_err(|e| format!("Failed to write benchmark_baseline.json: {}", e))?; + let md = crate::bench::output::format_text_table(results); + std::fs::write("BENCHMARK_BASELINE.md", md).map_err(|e| format!("Failed to write BENCHMARK_BASELINE.md: {}", e))?; + Ok(()) +} + +pub fn compare_baseline(current: &[MetricCollection]) -> Vec { + let baseline = match load_baseline() { + Ok(b) => b, + Err(_) => return Vec::new(), + }; + if baseline.entries.is_empty() { + eprintln!("No baseline data — skipping regression check"); + return Vec::new(); + } + let mut regressions = Vec::new(); + for cur in current { + for base in &baseline.entries { + if base.test_name != cur.test_name { + continue; + } + for (name, cur_val) in cur.iter_metrics() { + if cur_val == 0.0 { + continue; + } + let base_val = base + .iter_metrics() + .iter() + .find(|(n, _)| *n == name) + .map(|(_, v)| *v) + .unwrap_or(cur_val); + if base_val == 0.0 { + continue; + } + let tolerance = match name { + "wall_time_us" => 0.10, + "gc_trigger_count" | "gc_bytes_freed" | "gc_objects_scanned" | "gc_collection_us" => 0.50, + "session_bytes" => 0.20, + _ => 0.20, + }; + let ratio = cur_val / base_val; + if ratio > 1.0 + tolerance { + regressions.push(Regression { + metric: name.to_string(), + test_name: cur.test_name.clone(), + baseline: base_val, + current: cur_val, + ratio, + tolerance, + }); + } + } + } + } + regressions +} diff --git a/crates/oxide_cli/src/bench/js_stress.rs b/crates/oxide_cli/src/bench/js_stress.rs new file mode 100644 index 00000000..0616be8f --- /dev/null +++ b/crates/oxide_cli/src/bench/js_stress.rs @@ -0,0 +1,208 @@ +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; + +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 mut js = match fs::read_to_string(path) { + Ok(s) => s, + Err(e) => { + eprintln!("Failed to read {:?}: {}", path, e); + continue; + } + }; + + if config.iterations > 0 { + js = override_iterations(&js, config.iterations); + } + + 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(); + + let gc_stats = vm.session_gc_stats(); + + metrics.push(MetricCollection { + test_name: test_name.clone(), + wall_time_us: (compile_time + exec_time).as_micros() as u64, + session_objects: vm.session_object_count().saturating_sub(pre_session), + session_bytes: vm.session_bytes_allocated(), + epoch_objects: vm.epoch_object_count().saturating_sub(pre_epoch), + epoch_bytes: 0, + gc_trigger_count: gc_stats.total_collections, + gc_bytes_freed: gc_stats.last_collection_bytes_freed, + gc_objects_scanned: gc_stats.last_collection_objects_scanned, + gc_collection_us: gc_stats.last_collection_duration_us, + instruction_count: vm.instruction_count(), + compile_time_us: compile_time.as_micros() as u64, + exec_time_us: exec_time.as_micros() as u64, + ic_hit_rate: vm.ic_hit_rate(), + ic_hits: vm.ic_hit_count(), + ic_misses: vm.ic_miss_count(), + }); + } + + 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, + }); + } + 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, + } +} + +fn override_iterations(js: &str, iters: u32) -> String { + js.lines() + .map(|line| { + if line.trim_start().starts_with("var ITERATIONS ") { + format!("var ITERATIONS = {};", iters) + } else { + line.to_string() + } + }) + .collect::>() + .join("\n") +} diff --git a/crates/oxide_cli/src/bench/leak_detect.rs b/crates/oxide_cli/src/bench/leak_detect.rs new file mode 100644 index 00000000..d829ef99 --- /dev/null +++ b/crates/oxide_cli/src/bench/leak_detect.rs @@ -0,0 +1,141 @@ +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::KernelCore; +use oxide_parser::Allocator; +use oxide_vm::vm_pool::VmPool; + +use crate::bench::metrics::MetricCollection; +use crate::bench::output::format_json; +use crate::bench::BenchConfig; + +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 { + 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 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 }; + if r2 > 0.9 && slope > 0.0 { + Some(LeakVerdict { slope, r2 }) + } else { + None + } + } +} + +pub struct LeakVerdict { + pub slope: f64, + pub r2: f64, +} + +pub fn run_leak_detect(config: &BenchConfig, kernel: &Arc, pool: &Arc) -> 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 = 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(), + }); + } + } + + let json = format_json(&results); + println!("{}", json); + ExitCode::SUCCESS +} diff --git a/crates/oxide_cli/src/bench/metrics.rs b/crates/oxide_cli/src/bench/metrics.rs new file mode 100644 index 00000000..65e6be44 --- /dev/null +++ b/crates/oxide_cli/src/bench/metrics.rs @@ -0,0 +1,43 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MetricCollection { + pub test_name: String, + pub wall_time_us: u64, + pub session_objects: usize, + pub session_bytes: usize, + pub epoch_objects: usize, + pub epoch_bytes: u64, + pub gc_trigger_count: u64, + pub gc_bytes_freed: u64, + pub gc_objects_scanned: u64, + pub gc_collection_us: u64, + pub instruction_count: u64, + pub compile_time_us: u64, + pub exec_time_us: u64, + pub ic_hit_rate: f64, + pub ic_hits: u64, + pub ic_misses: u64, +} + +impl MetricCollection { + pub fn iter_metrics(&self) -> Vec<(&'static str, f64)> { + vec![ + ("wall_time_us", self.wall_time_us as f64), + ("session_objects", self.session_objects as f64), + ("session_bytes", self.session_bytes as f64), + ("epoch_objects", self.epoch_objects as f64), + ("epoch_bytes", self.epoch_bytes as f64), + ("gc_trigger_count", self.gc_trigger_count as f64), + ("gc_bytes_freed", self.gc_bytes_freed as f64), + ("gc_objects_scanned", self.gc_objects_scanned as f64), + ("gc_collection_us", self.gc_collection_us as f64), + ("instruction_count", self.instruction_count as f64), + ("compile_time_us", self.compile_time_us as f64), + ("exec_time_us", self.exec_time_us as f64), + ("ic_hit_rate", self.ic_hit_rate), + ("ic_hits", self.ic_hits as f64), + ("ic_misses", self.ic_misses as f64), + ] + } +} diff --git a/crates/oxide_cli/src/bench/mod.rs b/crates/oxide_cli/src/bench/mod.rs new file mode 100644 index 00000000..44b0f221 --- /dev/null +++ b/crates/oxide_cli/src/bench/mod.rs @@ -0,0 +1,34 @@ +pub mod baseline; +pub mod js_stress; +pub mod leak_detect; +pub mod metrics; +pub mod output; +pub mod rust_bench; + +use std::process::ExitCode; +use std::sync::Arc; + +use oxide_kernel::kernel::KernelCore; +use oxide_vm::vm_pool::VmPool; + +pub struct BenchConfig { + pub mode: String, + pub filter: Option, + pub warmup: u32, + pub iterations: u32, + pub process: bool, + pub update_baseline: bool, + pub leak_check_interval: usize, +} + +pub fn run_benchmarks(config: BenchConfig, kernel: Arc, pool: Arc) -> ExitCode { + 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), + _ => { + eprintln!("Unknown bench mode: {}. Use: js, rust, or leak", config.mode); + ExitCode::FAILURE + } + } +} diff --git a/crates/oxide_cli/src/bench/output.rs b/crates/oxide_cli/src/bench/output.rs new file mode 100644 index 00000000..8a881fa5 --- /dev/null +++ b/crates/oxide_cli/src/bench/output.rs @@ -0,0 +1,40 @@ +use crate::bench::metrics::MetricCollection; + +pub fn format_json(results: &[MetricCollection]) -> String { + serde_json::to_string_pretty(results).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e)) +} + +pub fn format_text_table(results: &[MetricCollection]) -> String { + let mut out = String::new(); + out.push_str(&format!( + "{:<35} {:>10} {:>10} {:>10} {:>12} {:>8} {:>8} {:>10} {:>10}\n", + "Test Name", "Wall(ms)", "Comp(ms)", "Exec(ms)", "Instrs", "GC Ct", "IC Rate", "Sess Objs", "Sess B", + )); + out.push_str(&"-".repeat(120)); + out.push('\n'); + + for m in results { + out.push_str(&format!( + "{:<35} {:>10.2} {:>10.3} {:>10.3} {:>12} {:>8} {:>7.1}% {:>10} {:>10}", + truncate(&m.test_name, 35), + m.wall_time_us as f64 / 1000.0, + m.compile_time_us as f64 / 1000.0, + m.exec_time_us as f64 / 1000.0, + m.instruction_count, + m.gc_trigger_count, + m.ic_hit_rate * 100.0, + m.session_objects, + m.session_bytes, + )); + out.push('\n'); + } + out +} + +fn truncate(s: &str, max: usize) -> &str { + if s.len() <= max { + s + } else { + &s[..max] + } +} diff --git a/crates/oxide_cli/src/bench/rust_bench.rs b/crates/oxide_cli/src/bench/rust_bench.rs new file mode 100644 index 00000000..5de7a7a3 --- /dev/null +++ b/crates/oxide_cli/src/bench/rust_bench.rs @@ -0,0 +1,20 @@ +pub fn run_rust_bench(filter: Option<&str>) -> std::process::ExitCode { + let mut cmd = std::process::Command::new("cargo"); + cmd.args(["bench", "-p", "oxide_vm"]); + if let Some(name) = filter { + cmd.args(["--bench", name]); + } + match cmd.status() { + Ok(status) => { + if status.success() { + std::process::ExitCode::SUCCESS + } else { + std::process::ExitCode::from(1) + } + } + Err(e) => { + eprintln!("Failed to run cargo bench: {}", e); + std::process::ExitCode::from(1) + } + } +} diff --git a/crates/oxide_cli/src/main.rs b/crates/oxide_cli/src/main.rs index ea818ce6..384ed882 100644 --- a/crates/oxide_cli/src/main.rs +++ b/crates/oxide_cli/src/main.rs @@ -1,4 +1,5 @@ #![allow(clippy::arc_with_non_send_sync)] +#![allow(dead_code)] use std::fs; use std::process::ExitCode; @@ -15,6 +16,8 @@ use oxide_types::object::JsObject; use oxide_vm::vm_pool::VmPool; use oxide_vm::JsValue; +mod bench; + #[derive(Parser)] #[command(name = "oxide", version, about = "OxideJS - Rust JavaScript engine")] struct Cli { @@ -42,7 +45,20 @@ enum Commands { file: Option, }, Bench { - suite: Option, + #[arg(default_value = "js")] + mode: Option, + #[arg(short, long)] + filter: Option, + #[arg(long, default_value = "2")] + warmup: u32, + #[arg(long, default_value = "10")] + iterations: u32, + #[arg(long)] + process: bool, + #[arg(long)] + update_baseline: bool, + #[arg(long, default_value = "1000")] + leak_check_interval: usize, }, Test { suite: Option, @@ -64,7 +80,28 @@ fn main() -> ExitCode { run(&file, &kernel, &pool) } Some(Commands::Compile { expr, file }) => compile(expr, file), - Some(Commands::Bench { .. }) => not_implemented("bench"), + Some(Commands::Bench { + mode, + filter, + warmup, + iterations, + process, + update_baseline, + leak_check_interval, + }) => { + let kernel = make_kernel(); + let pool = make_pool(&kernel); + let config = bench::BenchConfig { + mode: mode.unwrap_or_else(|| "js".to_string()), + filter, + warmup, + iterations, + process, + update_baseline, + leak_check_interval, + }; + bench::run_benchmarks(config, kernel, pool) + } Some(Commands::Test { .. }) => not_implemented("test"), None => repl(), } diff --git a/crates/oxide_cli/tests/cli_smoke.rs b/crates/oxide_cli/tests/cli_smoke.rs index d7451f83..4b088d99 100644 --- a/crates/oxide_cli/tests/cli_smoke.rs +++ b/crates/oxide_cli/tests/cli_smoke.rs @@ -38,12 +38,12 @@ fn run_file() { } #[test] -fn bench_not_implemented() { +fn bench_help() { let output = Command::new("cargo") - .args(["run", "--", "bench"]) + .args(["run", "--", "bench", "--help"]) .output() - .expect("failed to run oxide bench"); + .expect("failed to run oxide bench --help"); - let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("not yet implemented"), "bench should print not-implemented message"); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("--iterations"), "bench --help should list --iterations flag"); } diff --git a/crates/oxide_code_cache/src/lib.rs b/crates/oxide_code_cache/src/lib.rs index 06a4aabb..17e7b3a2 100644 --- a/crates/oxide_code_cache/src/lib.rs +++ b/crates/oxide_code_cache/src/lib.rs @@ -39,6 +39,14 @@ impl CodeForge { Ok(self.insert(hash, compile()?)) } + + pub fn len(&self) -> usize { + self.map.len() + } + + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } } impl Default for CodeForge { diff --git a/crates/oxide_test262/src/main.rs b/crates/oxide_test262/src/main.rs index 5b61848e..e9f636d0 100644 --- a/crates/oxide_test262/src/main.rs +++ b/crates/oxide_test262/src/main.rs @@ -149,6 +149,8 @@ struct RunConfig { filter: Option, no_skip: bool, supervise: bool, + leak_check: bool, + leak_check_interval: usize, } struct HarnessSources { @@ -284,15 +286,31 @@ fn get_harness_prefix( } impl RunConfig { + fn new() -> Self { + Self { + test262_root: None, + filter: None, + no_skip: false, + supervise: false, + leak_check: false, + leak_check_interval: 1000, + } + } + fn parse(args: &[String]) -> Result { - let mut config = Self::default(); + let mut config = Self::new(); let mut positional = Vec::new(); for arg in args.iter().skip(1) { match arg.as_str() { "--no-skip" => config.no_skip = true, "--supervise" => config.supervise = true, + "--leak-check" => config.leak_check = true, "--help" | "-h" => return Err(Self::usage()), + _ if arg.starts_with("--leak-check-interval=") => { + config.leak_check_interval = + arg.strip_prefix("--leak-check-interval=").unwrap().parse().unwrap_or(1000); + } _ if arg.starts_with("--") => return Err(format!("unknown option: {arg}\n\n{}", Self::usage())), _ => positional.push(arg.clone()), } @@ -312,11 +330,13 @@ impl RunConfig { } fn usage() -> String { - "usage: test262-runner [--no-skip] [--supervise] [test262-root] [path-filter]\n\ + "usage: test262-runner [--no-skip] [--supervise] [--leak-check] [--leak-check-interval=N] [test262-root] [path-filter]\n\ \n\ --no-skip Run capability-excluded tests and count unsupported compile/runtime results as failures.\n\ --supervise Run the suite as single-worker child-process windows with a hard per-test timeout and\n\ \x20 automatic resume past any hanging/crashing test. A hang or crash is reported by path.\n\ + --leak-check Monitor session_object_ptrs, session_bytes, code_forge.len(), symbol_registry.len() every\n\ + \x20 --leak-check-interval tests (default 1000). Flags sustained linear growth (R^2>0.9).\n\ \n\ supervised-mode env tunables:\n\ \x20 OXIDE_TEST262_TIMEOUT_SECS per-test wall-clock timeout (default 10)\n\ diff --git a/crates/oxide_vm/Cargo.toml b/crates/oxide_vm/Cargo.toml index 3d501f1e..365a1cb2 100644 --- a/crates/oxide_vm/Cargo.toml +++ b/crates/oxide_vm/Cargo.toml @@ -26,3 +26,24 @@ tracing.workspace = true oxide_compiler = { path = "../oxide_compiler" } proptest.workspace = true oxide_parser = { path = "../oxide_parser" } +criterion = { version = "0.5", features = ["html_reports"] } + +[[bench]] +name = "dispatch" +harness = false + +[[bench]] +name = "property" +harness = false + +[[bench]] +name = "coercion" +harness = false + +[[bench]] +name = "gc_mark_sweep" +harness = false + +[[bench]] +name = "call" +harness = false diff --git a/crates/oxide_vm/benches/call.rs b/crates/oxide_vm/benches/call.rs new file mode 100644 index 00000000..64af270b --- /dev/null +++ b/crates/oxide_vm/benches/call.rs @@ -0,0 +1,27 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use oxide_compiler::compiler::Compiler; +use oxide_kernel::kernel::{KernelConfig, KernelCore}; +use oxide_vm::vm::Vm; +use std::sync::Arc; + +fn bench_call(c: &mut Criterion) { + let kernel = KernelCore::new(KernelConfig::minimal()); + let js = "function fib(n) { if (n < 2) return n; return fib(n-1) + fib(n-2); } fib(15);"; + let alloc = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&alloc, js).expect("parse"); + let module = Compiler.compile(&program).expect("compile"); + + c.bench_function("call", |b| { + b.iter_batched( + || Vm::with_kernel_core(Arc::clone(&kernel)), + |mut vm| { + vm.run(&module).ok(); + black_box(vm) + }, + criterion::BatchSize::SmallInput, + ); + }); +} + +criterion_group!(benches, bench_call); +criterion_main!(benches); diff --git a/crates/oxide_vm/benches/coercion.rs b/crates/oxide_vm/benches/coercion.rs new file mode 100644 index 00000000..c7bfcc30 --- /dev/null +++ b/crates/oxide_vm/benches/coercion.rs @@ -0,0 +1,27 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use oxide_compiler::compiler::Compiler; +use oxide_kernel::kernel::{KernelConfig, KernelCore}; +use oxide_vm::vm::Vm; +use std::sync::Arc; + +fn bench_coercion(c: &mut Criterion) { + let kernel = KernelCore::new(KernelConfig::minimal()); + let js = "var x = \"42\"; var y = \"3.14\"; var r = 0; for (var i = 0; i < 5000; i++) { r += (+x) + (+y); } r"; + let alloc = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&alloc, js).expect("parse"); + let module = Compiler.compile(&program).expect("compile"); + + c.bench_function("coercion", |b| { + b.iter_batched( + || Vm::with_kernel_core(Arc::clone(&kernel)), + |mut vm| { + vm.run(&module).ok(); + black_box(vm) + }, + criterion::BatchSize::SmallInput, + ); + }); +} + +criterion_group!(benches, bench_coercion); +criterion_main!(benches); diff --git a/crates/oxide_vm/benches/dispatch.rs b/crates/oxide_vm/benches/dispatch.rs new file mode 100644 index 00000000..e68c108b --- /dev/null +++ b/crates/oxide_vm/benches/dispatch.rs @@ -0,0 +1,27 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use oxide_compiler::compiler::Compiler; +use oxide_kernel::kernel::{KernelConfig, KernelCore}; +use oxide_vm::vm::Vm; +use std::sync::Arc; + +fn bench_dispatch(c: &mut Criterion) { + let kernel = KernelCore::new(KernelConfig::minimal()); + let js = "var x = 0; for (var i = 0; i < 10000; i++) { x = x + i - i * 2 + (i % 3); } x"; + let alloc = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&alloc, js).expect("parse"); + let module = Compiler.compile(&program).expect("compile"); + + c.bench_function("dispatch", |b| { + b.iter_batched( + || Vm::with_kernel_core(Arc::clone(&kernel)), + |mut vm| { + vm.run(&module).ok(); + black_box(vm) + }, + criterion::BatchSize::SmallInput, + ); + }); +} + +criterion_group!(benches, bench_dispatch); +criterion_main!(benches); diff --git a/crates/oxide_vm/benches/gc_mark_sweep.rs b/crates/oxide_vm/benches/gc_mark_sweep.rs new file mode 100644 index 00000000..6d85c661 --- /dev/null +++ b/crates/oxide_vm/benches/gc_mark_sweep.rs @@ -0,0 +1,29 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use oxide_compiler::compiler::Compiler; +use oxide_kernel::kernel::{KernelConfig, KernelCore}; +use oxide_vm::vm::Vm; +use std::sync::Arc; + +fn bench_gc(c: &mut Criterion) { + let mut config = KernelConfig::minimal(); + config.session_gc_threshold = 1024 * 1024; + let kernel = KernelCore::new(config); + let js = "var g = {}; for (var i = 0; i < 500; i++) { var obj = { x: i, y: i * 2 }; g[i] = obj; } g[\"x\"]"; + let alloc = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&alloc, js).expect("parse"); + let module = Compiler.compile(&program).expect("compile"); + + c.bench_function("gc_mark_sweep", |b| { + b.iter_batched( + || Vm::with_kernel_core(Arc::clone(&kernel)), + |mut vm| { + vm.run(&module).ok(); + black_box(vm) + }, + criterion::BatchSize::SmallInput, + ); + }); +} + +criterion_group!(benches, bench_gc); +criterion_main!(benches); diff --git a/crates/oxide_vm/benches/property.rs b/crates/oxide_vm/benches/property.rs new file mode 100644 index 00000000..c7361c07 --- /dev/null +++ b/crates/oxide_vm/benches/property.rs @@ -0,0 +1,27 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use oxide_compiler::compiler::Compiler; +use oxide_kernel::kernel::{KernelConfig, KernelCore}; +use oxide_vm::vm::Vm; +use std::sync::Arc; + +fn bench_property(c: &mut Criterion) { + let kernel = KernelCore::new(KernelConfig::minimal()); + let js = "var obj = { a: 1, b: 2, c: 3, d: 4, e: 5 }; var sum = 0; for (var i = 0; i < 10000; i++) { sum += obj.a + obj.b + obj.c + obj.d + obj.e; } sum"; + let alloc = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&alloc, js).expect("parse"); + let module = Compiler.compile(&program).expect("compile"); + + c.bench_function("property", |b| { + b.iter_batched( + || Vm::with_kernel_core(Arc::clone(&kernel)), + |mut vm| { + vm.run(&module).ok(); + black_box(vm) + }, + criterion::BatchSize::SmallInput, + ); + }); +} + +criterion_group!(benches, bench_property); +criterion_main!(benches); diff --git a/crates/oxide_vm/src/dispatch/property.rs b/crates/oxide_vm/src/dispatch/property.rs index f68f418f..b9fbf9cd 100644 --- a/crates/oxide_vm/src/dispatch/property.rs +++ b/crates/oxide_vm/src/dispatch/property.rs @@ -203,8 +203,10 @@ impl Vm { if let Some(value) = ic_get_hit(obj, cached_shape_id, cached_slot) { self.regs[a] = value; + self.ic_hits.set(self.ic_hits.get() + 1); ic_trace!("IC_GET hit shape={} slot={}", cached_shape_id, cached_slot); } else if let Some(template) = self.kernel_core.prop_forge().get_template(obj.shape_id()) { + self.ic_misses.set(self.ic_misses.get() + 1); prop_cache_miss(); if template.prop_name == prop_name_si { if template.position < obj.prop_vec_len() as u32 { @@ -271,13 +273,16 @@ impl Vm { let cached_slot = ext1; if ic_set_hit(obj, cached_shape_id, cached_slot, value) { + self.ic_hits.set(self.ic_hits.get() + 1); ic_trace!("IC_SET hit shape={} slot={}", cached_shape_id, cached_slot); } else if let Some(pos) = self.kernel_core.shape_forge().lookup_position(obj.shape_id(), prop_name_si) { + self.ic_misses.set(self.ic_misses.get() + 1); prop_cache_miss(); obj.set_prop_at(pos, value); self.write_ic_back(obj.shape_id(), pos); ic_debug!("IC_SET write-back shape={} slot={}", obj.shape_id(), pos); } else { + self.ic_misses.set(self.ic_misses.get() + 1); prop_cache_miss(); let old_shape = obj.shape_id(); self.ordinary_set_dispatch(obj, prop_name_si, value, receiver)?; diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index cccd1350..3f73b71b 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -180,6 +180,9 @@ pub struct Vm { /// The dispatch loop checks this flag and skips writing `regs[target_reg]` from the /// call result — the value will be delivered by the RETURN handler instead. pub(crate) accessor_frame_target_reg: Option, + pub(crate) ic_hits: std::cell::Cell, + pub(crate) ic_misses: std::cell::Cell, + pub(crate) instruction_count: u64, } impl Vm { @@ -328,6 +331,43 @@ impl Vm { &self.session_gc } + pub fn session_object_count(&self) -> usize { + self.session_object_ptrs.len() + } + + pub fn session_bytes_allocated(&self) -> usize { + self.session_bytes_allocated + } + + pub fn epoch_object_count(&self) -> usize { + self.epoch_object_ptrs.len() + } + + pub fn ic_hit_rate(&self) -> f64 { + let total = self.ic_hits.get() + self.ic_misses.get(); + if total == 0 { + 0.0 + } else { + self.ic_hits.get() as f64 / total as f64 + } + } + + pub fn instruction_count(&self) -> u64 { + self.instruction_count + } + + pub fn symbol_registry_len(&self) -> usize { + self.symbol_registry.len() + } + + pub fn ic_hit_count(&self) -> u64 { + self.ic_hits.get() + } + + pub fn ic_miss_count(&self) -> u64 { + self.ic_misses.get() + } + pub(crate) fn checked_object_ptr( &mut self, val: JsValue, error_msg: &str, ) -> Result, String> { @@ -649,10 +689,12 @@ impl Vm { steps += 1; if let Some(max_steps) = self.kernel_core.config.max_steps { if steps > max_steps { + self.instruction_count = steps; return Err(format!("VM step limit exceeded at pc={}", self.pc)); } } if self.pc >= self.bytecode.len() { + self.instruction_count = steps; return Err("program counter out of bounds".into()); } @@ -666,7 +708,10 @@ impl Vm { match op { OpCode::NOP => {} - OpCode::HALT => return Ok(self.regs[0]), + OpCode::HALT => { + self.instruction_count = steps; + return Ok(self.regs[0]); + } OpCode::LOAD_CONST => { self.dispatch_load_const(rd, instr)?; diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 44c9163b..7eb5ed88 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -54,6 +54,9 @@ impl Vm { active_reg_limit: 0, native_call_depth: 0, accessor_frame_target_reg: None, + ic_hits: std::cell::Cell::new(0), + ic_misses: std::cell::Cell::new(0), + instruction_count: 0, } } @@ -96,6 +99,9 @@ impl Vm { active_reg_limit: 0, native_call_depth: 0, accessor_frame_target_reg: None, + ic_hits: std::cell::Cell::new(0), + ic_misses: std::cell::Cell::new(0), + instruction_count: 0, } } diff --git a/tests/stress/array.js b/tests/stress/array.js new file mode 100644 index 00000000..d6266174 --- /dev/null +++ b/tests/stress/array.js @@ -0,0 +1,11 @@ +var ITERATIONS = 50000; + +var arr = []; +for (var i = 0; i < ITERATIONS; i++) { + arr.push(i); +} +var sum = 0; +while (arr.length > 0) { + sum += arr.pop(); +} +sum diff --git a/tests/stress/call.js b/tests/stress/call.js new file mode 100644 index 00000000..8002001b --- /dev/null +++ b/tests/stress/call.js @@ -0,0 +1,7 @@ +var ITERATIONS = 200; + +function sumTo(n) { + if (n <= 0) return 0; + return n + sumTo(n - 1); +} +sumTo(ITERATIONS) diff --git a/tests/stress/code_forge.js b/tests/stress/code_forge.js new file mode 100644 index 00000000..e1cc8a6b --- /dev/null +++ b/tests/stress/code_forge.js @@ -0,0 +1,6 @@ +var ITERATIONS = 5000; + +for (var i = 0; i < ITERATIONS; i++) { + eval("var x" + i + " = " + i + "; x" + i + " + 1"); +} +1 diff --git a/tests/stress/coercion.js b/tests/stress/coercion.js new file mode 100644 index 00000000..494e082a --- /dev/null +++ b/tests/stress/coercion.js @@ -0,0 +1,7 @@ +var ITERATIONS = 50000; + +var sum = 0; +for (var i = 0; i < ITERATIONS; i++) { + sum += (+"42") + (+"3.14") + (+"0"); +} +sum diff --git a/tests/stress/gc.js b/tests/stress/gc.js new file mode 100644 index 00000000..230271d3 --- /dev/null +++ b/tests/stress/gc.js @@ -0,0 +1,8 @@ +var ITERATIONS = 100000; + +var sum = 0; +for (var i = 0; i < ITERATIONS; i++) { + var obj = { x: i, y: i * 2, nested: { a: 1, b: 2 } }; + sum += obj.x + obj.y; +} +sum diff --git a/tests/stress/ic_cache.js b/tests/stress/ic_cache.js new file mode 100644 index 00000000..ac37baf9 --- /dev/null +++ b/tests/stress/ic_cache.js @@ -0,0 +1,13 @@ +var ITERATIONS = 10000; + +var objs = []; +for (var i = 0; i < ITERATIONS; i++) { + objs.push({ x: i }); + if (i % 3 === 0) objs[i].y = i; + if (i % 5 === 0) objs[i].z = i; +} +var sum = 0; +for (var i = 0; i < ITERATIONS; i++) { + sum += objs[i].x; +} +sum diff --git a/tests/stress/property.js b/tests/stress/property.js new file mode 100644 index 00000000..8542d0d8 --- /dev/null +++ b/tests/stress/property.js @@ -0,0 +1,11 @@ +var ITERATIONS = 10000; + +var obj = {}; +for (var i = 0; i < ITERATIONS; i++) { + obj["key" + i] = i; +} +var sum = 0; +for (var i = 0; i < ITERATIONS; i++) { + sum += obj["key" + i]; +} +sum diff --git a/tests/stress/string.js b/tests/stress/string.js new file mode 100644 index 00000000..46f0cd78 --- /dev/null +++ b/tests/stress/string.js @@ -0,0 +1,7 @@ +var ITERATIONS = 10000; + +var s = ""; +for (var i = 0; i < ITERATIONS; i++) { + s += "a" + i; +} +s.length