Skip to content

Pallet Benchmarking

Introduction

After validating your pallet through testing and integrating it into your runtime, the next crucial step is benchmarking. Testing procedures were detailed in the Pallet Unit Testing tutorial, while runtime integration was covered in the Add Pallets to the Runtime guide.

Benchmarking assigns precise weight to each extrinsic, measuring their computational and storage costs. These derived weights enable accurate fee calculation and resource allocation within the runtime.

This tutorial demonstrates how to:

  • Configure your development environment for benchmarking
  • Create and implement benchmark tests for your extrinsics
  • Apply benchmark results to your pallet's extrinsics

For comprehensive information about benchmarking concepts, refer to the Benchmarking guide.

Environment Setup

Follow these steps to prepare your environment for pallet benchmarking:

  1. Install the frame-omni-bencher command-line tool:

    cargo install frame-omni-bencher
    
  2. Update your pallet's Cargo.toml file in the pallets/custom-pallet directory with the following modifications:

    1. Add the frame-benchmarking dependency:

      Cargo.toml
      [dependencies]
      ...
      frame-benchmarking = { optional = true, workspace = true }
      
    2. Enable benchmarking in the std features:

      Cargo.toml
      std = [
        "codec/std",
        "frame-support/std",
        "frame-system/std",
        "scale-info/std",
        "frame-benchmarking?/std",
      ]
      

    3. Add the runtime-benchmarks feature flag:

      Cargo.toml
      [features]
      ...
      runtime-benchmarks = [
        "frame-benchmarking/runtime-benchmarks",
        "frame-support/runtime-benchmarks",
        "frame-system/runtime-benchmarks",
        "sp-runtime/runtime-benchmarks",
      ]
      

  3. Add your pallet to the runtime's benchmark configuration:

    1. Register your pallet in runtime/src/benchmarks.rs:

      benchmarks.rs
      frame_benchmarking::define_benchmarks!(
          [frame_system, SystemBench::<Runtime>]
          [pallet_balances, Balances]
          [pallet_session, SessionBench::<Runtime>]
          [pallet_timestamp, Timestamp]
          [pallet_message_queue, MessageQueue]
          [pallet_sudo, Sudo]
          [pallet_collator_selection, CollatorSelection]
          [cumulus_pallet_parachain_system, ParachainSystem]
          [cumulus_pallet_xcmp_queue, XcmpQueue]
          [custom_pallet, CustomPallet]
      );
      

    2. Enable runtime benchmarking for your pallet in runtime/Cargo.toml:

      Cargo.toml
      runtime-benchmarks = [
        ...
        "custom-pallet/runtime-benchmarks",
      ]
      

  4. Set up the benchmarking module in your pallet:

    1. Create a new benchmarking.rs file in your pallet directory:

      touch benchmarking.rs
      

    2. Add the benchmarking module to your pallet. In the pallet lib.rs file add the following:

      lib.rs
      pub use pallet::*;
      
      #[cfg(test)]
      mod mock;
      
      #[cfg(test)]
      mod tests;
      
      #[cfg(feature = "runtime-benchmarks")]
      mod benchmarking;
      

    The benchmarking module is gated behind the runtime-benchmarks feature flag. It will only be compiled when this flag is explicitly enabled in your project's Cargo.toml or via the --features runtime-benchmarks compilation flag.

Implement Benchmark Tests

When writing benchmarking tests for your pallet, you'll create specialized test functions for each extrinsic, similar to unit tests. These tests use the mock runtime you created earlier for testing, allowing you to leverage its utility functions.

Every benchmark test must follow a three-step pattern:

  1. Setup - perform any necessary setup before calling the extrinsic. This might include creating accounts, setting initial states, or preparing test data
  2. Execute the extrinsic - execute the actual extrinsic using the #[extrinsic_call] macro. This must be a single line that calls your extrinsic function with the origin as its first argument
  3. Verification - check that the extrinsic worked correctly within the benchmark context by checking the expected state changes

Check the following example on how to benchmark the increment extrinsic:

#[benchmark]
fn increment() {
    let caller: T::AccountId = whitelisted_caller();

    assert_ok!(CustomPallet::<T>::set_counter_value(
        RawOrigin::Root.into(),
        5u32
    ));

    #[extrinsic_call]
    increment(RawOrigin::Signed(caller.clone()), 1);

    assert_eq!(CounterValue::<T>::get(), Some(6u32.into()));
    assert_eq!(UserInteractions::<T>::get(caller), 1u32.into());
}

This benchmark test:

  1. Creates a whitelisted caller and sets an initial counter value of 5
  2. Calls the increment extrinsic to increase the counter by 1
  3. Verifies that the counter was properly incremented to 6 and that the user's interaction was recorded in storage

This example demonstrates how to properly set up state, execute an extrinsic, and verify its effects during benchmarking.

Now, implement the complete set of benchmark tests. Copy the following content in the benchmarking.rs file:

benchmarking.rs
#![cfg(feature = "runtime-benchmarks")]

use super::{Pallet as CustomPallet, *};
use frame_benchmarking::v2::*;
use frame_support::assert_ok;

#[benchmarks]
mod benchmarks {
    use super::*;
    #[cfg(test)]
    use crate::pallet::Pallet as CustomPallet;
    use frame_system::RawOrigin;

    #[benchmark]
    fn set_counter_value() {
        #[extrinsic_call]
        set_counter_value(RawOrigin::Root, 5);

        assert_eq!(CounterValue::<T>::get(), Some(5u32.into()));
    }

