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
+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 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();
+5
View File
@@ -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)。
+17
View File
@@ -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<String>,
@@ -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)
}
+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 {
OpCode::NOP => {}
+16 -1
View File
@@ -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<Vec<*mut Cell>>,
/// 标签模板对象缓存(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<String> {
if !val.is_string() {
+74
View File
@@ -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 输出 `<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_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 命中/未命中计数与指令计数。
pub(crate) struct ProfilingState {
pub(crate) ic_hits: Cell<u64>,
+14 -1
View File
@@ -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;