referrerpolicy=no-referrer-when-downgrade

node_testing/
bench.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19//! Benchmarking module.
20//!
21//! Utilities to do full-scale benchmarks involving database. With `BenchDb` you
22//! can pregenerate seed database and `clone` it for every iteration of your benchmarks
23//! or tests to get consistent, smooth benchmark experience!
24
25use std::{
26	collections::BTreeMap,
27	path::{Path, PathBuf},
28	sync::Arc,
29};
30
31use crate::{
32	client::{Backend, Client},
33	keyring::*,
34};
35use codec::{Decode, Encode};
36use futures::executor;
37use kitchensink_runtime::{
38	constants::currency::DOLLARS, AccountId, BalancesCall, CheckedExtrinsic, MinimumPeriod,
39	RuntimeCall, Signature, SystemCall, UncheckedExtrinsic,
40};
41use node_primitives::Block;
42use sc_block_builder::BlockBuilderBuilder;
43use sc_client_api::{execution_extensions::ExecutionExtensions, UsageProvider};
44use sc_client_db::PruningMode;
45use sc_consensus::{BlockImport, BlockImportParams, ForkChoiceStrategy, ImportResult, ImportedAux};
46use sc_executor::{WasmExecutionMethod, WasmtimeInstantiationStrategy};
47use sp_api::ProvideRuntimeApi;
48use sp_block_builder::BlockBuilder;
49use sp_consensus::BlockOrigin;
50use sp_core::{
51	crypto::get_public_from_string_or_panic, ed25519, sr25519, traits::SpawnNamed, Pair,
52};
53use sp_crypto_hashing::blake2_256;
54use sp_inherents::InherentData;
55use sp_runtime::{
56	generic::{self, ExtrinsicFormat, Preamble},
57	traits::{Block as BlockT, IdentifyAccount, Verify},
58	OpaqueExtrinsic,
59};
60
61/// Keyring full of accounts for benching.
62///
63/// Accounts are ordered:
64///     //endowed-user//00
65///     //endowed-user//01
66///      ...
67///     //endowed-user//N
68#[derive(Clone)]
69pub struct BenchKeyring {
70	accounts: BTreeMap<AccountId, BenchPair>,
71}
72
73#[derive(Clone)]
74enum BenchPair {
75	Sr25519(sr25519::Pair),
76	Ed25519(ed25519::Pair),
77}
78
79impl BenchPair {
80	fn sign(&self, payload: &[u8]) -> Signature {
81		match self {
82			Self::Sr25519(pair) => pair.sign(payload).into(),
83			Self::Ed25519(pair) => pair.sign(payload).into(),
84		}
85	}
86}
87
88/// Drop system cache.
89///
90/// Will panic if cache drop is impossible.
91pub fn drop_system_cache() {
92	#[cfg(target_os = "windows")]
93	{
94		log::warn!(
95			target: "bench-logistics",
96			"Clearing system cache on windows is not supported. Benchmark might totally be wrong.",
97		);
98		return
99	}
100
101	std::process::Command::new("sync")
102		.output()
103		.expect("Failed to execute system cache clear");
104
105	#[cfg(target_os = "linux")]
106	{
107		log::trace!(target: "bench-logistics", "Clearing system cache...");
108		std::process::Command::new("echo")
109			.args(&["3", ">", "/proc/sys/vm/drop_caches", "2>", "/dev/null"])
110			.output()
111			.expect("Failed to execute system cache clear");
112
113		let temp = tempfile::tempdir().expect("Failed to spawn tempdir");
114		let temp_file_path = format!("of={}/buf", temp.path().to_string_lossy());
115
116		// this should refill write cache with 2GB of garbage
117		std::process::Command::new("dd")
118			.args(&["if=/dev/urandom", &temp_file_path, "bs=64M", "count=32"])
119			.output()
120			.expect("Failed to execute dd for cache clear");
121
122		// remove tempfile of previous command
123		std::process::Command::new("rm")
124			.arg(&temp_file_path)
125			.output()
126			.expect("Failed to remove temp file");
127
128		std::process::Command::new("sync")
129			.output()
130			.expect("Failed to execute system cache clear");
131
132		log::trace!(target: "bench-logistics", "Clearing system cache done!");
133	}
134
135	#[cfg(target_os = "macos")]
136	{
137		log::trace!(target: "bench-logistics", "Clearing system cache...");
138		if let Err(err) = std::process::Command::new("purge").output() {
139			log::error!("purge error {:?}: ", err);
140			panic!("Could not clear system cache. Run under sudo?");
141		}
142		log::trace!(target: "bench-logistics", "Clearing system cache done!");
143	}
144}
145
146/// Pre-initialized benchmarking database.
147///
148/// This is prepared database with genesis and keyring
149/// that can be cloned and then used for any benchmarking.
150pub struct BenchDb {
151	keyring: BenchKeyring,
152	directory_guard: Guard,
153	database_type: DatabaseType,
154}
155
156impl Clone for BenchDb {
157	fn clone(&self) -> Self {
158		let keyring = self.keyring.clone();
159		let database_type = self.database_type;
160		let dir = tempfile::tempdir().expect("temp dir creation failed");
161
162		let seed_dir = self.directory_guard.0.path();
163
164		log::trace!(
165			target: "bench-logistics",
166			"Copying seed db from {} to {}",
167			seed_dir.to_string_lossy(),
168			dir.path().to_string_lossy(),
169		);
170		let seed_db_files = std::fs::read_dir(seed_dir)
171			.expect("failed to list file in seed dir")
172			.map(|f_result| f_result.expect("failed to read file in seed db").path())
173			.collect::<Vec<PathBuf>>();
174		fs_extra::copy_items(&seed_db_files, dir.path(), &fs_extra::dir::CopyOptions::new())
175			.expect("Copy of seed database is ok");
176
177		// We clear system cache after db clone but before any warmups.
178		// This populates system cache with some data unrelated to actual
179		// data we will be querying further under benchmark (like what
180		// would have happened in real system that queries random entries
181		// from database).
182		drop_system_cache();
183
184		BenchDb { keyring, directory_guard: Guard(dir), database_type }
185	}
186}
187
188/// Type of block for generation
189#[derive(Debug, PartialEq, Clone, Copy)]
190pub enum BlockType {
191	/// Bunch of random transfers.
192	RandomTransfersKeepAlive,
193	/// Bunch of random transfers that drain all of the source balance.
194	RandomTransfersReaping,
195	/// Bunch of "no-op" calls.
196	Noop,
197}
198
199impl BlockType {
200	/// Create block content description with specified number of transactions.
201	pub fn to_content(self, size: Option<usize>) -> BlockContent {
202		BlockContent { block_type: self, size }
203	}
204}
205
206/// Content of the generated block.
207#[derive(Clone, Debug)]
208pub struct BlockContent {
209	block_type: BlockType,
210	size: Option<usize>,
211}
212
213/// Type of backend database.
214#[derive(Debug, PartialEq, Clone, Copy)]
215pub enum DatabaseType {
216	/// RocksDb backend.
217	RocksDb,
218	/// Parity DB backend.
219	ParityDb,
220}
221
222impl DatabaseType {
223	fn into_settings(self, path: PathBuf) -> sc_client_db::DatabaseSource {
224		match self {
225			Self::RocksDb => sc_client_db::DatabaseSource::RocksDb { path, cache_size: 512 },
226			Self::ParityDb => sc_client_db::DatabaseSource::ParityDb { path },
227		}
228	}
229}
230
231/// Benchmarking task executor.
232///
233/// Uses multiple threads as the regular executable.
234#[derive(Debug, Clone)]
235pub struct TaskExecutor {
236	pool: executor::ThreadPool,
237}
238
239impl TaskExecutor {
240	fn new() -> Self {
241		Self { pool: executor::ThreadPool::new().expect("Failed to create task executor") }
242	}
243}
244
245impl SpawnNamed for TaskExecutor {
246	fn spawn(
247		&self,
248		_: &'static str,
249		_: Option<&'static str>,
250		future: futures::future::BoxFuture<'static, ()>,
251	) {
252		self.pool.spawn_ok(future);
253	}
254
255	fn spawn_blocking(
256		&self,
257		_: &'static str,
258		_: Option<&'static str>,
259		future: futures::future::BoxFuture<'static, ()>,
260	) {
261		self.pool.spawn_ok(future);
262	}
263}
264
265/// Iterator for block content.
266pub struct BlockContentIterator<'a> {
267	iteration: usize,
268	content: BlockContent,
269	runtime_version: sc_executor::RuntimeVersion,
270	genesis_hash: node_primitives::Hash,
271	keyring: &'a BenchKeyring,
272}
273
274impl<'a> BlockContentIterator<'a> {
275	fn new(content: BlockContent, keyring: &'a BenchKeyring, client: &Client) -> Self {
276		let genesis_hash = client.chain_info().genesis_hash;
277		let runtime_version = client
278			.runtime_version_at(genesis_hash)
279			.expect("There should be runtime version at 0");
280
281		BlockContentIterator { iteration: 0, content, keyring, runtime_version, genesis_hash }
282	}
283}
284
285impl<'a> Iterator for BlockContentIterator<'a> {
286	type Item = OpaqueExtrinsic;
287
288	fn next(&mut self) -> Option<Self::Item> {
289		if self.content.size.map(|size| size <= self.iteration).unwrap_or(false) {
290			return None
291		}
292
293		let sender = self.keyring.at(self.iteration);
294		let receiver = get_public_from_string_or_panic::<sr25519::Public>(&format!(
295			"random-user//{}",
296			self.iteration
297		))
298		.into();
299
300		let signed = self.keyring.sign(
301			CheckedExtrinsic {
302				format: ExtrinsicFormat::Signed(
303					sender,
304					tx_ext(0, kitchensink_runtime::ExistentialDeposit::get() + 1),
305				),
306				function: match self.content.block_type {
307					BlockType::RandomTransfersKeepAlive =>
308						RuntimeCall::Balances(BalancesCall::transfer_keep_alive {
309							dest: sp_runtime::MultiAddress::Id(receiver),
310							value: kitchensink_runtime::ExistentialDeposit::get() + 1,
311						}),
312					BlockType::RandomTransfersReaping => {
313						RuntimeCall::Balances(BalancesCall::transfer_allow_death {
314							dest: sp_runtime::MultiAddress::Id(receiver),
315							// Transfer so that ending balance would be 1 less than existential
316							// deposit so that we kill the sender account.
317							value: 100 * DOLLARS -
318								(kitchensink_runtime::ExistentialDeposit::get() - 1),
319						})
320					},
321					BlockType::Noop =>
322						RuntimeCall::System(SystemCall::remark { remark: Vec::new() }),
323				},
324			},
325			self.runtime_version.spec_version,
326			self.runtime_version.transaction_version,
327			self.genesis_hash.into(),
328		);
329
330		let encoded = Encode::encode(&signed);
331
332		let opaque = OpaqueExtrinsic::decode(&mut &encoded[..]).expect("Failed  to decode opaque");
333
334		self.iteration += 1;
335
336		Some(opaque)
337	}
338}
339
340impl BenchDb {
341	/// New immutable benchmarking database.
342	///
343	/// See [`BenchDb::new`] method documentation for more information about the purpose
344	/// of this structure.
345	pub fn with_key_types(
346		database_type: DatabaseType,
347		keyring_length: usize,
348		key_types: KeyTypes,
349	) -> Self {
350		let keyring = BenchKeyring::new(keyring_length, key_types);
351
352		let dir = tempfile::tempdir().expect("temp dir creation failed");
353		log::trace!(
354			target: "bench-logistics",
355			"Created seed db at {}",
356			dir.path().to_string_lossy(),
357		);
358		let (_client, _backend, _task_executor) =
359			Self::bench_client(database_type, dir.path(), &keyring);
360		let directory_guard = Guard(dir);
361
362		BenchDb { keyring, directory_guard, database_type }
363	}
364
365	/// New immutable benchmarking database.
366	///
367	/// This will generate database files in random temporary directory
368	/// and keep it there until struct is dropped.
369	///
370	/// You can `clone` this database or you can `create_context` from it
371	/// (which also does `clone`) to run actual operation against new database
372	/// which will be identical to the original.
373	pub fn new(database_type: DatabaseType, keyring_length: usize) -> Self {
374		Self::with_key_types(database_type, keyring_length, KeyTypes::Sr25519)
375	}
376
377	// This should return client that is doing everything that full node
378	// is doing.
379	//
380	// - This client should use best wasm execution method.
381	// - This client should work with real database only.
382	fn bench_client(
383		database_type: DatabaseType,
384		dir: &std::path::Path,
385		keyring: &BenchKeyring,
386	) -> (Client, std::sync::Arc<Backend>, TaskExecutor) {
387		let db_config = sc_client_db::DatabaseSettings {
388			trie_cache_maximum_size: Some(16 * 1024 * 1024),
389			state_pruning: Some(PruningMode::ArchiveAll),
390			source: database_type.into_settings(dir.into()),
391			blocks_pruning: sc_client_db::BlocksPruning::KeepAll,
392			metrics_registry: None,
393		};
394		let task_executor = TaskExecutor::new();
395
396		let backend = sc_service::new_db_backend(db_config).expect("Should not fail");
397		let executor = sc_executor::WasmExecutor::builder()
398			.with_execution_method(WasmExecutionMethod::Compiled {
399				instantiation_strategy: WasmtimeInstantiationStrategy::PoolingCopyOnWrite,
400			})
401			.build();
402
403		let client_config = sc_service::ClientConfig::default();
404		let genesis_block_builder = sc_service::GenesisBlockBuilder::new(
405			keyring.as_storage_builder(),
406			!client_config.no_genesis,
407			backend.clone(),
408			executor.clone(),
409		)
410		.expect("Failed to create genesis block builder");
411
412		let client = sc_service::new_client(
413			backend.clone(),
414			executor.clone(),
415			genesis_block_builder,
416			None,
417			None,
418			ExecutionExtensions::new(None, Arc::new(executor)),
419			Box::new(task_executor.clone()),
420			None,
421			None,
422			client_config,
423		)
424		.expect("Should not fail");
425
426		(client, backend, task_executor)
427	}
428
429	/// Generate list of required inherents.
430	///
431	/// Uses already instantiated Client.
432	pub fn generate_inherents(&mut self, client: &Client) -> Vec<OpaqueExtrinsic> {
433		let mut inherent_data = InherentData::new();
434		let timestamp = 1 * MinimumPeriod::get();
435
436		inherent_data
437			.put_data(sp_timestamp::INHERENT_IDENTIFIER, &timestamp)
438			.expect("Put timestamp failed");
439
440		client
441			.runtime_api()
442			.inherent_extrinsics(client.chain_info().genesis_hash, inherent_data)
443			.expect("Get inherents failed")
444	}
445
446	/// Iterate over some block content with transaction signed using this database keyring.
447	pub fn block_content(&self, content: BlockContent, client: &Client) -> BlockContentIterator {
448		BlockContentIterator::new(content, &self.keyring, client)
449	}
450
451	/// Get client for this database operations.
452	pub fn client(&mut self) -> Client {
453		let (client, _backend, _task_executor) =
454			Self::bench_client(self.database_type, self.directory_guard.path(), &self.keyring);
455
456		client
457	}
458
459	/// Generate new block using this database.
460	pub fn generate_block(&mut self, content: BlockContent) -> Block {
461		let client = self.client();
462		let chain = client.usage_info().chain;
463
464		let mut block = BlockBuilderBuilder::new(&client)
465			.on_parent_block(chain.best_hash)
466			.with_parent_block_number(chain.best_number)
467			.build()
468			.expect("Failed to create block builder.");
469
470		for extrinsic in self.generate_inherents(&client) {
471			block.push(extrinsic).expect("Push inherent failed");
472		}
473
474		let start = std::time::Instant::now();
475		for opaque in self.block_content(content, &client) {
476			match block.push(opaque) {
477				Err(sp_blockchain::Error::ApplyExtrinsicFailed(
478					sp_blockchain::ApplyExtrinsicFailed::Validity(e),
479				)) if e.exhausted_resources() => break,
480				Err(err) => panic!("Error pushing transaction: {:?}", err),
481				Ok(_) => {},
482			}
483		}
484
485		let block = block.build().expect("Block build failed").block;
486
487		log::info!(
488			target: "bench-logistics",
489			"Block construction: {:#?} ({} tx)",
490			start.elapsed(), block.extrinsics.len()
491		);
492
493		block
494	}
495
496	/// Database path.
497	pub fn path(&self) -> &Path {
498		self.directory_guard.path()
499	}
500
501	/// Clone this database and create context for testing/benchmarking.
502	pub fn create_context(&self) -> BenchContext {
503		let BenchDb { directory_guard, keyring, database_type } = self.clone();
504		let (client, backend, task_executor) =
505			Self::bench_client(database_type, directory_guard.path(), &keyring);
506
507		BenchContext {
508			client: Arc::new(client),
509			db_guard: directory_guard,
510			backend,
511			spawn_handle: Box::new(task_executor),
512		}
513	}
514}
515
516/// Key types to be used in benching keyring
517pub enum KeyTypes {
518	/// sr25519 signing keys
519	Sr25519,
520	/// ed25519 signing keys
521	Ed25519,
522}
523
524impl BenchKeyring {
525	/// New keyring.
526	///
527	/// `length` is the number of accounts generated.
528	pub fn new(length: usize, key_types: KeyTypes) -> Self {
529		let mut accounts = BTreeMap::new();
530
531		for n in 0..length {
532			let seed = format!("//endowed-user/{}", n);
533			let (account_id, pair) = match key_types {
534				KeyTypes::Sr25519 => {
535					let pair =
536						sr25519::Pair::from_string(&seed, None).expect("failed to generate pair");
537					let account_id = AccountPublic::from(pair.public()).into_account();
538					(account_id, BenchPair::Sr25519(pair))
539				},
540				KeyTypes::Ed25519 => {
541					let pair = ed25519::Pair::from_seed(&blake2_256(seed.as_bytes()));
542					let account_id = AccountPublic::from(pair.public()).into_account();
543					(account_id, BenchPair::Ed25519(pair))
544				},
545			};
546			accounts.insert(account_id, pair);
547		}
548
549		Self { accounts }
550	}
551
552	/// Generated account id-s from keyring keypairs.
553	pub fn collect_account_ids(&self) -> Vec<AccountId> {
554		self.accounts.keys().cloned().collect()
555	}
556
557	/// Get account id at position `index`
558	pub fn at(&self, index: usize) -> AccountId {
559		self.accounts.keys().nth(index).expect("Failed to get account").clone()
560	}
561
562	/// Sign transaction with keypair from this keyring.
563	pub fn sign(
564		&self,
565		xt: CheckedExtrinsic,
566		spec_version: u32,
567		tx_version: u32,
568		genesis_hash: [u8; 32],
569	) -> UncheckedExtrinsic {
570		match xt.format {
571			ExtrinsicFormat::Signed(signed, tx_ext) => {
572				let payload = (
573					xt.function,
574					tx_ext.clone(),
575					spec_version,
576					tx_version,
577					genesis_hash,
578					genesis_hash,
579					// metadata_hash
580					None::<()>,
581				);
582				let key = self.accounts.get(&signed).expect("Account id not found in keyring");
583				let signature = payload.using_encoded(|b| {
584					if b.len() > 256 {
585						key.sign(&blake2_256(b))
586					} else {
587						key.sign(b)
588					}
589				});
590				generic::UncheckedExtrinsic::new_signed(
591					payload.0,
592					sp_runtime::MultiAddress::Id(signed),
593					signature,
594					tx_ext,
595				)
596				.into()
597			},
598			ExtrinsicFormat::Bare => generic::UncheckedExtrinsic::new_bare(xt.function).into(),
599			ExtrinsicFormat::General(ext_version, tx_ext) =>
600				generic::UncheckedExtrinsic::from_parts(
601					xt.function,
602					Preamble::General(ext_version, tx_ext),
603				)
604				.into(),
605		}
606	}
607
608	/// Generate genesis with accounts from this keyring endowed with some balance and
609	/// kitchensink_runtime code blob.
610	pub fn as_storage_builder(&self) -> &dyn sp_runtime::BuildStorage {
611		self
612	}
613}
614
615impl sp_runtime::BuildStorage for BenchKeyring {
616	fn assimilate_storage(&self, storage: &mut sp_core::storage::Storage) -> Result<(), String> {
617		storage.top.insert(
618			sp_core::storage::well_known_keys::CODE.to_vec(),
619			kitchensink_runtime::wasm_binary_unwrap().into(),
620		);
621		crate::genesis::config_endowed(self.collect_account_ids()).assimilate_storage(storage)
622	}
623}
624
625struct Guard(tempfile::TempDir);
626
627impl Guard {
628	fn path(&self) -> &Path {
629		self.0.path()
630	}
631}
632
633/// Benchmarking/test context holding instantiated client and backend references.
634pub struct BenchContext {
635	/// Node client.
636	pub client: Arc<Client>,
637	/// Node backend.
638	pub backend: Arc<Backend>,
639	/// Spawn handle.
640	pub spawn_handle: Box<dyn SpawnNamed>,
641
642	db_guard: Guard,
643}
644
645type AccountPublic = <Signature as Verify>::Signer;
646
647impl BenchContext {
648	/// Import some block.
649	pub fn import_block(&mut self, block: Block) {
650		let mut import_params =
651			BlockImportParams::new(BlockOrigin::NetworkBroadcast, block.header.clone());
652		import_params.body = Some(block.extrinsics().to_vec());
653		import_params.fork_choice = Some(ForkChoiceStrategy::LongestChain);
654
655		assert_eq!(self.client.chain_info().best_number, 0);
656
657		assert_eq!(
658			futures::executor::block_on(self.client.import_block(import_params))
659				.expect("Failed to import block"),
660			ImportResult::Imported(ImportedAux {
661				header_only: false,
662				clear_justification_requests: false,
663				needs_justification: false,
664				bad_justification: false,
665				is_new_best: true,
666			})
667		);
668
669		assert_eq!(self.client.chain_info().best_number, 1);
670	}
671
672	/// Database path for the current context.
673	pub fn path(&self) -> &Path {
674		self.db_guard.path()
675	}
676}