referrerpolicy=no-referrer-when-downgrade

frame_benchmarking_cli/storage/
cmd.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
18use sc_cli::{CliConfiguration, DatabaseParams, PruningParams, Result, SharedParams};
19use sc_client_api::{Backend as ClientBackend, StorageProvider, UsageProvider};
20use sc_client_db::DbHash;
21use sc_service::Configuration;
22use sp_api::CallApiAt;
23use sp_blockchain::HeaderBackend;
24use sp_database::{ColumnId, Database};
25use sp_runtime::traits::{Block as BlockT, HashingFor};
26use sp_state_machine::Storage;
27use sp_storage::{ChildInfo, ChildType, PrefixedStorageKey, StateVersion};
28
29use clap::{Args, Parser, ValueEnum};
30use log::info;
31use rand::prelude::*;
32use serde::Serialize;
33use sp_runtime::generic::BlockId;
34use std::{fmt::Debug, path::PathBuf, sync::Arc};
35
36use super::template::TemplateData;
37use crate::shared::{new_rng, HostInfoParams, WeightParams};
38
39/// The mode in which to run the storage benchmark.
40#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, ValueEnum)]
41pub enum StorageBenchmarkMode {
42	/// Run the benchmark for block import.
43	#[default]
44	ImportBlock,
45	/// Run the benchmark for block validation.
46	ValidateBlock,
47}
48
49/// Benchmark the storage speed of a chain snapshot.
50#[derive(Debug, Parser)]
51pub struct StorageCmd {
52	#[allow(missing_docs)]
53	#[clap(flatten)]
54	pub shared_params: SharedParams,
55
56	#[allow(missing_docs)]
57	#[clap(flatten)]
58	pub database_params: DatabaseParams,
59
60	#[allow(missing_docs)]
61	#[clap(flatten)]
62	pub pruning_params: PruningParams,
63
64	#[allow(missing_docs)]
65	#[clap(flatten)]
66	pub params: StorageParams,
67}
68
69/// Parameters for modifying the benchmark behaviour and the post processing of the results.
70#[derive(Debug, Default, Serialize, Clone, PartialEq, Args)]
71pub struct StorageParams {
72	#[allow(missing_docs)]
73	#[clap(flatten)]
74	pub weight_params: WeightParams,
75
76	#[allow(missing_docs)]
77	#[clap(flatten)]
78	pub hostinfo: HostInfoParams,
79
80	/// Skip the `read` benchmark.
81	#[arg(long)]
82	pub skip_read: bool,
83
84	/// Skip the `write` benchmark.
85	#[arg(long)]
86	pub skip_write: bool,
87
88	/// Specify the Handlebars template to use for outputting benchmark results.
89	#[arg(long)]
90	pub template_path: Option<PathBuf>,
91
92	/// Add a header to the generated weight output file.
93	///
94	/// Good for adding LICENSE headers.
95	#[arg(long, value_name = "PATH")]
96	pub header: Option<PathBuf>,
97
98	/// Path to write the raw 'read' results in JSON format to. Can be a file or directory.
99	#[arg(long)]
100	pub json_read_path: Option<PathBuf>,
101
102	/// Path to write the raw 'write' results in JSON format to. Can be a file or directory.
103	#[arg(long)]
104	pub json_write_path: Option<PathBuf>,
105
106	/// Rounds of warmups before measuring.
107	#[arg(long, default_value_t = 1)]
108	pub warmups: u32,
109
110	/// The `StateVersion` to use. Substrate `--dev` should use `V1` and Polkadot `V0`.
111	/// Selecting the wrong version can corrupt the DB.
112	#[arg(long, value_parser = clap::value_parser!(u8).range(0..=1))]
113	pub state_version: u8,
114
115	/// Trie cache size in bytes.
116	///
117	/// Providing `0` will disable the cache.
118	#[arg(long, value_name = "Bytes", default_value_t = 67108864)]
119	pub trie_cache_size: usize,
120
121	/// Enable the Trie cache.
122	///
123	/// This should only be used for performance analysis and not for final results.
124	#[arg(long)]
125	pub enable_trie_cache: bool,
126
127	/// Include child trees in benchmark.
128	#[arg(long)]
129	pub include_child_trees: bool,
130
131	/// Disable PoV recorder.
132	///
133	/// The recorder has impact on performance when benchmarking with the TrieCache enabled.
134	/// If the chain is recording a proof while building/importing a block, the pov recorder
135	/// should be activated.
136	///
137	/// Hence, when generating weights for a parachain this should be activated and when generating
138	/// weights for a standalone chain this should be deactivated.
139	#[arg(long, default_value = "false")]
140	pub disable_pov_recorder: bool,
141
142	/// The batch size for the read/write benchmark.
143	///
144	/// Since the write size needs to also include the cost of computing the storage root, which is
145	/// done once at the end of the block, the batch size is used to simulate multiple writes in a
146	/// block.
147	#[arg(long, default_value_t = 100_000)]
148	pub batch_size: usize,
149
150	/// The mode in which to run the storage benchmark.
151	///
152	/// PoV recorder must be activated to provide a storage proof for block validation at runtime.
153	#[arg(long, value_enum, default_value_t = StorageBenchmarkMode::ImportBlock)]
154	pub mode: StorageBenchmarkMode,
155
156	/// Number of rounds to execute block validation during the benchmark.
157	///
158	/// We need to run the benchmark several times to avoid fluctuations during runtime setup.
159	/// This is only used when `mode` is `validate-block`.
160	#[arg(long, default_value_t = 20)]
161	pub validate_block_rounds: u32,
162}
163
164impl StorageParams {
165	pub fn is_import_block_mode(&self) -> bool {
166		matches!(self.mode, StorageBenchmarkMode::ImportBlock)
167	}
168
169	pub fn is_validate_block_mode(&self) -> bool {
170		matches!(self.mode, StorageBenchmarkMode::ValidateBlock)
171	}
172}
173
174impl StorageCmd {
175	/// Calls into the Read and Write benchmarking functions.
176	/// Processes the output and writes it into files and stdout.
177	pub fn run<Block, BA, C>(
178		&self,
179		cfg: Configuration,
180		client: Arc<C>,
181		db: (Arc<dyn Database<DbHash>>, ColumnId),
182		storage: Arc<dyn Storage<HashingFor<Block>>>,
183		shared_trie_cache: Option<sp_trie::cache::SharedTrieCache<HashingFor<Block>>>,
184	) -> Result<()>
185	where
186		BA: ClientBackend<Block>,
187		Block: BlockT<Hash = DbHash>,
188		C: UsageProvider<Block>
189			+ StorageProvider<Block, BA>
190			+ HeaderBackend<Block>
191			+ CallApiAt<Block>,
192	{
193		let mut template = TemplateData::new(&cfg, &self.params)?;
194
195		let block_id = BlockId::<Block>::Number(client.usage_info().chain.best_number);
196		template.set_block_number(block_id.to_string());
197
198		if !self.params.skip_read {
199			self.bench_warmup(&client)?;
200			let record = self.bench_read(client.clone(), shared_trie_cache.clone())?;
201			if let Some(path) = &self.params.json_read_path {
202				record.save_json(&cfg, path, "read")?;
203			}
204			let stats = record.calculate_stats()?;
205			info!("Time summary [ns]:\n{:?}\nValue size summary:\n{:?}", stats.0, stats.1);
206			template.set_stats(Some(stats), None)?;
207		}
208
209		if !self.params.skip_write {
210			self.bench_warmup(&client)?;
211			let record = self.bench_write(client, db, storage, shared_trie_cache)?;
212			if let Some(path) = &self.params.json_write_path {
213				record.save_json(&cfg, path, "write")?;
214			}
215			let stats = record.calculate_stats()?;
216			info!("Time summary [ns]:\n{:?}\nValue size summary:\n{:?}", stats.0, stats.1);
217			template.set_stats(None, Some(stats))?;
218		}
219
220		template.write(&self.params.weight_params.weight_path, &self.params.template_path)
221	}
222
223	/// Returns the specified state version.
224	pub(crate) fn state_version(&self) -> StateVersion {
225		match self.params.state_version {
226			0 => StateVersion::V0,
227			1 => StateVersion::V1,
228			_ => unreachable!("Clap set to only allow 0 and 1"),
229		}
230	}
231
232	/// Returns Some if child node and None if regular
233	pub(crate) fn is_child_key(&self, key: Vec<u8>) -> Option<ChildInfo> {
234		if let Some((ChildType::ParentKeyId, storage_key)) =
235			ChildType::from_prefixed_key(&PrefixedStorageKey::new(key))
236		{
237			return Some(ChildInfo::new_default(storage_key))
238		}
239		None
240	}
241
242	/// Run some rounds of the (read) benchmark as warmup.
243	/// See `frame_benchmarking_cli::storage::read::bench_read` for detailed comments.
244	fn bench_warmup<B, BA, C>(&self, client: &Arc<C>) -> Result<()>
245	where
246		C: UsageProvider<B> + StorageProvider<B, BA>,
247		B: BlockT + Debug,
248		BA: ClientBackend<B>,
249	{
250		let hash = client.usage_info().chain.best_hash;
251		let mut keys: Vec<_> = client.storage_keys(hash, None, None)?.collect();
252		let (mut rng, _) = new_rng(None);
253		keys.shuffle(&mut rng);
254
255		for i in 0..self.params.warmups {
256			info!("Warmup round {}/{}", i + 1, self.params.warmups);
257			let mut child_nodes = Vec::new();
258
259			for key in keys.as_slice() {
260				let _ = client
261					.storage(hash, &key)
262					.expect("Checked above to exist")
263					.ok_or("Value unexpectedly empty");
264
265				if let Some(info) = self
266					.params
267					.include_child_trees
268					.then(|| self.is_child_key(key.clone().0))
269					.flatten()
270				{
271					// child tree key
272					for ck in client.child_storage_keys(hash, info.clone(), None, None)? {
273						child_nodes.push((ck.clone(), info.clone()));
274					}
275				}
276			}
277			for (key, info) in child_nodes.as_slice() {
278				client
279					.child_storage(hash, info, key)
280					.expect("Checked above to exist")
281					.ok_or("Value unexpectedly empty")?;
282			}
283		}
284
285		Ok(())
286	}
287}
288
289// Boilerplate
290impl CliConfiguration for StorageCmd {
291	fn shared_params(&self) -> &SharedParams {
292		&self.shared_params
293	}
294
295	fn database_params(&self) -> Option<&DatabaseParams> {
296		Some(&self.database_params)
297	}
298
299	fn pruning_params(&self) -> Option<&PruningParams> {
300		Some(&self.pruning_params)
301	}
302
303	fn trie_cache_maximum_size(&self) -> Result<Option<usize>> {
304		if self.params.enable_trie_cache && self.params.trie_cache_size > 0 {
305			Ok(Some(self.params.trie_cache_size))
306		} else {
307			Ok(None)
308		}
309	}
310}