Skip to main content

pop_chains/bench/
mod.rs

1// SPDX-License-Identifier: GPL-3.0
2
3use crate::{Error, utils::helpers::HostFunctions};
4use clap::Parser;
5use duct::cmd;
6use frame_benchmarking_cli::PalletCmd;
7pub use frame_benchmarking_cli::{BlockCmd, MachineCmd, OverheadCmd, StorageCmd};
8use serde::{Deserialize, Serialize};
9use sp_runtime::traits::BlakeTwo256;
10use std::{
11	collections::BTreeMap,
12	fmt::Display,
13	io::Read,
14	path::{Path, PathBuf},
15};
16use strum_macros::{EnumIter, EnumMessage as EnumMessageDerive};
17use tempfile::NamedTempFile;
18
19/// Provides functionality for sourcing binaries of the benchmarking CLI.
20pub mod binary;
21
22/// The default `development` preset used to communicate with the runtime via
23/// [`GenesisBuilder`](https://docs.rs/sp-genesis-builder/latest/sp_genesis_builder/trait.GenesisBuilder.html) interface.
24///
25/// (Recommended for testing with a single node, e.g., for benchmarking)
26pub const GENESIS_BUILDER_DEV_PRESET: &str = "development";
27
28/// Type alias for records where the key is the pallet name and the value is an array of its
29/// extrinsics.
30pub type PalletExtrinsicsRegistry = BTreeMap<String, Vec<String>>;
31
32/// Commands that can be executed by the `frame-benchmarking-cli` CLI.
33pub enum BenchmarkingCliCommand {
34	/// Execute a pallet benchmark.
35	Pallet,
36	/// Execute an overhead benchmark.
37	Overhead,
38	/// Execute a storage benchmark.
39	Storage,
40	/// Execute a machine benchmark.
41	Machine,
42	/// Execute a block benchmark.
43	Block,
44}
45
46impl Display for BenchmarkingCliCommand {
47	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48		let s = match self {
49			BenchmarkingCliCommand::Pallet => "pallet",
50			BenchmarkingCliCommand::Overhead => "overhead",
51			BenchmarkingCliCommand::Storage => "storage",
52			BenchmarkingCliCommand::Machine => "machine",
53			BenchmarkingCliCommand::Block => "block",
54		};
55		write!(f, "{}", s)
56	}
57}
58
59/// How the genesis state for benchmarking should be built.
60#[derive(
61	clap::ValueEnum,
62	Debug,
63	Eq,
64	PartialEq,
65	Clone,
66	Copy,
67	EnumIter,
68	EnumMessageDerive,
69	Serialize,
70	Deserialize,
71)]
72#[clap(rename_all = "kebab-case")]
73pub enum GenesisBuilderPolicy {
74	/// Do not provide any genesis state.
75	None,
76	/// Let the runtime build the genesis state through its `BuildGenesisConfig` runtime API.
77	Runtime,
78}
79
80impl Display for GenesisBuilderPolicy {
81	fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82		let s = match self {
83			GenesisBuilderPolicy::None => "none",
84			GenesisBuilderPolicy::Runtime => "runtime",
85		};
86		write!(f, "{}", s)
87	}
88}
89
90impl TryFrom<String> for GenesisBuilderPolicy {
91	type Error = String;
92
93	fn try_from(s: String) -> Result<Self, Self::Error> {
94		match s.as_str() {
95			"none" => Ok(GenesisBuilderPolicy::None),
96			"runtime" => Ok(GenesisBuilderPolicy::Runtime),
97			_ => Err(format!("Invalid genesis builder policy: {}", s)),
98		}
99	}
100}
101
102/// Get the runtime folder path and throws error if it does not exist.
103///
104/// # Arguments
105/// * `parent` - Parent path that contains the runtime folder.
106pub fn get_runtime_path(parent: &Path) -> Result<PathBuf, Error> {
107	["runtime", "runtimes"]
108		.iter()
109		.map(|f| parent.join(f))
110		.find(|path| path.exists())
111		.ok_or_else(|| Error::RuntimeNotFound(parent.to_str().unwrap().to_string()))
112}
113
114/// Runs pallet benchmarks using `frame-benchmarking-cli`.
115///
116/// # Arguments
117/// * `args` - Arguments to pass to the benchmarking command.
118pub fn generate_pallet_benchmarks(args: Vec<String>) -> Result<(), Error> {
119	let cmd = PalletCmd::try_parse_from(std::iter::once("".to_string()).chain(args.into_iter()))
120		.map_err(|e| Error::ParamParsingError(e.to_string()))?;
121
122	cmd.run_with_spec::<BlakeTwo256, HostFunctions>(None)
123		.map_err(|e| Error::BenchmarkingError(e.to_string()))
124}
125
126/// Generates binary benchmarks using `frame-benchmarking-cli`.
127///
128/// # Arguments
129/// * `binary_path` - Path to the binary of FRAME Omni Bencher.
130/// * `command` - Command to run for benchmarking.
131/// * `update_args` - Function to update the arguments before running the benchmark.
132/// * `excluded_args` - Arguments to exclude from the benchmarking command.
133pub fn generate_binary_benchmarks<F>(
134	binary_path: &PathBuf,
135	command: BenchmarkingCliCommand,
136	update_args: F,
137	excluded_args: &[&str],
138) -> Result<(), Error>
139where
140	F: Fn(Vec<String>) -> Vec<String>,
141{
142	// Get all arguments of the command and skip the program name.
143	let mut args = update_args(std::env::args().skip(3).collect::<Vec<String>>());
144	args = args
145		.into_iter()
146		.filter(|arg| !excluded_args.iter().any(|a| arg.starts_with(a)))
147		.collect::<Vec<String>>();
148	let mut cmd_args = vec!["benchmark".to_string(), command.to_string()];
149	cmd_args.append(&mut args);
150
151	if let Err(e) = cmd(binary_path, cmd_args).stderr_capture().run() {
152		return Err(Error::BenchmarkingError(e.to_string()));
153	}
154	Ok(())
155}
156
157/// Loads a mapping of pallets and their associated extrinsics from the runtime binary.
158///
159/// # Arguments
160/// * `runtime_path` - Path to the runtime binary.
161/// * `binary_path` - Path to the binary of FRAME Omni Bencher.
162pub async fn load_pallet_extrinsics(
163	runtime_path: &Path,
164	binary_path: &Path,
165) -> Result<PalletExtrinsicsRegistry, Error> {
166	let output = generate_omni_bencher_benchmarks(
167		binary_path,
168		BenchmarkingCliCommand::Pallet,
169		vec![
170			format!("--runtime={}", runtime_path.display()),
171			"--genesis-builder=none".to_string(),
172			"--list=all".to_string(),
173		],
174		false,
175	)?;
176	// Process the captured output and return the pallet extrinsics registry.
177	Ok(process_pallet_extrinsics(output))
178}
179
180fn process_pallet_extrinsics(output: String) -> PalletExtrinsicsRegistry {
181	// Process the captured output and return the pallet extrinsics registry.
182	let mut registry = PalletExtrinsicsRegistry::new();
183	let lines: Vec<String> = output.split("\n").map(String::from).skip(1).collect();
184	for line in lines {
185		if line.is_empty() {
186			continue;
187		}
188		let record: Vec<String> = line.split(", ").map(String::from).collect();
189		let pallet = record[0].trim().to_string();
190		let extrinsic = record[1].trim().to_string();
191		registry.entry(pallet).or_default().push(extrinsic);
192	}
193
194	// Sort the extrinsics by alphabetical order for each pallet.
195	for extrinsics in registry.values_mut() {
196		extrinsics.sort();
197	}
198	registry
199}
200
201/// Run command for benchmarking with a provided `frame-omni-bencher` binary.
202///
203/// # Arguments
204/// * `binary_path` - Path to the binary to run.
205/// * `command` - Command to run. `frame-omni-bencher` only supports `pallet` and `overhead`.
206/// * `args` - Additional arguments to pass to the binary.
207/// * `log_enabled` - Whether to enable logging.
208pub fn generate_omni_bencher_benchmarks(
209	binary_path: &Path,
210	command: BenchmarkingCliCommand,
211	args: Vec<String>,
212	log_enabled: bool,
213) -> Result<String, Error> {
214	let stdout_file = NamedTempFile::new()?;
215	let stdout_path = stdout_file.path().to_owned();
216
217	let stderror_file = NamedTempFile::new()?;
218	let stderror_path = stderror_file.path().to_owned();
219
220	let mut cmd_args = vec!["v1".to_string(), "benchmark".to_string(), command.to_string()];
221	cmd_args.extend(args);
222
223	let cmd = cmd(binary_path, cmd_args)
224		.env("RUST_LOG", if log_enabled { "info" } else { "none" })
225		.stderr_path(&stderror_path)
226		.stdout_path(&stdout_path);
227
228	if let Err(e) = cmd.run() {
229		let mut error_output = String::new();
230		std::fs::File::open(&stderror_path)?.read_to_string(&mut error_output)?;
231		return Err(Error::BenchmarkingError(
232			if error_output.is_empty() { e.to_string() } else { error_output }
233				.trim()
234				.to_string(),
235		));
236	}
237
238	let mut stdout_output = String::new();
239	std::fs::File::open(&stdout_path)?.read_to_string(&mut stdout_output)?;
240	Ok(stdout_output)
241}
242
243#[cfg(test)]
244mod tests {
245	use super::*;
246	use binary::omni_bencher_generator;
247	use std::fs;
248	use tempfile::tempdir;
249
250	#[test]
251	fn get_runtime_path_works() -> Result<(), Error> {
252		let temp_dir = tempdir()?;
253		let path = temp_dir.path();
254		let path_str = path.to_str().unwrap().to_string();
255
256		assert_eq!(
257			get_runtime_path(path).unwrap_err().to_string(),
258			format!("Failed to find the runtime {}", path_str)
259		);
260		for name in ["runtime", "runtimes"] {
261			fs::create_dir(path.join(name))?;
262		}
263		assert!(get_runtime_path(path).is_ok());
264		Ok(())
265	}
266
267	#[tokio::test]
268	async fn load_pallet_extrinsics_works() -> Result<(), Error> {
269		let temp_dir = tempdir()?;
270		let runtime_path = get_mock_runtime_path(true);
271		let binary = omni_bencher_generator(temp_dir.path().to_path_buf(), None).await?;
272		binary.source(false, &(), true).await?;
273
274		let registry = load_pallet_extrinsics(&runtime_path, &binary.path()).await?;
275		let pallets: Vec<String> = registry.keys().cloned().collect();
276		assert_eq!(
277			pallets,
278			vec![
279				"cumulus_pallet_parachain_system",
280				"cumulus_pallet_xcmp_queue",
281				"frame_system",
282				"pallet_balances",
283				"pallet_collator_selection",
284				"pallet_message_queue",
285				"pallet_session",
286				"pallet_sudo",
287				"pallet_timestamp"
288			]
289		);
290		assert_eq!(
291			registry.get("pallet_timestamp").cloned().unwrap_or_default(),
292			["on_finalize", "set"]
293		);
294		assert_eq!(
295			registry.get("pallet_sudo").cloned().unwrap_or_default(),
296			["check_only_sudo_account", "remove_key", "set_key", "sudo", "sudo_as"]
297		);
298		Ok(())
299	}
300
301	#[tokio::test]
302	async fn load_pallet_extrinsics_missing_runtime_benchmarks_fails() -> Result<(), Error> {
303		let temp_dir = tempdir()?;
304		let runtime_path = get_mock_runtime_path(false);
305		let binary = omni_bencher_generator(temp_dir.path().to_path_buf(), None).await?;
306		binary.source(false, &(), true).await?;
307
308		assert_eq!(
309			load_pallet_extrinsics(&runtime_path, &binary.path())
310				.await
311				.err()
312				.unwrap()
313				.to_string(),
314			"Failed to run benchmarking: Error: Input(\"Did not find the benchmarking runtime api. This could mean that you either did not build the node correctly with the `--features runtime-benchmarks` flag, or the chain spec that you are using was not created by a node that was compiled with the flag\")"
315		);
316		Ok(())
317	}
318
319	fn get_mock_runtime_path(with_runtime_benchmarks: bool) -> PathBuf {
320		let binary_path = format!(
321			"../../tests/runtimes/{}.wasm",
322			if with_runtime_benchmarks { "base_parachain_benchmark" } else { "base_parachain" }
323		);
324		std::env::current_dir().unwrap().join(binary_path).canonicalize().unwrap()
325	}
326}