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)
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.

CI will generate real weights automatically when the PR is opened.

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.

CI will generate real weights automatically when the PR is opened.

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 every PR:

  1. Builds the node with --features runtime-benchmarks
  2. Runs benchmarks for every pallet, generating new weights.rs to temp files
  3. Uses weight-compare to compare old vs new values with a 40% 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/
  5. Adding the apply-benchmark-patch label auto-applies the patch

To skip benchmarks on a PR, add the skip-validate-benchmarks label. This can be added at any point during the job — it's checked between expensive steps.

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.