diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock index 0246844271..37e52797fe 100644 --- a/substrate/Cargo.lock +++ b/substrate/Cargo.lock @@ -3692,6 +3692,7 @@ name = "srml-staking" version = "2.0.0" dependencies = [ "parity-codec 3.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "safe-mix 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.91 (registry+https://github.com/rust-lang/crates.io-index)", "sr-io 2.0.0", diff --git a/substrate/srml/staking/Cargo.toml b/substrate/srml/staking/Cargo.toml index 857ea73ee4..62fc187176 100644 --- a/substrate/srml/staking/Cargo.toml +++ b/substrate/srml/staking/Cargo.toml @@ -21,8 +21,10 @@ session = { package = "srml-session", path = "../session", default-features = fa substrate-primitives = { path = "../../core/primitives" } timestamp = { package = "srml-timestamp", path = "../timestamp" } balances = { package = "srml-balances", path = "../balances" } +rand = "0.6.5" [features] +bench = [] default = ["std"] std = [ "serde", diff --git a/substrate/srml/staking/src/benches.rs b/substrate/srml/staking/src/benches.rs new file mode 100644 index 0000000000..e3ee00b9e9 --- /dev/null +++ b/substrate/srml/staking/src/benches.rs @@ -0,0 +1,114 @@ +// Copyright 2019 Parity Technologies +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Benchmarks of the phragmen election algorithm. +//! Note that execution times will not be accurate in an absolute scale, since +//! - Everything is executed in the context of `TestExternalities` +//! - Everything is executed in native environment. +//! +//! Run using: +//! +//! ```zsh +//! cargo bench --features bench --color always +//! ``` + +use test::Bencher; +use runtime_io::with_externalities; +use mock::*; +use super::*; +use rand::{self, Rng}; + +const VALIDATORS: u64 = 1000; +const NOMINATORS: u64 = 10_000; +const EDGES: u64 = 2; +const TO_ELECT: usize = 100; +const STAKE: u64 = 1000; + +fn do_phragmen(b: &mut Bencher, num_vals: u64, num_noms: u64, count: usize, votes_per: u64) { + with_externalities(&mut ExtBuilder::default().nominate(false).build(), || { + assert!(num_vals > votes_per); + let rr = |a, b| rand::thread_rng().gen_range(a as usize, b as usize) as u64; + + // prefix to distinguish the validator and nominator account ranges. + let np = 10_000; + + (1 ..= 2*num_vals) + .step_by(2) + .for_each(|acc| bond_validator(acc, STAKE + rr(10, 50))); + + (np ..= (np + 2*num_noms)) + .step_by(2) + .for_each(|acc| { + let mut stashes_to_vote = (1 ..= 2*num_vals) + .step_by(2) + .map(|ctrl| ctrl + 1) + .collect::>(); + let votes = (0 .. votes_per) + .map(|_| { + stashes_to_vote.remove(rr(0, stashes_to_vote.len()) as usize) + }) + .collect::>(); + bond_nominator(acc, STAKE + rr(10, 50), votes); + }); + + b.iter(|| { + let _ = phragmen::elect::( + count, + 1_usize, + >::enumerate(), + >::enumerate(), + Staking::slashable_balance_of + ); + }) + }) +} + +macro_rules! phragmen_benches { + ($($name:ident: $tup:expr,)*) => { + $( + #[bench] + fn $name(b: &mut Bencher) { + let (v, n, t, e) = $tup; + println!(""); + println!( + "++ Benchmark: {} Validators // {} Nominators // {} Edges-per-nominator // {} total edges // electing {}", + v, n, e, e * n, t + ); + do_phragmen(b, v, n, t, e); + } + )* + } +} + +phragmen_benches! { + bench_1_1: (VALIDATORS, NOMINATORS, TO_ELECT, EDGES), + bench_1_2: (VALIDATORS*2, NOMINATORS, TO_ELECT, EDGES), + bench_1_3: (VALIDATORS*4, NOMINATORS, TO_ELECT, EDGES), + bench_1_4: (VALIDATORS*8, NOMINATORS, TO_ELECT, EDGES), + + bench_0_1: (VALIDATORS, NOMINATORS, TO_ELECT, EDGES), + bench_0_2: (VALIDATORS, NOMINATORS, TO_ELECT * 4, EDGES), + bench_0_3: (VALIDATORS, NOMINATORS, TO_ELECT * 8, EDGES), + bench_0_4: (VALIDATORS, NOMINATORS, TO_ELECT * 16, EDGES), + + bench_2_1: (VALIDATORS, NOMINATORS, TO_ELECT, EDGES), + bench_2_2: (VALIDATORS, NOMINATORS*2, TO_ELECT, EDGES), + bench_2_3: (VALIDATORS, NOMINATORS*4, TO_ELECT, EDGES), + bench_2_4: (VALIDATORS, NOMINATORS*8, TO_ELECT, EDGES), + + bench_3_1: (VALIDATORS, NOMINATORS, TO_ELECT, EDGES), + bench_3_2: (VALIDATORS, NOMINATORS, TO_ELECT, EDGES*2), + bench_3_3: (VALIDATORS, NOMINATORS, TO_ELECT, EDGES*4), + bench_3_4: (VALIDATORS, NOMINATORS, TO_ELECT, EDGES*8), +} diff --git a/substrate/srml/staking/src/lib.rs b/substrate/srml/staking/src/lib.rs index f84bf0a9ce..3f6905f654 100644 --- a/substrate/srml/staking/src/lib.rs +++ b/substrate/srml/staking/src/lib.rs @@ -240,16 +240,31 @@ //! stored in the Session module's `Validators` at the end of each era. #![cfg_attr(not(feature = "std"), no_std)] +#![cfg_attr(all(feature = "bench", test), feature(test))] + +#[cfg(all(feature = "bench", test))] +extern crate test; + +#[cfg(any(feature = "bench", test))] +mod mock; + +#[cfg(test)] +mod tests; + +mod phragmen; + +#[cfg(all(feature = "bench", test))] +mod benches; #[cfg(feature = "std")] use runtime_io::with_storage; use rstd::{prelude::*, result, collections::btree_map::BTreeMap}; use parity_codec::{HasCompact, Encode, Decode}; -use srml_support::{StorageValue, StorageMap, EnumerableStorageMap, dispatch::Result}; -use srml_support::{decl_module, decl_event, decl_storage, ensure}; -use srml_support::traits::{ - Currency, OnFreeBalanceZero, OnDilution, LockIdentifier, LockableCurrency, WithdrawReasons, - OnUnbalanced, Imbalance, +use srml_support::{ StorageValue, StorageMap, EnumerableStorageMap, dispatch::Result, + decl_module, decl_event, decl_storage, ensure, + traits::{Currency, OnFreeBalanceZero, OnDilution, LockIdentifier, LockableCurrency, + WithdrawReasons, OnUnbalanced, Imbalance + } }; use session::OnSessionChange; use primitives::Perbill; @@ -261,10 +276,6 @@ use primitives::traits::{ use primitives::{Serialize, Deserialize}; use system::ensure_signed; -mod mock; -mod tests; -mod phragmen; - use phragmen::{elect, ACCURACY, ExtendedBalance}; const RECENT_OFFLINE_COUNT: usize = 32; diff --git a/substrate/srml/staking/src/mock.rs b/substrate/srml/staking/src/mock.rs index 88b401b19e..5b3b22dda5 100644 --- a/substrate/srml/staking/src/mock.rs +++ b/substrate/srml/staking/src/mock.rs @@ -16,14 +16,12 @@ //! Test utilities -#![cfg(test)] - use primitives::{traits::{IdentityLookup, Convert}, BuildStorage, Perbill}; use primitives::testing::{Digest, DigestItem, Header, UintAuthorityId, ConvertUintAuthorityId}; use substrate_primitives::{H256, Blake2Hasher}; use runtime_io; -use srml_support::impl_outer_origin; -use crate::{GenesisConfig, Module, Trait, StakerStatus}; +use srml_support::{impl_outer_origin, assert_ok, traits::Currency}; +use crate::{GenesisConfig, Module, Trait, StakerStatus, ValidatorPrefs, RewardDestination}; /// The AccountId alias in this test module. pub type AccountIdType = u64; @@ -241,3 +239,33 @@ pub type Balances = balances::Module; pub type Session = session::Module; pub type Timestamp = timestamp::Module; pub type Staking = Module; + +pub fn check_exposure(acc: u64) { + let expo = Staking::stakers(&acc); + assert_eq!(expo.total as u128, expo.own as u128 + expo.others.iter().map(|e| e.value as u128).sum::()); +} + +pub fn check_exposure_all() { + Staking::current_elected().into_iter().for_each(|acc| check_exposure(acc)); +} + +pub fn assert_total_expo(acc: u64, val: u64) { + let expo = Staking::stakers(&acc); + assert_eq!(expo.total, val); +} + +pub fn bond_validator(acc: u64, val: u64) { + // a = controller + // a + 1 = stash + let _ = Balances::make_free_balance_be(&(acc+1), val); + assert_ok!(Staking::bond(Origin::signed(acc+1), acc, val, RewardDestination::Controller)); + assert_ok!(Staking::validate(Origin::signed(acc), ValidatorPrefs::default())); +} + +pub fn bond_nominator(acc: u64, val: u64, target: Vec) { + // a = controller + // a + 1 = stash + let _ = Balances::make_free_balance_be(&(acc+1), val); + assert_ok!(Staking::bond(Origin::signed(acc+1), acc, val, RewardDestination::Controller)); + assert_ok!(Staking::nominate(Origin::signed(acc), target)); +} \ No newline at end of file diff --git a/substrate/srml/staking/src/phragmen.rs b/substrate/srml/staking/src/phragmen.rs index 888e38a7ec..ba79de4869 100644 --- a/substrate/srml/staking/src/phragmen.rs +++ b/substrate/srml/staking/src/phragmen.rs @@ -107,29 +107,29 @@ pub fn elect( // Candidates who have 0 stake => have no votes or all null-votes. Kick them out not. let mut nominators: Vec> = Vec::with_capacity(validator_iter.size_hint().0 + nominator_iter.size_hint().0); let mut candidates = validator_iter.map(|(who, _)| { - let stash_balance = stash_of(&who); - (Candidate { who, ..Default::default() }, stash_balance) - }) - .filter_map(|(mut c, s)| { - c.approval_stake += to_votes(s); - if c.approval_stake.is_zero() { - None - } else { - Some((c, s)) - } - }) - .enumerate() - .map(|(idx, (c, s))| { - nominators.push(Nominator { - who: c.who.clone(), - edges: vec![ Edge { who: c.who.clone(), candidate_index: idx, ..Default::default() }], - budget: to_votes(s), - load: Fraction::zero(), - }); - c_idx_cache.insert(c.who.clone(), idx); - c - }) - .collect::>>(); + let stash_balance = stash_of(&who); + (Candidate { who, ..Default::default() }, stash_balance) + }) + .filter_map(|(mut c, s)| { + c.approval_stake += to_votes(s); + if c.approval_stake.is_zero() { + None + } else { + Some((c, s)) + } + }) + .enumerate() + .map(|(idx, (c, s))| { + nominators.push(Nominator { + who: c.who.clone(), + edges: vec![ Edge { who: c.who.clone(), candidate_index: idx, ..Default::default() }], + budget: to_votes(s), + load: Fraction::zero(), + }); + c_idx_cache.insert(c.who.clone(), idx); + c + }) + .collect::>>(); // 2- Collect the nominators with the associated votes. // Also collect approval stake along the way. diff --git a/substrate/srml/staking/src/tests.rs b/substrate/srml/staking/src/tests.rs index fecd38bb01..eba6ce77a5 100644 --- a/substrate/srml/staking/src/tests.rs +++ b/substrate/srml/staking/src/tests.rs @@ -16,50 +16,13 @@ //! Tests for the module. -#![cfg(test)] - use super::*; use runtime_io::with_externalities; use phragmen; use srml_support::{assert_ok, assert_noop, assert_eq_uvec, EnumerableStorageMap}; -use mock::{Balances, Session, Staking, System, Timestamp, Test, ExtBuilder, Origin}; +use mock::*; use srml_support::traits::{Currency, ReservableCurrency}; -#[inline] -fn check_exposure(acc: u64) { - let expo = Staking::stakers(&acc); - assert_eq!(expo.total as u128, expo.own as u128 + expo.others.iter().map(|e| e.value as u128).sum::()); -} - -#[inline] -fn check_exposure_all() { - Staking::current_elected().into_iter().for_each(|acc| check_exposure(acc)); -} - -#[inline] -fn assert_total_expo(acc: u64, val: u64) { - let expo = Staking::stakers(&acc); - assert_eq!(expo.total, val); -} - -#[inline] -fn bond_validator(acc: u64, val: u64) { - // a = controller - // a + 1 = stash - let _ = Balances::make_free_balance_be(&(acc+1), val); - assert_ok!(Staking::bond(Origin::signed(acc+1), acc, val, RewardDestination::Controller)); - assert_ok!(Staking::validate(Origin::signed(acc), ValidatorPrefs::default())); -} - -#[inline] -fn bond_nominator(acc: u64, val: u64, target: Vec) { - // a = controller - // a + 1 = stash - let _ = Balances::make_free_balance_be(&(acc+1), val); - assert_ok!(Staking::bond(Origin::signed(acc+1), acc, val, RewardDestination::Controller)); - assert_ok!(Staking::nominate(Origin::signed(acc), target)); -} - #[test] fn basic_setup_works() { // Verifies initial conditions of mock