feat: 新增采样器按指令周期记录样本并输出热点直方图

This commit is contained in:
2026-10-04 21:40:26 +00:00
parent 7390d66f3e
commit b77f75b237
10 changed files with 181 additions and 21 deletions
Generated
+18 -18
View File
@@ -1095,7 +1095,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_builtins" name = "oxide_builtins"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"chrono", "chrono",
"fast-float", "fast-float",
@@ -1120,7 +1120,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_bytecode" name = "oxide_bytecode"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"oxide_log", "oxide_log",
@@ -1128,7 +1128,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_cfg" name = "oxide_cfg"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_emit", "oxide_emit",
@@ -1139,7 +1139,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_cli" name = "oxide_cli"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"ansi_term", "ansi_term",
"bumpalo", "bumpalo",
@@ -1159,7 +1159,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_code_cache" name = "oxide_code_cache"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"lru", "lru",
"oxide_bytecode", "oxide_bytecode",
@@ -1168,7 +1168,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_compiler" name = "oxide_compiler"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1187,7 +1187,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_dce" name = "oxide_dce"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1201,7 +1201,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_emit" name = "oxide_emit"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"oxc_allocator", "oxc_allocator",
@@ -1216,7 +1216,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_ir" name = "oxide_ir"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_log", "oxide_log",
@@ -1225,7 +1225,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_kernel" name = "oxide_kernel"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"dashmap", "dashmap",
@@ -1240,7 +1240,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_liveness" name = "oxide_liveness"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1252,7 +1252,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_log" name = "oxide_log"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"tracing", "tracing",
"tracing-appender", "tracing-appender",
@@ -1261,7 +1261,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_parser" name = "oxide_parser"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"oxc_allocator", "oxc_allocator",
"oxc_ast", "oxc_ast",
@@ -1276,7 +1276,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_regalloc" name = "oxide_regalloc"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_cfg", "oxide_cfg",
@@ -1292,7 +1292,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_runtime_api" name = "oxide_runtime_api"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"num-bigint", "num-bigint",
"num-traits", "num-traits",
@@ -1304,7 +1304,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_test262" name = "oxide_test262"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"oxide_bytecode", "oxide_bytecode",
"oxide_compiler", "oxide_compiler",
@@ -1321,7 +1321,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_types" name = "oxide_types"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"hashbrown 0.14.5", "hashbrown 0.14.5",
@@ -1333,7 +1333,7 @@ dependencies = [
[[package]] [[package]]
name = "oxide_vm" name = "oxide_vm"
version = "0.6.5" version = "0.6.6"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"chrono", "chrono",
+1 -1
View File
@@ -22,7 +22,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.6.5" version = "0.6.6"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
rust-version = "1.80" rust-version = "1.80"
+5
View File
@@ -86,6 +86,11 @@ pub fn run_js_stress_bench(config: &BenchConfig, kernel: &Arc<KernelCore>, pool:
let mut guard = pool.spawn(); let mut guard = pool.spawn();
let vm = guard.vm_mut(); 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_session = vm.session_object_count();
let pre_epoch = vm.epoch_object_count(); let pre_epoch = vm.epoch_object_count();
+5
View File
@@ -32,6 +32,11 @@ pub struct BenchConfig {
pub iterations: u32, pub iterations: u32,
pub update_baseline: bool, pub update_baseline: bool,
pub leak_check_interval: usize, 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)。 /// 按 mode 分派到具体 benchmark 实现(js/rust/leak)。
+17
View File
@@ -87,6 +87,13 @@ enum Commands {
update_baseline: bool, update_baseline: bool,
#[arg(long, default_value = "1000")] #[arg(long, default_value = "1000")]
leak_check_interval: usize, 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 { Test {
suite: Option<String>, suite: Option<String>,
@@ -130,9 +137,17 @@ fn main() -> ExitCode {
iterations, iterations,
update_baseline, update_baseline,
leak_check_interval, leak_check_interval,
sample,
sample_top,
}) => { }) => {
let kernel = make_kernel(false, false); let kernel = make_kernel(false, false);
let pool = make_pool(&kernel); 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 { let config = bench::BenchConfig {
mode: mode.unwrap_or_else(|| "js".to_string()), mode: mode.unwrap_or_else(|| "js".to_string()),
filter, filter,
@@ -140,6 +155,8 @@ fn main() -> ExitCode {
iterations, iterations,
update_baseline, update_baseline,
leak_check_interval, leak_check_interval,
sample_period: sample,
sample_top,
}; };
bench::run_benchmarks(config, kernel, pool) bench::run_benchmarks(config, kernel, pool)
} }
+9
View File
@@ -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 { match op {
OpCode::NOP => {} OpCode::NOP => {}
+16 -1
View File
@@ -18,7 +18,7 @@ use smallvec::SmallVec;
pub use crate::bindings::init_kernel_builtins; pub use crate::bindings::init_kernel_builtins;
use crate::native::NativeFn; use crate::native::NativeFn;
use crate::vm_debug; 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_kernel::kernel::{KernelCore, KernelSession};
use oxide_types::error::JsErrorKind; use oxide_types::error::JsErrorKind;
use oxide_types::mem::P; use oxide_types::mem::P;
@@ -354,6 +354,8 @@ pub struct Vm {
pub(crate) iters: IterState, pub(crate) iters: IterState,
/// 分组保存 inline cache 与指令计数器。 /// 分组保存 inline cache 与指令计数器。
pub(crate) profiling: ProfilingState, pub(crate) profiling: ProfilingState,
/// 分组保存指令周期采样状态(周期、top-K、样本记录)。
pub(crate) sampling: SampleState,
pub(crate) cell_stack: Vec<Vec<*mut Cell>>, pub(crate) cell_stack: Vec<Vec<*mut Cell>>,
/// 标签模板对象缓存(GetTemplateObject):键 = (表代际, 模块 flat_id, site 序号)。 /// 标签模板对象缓存(GetTemplateObject):键 = (表代际, 模块 flat_id, site 序号)。
/// 同一代际同 flat_id 同 site 恒返回同一对象;每次 `run()` 清空(缓存只留 /// 同一代际同 flat_id 同 site 恒返回同一对象;每次 `run()` 清空(缓存只留
@@ -455,6 +457,19 @@ impl Vm {
self.pc_watch = path; 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`。 /// 若 `val` 是字符串,返回其内容的 `String` 副本;否则返回 `None`。
pub fn lookup_str(&self, val: JsValue) -> Option<String> { pub fn lookup_str(&self, val: JsValue) -> Option<String> {
if !val.is_string() { if !val.is_string() {
+74
View File
@@ -1,6 +1,8 @@
use std::collections::HashMap;
use std::sync::{Arc, OnceLock}; use std::sync::{Arc, OnceLock};
use oxide_bytecode::module::CompiledModule; use oxide_bytecode::module::CompiledModule;
use oxide_bytecode::opcode::OpCode;
use crate::vm::{CallFrame, FrameArgs, FrameContinuation, InlineSyncState, TableGen, Vm}; use crate::vm::{CallFrame, FrameArgs, FrameContinuation, InlineSyncState, TableGen, Vm};
use crate::{vm_debug, vm_info, vm_trace, vm_warn}; use crate::{vm_debug, vm_info, vm_trace, vm_warn};
@@ -702,9 +704,81 @@ impl Vm {
let result = self.dispatch(); let result = self.dispatch();
// 顶层执行结束后 drain 微任务队列:Promise reactions 与 thenable 委托在此执行。 // 顶层执行结束后 drain 微任务队列:Promise reactions 与 thenable 委托在此执行。
self.drain_job_queue(); self.drain_job_queue();
// 指令周期采样:run 末聚合本 run 的样本并输出 top-K 直方图(关闭时零开销
// 短路)。放 dispatch 返回后而非循环内:嵌套 dispatch 会多次返回,直方图
// 只应输出一次。
self.print_sample_histogram();
result result
} }
/// 按 flat_id 聚合采样记录并向 stderr 输出 top-K 热点直方图。
///
/// 聚合键取 (flat_id, pc):样本数按 flat_id 汇总,top pc 取该 flat_id 下
/// 样本数最多的字节码偏移(与反汇编 offset 列同单位)。函数名经当前表代际
/// 平表解析(顶层为 0),未知 flat_id 输出 `<flat N>`。
///
/// # 边界
/// - 采样关闭(`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<u32, u32> = 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<u32, u64> = 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!("<flat {flat}>"));
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_stack` 查找最近可接管的 catch/finally,无则逐帧回退并返回未捕获错误。
/// ///
/// 与 `TRY_FINALLY_END` 的完成穿越逻辑对应:finally 体在途异常经 `pending_exception` /// 与 `TRY_FINALLY_END` 的完成穿越逻辑对应:finally 体在途异常经 `pending_exception`
+22
View File
@@ -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 命中/未命中计数与指令计数。 /// inline cache 命中/未命中计数与指令计数。
pub(crate) struct ProfilingState { pub(crate) struct ProfilingState {
pub(crate) ic_hits: Cell<u64>, pub(crate) ic_hits: Cell<u64>,
+14 -1
View File
@@ -8,7 +8,7 @@ use oxide_bytecode::module::Constant;
use crate::bindings; use crate::bindings;
use crate::vm::{TableGen, Vm}; use crate::vm::{TableGen, Vm};
use crate::vm_info; 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::kernel::{KernelConfig, KernelCore, KernelSession};
use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID;
use oxide_types::mem::P; use oxide_types::mem::P;
@@ -149,6 +149,11 @@ impl Vm {
ic_misses: std::cell::Cell::new(0), ic_misses: std::cell::Cell::new(0),
instruction_count: 0, instruction_count: 0,
}, },
sampling: SampleState {
period: 0,
top_k: 10,
records: Vec::new(),
},
cell_stack: Vec::new(), cell_stack: Vec::new(),
template_objects: std::collections::HashMap::new(), template_objects: std::collections::HashMap::new(),
active_flat_id: 0, active_flat_id: 0,
@@ -283,6 +288,11 @@ impl Vm {
ic_misses: std::cell::Cell::new(0), ic_misses: std::cell::Cell::new(0),
instruction_count: 0, instruction_count: 0,
}, },
sampling: SampleState {
period: 0,
top_k: 10,
records: Vec::new(),
},
cell_stack: Vec::new(), cell_stack: Vec::new(),
template_objects: std::collections::HashMap::new(), template_objects: std::collections::HashMap::new(),
active_flat_id: 0, active_flat_id: 0,
@@ -522,6 +532,9 @@ impl Vm {
// per-run 指标(ic_hit_rate)跨文件累积,报告失真。 // per-run 指标(ic_hit_rate)跨文件累积,报告失真。
self.profiling.ic_hits.set(0); self.profiling.ic_hits.set(0);
self.profiling.ic_misses.set(0); self.profiling.ic_misses.set(0);
// 采样记录同属执行期状态:池化 Vm 跨 run 复用,不清空则上一 run 的
// 样本混入本 run 的直方图,热点分布失真。
self.sampling.clear_records();
// GC 统计同属执行期状态:池化 Vm 跨 run 复用,不清零则 per-run // GC 统计同属执行期状态:池化 Vm 跨 run 复用,不清零则 per-run
// 指标(gc_trigger_count)跨文件累积,报告失真。 // 指标(gc_trigger_count)跨文件累积,报告失真。
let gc = &mut self.gc_state.session_gc; let gc = &mut self.gc_state.session_gc;