Skip to main content

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