referrerpolicy=no-referrer-when-downgrade

frame_benchmarking_cli/block/
bench.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Contains the core benchmarking logic.
19
20use codec::DecodeAll;
21use frame_support::weights::constants::WEIGHT_REF_TIME_PER_NANOS;
22use frame_system::ConsumedWeight;
23use sc_block_builder::BlockBuilderApi;
24use sc_cli::{Error, Result};
25use sc_client_api::{
26	Backend as ClientBackend, BlockBackend, HeaderBackend, StorageProvider, UsageProvider,
27};
28use sp_api::{ApiExt, Core, ProvideRuntimeApi};
29use sp_blockchain::Error::RuntimeApiError;
30use sp_runtime::{
31	generic::BlockId,
32	traits::{Block as BlockT, Header as HeaderT},
33	DigestItem, OpaqueExtrinsic,
34};
35use sp_storage::StorageKey;
36
37use clap::Args;
38use log::{info, warn};
39use serde::Serialize;
40use std::{fmt::Debug, marker::PhantomData, sync::Arc, time::Instant};
41use thousands::Separable;
42
43use crate::shared::{StatSelect, Stats};
44
45/// Log target for printing block weight info.
46const LOG_TARGET: &'static str = "benchmark::block::weight";
47
48/// Parameters for modifying the benchmark behaviour.
49#[derive(Debug, Default, Serialize, Clone, PartialEq, Args)]
50pub struct BenchmarkParams {
51	/// Number of the first block to consider.
52	#[arg(long)]
53	pub from: u32,
54
55	/// Last block number to consider.
56	#[arg(long)]
57	pub to: u32,
58
59	/// Number of times that the benchmark should be repeated for each block.
60	#[arg(long, default_value_t = 10)]
61	pub repeat: u32,
62}
63
64/// Convenience closure for the [`Benchmark::run()`] function.
65pub struct Benchmark<Block, BA, C> {
66	client: Arc<C>,
67	params: BenchmarkParams,
68	_p: PhantomData<(Block, BA, C)>,
69}
70
71/// Helper for nano seconds.
72type NanoSeconds = u64;
73
74impl<Block, BA, C> Benchmark<Block, BA, C>
75where
76	Block: BlockT<Extrinsic = OpaqueExtrinsic>,
77	BA: ClientBackend<Block>,
78	C: ProvideRuntimeApi<Block>
79		+ StorageProvider<Block, BA>
80		+ UsageProvider<Block>
81		+ BlockBackend<Block>
82		+ HeaderBackend<Block>,
83	C::Api: ApiExt<Block> + BlockBuilderApi<Block>,
84{
85	/// Returns a new [`Self`] from the arguments.
86	pub fn new(client: Arc<C>, params: BenchmarkParams) -> Self {
87		Self { client, params, _p: PhantomData }
88	}
89
90	/// Benchmark the execution speed of historic blocks and log the results.
91	pub fn run(&self) -> Result<()> {
92		if self.params.from == 0 {
93			return Err("Cannot benchmark the genesis block".into())
94		}
95
96		for i in self.params.from..=self.params.to {
97			let block_num = BlockId::Number(i.into());
98			let hash = self.client.expect_block_hash_from_id(&block_num)?;
99			let consumed = self.consumed_weight(hash)?;
100
101			let block = self.client.block(hash)?.ok_or(format!("Block {} not found", block_num))?;
102			let block = self.unsealed(block.block);
103			let took = self.measure_block(&block, *block.header().parent_hash())?;
104
105			self.log_weight(i, block.extrinsics().len(), consumed, took);
106		}
107
108		Ok(())
109	}
110
111	/// Return the average *execution* aka. *import* time of the block.
112	fn measure_block(&self, block: &Block, parent_hash: Block::Hash) -> Result<NanoSeconds> {
113		let mut record = Vec::<NanoSeconds>::default();
114		// Interesting part here:
115		// Execute the block multiple times and collect stats about its execution time.
116		for _ in 0..self.params.repeat {
117			let block = block.clone();
118			let runtime_api = self.client.runtime_api();
119			let start = Instant::now();
120
121			runtime_api
122				.execute_block(parent_hash, block)
123				.map_err(|e| Error::Client(RuntimeApiError(e)))?;
124
125			record.push(start.elapsed().as_nanos() as NanoSeconds);
126		}
127
128		let took = Stats::new(&record)?.select(StatSelect::Average);
129		Ok(took)
130	}
131
132	/// Returns the total nanoseconds of a [`frame_system::ConsumedWeight`] for a block number.
133	///
134	/// This is the post-dispatch corrected weight and is only available
135	/// after executing the block.
136	fn consumed_weight(&self, block_hash: Block::Hash) -> Result<NanoSeconds> {
137		// Hard-coded key for System::BlockWeight. It could also be passed in as argument
138		// for the benchmark, but I think this should work as well.
139		let hash = array_bytes::hex2bytes(
140			"26aa394eea5630e07c48ae0c9558cef734abf5cb34d6244378cddbf18e849d96",
141		)?;
142		let key = StorageKey(hash);
143
144		let mut raw_weight = &self
145			.client
146			.storage(block_hash, &key)?
147			.ok_or(format!("Could not find System::BlockWeight for block: {}", block_hash))?
148			.0[..];
149
150		let weight = ConsumedWeight::decode_all(&mut raw_weight)?;
151		// Should be divisible, but still use floats in case we ever change that.
152		Ok((weight.total().ref_time() as f64 / WEIGHT_REF_TIME_PER_NANOS as f64).floor()
153			as NanoSeconds)
154	}
155
156	/// Prints the weight info of a block to the console.
157	fn log_weight(&self, num: u32, num_ext: usize, consumed: NanoSeconds, took: NanoSeconds) {
158		// The ratio of weight that the block used vs what it consumed.
159		// This should in general not exceed 100% (minus outliers).
160		let percent = (took as f64 / consumed as f64) * 100.0;
161
162		let msg = format!(
163			"Block {} with {: >5} tx used {: >6.2}% of its weight ({: >14} of {: >14} ns)",
164			num,
165			num_ext,
166			percent,
167			took.separate_with_commas(),
168			consumed.separate_with_commas()
169		);
170
171		if took <= consumed {
172			info!(target: LOG_TARGET, "{}", msg);
173		} else {
174			warn!(target: LOG_TARGET, "{} - OVER WEIGHT!", msg);
175		}
176	}
177
178	/// Removes the consensus seal from the block.
179	fn unsealed(&self, block: Block) -> Block {
180		let (mut header, exts) = block.deconstruct();
181		header.digest_mut().logs.retain(|item| !matches!(item, DigestItem::Seal(_, _)));
182		Block::new(header, exts)
183	}
184}