diff --git a/Cargo.lock b/Cargo.lock index 392e6310..4d4351ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1095,7 +1095,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.6.5" +version = "0.6.6" dependencies = [ "chrono", "fast-float", @@ -1120,7 +1120,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.6.5" +version = "0.6.6" dependencies = [ "num-bigint", "oxide_log", @@ -1128,7 +1128,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.6.5" +version = "0.6.6" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1139,7 +1139,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.6.5" +version = "0.6.6" dependencies = [ "ansi_term", "bumpalo", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.6.5" +version = "0.6.6" dependencies = [ "lru", "oxide_bytecode", @@ -1168,7 +1168,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.6.5" +version = "0.6.6" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1187,7 +1187,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.6.5" +version = "0.6.6" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1201,7 +1201,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.6.5" +version = "0.6.6" dependencies = [ "num-bigint", "oxc_allocator", @@ -1216,7 +1216,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.6.5" +version = "0.6.6" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1225,7 +1225,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.6.5" +version = "0.6.6" dependencies = [ "bumpalo", "dashmap", @@ -1240,7 +1240,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.6.5" +version = "0.6.6" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1252,7 +1252,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.6.5" +version = "0.6.6" dependencies = [ "tracing", "tracing-appender", @@ -1261,7 +1261,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.6.5" +version = "0.6.6" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1276,7 +1276,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.6.5" +version = "0.6.6" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1292,7 +1292,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.6.5" +version = "0.6.6" dependencies = [ "num-bigint", "num-traits", @@ -1304,7 +1304,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.6.5" +version = "0.6.6" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1321,7 +1321,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.6.5" +version = "0.6.6" dependencies = [ "bumpalo", "hashbrown 0.14.5", @@ -1333,7 +1333,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.6.5" +version = "0.6.6" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index ef0f4274..c7b7607b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ members = [ ] [workspace.package] -version = "0.6.5" +version = "0.6.6" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_cli/src/bench/js_stress.rs b/crates/oxide_cli/src/bench/js_stress.rs index 1da2cbc9..3831a228 100644 --- a/crates/oxide_cli/src/bench/js_stress.rs +++ b/crates/oxide_cli/src/bench/js_stress.rs @@ -86,6 +86,11 @@ pub fn run_js_stress_bench(config: &BenchConfig, kernel: &Arc, pool: let mut guard = pool.spawn(); let vm = guard.vm_mut(); + // 指令周期采样只对测量迭代开启:预热不采样,直方图不混入 + // 预热样本,stderr 输出也不随预热迭代重复。 + vm.set_sample_period(config.sample_period); + vm.set_sample_top_k(config.sample_top); + let pre_session = vm.session_object_count(); let pre_epoch = vm.epoch_object_count(); diff --git a/crates/oxide_cli/src/bench/mod.rs b/crates/oxide_cli/src/bench/mod.rs index a4703bee..a6a1362f 100644 --- a/crates/oxide_cli/src/bench/mod.rs +++ b/crates/oxide_cli/src/bench/mod.rs @@ -32,6 +32,11 @@ pub struct BenchConfig { pub iterations: u32, pub update_baseline: bool, pub leak_check_interval: usize, + /// 指令周期采样周期(2 的幂,0 关闭):开启时测量迭代每 period 条指令 + /// 记一条样本,run 末按 flat_id 输出 top-K 直方图。 + pub sample_period: u64, + /// 采样直方图 top-K 大小(默认 10)。 + pub sample_top: usize, } /// 按 mode 分派到具体 benchmark 实现(js/rust/leak)。 diff --git a/crates/oxide_cli/src/main.rs b/crates/oxide_cli/src/main.rs index 069f5fbe..66431bd0 100644 --- a/crates/oxide_cli/src/main.rs +++ b/crates/oxide_cli/src/main.rs @@ -87,6 +87,13 @@ enum Commands { update_baseline: bool, #[arg(long, default_value = "1000")] leak_check_interval: usize, + /// 指令周期采样周期(2 的幂,0 关闭,默认关闭)。开启时测量迭代每 + /// period 条指令记一条样本,run 末按 flat_id 输出 top-K 直方图。 + #[arg(long, default_value = "0")] + sample: u64, + /// 采样直方图 top-K 大小(默认 10)。 + #[arg(long, default_value = "10")] + sample_top: usize, }, Test { suite: Option, @@ -130,9 +137,17 @@ fn main() -> ExitCode { iterations, update_baseline, leak_check_interval, + sample, + sample_top, }) => { let kernel = make_kernel(false, false); let pool = make_pool(&kernel); + // 采样周期边界检查:须为 0(关闭)或 2 的幂,否则指令边界的 + // `steps & (period - 1) == 0` 判定退化(周期不整除步数序列)。 + if sample != 0 && sample & (sample - 1) != 0 { + eprintln!("--sample period must be 0 (disabled) or a power of two, got {sample}"); + return ExitCode::FAILURE; + } let config = bench::BenchConfig { mode: mode.unwrap_or_else(|| "js".to_string()), filter, @@ -140,6 +155,8 @@ fn main() -> ExitCode { iterations, update_baseline, leak_check_interval, + sample_period: sample, + sample_top, }; bench::run_benchmarks(config, kernel, pool) } diff --git a/crates/oxide_vm/src/vm/dispatch_loop.rs b/crates/oxide_vm/src/vm/dispatch_loop.rs index 1c23ad87..00ee2d3f 100644 --- a/crates/oxide_vm/src/vm/dispatch_loop.rs +++ b/crates/oxide_vm/src/vm/dispatch_loop.rs @@ -234,6 +234,15 @@ impl Vm { } } + // 指令周期采样:开启时(周期为 2 的幂)每 period 条指令记一条样本 + // (flat_id, pc, opcode, frames 深度),run 末聚合成 top-K 直方图; + // 关闭时仅这一条可预测分支,零写零分配。 + if self.sampling.period != 0 && (steps & (self.sampling.period - 1)) == 0 { + self.sampling + .records + .push((self.active_flat_id, pc as u32, op as u8, self.frames.len() as u32)); + } + match op { OpCode::NOP => {} diff --git a/crates/oxide_vm/src/vm/mod.rs b/crates/oxide_vm/src/vm/mod.rs index d814266b..4301ea52 100644 --- a/crates/oxide_vm/src/vm/mod.rs +++ b/crates/oxide_vm/src/vm/mod.rs @@ -18,7 +18,7 @@ use smallvec::SmallVec; pub use crate::bindings::init_kernel_builtins; use crate::native::NativeFn; use crate::vm_debug; -use crate::vm_state::{GcState, IterState, ProfilingState, SymbolState}; +use crate::vm_state::{GcState, IterState, ProfilingState, SampleState, SymbolState}; use oxide_kernel::kernel::{KernelCore, KernelSession}; use oxide_types::error::JsErrorKind; use oxide_types::mem::P; @@ -354,6 +354,8 @@ pub struct Vm { pub(crate) iters: IterState, /// 分组保存 inline cache 与指令计数器。 pub(crate) profiling: ProfilingState, + /// 分组保存指令周期采样状态(周期、top-K、样本记录)。 + pub(crate) sampling: SampleState, pub(crate) cell_stack: Vec>, /// 标签模板对象缓存(GetTemplateObject):键 = (表代际, 模块 flat_id, site 序号)。 /// 同一代际同 flat_id 同 site 恒返回同一对象;每次 `run()` 清空(缓存只留 @@ -455,6 +457,19 @@ impl Vm { self.pc_watch = path; } + /// 设置指令周期采样周期(2 的幂,0 关闭)。开启后 dispatch 主循环每 + /// `period` 条指令记一条样本(flat_id、pc、opcode、frames 深度),run 末 + /// 按 flat_id 聚合并向 stderr 输出 top-K 直方图;关闭时热路径仅一次 + /// 可预测分支,零写零分配。 + pub fn set_sample_period(&mut self, period: u64) { + self.sampling.period = period; + } + + /// 设置采样直方图的 top-K 大小(默认 10)。 + pub fn set_sample_top_k(&mut self, k: usize) { + self.sampling.top_k = k; + } + /// 若 `val` 是字符串,返回其内容的 `String` 副本;否则返回 `None`。 pub fn lookup_str(&self, val: JsValue) -> Option { if !val.is_string() { diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index e27647c0..1efc07e4 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -1,6 +1,8 @@ +use std::collections::HashMap; use std::sync::{Arc, OnceLock}; use oxide_bytecode::module::CompiledModule; +use oxide_bytecode::opcode::OpCode; use crate::vm::{CallFrame, FrameArgs, FrameContinuation, InlineSyncState, TableGen, Vm}; use crate::{vm_debug, vm_info, vm_trace, vm_warn}; @@ -702,9 +704,81 @@ impl Vm { let result = self.dispatch(); // 顶层执行结束后 drain 微任务队列:Promise reactions 与 thenable 委托在此执行。 self.drain_job_queue(); + // 指令周期采样:run 末聚合本 run 的样本并输出 top-K 直方图(关闭时零开销 + // 短路)。放 dispatch 返回后而非循环内:嵌套 dispatch 会多次返回,直方图 + // 只应输出一次。 + self.print_sample_histogram(); result } + /// 按 flat_id 聚合采样记录并向 stderr 输出 top-K 热点直方图。 + /// + /// 聚合键取 (flat_id, pc):样本数按 flat_id 汇总,top pc 取该 flat_id 下 + /// 样本数最多的字节码偏移(与反汇编 offset 列同单位)。函数名经当前表代际 + /// 平表解析(顶层为 0),未知 flat_id 输出 ``。 + /// + /// # 边界 + /// - 采样关闭(`period == 0`)或无样本时直接返回,零输出零分配。 + /// - 只读采样记录与平表,不改写任何执行状态。 + pub(crate) fn print_sample_histogram(&self) { + if self.sampling.period == 0 || self.sampling.records.is_empty() { + return; + } + // 聚合:(flat_id, pc) → 样本数与 opcode;flat_id → 最大 frames 深度。 + let mut pc_hist: HashMap<(u32, u32), (u64, u8)> = HashMap::new(); + let mut flat_max_frames: HashMap = HashMap::new(); + for &(flat, pc, op, frames) in &self.sampling.records { + let entry = pc_hist.entry((flat, pc)).or_insert((0, op)); + entry.0 += 1; + let top = flat_max_frames.entry(flat).or_insert(frames); + if frames > *top { + *top = frames; + } + } + // 按 flat_id 汇总样本数,降序排列后取 top-K。 + let mut flat_total: HashMap = HashMap::new(); + for (&(flat, _), &(count, _)) in &pc_hist { + *flat_total.entry(flat).or_insert(0) += count; + } + let mut ranked: Vec<(u32, u64)> = flat_total.into_iter().collect(); + ranked.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0))); + let k = self.sampling.top_k.min(ranked.len()); + eprintln!( + "sample top {k} (period {}, {} samples, {} instructions):", + self.sampling.period, + self.sampling.records.len(), + self.profiling.instruction_count + ); + for (rank, &(flat, count)) in ranked.iter().take(k).enumerate() { + // top pc:该 flat_id 下样本数最多的字节码偏移(并列取小 pc)。 + let mut top_pc: Option<(u64, u32, u8)> = None; + for (&(f, pc), &(c, op)) in &pc_hist { + if f != flat { + continue; + } + match top_pc { + None => top_pc = Some((c, pc, op)), + Some((tc, _, _)) if c > tc => top_pc = Some((c, pc, op)), + Some(_) => {} + } + } + let (top_count, top_pc, top_op) = top_pc.unwrap_or((0, 0, 0)); + let op_name = OpCode::try_from(top_op) + .map(|o| o.to_string()) + .unwrap_or_else(|_| format!("op{top_op}")); + let name = self + .tables + .get(&self.current_gen) + .and_then(|t| t.modules.get(flat as usize)) + .and_then(|m| m.function_name.clone()) + .unwrap_or_else(|| format!("")); + let frames = flat_max_frames.get(&flat).copied().unwrap_or(0); + eprintln!( + " {rank:>2}. {name:<24} (flat {flat}) {count:>8} samples top pc={top_pc} {op_name} ({top_count}) frames={frames}" + ); + } + } + /// 异常展开:沿 `try_stack` 查找最近可接管的 catch/finally,无则逐帧回退并返回未捕获错误。 /// /// 与 `TRY_FINALLY_END` 的完成穿越逻辑对应:finally 体在途异常经 `pending_exception` diff --git a/crates/oxide_vm/src/vm_state.rs b/crates/oxide_vm/src/vm_state.rs index 4ab5607d..a78056f0 100644 --- a/crates/oxide_vm/src/vm_state.rs +++ b/crates/oxide_vm/src/vm_state.rs @@ -235,6 +235,28 @@ impl IterState { } } +/// 指令周期采样状态:采样周期(2 的幂)与样本记录表。 +/// +/// 开启时 dispatch 主循环每 `period` 条指令记一条样本(flat_id、pc、opcode、 +/// frames 深度),run 末按 flat_id 聚合并输出 top-K 直方图(stderr)。 +/// 关闭(`period == 0`)时热路径仅一次可预测分支,零写零分配。 +pub(crate) struct SampleState { + /// 采样周期(2 的幂,0 = 关闭)。 + pub(crate) period: u64, + /// 直方图 top-K 大小(默认 10)。 + pub(crate) top_k: usize, + /// 样本记录:(flat_id, pc, opcode, frames 深度),run 边界清空。 + pub(crate) records: Vec<(u32, u32, u8, u32)>, +} + +impl SampleState { + /// 清空样本记录(run 边界)。只清记录:period 与 top_k 是调用方 + /// 设置的配置,不随 run 边界重置。 + pub(crate) fn clear_records(&mut self) { + self.records.clear(); + } +} + /// inline cache 命中/未命中计数与指令计数。 pub(crate) struct ProfilingState { pub(crate) ic_hits: Cell, diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index 5d4a8e53..d7d051d5 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -8,7 +8,7 @@ use oxide_bytecode::module::Constant; use crate::bindings; use crate::vm::{TableGen, Vm}; use crate::vm_info; -use crate::vm_state::{GcState, IterState, ProfilingState, SymbolState}; +use crate::vm_state::{GcState, IterState, ProfilingState, SampleState, SymbolState}; use oxide_kernel::kernel::{KernelConfig, KernelCore, KernelSession}; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_types::mem::P; @@ -149,6 +149,11 @@ impl Vm { ic_misses: std::cell::Cell::new(0), instruction_count: 0, }, + sampling: SampleState { + period: 0, + top_k: 10, + records: Vec::new(), + }, cell_stack: Vec::new(), template_objects: std::collections::HashMap::new(), active_flat_id: 0, @@ -283,6 +288,11 @@ impl Vm { ic_misses: std::cell::Cell::new(0), instruction_count: 0, }, + sampling: SampleState { + period: 0, + top_k: 10, + records: Vec::new(), + }, cell_stack: Vec::new(), template_objects: std::collections::HashMap::new(), active_flat_id: 0, @@ -522,6 +532,9 @@ impl Vm { // per-run 指标(ic_hit_rate)跨文件累积,报告失真。 self.profiling.ic_hits.set(0); self.profiling.ic_misses.set(0); + // 采样记录同属执行期状态:池化 Vm 跨 run 复用,不清空则上一 run 的 + // 样本混入本 run 的直方图,热点分布失真。 + self.sampling.clear_records(); // GC 统计同属执行期状态:池化 Vm 跨 run 复用,不清零则 per-run // 指标(gc_trigger_count)跨文件累积,报告失真。 let gc = &mut self.gc_state.session_gc;