From 13971fe2a7de14cf9f1be780aad014844ac6d267 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi <shawntabrizi@gmail.com> Date: Wed, 12 Feb 2020 12:11:20 +0100 Subject: [PATCH] Benchmark the Balances Pallet (#4879) * Initial transfer bench * Add best case * Transfer keep alive * Set balance benchmarks * Bump impl * Fix text Co-authored-by: Gavin Wood <github@gavwood.com> --- substrate/bin/node/runtime/src/lib.rs | 1 + substrate/frame/balances/src/benchmarking.rs | 322 +++++++++++++++++++ substrate/frame/balances/src/lib.rs | 1 + 3 files changed, 324 insertions(+) create mode 100644 substrate/frame/balances/src/benchmarking.rs diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 209208c58fa..4800c08260d 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -812,6 +812,7 @@ impl_runtime_apis! { -> Option<Vec<BenchmarkResults>> { match module.as_slice() { + b"pallet-balances" | b"balances" => Balances::run_benchmark(extrinsic, steps, repeat).ok(), b"pallet-identity" | b"identity" => Identity::run_benchmark(extrinsic, steps, repeat).ok(), b"pallet-timestamp" | b"timestamp" => Timestamp::run_benchmark(extrinsic, steps, repeat).ok(), _ => return None, diff --git a/substrate/frame/balances/src/benchmarking.rs b/substrate/frame/balances/src/benchmarking.rs new file mode 100644 index 00000000000..f48220c1ba7 --- /dev/null +++ b/substrate/frame/balances/src/benchmarking.rs @@ -0,0 +1,322 @@ +// Copyright 2020 Parity Technologies (UK) Ltd. +// This file is part of Substrate. + +// Substrate is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Substrate is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Substrate. If not, see <http://www.gnu.org/licenses/>. + +//! Balances pallet benchmarking. + +use super::*; + +use frame_system::RawOrigin; +use sp_io::hashing::blake2_256; +use sp_runtime::{BenchmarkResults, BenchmarkParameter}; +use sp_runtime::traits::{Bounded, Benchmarking, BenchmarkingSetup, Dispatchable}; + +use crate::Module as Balances; + +// Support Functions +fn account<T: Trait>(name: &'static str, index: u32) -> T::AccountId { + let entropy = (name, index).using_encoded(blake2_256); + T::AccountId::decode(&mut &entropy[..]).unwrap_or_default() +} + +// Benchmark `transfer` extrinsic with the worst possible conditions: +// * Transfer will kill the sender account. +// * Transfer will create the recipient account. +struct Transfer; +impl<T: Trait> BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>> for Transfer { + fn components(&self) -> Vec<(BenchmarkParameter, u32, u32)> { + vec![ + // Existential Deposit Multiplier + (BenchmarkParameter::E, 2, 1000), + // User Seed + (BenchmarkParameter::U, 1, 1000), + ] + } + + fn instance(&self, components: &[(BenchmarkParameter, u32)]) + -> Result<(crate::Call<T>, RawOrigin<T::AccountId>), &'static str> + { + // Constants + let ed = T::ExistentialDeposit::get(); + + // Select an account + let u = components.iter().find(|&c| c.0 == BenchmarkParameter::U).unwrap().1; + let user = account::<T>("user", u); + let user_origin = RawOrigin::Signed(user.clone()); + + // Give some multiple of the existential deposit + creation fee + transfer fee + let e = components.iter().find(|&c| c.0 == BenchmarkParameter::E).unwrap().1; + let mut balance = ed.saturating_mul(e.into()); + balance += T::CreationFee::get(); + let _ = <Balances<T> as Currency<_>>::make_free_balance_be(&user, balance); + + // Transfer `e - 1` existential deposits + 1 unit, which guarantees to create one account, and reap this user. + let recipient = account::<T>("recipient", u); + let recipient_lookup: <T::Lookup as StaticLookup>::Source = T::Lookup::unlookup(recipient); + let transfer_amt = ed.saturating_mul((e - 1).into()) + 1.into(); + + // Return the `transfer` call + Ok((crate::Call::<T>::transfer(recipient_lookup, transfer_amt), user_origin)) + } +} + +// Benchmark `transfer` with the best possible condition: +// * Both accounts exist and will continue to exist. +struct TransferBestCase; +impl<T: Trait> BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>> for TransferBestCase { + fn components(&self) -> Vec<(BenchmarkParameter, u32, u32)> { + vec![ + // Existential Deposit Multiplier + (BenchmarkParameter::E, 2, 1000), + // User Seed + (BenchmarkParameter::U, 1, 1000), + ] + } + + fn instance(&self, components: &[(BenchmarkParameter, u32)]) + -> Result<(crate::Call<T>, RawOrigin<T::AccountId>), &'static str> + { + // Constants + let ed = T::ExistentialDeposit::get(); + + // Select a sender + let u = components.iter().find(|&c| c.0 == BenchmarkParameter::U).unwrap().1; + let user = account::<T>("user", u); + let user_origin = RawOrigin::Signed(user.clone()); + + // Select a recipient + let recipient = account::<T>("recipient", u); + let recipient_lookup: <T::Lookup as StaticLookup>::Source = T::Lookup::unlookup(recipient.clone()); + + // Get the existential deposit multiplier + let e = components.iter().find(|&c| c.0 == BenchmarkParameter::E).unwrap().1; + + // Give the sender account max funds for transfer (their account will never reasonably be killed). + let _ = <Balances<T> as Currency<_>>::make_free_balance_be(&user, T::Balance::max_value()); + + // Give the recipient account existential deposit (thus their account already exists). + let _ = <Balances<T> as Currency<_>>::make_free_balance_be(&recipient, ed); + + // Transfer e * existential deposit. + let transfer_amt = ed.saturating_mul(e.into()); + + // Return the `transfer` call + Ok((crate::Call::<T>::transfer(recipient_lookup, transfer_amt), user_origin)) + } +} + +// Benchmark `transfer_keep_alive` with the worst possible condition: +// * The recipient account is created. +struct TransferKeepAlive; +impl<T: Trait> BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>> for TransferKeepAlive { + fn components(&self) -> Vec<(BenchmarkParameter, u32, u32)> { + vec![ + // Existential Deposit Multiplier + (BenchmarkParameter::E, 2, 1000), + // User Seed + (BenchmarkParameter::U, 1, 1000), + ] + } + + fn instance(&self, components: &[(BenchmarkParameter, u32)]) + -> Result<(crate::Call<T>, RawOrigin<T::AccountId>), &'static str> + { + // Constants + let ed = T::ExistentialDeposit::get(); + + // Select a sender + let u = components.iter().find(|&c| c.0 == BenchmarkParameter::U).unwrap().1; + let user = account::<T>("user", u); + let user_origin = RawOrigin::Signed(user.clone()); + + // Select a recipient + let recipient = account::<T>("recipient", u); + let recipient_lookup: <T::Lookup as StaticLookup>::Source = T::Lookup::unlookup(recipient.clone()); + + // Get the existential deposit multiplier + let e = components.iter().find(|&c| c.0 == BenchmarkParameter::E).unwrap().1; + + // Give the sender account max funds, thus a transfer will not kill account. + let _ = <Balances<T> as Currency<_>>::make_free_balance_be(&user, T::Balance::max_value()); + + // Transfer e * existential deposit. + let transfer_amt = ed.saturating_mul(e.into()); + + // Return the `transfer_keep_alive` call + Ok((crate::Call::<T>::transfer_keep_alive(recipient_lookup, transfer_amt), user_origin)) + } +} + +// Benchmark `set_balance` coming from ROOT account. This always creates an account. +struct SetBalance; +impl<T: Trait> BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>> for SetBalance { + fn components(&self) -> Vec<(BenchmarkParameter, u32, u32)> { + vec![ + // Existential Deposit Multiplier + (BenchmarkParameter::E, 2, 1000), + // User Seed + (BenchmarkParameter::U, 1, 1000), + ] + } + + fn instance(&self, components: &[(BenchmarkParameter, u32)]) + -> Result<(crate::Call<T>, RawOrigin<T::AccountId>), &'static str> + { + // Constants + let ed = T::ExistentialDeposit::get(); + + // Select a sender + let u = components.iter().find(|&c| c.0 == BenchmarkParameter::U).unwrap().1; + let user = account::<T>("user", u); + let user_lookup: <T::Lookup as StaticLookup>::Source = T::Lookup::unlookup(user.clone()); + + // Get the existential deposit multiplier for free and reserved + let e = components.iter().find(|&c| c.0 == BenchmarkParameter::E).unwrap().1; + let balance_amt = ed.saturating_mul(e.into()); + + // Return the `set_balance` call + Ok((crate::Call::<T>::set_balance(user_lookup, balance_amt, balance_amt), RawOrigin::Root)) + } +} + +// Benchmark `set_balance` coming from ROOT account. This always kills an account. +struct SetBalanceKilling; +impl<T: Trait> BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>> for SetBalanceKilling { + fn components(&self) -> Vec<(BenchmarkParameter, u32, u32)> { + vec![ + // Existential Deposit Multiplier + (BenchmarkParameter::E, 2, 1000), + // User Seed + (BenchmarkParameter::U, 1, 1000), + ] + } + + fn instance(&self, components: &[(BenchmarkParameter, u32)]) + -> Result<(crate::Call<T>, RawOrigin<T::AccountId>), &'static str> + { + // Constants + let ed = T::ExistentialDeposit::get(); + + // Select a sender + let u = components.iter().find(|&c| c.0 == BenchmarkParameter::U).unwrap().1; + let user = account::<T>("user", u); + let user_lookup: <T::Lookup as StaticLookup>::Source = T::Lookup::unlookup(user.clone()); + + // Get the existential deposit multiplier for free and reserved + let e = components.iter().find(|&c| c.0 == BenchmarkParameter::E).unwrap().1; + // Give the user some initial balance + let balance_amt = ed.saturating_mul(e.into()); + let _ = <Balances<T> as Currency<_>>::make_free_balance_be(&user, balance_amt); + + // Return the `set_balance` call that will kill the account + Ok((crate::Call::<T>::set_balance(user_lookup, 0.into(), 0.into()), RawOrigin::Root)) + } +} + +// The list of available benchmarks for this pallet. +enum SelectedBenchmark { + Transfer, + TransferBestCase, + TransferKeepAlive, + SetBalance, + SetBalanceKilling, +} + +// Allow us to select a benchmark from the list of available benchmarks. +impl<T: Trait> BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>> for SelectedBenchmark { + fn components(&self) -> Vec<(BenchmarkParameter, u32, u32)> { + match self { + Self::Transfer => <Transfer as BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>>>::components(&Transfer), + Self::TransferBestCase => <TransferBestCase as BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>>>::components(&TransferBestCase), + Self::TransferKeepAlive => <TransferKeepAlive as BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>>>::components(&TransferKeepAlive), + Self::SetBalance => <SetBalance as BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>>>::components(&SetBalance), + Self::SetBalanceKilling => <SetBalanceKilling as BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>>>::components(&SetBalanceKilling), + } + } + + fn instance(&self, components: &[(BenchmarkParameter, u32)]) + -> Result<(crate::Call<T>, RawOrigin<T::AccountId>), &'static str> + { + match self { + Self::Transfer => <Transfer as BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>>>::instance(&Transfer, components), + Self::TransferBestCase => <TransferBestCase as BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>>>::instance(&TransferBestCase, components), + Self::TransferKeepAlive => <TransferKeepAlive as BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>>>::instance(&TransferKeepAlive, components), + Self::SetBalance => <SetBalance as BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>>>::instance(&SetBalance, components), + Self::SetBalanceKilling => <SetBalanceKilling as BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>>>::instance(&SetBalanceKilling, components), + } + } +} + +impl<T: Trait> Benchmarking<BenchmarkResults> for Module<T> { + fn run_benchmark(extrinsic: Vec<u8>, steps: u32, repeat: u32) -> Result<Vec<BenchmarkResults>, &'static str> { + // Map the input to the selected benchmark. + let selected_benchmark = match extrinsic.as_slice() { + b"transfer" => SelectedBenchmark::Transfer, + b"transfer_best_case" => SelectedBenchmark::TransferBestCase, + b"transfer_keep_alive" => SelectedBenchmark::TransferKeepAlive, + b"set_balance" => SelectedBenchmark::SetBalance, + b"set_balance_killing" => SelectedBenchmark::SetBalanceKilling, + _ => return Err("Could not find extrinsic."), + }; + + // Warm up the DB + sp_io::benchmarking::commit_db(); + sp_io::benchmarking::wipe_db(); + + let components = <SelectedBenchmark as BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>>>::components(&selected_benchmark); + // results go here + let mut results: Vec<BenchmarkResults> = Vec::new(); + // Select the component we will be benchmarking. Each component will be benchmarked. + for (name, low, high) in components.iter() { + // Create up to `STEPS` steps for that component between high and low. + let step_size = ((high - low) / steps).max(1); + let num_of_steps = (high - low) / step_size; + for s in 0..num_of_steps { + // This is the value we will be testing for component `name` + let component_value = low + step_size * s; + + // Select the mid value for all the other components. + let c: Vec<(BenchmarkParameter, u32)> = components.iter() + .map(|(n, l, h)| + (*n, if n == name { component_value } else { (h - l) / 2 + l }) + ).collect(); + + // Run the benchmark `repeat` times. + for _r in 0..repeat { + // Set up the externalities environment for the setup we want to benchmark. + let (call, caller) = <SelectedBenchmark as BenchmarkingSetup<T, crate::Call<T>, RawOrigin<T::AccountId>>>::instance(&selected_benchmark, &c)?; + // Commit the externalities to the database, flushing the DB cache. + // This will enable worst case scenario for reading from the database. + sp_io::benchmarking::commit_db(); + // Run the benchmark. + let start = sp_io::benchmarking::current_time(); + call.dispatch(caller.clone().into())?; + let finish = sp_io::benchmarking::current_time(); + let elapsed = finish - start; + sp_std::if_std!{ + if let RawOrigin::Signed(who) = caller.clone() { + let balance = Account::<T>::get(&who).free; + println!("Free Balance {:?}", balance); + } + } + results.push((c.clone(), elapsed)); + // Wipe the DB back to the genesis state. + sp_io::benchmarking::wipe_db(); + } + } + } + return Ok(results); + } +} diff --git a/substrate/frame/balances/src/lib.rs b/substrate/frame/balances/src/lib.rs index ac651a5c242..9ddfd81daee 100644 --- a/substrate/frame/balances/src/lib.rs +++ b/substrate/frame/balances/src/lib.rs @@ -153,6 +153,7 @@ mod mock; #[cfg(test)] mod tests; mod migration; +mod benchmarking; use sp_std::prelude::*; use sp_std::{cmp, result, mem, fmt::Debug, ops::BitOr}; -- GitLab