    #[benchmark]
    fn increment() {
        let caller: T::AccountId = whitelisted_caller();

        assert_ok!(CustomPallet::<T>::set_counter_value(
            RawOrigin::Root.into(),
            5u32
        ));

        #[extrinsic_call]
        increment(RawOrigin::Signed(caller.clone()), 1);

        assert_eq!(CounterValue::<T>::get(), Some(6u32.into()));
        assert_eq!(UserInteractions::<T>::get(caller), 1u32.into());
    }

    #[benchmark]
    fn decrement() {
        let caller: T::AccountId = whitelisted_caller();

        assert_ok!(CustomPallet::<T>::set_counter_value(
            RawOrigin::Root.into(),
            5u32
        ));

        #[extrinsic_call]
        decrement(RawOrigin::Signed(caller.clone()), 1);

        assert_eq!(CounterValue::<T>::get(), Some(4u32.into()));
        assert_eq!(UserInteractions::<T>::get(caller), 1u32.into());
    }

    impl_benchmark_test_suite!(CustomPallet, crate::mock::new_test_ext(), crate::mock::Test);
}

The #[benchmark] macro marks these functions as benchmark tests, while the #[extrinsic_call] macro specifically identifies which line contains the extrinsic being measured. For more information, see the frame_benchmarking Rust docs.

Execute the Benchmarking

After implementing your benchmark test suite, you'll need to execute the tests and generate the weights for your extrinsics. This process involves building your runtime with benchmarking features enabled and using the frame-omni-bencher CLI tool. To do that, follow these steps:

  1. Build your runtime with the runtime-benchmarks feature enabled:

    cargo build --features runtime-benchmarks --release
    

    This special build includes all the necessary benchmarking code that's normally excluded from production builds.

  2. Create a weights.rs file in your pallet's src/ directory. This file will store the auto-generated weight calculations:

    touch weights.rs
    
  3. Before running the benchmarking tool, you'll need a template file that defines how weight information should be formatted. Download the official template from the Polkadot SDK repository and save it in your project folders for future use:

    mkdir ./pallets/benchmarking && \
    curl https://raw.githubusercontent.com/paritytech/polkadot-sdk/refs/tags/polkadot-stable2412/substrate/.maintain/frame-weight-template.hbs \
    --output ./pallets/benchmarking/frame-weight-template.hbs
    
  4. Execute the benchmarking process using the frame-omni-bencher CLI:

    frame-omni-bencher v1 benchmark pallet \
    --runtime target/release/wbuild/parachain-template-runtime/parachain_template_runtime.compact.compressed.wasm \
    --pallet "custom_pallet" \
    --extrinsic "" \
    --template ./pallets/benchmarking/frame-weight-template.hbs \
    --output ./pallets/custom-pallet/src/weights.rs
    

When the benchmarking process completes, your weights.rs file will contain auto-generated code with weight calculations for each of your pallet's extrinsics. These weights help ensure fair and accurate fee calculations when your pallet is used in a production environment.

Add Benchmarking Weights to the Pallet

After generating the weight calculations, you need to integrate these weights into your pallet's code. This integration ensures your pallet properly accounts for computational costs in its extrinsics.

First, add the necessary module imports to your pallet. These imports make the weights available to your code:

lib.rs
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;

pub mod weights;
use crate::weights::WeightInfo;

Next, update your pallet's Config trait to include weight information. Define the WeightInfo type:

lib.rs
// Configuration trait for the pallet.
#[pallet::config]
pub trait Config: frame_system::Config {
    // Defines the event type for the pallet.
    type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;

    // Defines the maximum value the counter can hold.
    #[pallet::constant]
    type CounterMaxValue: Get<u32>;

    /// A type representing the weights required by the dispatchables of this pallet.
    type WeightInfo: WeightInfo;
}

Now you can assign weights to your extrinsics. Here's how to add weight calculations to the set_counter_value function:

lib.rs
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::set_counter_value())]
pub fn set_counter_value(origin: OriginFor<T>, new_value: u32) -> DispatchResult {
    ensure_root(origin)?;

    ensure!(
        new_value <= T::CounterMaxValue::get(),
        Error::<T>::CounterValueExceedsMax
    );

    CounterValue::<T>::put(new_value);

    Self::deposit_event(Event::<T>::CounterValueSet {
        counter_value: new_value,
    });

    Ok(())
}

You must apply similar weight annotations to the other extrinsics in your pallet. Add the #[pallet::weight(T::WeightInfo::function_name())] attribute to both increment and decrement, replacing function_name with the respective function names from your WeightInfo trait.

For testing purposes, you must implement the weight calculations in your mock runtime. Open custom-pallet/src/mock.rs and add:

mock.rs
impl custom_pallet::Config for Test {
    type RuntimeEvent = RuntimeEvent;
    type CounterMaxValue = CounterMaxValue;
    type WeightInfo = custom_pallet::weights::SubstrateWeight<Test>;
}

Finally, configure the actual weight values in your production runtime. In runtime/src/config/mod.rs, add:

mod.rs
// Configure custom pallet.
impl custom_pallet::Config for Runtime {
    type RuntimeEvent = RuntimeEvent;
    type CounterMaxValue = CounterMaxValue;
    type WeightInfo = custom_pallet::weights::SubstrateWeight<Runtime>;
}

Your pallet is now complete with full testing and benchmarking support, ready for production use.

Where to Go Next

  • Tutorial Deploy on Paseo TestNet


    Deploy your Polkadot SDK blockchain on Paseo! Follow this step-by-step guide for a seamless journey to a successful TestNet deployment.

    Get Started

Last update: February 5, 2025
| Created: January 9, 2025