background-shape
feature-image

Introduction

Substrate provides a way to compute the computational resources used when extrinsics and hooks are called to keep the runtime safe to attack. Substrate uses the concept of weight to represent the execution time. In this post, we will quickly see how to implement the benchmark for one pallet and, more precisely, how to use the generated weight file in the runtime using examples from the Duniter and Polkadot runtime.

Writing benchmark

General

The goal of benchmarking is simple: find the longest path (i.e., the more computational) of each extrinsic and use this path in the benchmark. Each benchmark is constituted of three parts:

  • Prepare the pallet to trigger the extrinsic longest path.
  • Run the benchmark.
  • Check the result (option).

Let’s take an example to see how it is done for a simple pallet.

The pallet im_online has one extrinsic. The complexity of this extrinsic depends on the length of the key and of heartbeat address that needs to be decoded and/or encoded. The benchmark is defined in this file. The complexity parameters use the syntax let k in 1 .. MAX and will be used in the WeightInfo generated after the benchmark. In this example, there is no verify statement; one can see that we can verify that the event HeartbeatReceived is deposited (see this example that verify events).

Instance case

For an instance pallet, i.e., a pallet with several instances, the benchmark is defined using the macro benchmarks_instance_pallet. The process is the same as for a regular pallet; the benchmark needs to be included in the runtime for each pallet. The benchmark will generate one weight file by instance.

Running benchmarks

Running benchmarks in test mock and the runtime is documented at https://docs.substrate.io/test/benchmark/ and is relatively easy to do.

Add WeightInfo

When the benchmarks run in the runtime, the next step is to use the generated weight file inside the pallet and the mock.

  1. We replace all the fixed weights with the weights computed in the generated file. Example for the prune_item_identities_names extrinsic that have one complexity parameter : #[pallet::weight(1_000_000_000)] is replaced by #[pallet::weight(T::WeightInfo::prune_item_identities_names(names.len() as u32))].
  2. If the mock needs a WeightInfo for the pallet, we must manually create trait for (). To do so, it is possible to copy the WeightInfo trait for SubstrateWeight<T> from the generated file and replace T::DbWeight:: by RocksDbWeight:: (see the im_pallet for a complete example).

Use benchmark in runtime

The last step is to include the computed weights in the runtime. We will see two runtimes design:

  • Polkadot, where the weights are generated for each chain.
  • Duniter, where the weights are shared for all the chains.

First, we need to replace the Substrate weights with our computed weight, for example, replacing type WeightInfo = pallet_im_online::weights::SubstrateWeight<Runtime>; by type WeightInfo = common_runtime::weights::pallet_im_online::WeightInfo<Runtime>;

Note: The path resolving between the name defined in the define_benchmark macro and the WeightInfo type can sometimes be confusing. The name defined in the macro will directly impact the trait that the benchmark will generate. For example [pallet_im_online, ImOnline] will generate impl<T: frame_system::Config> pallet_im_online::WeightInfo for WeightInfo<T>. Then depending on where you store the weight files and how you include and where you define the weight mod, you can add a prefix so the trait and the type WeightInfo can resolve correctly.

In Duniter, the pallet names in the define_benchmark are not prefixed, but the WeightInfo in the Config trait is prefixed by common_runtime. This is because the Config trait is defined by a macro in the common runtime with weight files stored in the common runtime. The common runtime is then included in the runtime of each chain, hence the prefix.

In contrast, Polkadot defined the Config trait in each chain or in the common runtime depending on the pallet, but all the weight files are stored in the chain folder, thus the prefix in the define_benchmark.

Polkadot

In Polkadot, the pallet’s weights used in the runtime are stored in each chain. For example, for the Polkadot chain, they are placed in the folder runtime/polkadot/src/weights/. To resolve the paths correctly, we need to define the benchmark accurately in the runtime, for example [pallet_im_online, ImOnline] in the define_benchmarks macro. The weight mod is defined in the mod.rs file, including all the weight files and then included in the runtime. The WeightInfo defined as type WeightInfo = weights::pallet_im_online::WeightInfo<Runtime>;.

Duniter

In Duniter, the pallet’s weights are stored in the folder duniter-v2s/runtime/common/src/weights/ and will be used for all the chains. To resolve the paths correctly, we need to define the benchmark correctly in the runtime, for example, [pallet_im_online, ImOnline] in the define_benchmarks macro. The weight mod is defined in duniter-v2s/runtime/common/src/weights/weight.rs and included in the common runtime and will be used in each chain. The WeightInfo defined as type WeightInfo = common_runtime::weights::pallet_im_online::WeightInfo<Runtime>;.