Internals
Benchmarks and weights
How extrinsic weights are benchmarked, generated, and validated in CI.
Every extrinsic in the runtime has a weight — a measure of the computational resources it consumes. Weights are used to calculate transaction fees and to prevent blocks from being overloaded.
Weights are defined in weights.rs files inside each pallet and are generated
by running benchmarks on reference hardware.
Tools
| Tool | Purpose |
|---|---|
scripts/benchmark_all.sh | Generate weights.rs for one or all pallets (runs real benchmarks) |
scripts/benchmark_action.sh | Validate selected benchmarks and prepare a patch for drifted weights |
weight-compare | Compare two weights.rs files and report drift (used by CI) |
weight-compare lives in support/weight-tools/ and has no heavy dependencies
(no runtime build required).
Adding a new pallet
-
Write your benchmarks in
pallets/<name>/src/benchmarking.rsusing#[benchmarks]and#[benchmark]macros. -
Create
pallets/<name>/src/weights.rsmanually. Copy the structure from any existing pallet (e.g.pallets/drand/src/weights.rs) and replace the function signatures with yours, usingWeight::from_parts(0, 0)as the body so the pallet compiles immediately:pub trait WeightInfo { fn my_extrinsic() -> Weight; } pub struct SubstrateWeight<T>(PhantomData<T>); impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> { fn my_extrinsic() -> Weight { Weight::from_parts(0, 0) } } impl WeightInfo for () { fn my_extrinsic() -> Weight { Weight::from_parts(0, 0) } } -
Add
pub mod weights;to your pallet'slib.rs. -
Add
type WeightInfo: crate::weights::WeightInfo;to your pallet'sConfigtrait. -
Use
T::WeightInfo::extrinsic_name()in#[pallet::weight(...)]annotations instead of hardcodedWeight::from_parts(...). -
Wire up in
runtime/src/lib.rs:type WeightInfo = pallet_<name>::weights::SubstrateWeight<Runtime>; -
Add
type WeightInfo = ();to all test mocks implementing your pallet'sConfig. -
Register the pallet in the
define_benchmarks!macro inruntime/src/lib.rsso the benchmark runner can discover it:define_benchmarks!( // ...existing pallets... [pallet_<name>, PalletInstance] );
The benchmark scripts auto-discover pallets by scanning for directories under
pallets/ that have src/weights.rs plus src/benchmarking.rs (or
src/benchmarks.rs), and that are registered in define_benchmarks!. No
manual registration in scripts is needed. Careful with step 8
(define_benchmarks!): an unregistered pallet is filtered out by discovery
and silently skipped in all-pallets mode and in CI; only the single-pallet
invocation (./scripts/benchmark_all.sh pallet_<name>) fails, with an
"unknown pallet" error.
Request reference measurements using the CI workflow described below.
Adding a new extrinsic to an existing pallet
-
Write the benchmark in
benchmarking.rs. -
Add the function signature to the
WeightInfotrait inweights.rs, and aWeight::from_parts(0, 0)body to both theSubstrateWeight<T>and()impls so the pallet continues to compile:// in trait WeightInfo: fn new_extrinsic() -> Weight; // in both impls: fn new_extrinsic() -> Weight { Weight::from_parts(0, 0) } -
Add
#[pallet::weight(T::WeightInfo::new_extrinsic())]to the extrinsic.
Request reference measurements using the CI workflow described below.
Parameterized weights
For extrinsics whose cost scales with an input, use Linear<min, max>
parameters in the benchmark. You can use one or more parameters:
// Single parameter
#[benchmark]
fn refund(k: Linear<1, 100>) {
// setup with k contributors...
#[extrinsic_call]
_(origin, crowdloan_id);
}
// Multiple parameters
#[benchmark]
fn transfer_batch(n: Linear<1, 256>, m: Linear<1, 64>) {
// setup with n recipients and m tokens each...
#[extrinsic_call]
_(origin, recipients, amounts);
}This generates weight functions with matching signatures:
fn refund(k: u32) -> Weight;
fn transfer_batch(n: u32, m: u32) -> Weight;The generated weight includes base values plus per-parameter slope terms
(e.g., base + slope_k * k for single parameter, or
base + slope_n * n + slope_m * m for multiple). Reference them as:
#[pallet::weight(T::WeightInfo::refund(T::MaxContributors::get()))]
#[pallet::weight(T::WeightInfo::transfer_batch(recipients.len() as u32, max_tokens))]CI workflow
The Validate-Benchmarks workflow (.github/workflows/run-benchmarks.yml)
runs on PRs carrying the run-benchmarks label, nightly on the default branch,
and through Actions → Validate-Benchmarks → Run workflow. PR-label and nightly
runs cover all registered pallets. Manual runs accept two optional inputs:
| Input | Selection | Example |
|---|---|---|
pallet_dirs | Directory names under pallets/ or runtime pallet names | subtensor,hashed-accounts or pallet_subtensor |
extrinsics | Exact benchmark function names | add_stake,remove_stake |
Both inputs accept comma-separated or whitespace-separated lists. Blank or *
means all. For example, pallet_dirs=subtensor with blank extrinsics runs the
entire Subtensor pallet; adding extrinsics=add_stake runs only that benchmark.
Benchmark variants must be named explicitly; prefix patterns such as add_*
are not supported.
You can also dispatch a targeted run with GitHub CLI:
gh workflow run run-benchmarks.yml --ref YOUR_BRANCH \
-f pallet_dirs=subtensor \
-f extrinsics=add_stake,remove_stakeAn extrinsic list may span multiple pallets. Each pallet runs only its matching
benchmarks; pallets with no matches are skipped. Every requested pallet and
extrinsic must be recognized, so a typo fails the run before measurements begin.
Pallet names and input syntax are checked before the build; extrinsic names are
checked against the built runtime's benchmark registry. The branch's
.maintain/benchmark-focus.json is no longer loaded implicitly: blank manual
inputs always request the full suite.
The workflow:
- Builds the node with
--features runtime-benchmarks - Resolves the selection and generates measured weights into temporary files
- Uses
weight-compareto compare old vs new values with a 75% threshold- Base weight: threshold-based (allows measurement noise)
- Reads/writes: exact match (these are deterministic)
- Component slopes: threshold-based for weights, exact for reads/writes
- If drift is detected, prepares a patch in
.bench_patch/and exits with a drift failure - Uploads the
bench-patchartifact, including the selected scope, summary, and raw generated weights
Targeted runs preserve unselected methods in the trait and both weight implementations. Only measured methods that drift are included in the patch. Completed results are checkpointed between pallets so a later failure or timeout does not discard earlier measurements. A successful targeted run validates only its recorded selection.
After the run finishes, add apply-benchmark-patch to the PR to commit and push
the latest benchmark results for its branch and current commit. This works for
manual and PR-label runs, whether they measured the full suite, one pallet, or
individual extrinsics. Only drifted weights from that run's selection are applied.
Benchmarks themselves never commit or push changes.
The apply workflow selects the latest actual benchmark execution, including reruns. It verifies the artifact's source commit and run attempt before applying it. A completed run with no drift makes no changes; an unfinished run, missing artifact, or incomplete measurements fails clearly instead of applying an older patch. A drift failure is expected and can be applied when all measurements completed. Adding or removing the apply label does not start another benchmark.
The validation workflow runs PR tooling tests in a separate read-only job on a
fresh GitHub-hosted runner. The writer uses another fresh runner, loads its helper
from the immutable commit supplying the trusted apply workflow, and treats the
exact labeled head commit as data.
It accepts only changes to existing regular pallets/*/src/weights.rs files and
publishes only if the PR branch still points to that head. PR scripts, imports,
and Git hooks never run with the writer's credentials.
The trusted apply workflow and its helper must first be deployed together to the default branch. If trusted tooling is missing, applying fails; it never falls back to the PR's copy of the helper.
Running benchmarks locally
# Build + generate weights for all pallets
./scripts/benchmark_all.sh
# Build + generate weights for a single pallet
./scripts/benchmark_all.sh pallet_subtensor
# Run benchmark unit tests (fast, no real measurements — just checks setup)
cargo test -p pallet-subtensor --features runtime-benchmarks benchmarks
# Compare two weight files
cargo run -p subtensor-weight-tools --bin weight-compare -- \
--old pallets/foo/src/weights.rs \
--new /tmp/new_weights.rs \
--threshold 40Weight file structure
Generated weights.rs files contain:
WeightInfotrait — one function per benchmarked extrinsicSubstrateWeight<T>impl — used in the runtime, referencesT::DbWeight()impl — used in tests, referencesRocksDbWeight
The () fallback uses RocksDbWeight constants directly. The production
runtime uses T::DbWeight::get(), which runtime/src/lib.rs also configures
as type DbWeight = RocksDbWeight (matching the node's default RocksDb
backend), so in this repo both impls resolve to the same constants.
The files are generated by the frame-benchmarking-cli using the Handlebars
template at .maintain/frame-weight-template.hbs.