Internals

Benchmarks and weights

How extrinsic weights are benchmarked, generated, and validated in CI.

View as Markdown

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

ToolPurpose
scripts/benchmark_all.shGenerate weights.rs for one or all pallets (runs real benchmarks)
scripts/benchmark_action.shValidate selected benchmarks and prepare a patch for drifted weights
weight-compareCompare 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

  1. Write your benchmarks in pallets/<name>/src/benchmarking.rs using #[benchmarks] and #[benchmark] macros.

  2. Create pallets/<name>/src/weights.rs manually. Copy the structure from any existing pallet (e.g. pallets/drand/src/weights.rs) and replace the function signatures with yours, using Weight::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)
        }
    }
  3. Add pub mod weights; to your pallet's lib.rs.

  4. Add type WeightInfo: crate::weights::WeightInfo; to your pallet's Config trait.

  5. Use T::WeightInfo::extrinsic_name() in #[pallet::weight(...)] annotations instead of hardcoded Weight::from_parts(...).

  6. Wire up in runtime/src/lib.rs:

    type WeightInfo = pallet_<name>::weights::SubstrateWeight<Runtime>;
  7. Add type WeightInfo = (); to all test mocks implementing your pallet's Config.

  8. Register the pallet in the define_benchmarks! macro in runtime/src/lib.rs so 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

  1. Write the benchmark in benchmarking.rs.

  2. Add the function signature to the WeightInfo trait in weights.rs, and a Weight::from_parts(0, 0) body to both the SubstrateWeight<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)
    }
  3. 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:

InputSelectionExample
pallet_dirsDirectory names under pallets/ or runtime pallet namessubtensor,hashed-accounts or pallet_subtensor
extrinsicsExact benchmark function namesadd_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_stake

An 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:

  1. Builds the node with --features runtime-benchmarks
  2. Resolves the selection and generates measured weights into temporary files
  3. Uses weight-compare to 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
  4. If drift is detected, prepares a patch in .bench_patch/ and exits with a drift failure
  5. Uploads the bench-patch artifact, 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 40

Weight file structure

Generated weights.rs files contain:

  • WeightInfo trait — one function per benchmarked extrinsic
  • SubstrateWeight<T> impl — used in the runtime, references T::DbWeight
  • () impl — used in tests, references RocksDbWeight

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.