首页
/ Deno 基准测试工具 deno_bench_util:为 deno_core 的 op 编写性能基准与火焰图剖析指南

Deno 基准测试工具 deno_bench_util:为 deno_core 的 op 编写性能基准与火焰图剖析指南

2026-09-06 19:13:59作者:宣利权Counsellor

本文围绕 tests/bench_util 目录下的 deno_bench_util 基准测试工具包展开,讲解它如何帮助开发者针对 deno_core 的 op(operations)系统编写高性能基准测试:从最小可用的示例入手,深入其 JS 运行时创建、循环展开、同步/异步两种 bench 入口,以及 bench_or_profile! 宏支持的“基准测试与 CPU 剖析二合一”模式,并给出仓库中真实 bench 用例(如 UTF-8 编解码、TextEncoder/TextDecoder、op 调用开销)的完整实现剖析。读完后你可以直接在任意 Deno crate 中搭建可复现、可剖析的 JS 侧性能基准。

这个 crate 解决什么问题

在 Deno 中,Rust 侧通过 #[op2] 宏定义的 op 会被暴露给 JS 侧的 Deno.core.ops 命名空间。评估一次 JS→Rust 调用的真实开销,需要把 JsRuntime 的构建、op 的注册、JS 脚本的编译与循环执行全部串起来手写一遍——重复且易错。deno_bench_util(crate 名 deno_bench_util,见 tests/bench_util/Cargo.toml)把这套流程封装成几个函数和一个宏,让基准代码只需关注“跑什么 JS”。

tests/bench_util/lib.rs 可以看到整个 crate 的对外导出只有三行核心内容:

mod js_runtime;
mod profiling;

pub use bencher;
pub use js_runtime::*;
pub use profiling::*; // Exports bench_or_profile! macro
  • bencher:直接 re-export 第三方 bencher crate(Rust 侧的标准 bench harness),Bencherbenchmark_group! 等均来自它;
  • js_runtime::*:即 tests/bench_util/js_runtime.rs,提供运行时创建与 bench_js_sync/bench_js_async 等入口;
  • profiling::*:即 tests/bench_util/profiling.rs,提供 bench_or_profile! 宏与 PROFILING 模式。

