diff --git a/Cargo.lock b/Cargo.lock index ea34afc3..392e6310 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1095,7 +1095,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.6.4" +version = "0.6.5" dependencies = [ "chrono", "fast-float", @@ -1120,7 +1120,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.6.4" +version = "0.6.5" dependencies = [ "num-bigint", "oxide_log", @@ -1128,7 +1128,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.6.4" +version = "0.6.5" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1139,7 +1139,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.6.4" +version = "0.6.5" dependencies = [ "ansi_term", "bumpalo", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.6.4" +version = "0.6.5" dependencies = [ "lru", "oxide_bytecode", @@ -1168,7 +1168,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.6.4" +version = "0.6.5" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1187,7 +1187,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.6.4" +version = "0.6.5" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1201,7 +1201,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.6.4" +version = "0.6.5" dependencies = [ "num-bigint", "oxc_allocator", @@ -1216,7 +1216,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.6.4" +version = "0.6.5" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1225,7 +1225,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.6.4" +version = "0.6.5" dependencies = [ "bumpalo", "dashmap", @@ -1240,7 +1240,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.6.4" +version = "0.6.5" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1252,7 +1252,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.6.4" +version = "0.6.5" dependencies = [ "tracing", "tracing-appender", @@ -1261,7 +1261,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.6.4" +version = "0.6.5" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1276,7 +1276,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.6.4" +version = "0.6.5" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1292,7 +1292,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.6.4" +version = "0.6.5" dependencies = [ "num-bigint", "num-traits", @@ -1304,7 +1304,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.6.4" +version = "0.6.5" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1321,7 +1321,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.6.4" +version = "0.6.5" dependencies = [ "bumpalo", "hashbrown 0.14.5", @@ -1333,7 +1333,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.6.4" +version = "0.6.5" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 1b287e61..ef0f4274 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ members = [ ] [workspace.package] -version = "0.6.4" +version = "0.6.5" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_cli/src/bench/mod.rs b/crates/oxide_cli/src/bench/mod.rs index fa7af6fd..a4703bee 100644 --- a/crates/oxide_cli/src/bench/mod.rs +++ b/crates/oxide_cli/src/bench/mod.rs @@ -13,6 +13,8 @@ pub mod leak_detect; pub mod metrics; /// 结果格式化输出。 pub mod output; +/// `--profile` 旗标的聚合计数器快照。 +pub mod profile; /// Rust 原生 benchmark 委托。 pub mod rust_bench; diff --git a/crates/oxide_cli/src/bench/profile.rs b/crates/oxide_cli/src/bench/profile.rs new file mode 100644 index 00000000..62a8723f --- /dev/null +++ b/crates/oxide_cli/src/bench/profile.rs @@ -0,0 +1,104 @@ +//! `--profile` 旗标的聚合计数器快照:只读聚合现有计数器(GC 统计、内联缓存 +//! 命中/未命中、session 堆账目、指令数),按每指标一行输出。 +//! +//! 零热路径写:`collect` 只从 VM 内省面读 getter,不写任何计数器;旗标默认 +//! 关闭,关闭时不进入采集路径。 + +use oxide_vm::vm::Vm; + +/// 单次 run 的聚合计数器快照(现有计数器的只读聚合)。 +#[derive(Debug, Clone)] +pub struct ProfileOutput { + // GC 统计(session 累计口径,12 字段) + pub gc_total_collections: u64, + pub gc_total_bytes_freed: u64, + pub gc_total_objects_scanned: u64, + pub gc_total_objects_live: u64, + pub gc_total_objects_dead: u64, + pub gc_last_objects_scanned: u64, + pub gc_last_objects_live: u64, + pub gc_last_objects_dead: u64, + pub gc_last_bytes_freed: u64, + pub gc_last_duration_us: u64, + pub gc_max_duration_us: u64, + pub gc_min_duration_us: u64, + // 内联缓存命中/未命中 + pub ic_hits: u64, + pub ic_misses: u64, + pub ic_hit_rate: f64, + // session 堆账目 + pub session_objects: usize, + pub session_bytes: usize, + pub session_bytes_peak: usize, + pub run_alloc_peak: usize, + pub run_alloc_bytes: usize, + // 指令数与执行时长 + pub instruction_count: u64, + pub exec_time_us: u64, +} + +impl ProfileOutput { + /// 从 VM 内省面只读聚合(零热路径写,不写任何计数器)。 + /// + /// `exec_time_us` 由调用方在 `run` 前后计时后传入。 + pub fn collect(vm: &Vm, exec_time_us: u64) -> Self { + let gc = vm.session_gc_stats(); + Self { + gc_total_collections: gc.total_collections, + gc_total_bytes_freed: gc.total_bytes_freed, + gc_total_objects_scanned: gc.total_objects_scanned, + gc_total_objects_live: gc.total_objects_live, + gc_total_objects_dead: gc.total_objects_dead, + gc_last_objects_scanned: gc.last_collection_objects_scanned, + gc_last_objects_live: gc.last_collection_objects_live, + gc_last_objects_dead: gc.last_collection_objects_dead, + gc_last_bytes_freed: gc.last_collection_bytes_freed, + gc_last_duration_us: gc.last_collection_duration_us, + gc_max_duration_us: gc.max_collection_duration_us, + // 无收集时 min 字段为 u64::MAX 哨兵,映射为 0 避免输出哨兵值。 + gc_min_duration_us: if gc.min_collection_duration_us == u64::MAX { + 0 + } else { + gc.min_collection_duration_us + }, + ic_hits: vm.ic_hit_count(), + ic_misses: vm.ic_miss_count(), + ic_hit_rate: vm.ic_hit_rate(), + session_objects: vm.session_object_count(), + session_bytes: vm.session_bytes_allocated(), + session_bytes_peak: vm.session_bytes_peak(), + run_alloc_peak: vm.run_alloc_peak(), + run_alloc_bytes: vm.run_alloc_bytes_total(), + instruction_count: vm.instruction_count(), + exec_time_us, + } + } + + /// 导出为 `(指标名, 值)` 列表,每指标一行,与 bench 输出对齐。 + pub fn iter_metrics(&self) -> Vec<(&'static str, String)> { + vec![ + ("instruction_count", self.instruction_count.to_string()), + ("session_objects", self.session_objects.to_string()), + ("session_bytes", self.session_bytes.to_string()), + ("session_bytes_peak", self.session_bytes_peak.to_string()), + ("run_alloc_peak", self.run_alloc_peak.to_string()), + ("run_alloc_bytes", self.run_alloc_bytes.to_string()), + ("ic_hit_rate", format!("{:.4}", self.ic_hit_rate)), + ("ic_hits", self.ic_hits.to_string()), + ("ic_misses", self.ic_misses.to_string()), + ("gc_total_collections", self.gc_total_collections.to_string()), + ("gc_total_bytes_freed", self.gc_total_bytes_freed.to_string()), + ("gc_total_objects_scanned", self.gc_total_objects_scanned.to_string()), + ("gc_total_objects_live", self.gc_total_objects_live.to_string()), + ("gc_total_objects_dead", self.gc_total_objects_dead.to_string()), + ("gc_last_objects_scanned", self.gc_last_objects_scanned.to_string()), + ("gc_last_objects_live", self.gc_last_objects_live.to_string()), + ("gc_last_objects_dead", self.gc_last_objects_dead.to_string()), + ("gc_last_bytes_freed", self.gc_last_bytes_freed.to_string()), + ("gc_last_duration_us", self.gc_last_duration_us.to_string()), + ("gc_max_duration_us", self.gc_max_duration_us.to_string()), + ("gc_min_duration_us", self.gc_min_duration_us.to_string()), + ("exec_time_us", self.exec_time_us.to_string()), + ] + } +} diff --git a/crates/oxide_cli/src/main.rs b/crates/oxide_cli/src/main.rs index d13dbe07..069f5fbe 100644 --- a/crates/oxide_cli/src/main.rs +++ b/crates/oxide_cli/src/main.rs @@ -4,6 +4,7 @@ use std::fs; use std::process::ExitCode; use std::sync::Arc; +use std::time::Instant; use ansi_term::Colour::Red; use clap::{Parser, Subcommand}; @@ -40,6 +41,11 @@ struct Cli { #[arg(short, long, global = true)] quiet: bool, + + /// 聚合并输出现有计数器(GC 统计、内联缓存、session 堆账目、指令数)。 + /// 默认关闭,关闭时零开销;输出每指标一行,与 bench 输出对齐。 + #[arg(long, global = true)] + profile: bool, } #[derive(Subcommand)] @@ -94,13 +100,13 @@ fn main() -> ExitCode { Some(Commands::Eval { code, trace }) => { let kernel = make_kernel(cli.verbose, cli.quiet); let pool = make_pool(&kernel); - eval(&code, &kernel, &pool, trace, true) + eval(&code, &kernel, &pool, trace, cli.profile, true) } Some(Commands::Run { file, repeat, trace }) => { let kernel = make_kernel(cli.verbose, cli.quiet); let pool = make_pool(&kernel); for n in 0..repeat { - let code = run(&file, &kernel, &pool, trace); + let code = run(&file, &kernel, &pool, trace, cli.profile); // 失败即终止并传播退出码,供脚本与 CI 区分成败。 if code != ExitCode::SUCCESS { return code; @@ -158,7 +164,9 @@ fn make_pool(kernel: &Arc) -> Arc { VmPool::new(Arc::clone(kernel), kernel.config.min_pool_size, kernel.config.max_pool_size) } -fn eval(code: &str, kernel: &Arc, pool: &Arc, trace: bool, print_result: bool) -> ExitCode { +fn eval( + code: &str, kernel: &Arc, pool: &Arc, trace: bool, profile: bool, print_result: bool, +) -> ExitCode { let allocator = Allocator::default(); let program = match oxide_parser::parse(&allocator, code) { Ok(p) => p, @@ -184,8 +192,16 @@ fn eval(code: &str, kernel: &Arc, pool: &Arc, trace: bool, p let mut guard = pool.spawn(); guard.vm_mut().set_instruction_trace(trace); + let exec_start = Instant::now(); match guard.vm_mut().run(&module) { Ok(result) => { + // --profile 开启时聚合现有计数器,每指标一行输出到 stderr(stdout 保持纯结果)。 + if profile { + let p = bench::profile::ProfileOutput::collect(guard.vm(), exec_start.elapsed().as_micros() as u64); + for (name, value) in p.iter_metrics() { + eprintln!("{name} = {value}"); + } + } // eval 臂按 REPL 语义打印完成值;run 臂脚本只输出自身产生内容。 if print_result { format_result(guard.vm(), kernel.perm_interner().as_ref(), kernel.shape_forge().as_ref(), result); @@ -292,9 +308,9 @@ fn format_array( format!("[{}]", items.join(", ")) } -fn run(file: &str, kernel: &Arc, pool: &Arc, trace: bool) -> ExitCode { +fn run(file: &str, kernel: &Arc, pool: &Arc, trace: bool, profile: bool) -> ExitCode { match fs::read_to_string(file) { - Ok(source) => eval(&source, kernel, pool, trace, false), + Ok(source) => eval(&source, kernel, pool, trace, profile, false), Err(err) => { kernel_error!("cannot read {}: {}", file, err); eprintln!("{}", Red.paint(format!("Cannot read {file}: {err}"))); diff --git a/crates/oxide_vm/src/vm/gc_hooks.rs b/crates/oxide_vm/src/vm/gc_hooks.rs index fbca1fdc..63d13ad1 100644 --- a/crates/oxide_vm/src/vm/gc_hooks.rs +++ b/crates/oxide_vm/src/vm/gc_hooks.rs @@ -185,6 +185,14 @@ impl Vm { + self.gc_state.session_cell_ptrs.borrow().len() * std::mem::size_of::() } + /// 本 run 累计分配字节(O(1) 读)。 + /// + /// 公开包装,供 profiling 统一出口跨 crate 读取:与 `run_alloc_bytes` 同一读 + /// 口径,零行为变化。 + pub fn run_alloc_bytes_total(&self) -> usize { + self.run_alloc_bytes() + } + /// 本 run 累计分配字节的全量重算版:各分量按当前值各计一次、互不相交—— /// 存活对象头加堆数据(属性/元素/meta 向量容量、upvalue 列表容量、native /// 状态盒)+ 存活串(头加 payload)+ BigInt 表长 × 尺寸 + cell 表长 ×