原文作者:PaperMoon团队
在 Polkadot SDK(FRAME)中,Benchmark(基准测试)指的是:测量 Pallet 的各个 Extrinsic(可调度调用)所消耗的计算资源,主要包括:
• 执行时间(Execution Time)
• 存储访问开销(Storage reads/writes)
这些测量结果会被用于生成 Weight(权重)。Weight 的准确性非常关键,因为它直接影响:
• 链处理交易的效率(决定一个区块能装多少"资源")
• 交易费计算(费用往往与 Weight 相关)
• 对 DoS(拒绝服务)攻击的防护能力(避免攻击者用低成本触发高消耗逻辑)
本指南将演示如何为 Pallet 编写 Benchmark,并把生成的权重结果接入到运行时(Runtime)中。
开始之前,请确保你已具备:
- 一个需要 Benchmark 的 Pallet
• 如果你跟随了 Pallet 开发教程,可以直接使用 Create a Pallet 章节的 Counter Pallet。
• 如果是其他 Pallet,则需要你在后续步骤中:
• 修改 benchmarking.rs 中各 benchmark 函数的测试逻辑
• 同步更新后续步骤里对这些函数的引用,确保生成的 Weight 与你的实际 Extrinsic 对应
-
对 计算复杂度(computational complexity)有基本理解
-
熟悉 Rust 的测试框架(Rust testing framework)
-
熟悉 Polkadot Omni Node 与 Polkadot Chain Spec Builder 的搭建流程
创建 Benchmarking 模块
在 Pallet 的 src 目录下创建一个新文件 benchmarking.rs,并加入如下代码:
文件路径:pallets/pallet-custom/src/benchmarking.rs
#![cfg(feature = "runtime-benchmarks")]
use super::*;
use frame::deps::frame_benchmarking::v2::*;
use frame::benchmarking::prelude::RawOrigin;
#[benchmarks]
mod benchmarks {
use super::*;
#[benchmark]
fn set_counter_value() {
let new_value: u32 = 100;
#[extrinsic_call]
_(RawOrigin::Root, new_value);
assert_eq!(CounterValue::<T>::get(), new_value);
}
#[benchmark]
fn increment() {
let caller: T::AccountId = whitelisted_caller();
let amount: u32 = 50;
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()), amount);
assert_eq!(CounterValue::<T>::get(), amount);
assert_eq!(UserInteractions::<T>::get(caller), 1);
}
#[benchmark]
fn decrement() {
// First, set the counter to a non-zero value
CounterValue::<T>::put(100);
let caller: T::AccountId = whitelisted_caller();
let amount: u32 = 30;
#[extrinsic_call]
_(RawOrigin::Signed(caller.clone()), amount);
assert_eq!(CounterValue::<T>::get(), 70);
assert_eq!(UserInteractions::<T>::get(caller), 1);
}
impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test);
}
补充说明(便于理解)
• 该模块包含 Pallet 的全部 Benchmark 定义。
• #[benchmark] 标记每一个基准测试函数,它通常对应某个 Extrinsic 的测量流程。
• #[extrinsic_call] (origin, ...) 是 v2 Benchmark 框架提供的调用写法, 指代当前 benchmark 对应的 extrinsic。
• whitelisted_caller() 用于生成"白名单账户",以减少与测试无关的额外开销干扰(让测量更聚焦在业务逻辑本身)。
• 末尾的 impl_benchmark_test_suite! 会为每个 benchmark 生成测试用例,便于你先用 cargo test 验证 benchmark 的编译与逻辑正确性。
如果你要 Benchmark 其他 Pallet,需要按你的业务逻辑修改断言(assert_eq!)部分,以验证该 Extrinsic 调用后状态是否符合预期。
定义 Weight Trait
为 Pallet 添加一个 weights 模块,并定义 WeightInfo trait。示例代码如下:
文件路径:pallets/pallet-custom/src/weights.rs
#[frame::pallet]
pub mod pallet {
use frame::prelude::*;
pub use weights::WeightInfo;
pub mod weights {
use frame::prelude::*;
pub trait WeightInfo {
fn set_counter_value() -> Weight;
fn increment() -> Weight;
fn decrement() -> Weight;
}
impl WeightInfo for () {
fn set_counter_value() -> Weight {
Weight::from_parts(10_000, 0)
}
fn increment() -> Weight {
Weight::from_parts(15_000, 0)
}
fn decrement() -> Weight {
Weight::from_parts(15_000, 0)
}
}
}
// ... rest of pallet
}
解释
• WeightInfo 是权重接口(trait),用于抽象不同环境下的权重实现。
• impl WeightInfo for () 是占位权重(placeholder weights),主要用于开发阶段与测试阶段,让 Pallet 在尚未生成真实权重文件时也能正常编译运行。
• 如果你使用的是其他 Pallet,请将 trait 中的函数名替换为你 Pallet 中实际的 extrinsic 名称,确保一一对应。
在 Config 中加入 WeightInfo
更新 Pallet 的 Config trait,加入 WeightInfo:
文件路径:pallets/pallet-custom/src/lib.rs
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
#[pallet::constant]
type CounterMaxValue: Get<u32>;
type WeightInfo: weights::WeightInfo;
}
为什么要把 WeightInfo 放进 Config?
WeightInfo 作为 Config 的关联类型(associated type),能让 runtime 在集成该 Pallet 时选择不同的权重实现,例如:
• 开发/测试:使用 () 的占位实现
• 生产:使用 benchmark 生成的真实权重实现
这样你无需修改 Pallet 源码,只需在 runtime 配置中切换即可。
更新 Extrinsic 的 weight 标注
将 extrinsic 上的权重标注从硬编码替换为 T::WeightInfo::...():
文件路径:pallets/pallet-custom/src/lib.rs
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::set_counter_value())]
pub fn set_counter_value(origin: OriginFor<T>, new_value: u32) -> DispatchResult {
// ... implementation
}
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::increment())]
pub fn increment(origin: OriginFor<T>, amount: u32) -> DispatchResult {
// ... implementation
}
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::decrement())]
pub fn decrement(origin: OriginFor<T>, amount: u32) -> DispatchResult {
// ... implementation
}
}
说明
通过 T::WeightInfo::function_name() 调用,而不是硬编码 Weight::from_parts(),可以让 extrinsic 自动使用 runtime 中配置的权重实现:
• 测试时可用占位权重
• 生产时可用真实测量权重
• 且无需改动 Pallet 代码本身
引入 Benchmarking 模块
在 lib.rs 文件顶部加入模块声明:
文件路径:pallets/pallet-custom/src/lib.rs
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::vec::Vec;
pub use pallet::*;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
// Additional pallet code
这里的 #[cfg(feature = "runtime-benchmarks")] 表示:只有启用 runtime-benchmarks feature 时才会编译 benchmark 代码,从而避免生产构建携带无关内容,保持 runtime 轻量高效。
配置 Pallet 依赖
更新 Pallet 的 Cargo.toml,开启 benchmark feature:
文件路径:pallets/pallet-custom/Cargo.toml
[dependencies]
codec = { features = ["derive"], workspace = true }
scale-info = { features = ["derive"], workspace = true }
frame = { features = ["experimental", "runtime"], workspace = true }
[features]
default = ["std"]
runtime-benchmarks = [
"frame/runtime-benchmarks",
]
std = [
"codec/std",
"scale-info/std",
"frame/std",
]
说明
Rust 的 feature flag 机制允许按需编译。这里通过定义 runtime-benchmarks feature,并让其级联启用 frame/runtime-benchmarks,实现:
• benchmark 时:依赖完整、功能齐全
• 生产构建:不编译 benchmark 代码与相关依赖,减少体积与复杂度
更新 Mock Runtime
在测试用的 mock.rs 中加入 WeightInfo 配置:
文件路径:pallets/pallet-custom/src/mock.rs
impl pallet_custom::Config for Test {
type RuntimeEvent = RuntimeEvent;
type CounterMaxValue = ConstU32<1000>;
type WeightInfo = ();
}
配置 Runtime Benchmarking
要运行 benchmark,你的 Pallet 必须集成进 runtime 的 benchmark 基础设施中。
1)更新 runtime/Cargo.toml
把 Pallet 加入 runtime 的 runtime-benchmarks feature:
文件路径:runtime/Cargo.toml
runtime-benchmarks = [
"cumulus-pallet-parachain-system/runtime-benchmarks",
"hex-literal",
"pallet-parachain-template/runtime-benchmarks",
"polkadot-sdk/runtime-benchmarks",
"pallet-custom/runtime-benchmarks",
]
当你使用 --features runtime-benchmarks 构建 runtime 时,这会确保所有 pallet(包括你的 pallet)都包含必要的 benchmark 代码。
2)runtime 配置先使用占位权重
文件路径:runtime/src/configs/mod.rs
impl pallet_custom::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type CounterMaxValue = ConstU32<1000>;
type WeightInfo = ();
}
3)注册 benchmark 列表
将 pallet 加入 runtime 的 benchmark 注册文件:
文件路径:runtime/src/benchmarks.rs
polkadot_sdk::frame_benchmarking::define_benchmarks!(
[frame_system, SystemBench::<Runtime>]
[pallet_balances, Balances]
// ... other pallets
[pallet_custom, CustomPallet]
);
define_benchmarks! 宏会生成 CLI 可发现的 benchmark 执行入口,使 benchmark 工具能够定位并运行你 Pallet 的 benchmark。
验证 Benchmark 编译与测试
运行以下命令确认 benchmark 能编译,并以测试形式执行:
cargo test -p pallet-custom --features runtime-benchmarks
你会看到类似输出:
test benchmarking::benchmarks::bench_set_counter_value ... ok
test benchmarking::benchmarks::bench_increment ... ok
test benchmarking::benchmarks::bench_decrement ... ok
这里的重点是:impl_benchmark_test_suite! 会为每个 benchmark 生成单元测试,提前发现编译错误或断言失败,避免你在构建整个 runtime 后才定位问题。
构建带 Benchmark 的 Runtime
使用启用 benchmark 的方式构建 runtime(生成 WASM):
cargo build --release --features runtime-benchmarks
通常生成的 runtime WASM 文件位于:
target/release/wbuild/parachain-template-runtime/parachain_template_runtime.wasm
该构建会包含 benchmark 所需的 host functions 与基础设施,从而让 benchmark 工具可以在 WASM 执行环境中进行测量。后续真正上线生产时,你会使用不包含 benchmark 的构建产物。
安装 Benchmark 工具
安装官方工具 frame-omni-bencher:
cargo install frame-omni-bencher --locked
frame-omni-bencher 是 Polkadot SDK 官方的 FRAME Pallet Benchmark 工具,用于标准化地:
• 执行 benchmarks
• 测量执行时间与存储操作
• 生成规范的 weight 文件,并与 FRAME weight 系统完整对接
下载 Weight 模板
下载官方 weight 模板文件(Handlebars 模板):
bash
curl -L https://raw.githubusercontent.com/paritytech/polkadot-sdk/refs/tags/polkadot-stable2512/substrate/.maintain/frame-weight-template.hbs \
--output ./pallets/pallet-custom/frame-weight-template.hbs
模板的作用说明
weight 模板用于将 benchmark 的原始测量数据转换成格式正确的 Rust 源码文件(通常是 weights.rs),并定义生成文件的结构,包括:
• imports
• trait 定义与实现
• 文档注释
• 统一格式排版
• benchmark 参数、存储读写次数、硬件信息等元数据
使用官方模板可以确保输出符合 Polkadot SDK 的规范与风格。
执行 Benchmarks 并生成 weights.rs
运行 benchmark,生成权重文件:
bash
frame-omni-bencher v1 benchmark pallet \
--runtime ./target/release/wbuild/parachain-template-runtime/parachain_template_runtime.wasm \
--pallet pallet_custom \
--extrinsic "" \
--template ./pallets/pallet-custom/frame-weight-template.hbs \
--output ./pallets/pallet-custom/src/weights.rs
为什么要基于 WASM 进行 Benchmark?
因为区块链生产环境中真正运行的是 WASM runtime,而不是本地 native。WASM 的执行特性(编译、沙盒、运行开销)可能与 native 不同,因此基于 WASM 测得的权重更贴近真实链上情况。
使用生成的权重
运行完成后,会生成 weights.rs 文件,里面包含基于真实硬件测量得到的权重数据,能更准确反映:
• 逻辑复杂度
• 存储访问模式
• 具体计算开销
1)在 Pallet 中引入 weights 模块
文件路径:pallets/pallet-custom/src/lib.rs
bash
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::vec::Vec;
pub use pallet::*;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
#[frame::pallet]
pub mod pallet {
use super::*;
use frame::prelude::*;
use crate::weights::WeightInfo;
// ... rest of pallet
}
注意:benchmarking 模块只在跑 benchmark 时需要;但 weights 模块必须在所有构建中可用,因为 runtime 日常执行需要调用权重函数来计算手续费和执行区块资源限制。
2)在 runtime 中启用生成的权重实现
将 runtime 配置从 () 替换为生成的权重类型:
文件路径:runtime/src/configs/mod.rs
bash
impl pallet_custom::Config for Runtime {
type RuntimeEvent = RuntimeEvent;
type CounterMaxValue = ConstU32<1000>;
type WeightInfo = pallet_custom::weights::SubstrateWeight<Runtime>;
}
完成后,你的生产 runtime 将使用真实测量的权重来:
• 计算交易费用
• 约束区块资源消耗
• 提升系统整体的资源计量准确性与安全性
示例:生成的 weight 文件
原文只提示"示例生成文件"。实际生成内容会根据你的硬件与代码行为变化,因此以你本地生成的 ./pallets/pallet-custom/src/weights.rs 为准。
完成
至此,你已经成功完成:
• 为 Pallet 编写并验证 benchmark
• 构建带 benchmark 的 runtime WASM
• 使用 frame-omni-bencher 生成权重文件
• 在 runtime 中启用生成的真实权重
这意味着你的 Pallet 在性能计量与交易资源约束方面已达到更接近生产可用的状态。
原文链接:https://docs.polkadot.com/parachains/customize-runtime/pallet-development/benchmark-pallet/