依赖项只有三个(见 tests/bench_util/Cargo.toml):bencherdeno_coretokio。该 crate publish = true,会被 Deno 仓库内的 ext/* 各扩展与 libs/core 的 benches 以 workspace 依赖方式引用。

快速开始:一个最小的 op 基准

下面是 tests/bench_util/README.md 中给出的完整示例,它展示了使用 deno_bench_util 的标准姿势:定义一个 #[op2] op,写一个 setup() 返回 Vec<Extension>,然后用 bench_js_syncDeno.core.ops.op_nop() 的一次 JS 侧调用计时。

use deno_bench_util::bench_js_sync;
use deno_bench_util::bench_or_profile;
use deno_bench_util::bencher::Bencher;
use deno_bench_util::bencher::benchmark_group;
use deno_core::Extension;

#[op2]
#[number]
fn op_nop() -> usize {
  9
}

fn setup() -> Vec<Extension> {
  vec![Extension {
    name: "my_ext",
    ops: std::borrow::Cow::Borrowed(&[op_nop::DECL]),
  }]
}

fn bench_op_nop(b: &mut Bencher) {
  bench_js_sync(b, r#"Deno.core.ops.op_nop();"#, setup);
}

benchmark_group!(benches, bench_op_nop);
bench_or_profile!(benches);

几个要点:

  1. setup() 是扩展注入点deno_core 中 op 必须挂在某个 Extension 上才会注册进运行时;示例里用了结构体字面量构造 Extension 并借用 op_nop::DECLop2 宏会为每个 op 生成 DECL 声明项)。更常见的写法是用 deno_core::extension! 宏(见下文 UTF-8 用例),可以一次注入 ops、JS 源码和 state。
  2. bench 函数签名固定为 fn(&mut Bencher),内部把 b 交给 bench_js_sync,由工具包接管计时循环。
  3. benchmark_group!(benches, ...) 生成一个返回 Vec<TestDescAndFn>benches() 分组函数;bench_or_profile!(benches) 生成 fn main(),根据环境变量自动选择跑基准还是跑剖析(原理见下文“剖析模式”一节)。注意该宏自行实现了 main,所以对应 [[bench]] 需要 harness = false

运行方式与普通 cargo bench 一致,例如在该 crate 目录内:

cargo bench            # 跑基准
PROFILING=1 cargo bench   # 进入单次执行的剖析模式
cargo bench -- utf8_encode_12_kb   # 按名字过滤单个 bench

过滤逻辑来自 tests/bench_util/profiling.rsbench_or_profile! 展开的 main 会取第一个非 --bench 的命令行参数作为 TestOpts::filter(第 19~22 行),随后 bencher::run_tests_console 只跑名称包含该过滤串的用例。

源码剖析:运行时创建、循环展开与 BenchOptions

create_js_runtime:每次 bench 一个全新运行时

tests/bench_util/js_runtime.rs 中:

pub fn create_js_runtime(setup: impl FnOnce() -> Vec<Extension>) -> JsRuntime {
  JsRuntime::new(RuntimeOptions {
    extensions: setup(),
    module_loader: None,
    ..Default::default()
  })
}

要点:

  • extensions: setup() 直接把调用方传入的扩展列表装进 RuntimeOptions,因此 setup 里挂了哪些扩展,JS 侧就能看到哪些 Deno.core 成员(ops、内联 JS 等);
  • module_loader: None 表明基准只执行 execute_script/脚本编译,不涉及 ESM 模块加载——需要 import 语义的 bench 应像 ext/web/benches/encoding.rs 那样在扩展里写 esm 入口,而不是依赖模块加载器。

loop_code 与迭代次数:让每次计时“有肉”

tests/bench_util/js_runtime.rs 用一个字符串模板把待测源码包进 for 循环:

fn loop_code(iters: u64, src: &str) -> String {
  format!(r#"for(let i=0; i < {iters}; i++) {{ {src} }}"#)
}

这是典型的内层循环放大技巧:单次 op 调用可能在纳秒级,直接 b.iter 每次跑一条语句会放大计时噪声与 JS 引擎调用开销,内层循环 1000 次(默认值)后每次外层迭代就是一个可稳定计时的量。

BenchOptions:基准与剖析使用不同的迭代预算

tests/bench_util/js_runtime.rs 定义了控制迭代次数的结构体:

#[derive(Copy, Clone)]
pub struct BenchOptions {
  pub benching_inner: u64,   // 默认 1_000:非剖析模式下每次 b.iter 内层循环次数
  pub profiling_inner: u64,  // 默认 1_000:剖析模式的内层循环次数
  pub profiling_outer: u64,  // 默认 10_000:剖析模式的“执行批次数”
}

bench_js_sync_withtests/bench_util/js_runtime.rs)中二者的组合逻辑值得细看:

// Increase JS iterations if profiling for nicer flamegraphs
let inner_iters = if is_profiling() {
  opts.profiling_inner * opts.profiling_outer
} else {
  opts.benching_inner
};
let looped_src = loop_code(inner_iters, src);

let code = v8::String::new(scope, looped_src.as_ref()).unwrap();
let script = v8::Script::compile(scope, code, None).unwrap();
// Run once if profiling, otherwise regular bench loop
if is_profiling() {
  script.run(scope).unwrap();
} else {
  b.iter(|| {
    script.run(scope).unwrap();
  });
}
  • 基准模式:JS 侧循环 benching_inner 次(默认 1000),由 bencherb.iter 在 Rust 侧再循环足够多次做统计——两级循环各司其职;
  • 剖析模式:一次性执行 profiling_inner * profiling_outer(默认 10 万次)条语句。注释说明这是为了让采样型 profiler 采到足够多的样本、画出更漂亮的火焰图(flamegraph);剖析时不走 b.iter 的统计循环,只跑一次。
  • 脚本通过 v8::Script::compile 只编译一次,随后反复 run,避免把编译时间计入基准。

bench_js_async:把异步 op 放进 tokio 单线程运行时

对涉及 op2(async) 的用例,tests/bench_util/js_runtime.rs 提供 bench_js_async / bench_js_async_with

let mut runtime = create_js_runtime(setup);
let tokio_runtime = tokio::runtime::Builder::new_current_thread()
  .enable_all()
  .build()
  .unwrap();

let looped = loop_code(inner_iters, src);
// Get a &'static str by leaking -- this is fine because it's benchmarking code
let src = Box::leak(looped.into_boxed_str());
if is_profiling() {
  for _ in 0..opts.profiling_outer {
    tokio_runtime.block_on(inner_async(src, &mut runtime));
  }
} else {
  b.iter(|| {
    tokio_runtime.block_on(inner_async(src, &mut runtime));
  });
}

async fn inner_async(src: &'static str, runtime: &mut JsRuntime) {
  runtime.execute_script("inner_loop", src).unwrap();
  runtime
    .run_event_loop(PollEventLoopOptions::default())
    .await
    .unwrap();
}

与同步版的关键差异:

  1. 每轮迭代都跑完整事件循环execute_script 只是把 JS 任务调度起来,异步 op 真正完成还需要 run_event_loop 轮询 op 回调。基准测的是“JS 发起 + Rust 执行 + 事件循环完成”的端到端耗时,这正是异步 op 的真实成本构成。
  2. new_current_thread 单线程 tokio runtime:刻意避免多线程带来的调度噪声,保证计时可复现。
  3. Box::leak 换取 &'static str:注释直言“这是基准代码,泄漏可以接受”——execute_script 需要静态生命周期参数,基准进程本来就短命。
  4. 剖析模式的批次不同:同步版一次跑完 inner * outer 条语句;异步版则是重复 profiling_outer“执行 + 事件循环”,每批内含 profiling_inner 次内层循环——因为每批都要等事件循环排空,无法像同步版那样一次性展开成一个巨大的 for 循环。

bench_or_profile! 宏:基准与剖析共用一套代码

tests/bench_util/profiling.rs 是整个 crate 的“调度核心”。is_profiling() 的判断非常简单:

pub fn is_profiling() -> bool {
  std::env::var("PROFILING").is_ok()
}

即只要环境变量 PROFILING 存在(取值任意),所有 bench 函数就会自动切换到剖析路径。bench_or_profile! 宏(第 11~40 行)展开后的 main 大致做了三件事:

  1. 解析命令行第一个非 --bench 参数,填入 TestOpts::filter,实现按名称过滤;
  2. 收集各 benchmark_group! 生成的 bench 列表;
  3. 根据 is_profiling() 二选一:
if $crate::is_profiling() {
  // Run profiling
  $crate::run_profiles(&test_opts, benches);
} else {
  // Run benches
  run_tests_console(&test_opts, benches).unwrap();
}

剖析路径 run_profiles(第 43~53 行)对每个(过滤后剩下的)用例打印 Profiling <名字>,然后调用 run_profilebencher::bench::run_once 执行该用例恰好一次

fn run_profile(test: TestDescAndFn) {
  match test.testfn {
    DynBenchFn(bencher) => {
      bencher::bench::run_once(|harness| bencher.run(harness));
    }
    StaticBenchFn(benchfn) => {
      bencher::bench::run_once(benchfn);
    }
  }
}

这套设计的实际效果是:同一段 bench 代码,cargo bench 给出吞吐统计,PROFILING=1 下则变成一次可被 cargo flamegraph 或 perf 采样采样的长执行。过滤函数 filter_tests(第 67~86 行)按名称子串过滤并按字母序排序,保证每次剖析的用例顺序稳定。

真实用例一:UTF-8 编解码基准(benches/utf8.rs)

tests/bench_util/benches/utf8.rsdeno_bench_util 自带的基准,演示了如何用 deno_core::extension! 注入 JS 侧测试数据,并用 bench_js_sync_with 为不同数据规模分别调参。

测试数据准备(第 10~26 行):扩展 bench_setup 内联了一段 JS,构造 12 B、约 12 kB、约 12 MB 三档字符串及其 Deno.core.encode 的字节结果:

fn setup() -> Vec<Extension> {
  deno_core::extension!(
    bench_setup,
    js = ["ext:bench_setup/setup.js" = {
      source = r#"
        const hello = "hello world\n";
        const hello1k = hello.repeat(1e3);
        const hello1m = hello.repeat(1e6);
        const helloEncoded = Deno.core.encode(hello);
        const hello1kEncoded = Deno.core.encode(hello1k);
        const hello1mEncoded = Deno.core.encode(hello1m);
      "#
    }]
  );

  vec![bench_setup::init()]
}

六个 bench 用例覆盖 encode/decode 两个方向 × 三档数据规模。小数据量用例把 benching_inner 调成 1(12 字节的编码本来就很便宜,放大 1000 次反而失真):

fn bench_utf8_encode_12_b(b: &mut Bencher) {
  bench_js_sync_with(
    b,
    r#"Deno.core.encode(hello);"#,
    setup,
    BenchOptions {
      benching_inner: 1,
      ..Default::default()
    },
  );
}

而 12 MB 大对象用例同时调小了剖析参数(第 52~63 行):profiling_inner: 10, profiling_outer: 10。原因是 12 MB 字符串每轮拷贝/编码成本很高,1000 × 10000 = 一千万次 的默认剖析量会长时间不返回,缩小批次让剖析也能在合理时间内完成。这个对照非常实用:bench_js_sync_with 的第四参数就是为“不同数据规模需要不同迭代预算”准备的逃生口,而 bench_js_sync 等价于用 Default::default() 调它tests/bench_util/js_runtime.rs)。

最后照例用宏收口:

benchmark_group!(
  benches,
  bench_utf8_encode_12_b,
  bench_utf8_encode_12_kb,
  bench_utf8_encode_12_mb,
  bench_utf8_decode_12_b,
  bench_utf8_decode_12_kb,
  bench_utf8_decode_12_mb,
);
bench_or_profile!(benches);

对应的 tests/bench_util/Cargo.toml 中登记了 [[bench]] name = "utf8" harness = false——harness = false 是必须的,因为 bench_or_profile! 自带 main,不能与 cargo 默认的 bench harness 共存。

真实用例二:在扩展 crate 中衡量 Web API 与 op

这套工具包在仓库内的主要消费者是各 ext/* crate 的 benches,例如 ext/web/benches/encoding.rs(TextEncoder/TextDecoder)、ext/web/benches/timers_ops.rsext/web/benches/url_ops.rsext/fetch/benches/headers_methods.rs 等,以及 libs/core/benches/ops/sync.rslibs/core/benches/ops/async.rs 中成体系的 op 调用开销基准(op_voidop_stringop_buffer 等 fast/nofast 变体)。

ext/web/benches/encoding.rs 为例,它展示了“被测 API 不是裸 op 而是完整 Web 扩展”时的标准搭法:

  • setup()deno_core::extension! 声明 esm_entry_point = "ext:bench_setup/setup",入口 JS 里 import { TextDecoder, TextEncoder } from "ext:deno_web/08_text_encoding.js" 并挂到 globalThis,同时预先把各档测试数据编码成 Uint8ArrayglobalThis.hello12k = Deno.core.encode("hello world\n".repeat(1e3)););
  • 还特意放了一条非 ASCII 数据(4 字节 emoji 混合中文,约 2.4 kB),注释写明 // exercises the non-ASCII path,覆盖 UTF-8 多字节路径;
  • setup() 返回 deno_webidl::init()deno_web::init(...)bench_setup::init() 三个扩展——被测对象依赖哪些扩展,就得在运行时里装齐;
  • bench 用例本身一行:bench_js_sync(b, r#"new TextDecoder().decode(hello12k);"#, setup);,另有 dec.decode(...) 复用解码器的对照组,用于区分“构造开销”与“解码开销”。

这说明 deno_bench_util 的抽象层次足够高:setup 闭包里可以装任意多个、任意复杂度的扩展组合,工具包只负责运行时创建与计时循环。

使用要点小结

结合 tests/bench_util/README.mdjs_runtime.rsprofiling.rs,把一份新的 JS 侧基准写进任意 Deno crate 的步骤与注意事项如下:

  1. 依赖:在 Cargo.tomldeno_bench_util(workspace 依赖),并声明 [[bench]] ... harness = false
  2. setup():优先用 deno_core::extension! 宏注入 ops(ops = [...]Cow::Borrowed(&[op::DECL]))、内联 JS(js = [...])、ESM 入口(esm = [...])与 state;需要 Web API 时把对应 ext 扩展一并返回;
  3. 选入口:同步 op / 纯 JS 逻辑用 bench_js_sync(需要调迭代参数用 bench_js_sync_with);异步 op 用 bench_js_async(每轮迭代都会完整跑 run_event_loop,测的是端到端成本);
  4. BenchOptions:默认 benching_inner = 1000;数据量大或单次成本高的用例应调小,反之可放大;剖析量按 profiling_inner × profiling_outer(同步)或 profiling_outer 批(异步)估计执行时长;
  5. 收口benchmark_group!(benches, ...) + bench_or_profile!(benches)
  6. 运行cargo bench 出统计结果;cargo bench -- <过滤串> 只跑指定用例;PROFILING=1 cargo bench 切到单次长执行模式,配合外部采样器拿火焰图。

需要提醒的边界:这套工具假设 bench 进程是短命进程,因此 bench_js_async_withBox::leak 泄漏字符串、剖析模式单次执行等行为都是刻意为基准/剖析场景服务的,不适合作为长期运行代码的参考;基准测的是当前机器上 V8 + Rust 的相对性能,跨环境比较结论需谨慎。

参考文件索引

文件 内容
tests/bench_util/README.md 最小示例:op_nop 基准完整代码
tests/bench_util/lib.rs crate 导出结构(bencher / js_runtime / profiling
tests/bench_util/Cargo.toml 依赖(bencher、deno_core、tokio)与 [[bench]] utf8 harness = false
tests/bench_util/js_runtime.rs create_js_runtimeBenchOptionsbench_js_sync(_with)bench_js_async(_with) 实现
tests/bench_util/profiling.rs is_profilingbench_or_profile! 宏、run_profiles 单次执行剖析
tests/bench_util/benches/utf8.rs UTF-8 encode/decode 三档数据规模的完整 bench 用例
ext/web/benches/encoding.rs 在扩展 crate 中 bench Web API 的真实示例
libs/core/benches/ops/sync.rslibs/core/benches/ops/async.rs deno_core op 调用开销(fast/nofast 等变体)的基准体系
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.81 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
920
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.79 K
1.02 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
390