// This file is part of Substrate. // Copyright (C) 2020-2021 Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // 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. //! Elections-Phragmen pallet benchmarking. #![cfg(feature = "runtime-benchmarks")] use super::*; use frame_benchmarking::{ account, benchmarks, impl_benchmark_test_suite, whitelist, BenchmarkError, BenchmarkResult, }; use frame_support::{ dispatch::{DispatchResultWithPostInfo, UnfilteredDispatchable}, traits::OnInitialize, }; use frame_system::RawOrigin; use crate::Pallet as Elections; const BALANCE_FACTOR: u32 = 250; const MAX_VOTERS: u32 = 500; const MAX_CANDIDATES: u32 = 200; type Lookup = <::Lookup as StaticLookup>::Source; /// grab new account with infinite balance. fn endowed_account(name: &'static str, index: u32) -> T::AccountId { let account: T::AccountId = account(name, index, 0); let amount = default_stake::(BALANCE_FACTOR); let _ = T::Currency::make_free_balance_be(&account, amount); // important to increase the total issuance since T::CurrencyToVote will need it to be sane for // phragmen to work. T::Currency::issue(amount); account } /// Account to lookup type of system trait. fn as_lookup(account: T::AccountId) -> Lookup { T::Lookup::unlookup(account) } /// Get a reasonable amount of stake based on the execution trait's configuration fn default_stake(factor: u32) -> BalanceOf { let factor = BalanceOf::::from(factor); T::Currency::minimum_balance() * factor } /// Get the current number of candidates. fn candidate_count() -> u32 { >::decode_len().unwrap_or(0usize) as u32 } /// Add `c` new candidates. fn submit_candidates( c: u32, prefix: &'static str, ) -> Result, &'static str> { (0..c) .map(|i| { let account = endowed_account::(prefix, i); >::submit_candidacy( RawOrigin::Signed(account.clone()).into(), candidate_count::(), ) .map_err(|_| "failed to submit candidacy")?; Ok(account) }) .collect::>() } /// Add `c` new candidates with self vote. fn submit_candidates_with_self_vote( c: u32, prefix: &'static str, ) -> Result, &'static str> { let candidates = submit_candidates::(c, prefix)?; let stake = default_stake::(BALANCE_FACTOR); let _ = candidates .iter() .map(|c| submit_voter::(c.clone(), vec![c.clone()], stake).map(|_| ())) .collect::>()?; Ok(candidates) } /// Submit one voter. fn submit_voter( caller: T::AccountId, votes: Vec, stake: BalanceOf, ) -> DispatchResultWithPostInfo { >::vote(RawOrigin::Signed(caller).into(), votes, stake) } /// create `num_voter` voters who randomly vote for at most `votes` of `all_candidates` if /// available. fn distribute_voters( mut all_candidates: Vec, num_voters: u32, votes: usize, ) -> Result<(), &'static str> { let stake = default_stake::(BALANCE_FACTOR); for i in 0..num_voters { // to ensure that votes are different all_candidates.rotate_left(1); let votes = all_candidates.iter().cloned().take(votes).collect::>(); let voter = endowed_account::("voter", i); submit_voter::(voter, votes, stake)?; } Ok(()) } /// Fill the seats of members and runners-up up until `m`. Note that this might include either only /// members, or members and runners-up. fn fill_seats_up_to(m: u32) -> Result, &'static str> { let _ = submit_candidates_with_self_vote::(m, "fill_seats_up_to")?; assert_eq!(>::candidates().len() as u32, m, "wrong number of candidates."); >::do_phragmen(); assert_eq!(>::candidates().len(), 0, "some candidates remaining."); assert_eq!( >::members().len() + >::runners_up().len(), m as usize, "wrong number of members and runners-up", ); Ok(>::members() .into_iter() .map(|m| m.who) .chain(>::runners_up().into_iter().map(|r| r.who)) .collect()) } /// removes all the storage items to reverse any genesis state. fn clean() { >::kill(); >::kill(); >::kill(); >::remove_all(None); } benchmarks! { // -- Signed ones vote_equal { let v in 1 .. (MAXIMUM_VOTE as u32); clean::(); // create a bunch of candidates. let all_candidates = submit_candidates::(v, "candidates")?; let caller = endowed_account::("caller", 0); let stake = default_stake::(BALANCE_FACTOR); // original votes. let mut votes = all_candidates; submit_voter::(caller.clone(), votes.clone(), stake)?; // new votes. votes.rotate_left(1); whitelist!(caller); }: vote(RawOrigin::Signed(caller), votes, stake) vote_more { let v in 2 .. (MAXIMUM_VOTE as u32); clean::(); // create a bunch of candidates. let all_candidates = submit_candidates::(v, "candidates")?; let caller = endowed_account::("caller", 0); let stake = default_stake::(BALANCE_FACTOR); // original votes. let mut votes = all_candidates.iter().skip(1).cloned().collect::>(); submit_voter::(caller.clone(), votes.clone(), stake / >::from(10u32))?; // new votes. votes = all_candidates; assert!(votes.len() > >::get(caller.clone()).votes.len()); whitelist!(caller); }: vote(RawOrigin::Signed(caller), votes, stake / >::from(10u32)) vote_less { let v in 2 .. (MAXIMUM_VOTE as u32); clean::(); // create a bunch of candidates. let all_candidates = submit_candidates::(v, "candidates")?; let caller = endowed_account::("caller", 0); let stake = default_stake::(BALANCE_FACTOR); // original votes. let mut votes = all_candidates; submit_voter::(caller.clone(), votes.clone(), stake)?; // new votes. votes = votes.into_iter().skip(1).collect::>(); assert!(votes.len() < >::get(caller.clone()).votes.len()); whitelist!(caller); }: vote(RawOrigin::Signed(caller), votes, stake) remove_voter { // we fix the number of voted candidates to max let v = MAXIMUM_VOTE as u32; clean::(); // create a bunch of candidates. let all_candidates = submit_candidates::(v, "candidates")?; let caller = endowed_account::("caller", 0); let stake = default_stake::(BALANCE_FACTOR); submit_voter::(caller.clone(), all_candidates, stake)?; whitelist!(caller); }: _(RawOrigin::Signed(caller)) submit_candidacy { // number of already existing candidates. let c in 1 .. MAX_CANDIDATES; // we fix the number of members to the number of desired members and runners-up. We'll be in // this state almost always. let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); clean::(); let stake = default_stake::(BALANCE_FACTOR); // create m members and runners combined. let _ = fill_seats_up_to::(m)?; // create previous candidates; let _ = submit_candidates::(c, "candidates")?; // we assume worse case that: extrinsic is successful and candidate is not duplicate. let candidate_account = endowed_account::("caller", 0); whitelist!(candidate_account); }: _(RawOrigin::Signed(candidate_account.clone()), candidate_count::()) verify { #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } renounce_candidacy_candidate { // this will check members, runners-up and candidate for removal. Members and runners-up are // limited by the runtime bound, nonetheless we fill them by `m`. // number of already existing candidates. let c in 1 .. MAX_CANDIDATES; // we fix the number of members to the number of desired members and runners-up. We'll be in // this state almost always. let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); clean::(); // create m members and runners combined. let _ = fill_seats_up_to::(m)?; let all_candidates = submit_candidates::(c, "caller")?; let bailing = all_candidates[0].clone(); // Should be ("caller", 0) let count = candidate_count::(); whitelist!(bailing); }: renounce_candidacy(RawOrigin::Signed(bailing), Renouncing::Candidate(count)) verify { #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } renounce_candidacy_members { // removing members and runners will be cheaper than a candidate. // we fix the number of members to when members and runners-up to the desired. We'll be in // this state almost always. let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); clean::(); // create m members and runners combined. let members_and_runners_up = fill_seats_up_to::(m)?; let bailing = members_and_runners_up[0].clone(); assert!(>::is_member(&bailing)); whitelist!(bailing); }: renounce_candidacy(RawOrigin::Signed(bailing.clone()), Renouncing::Member) verify { #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } renounce_candidacy_runners_up { // removing members and runners will be cheaper than a candidate. // we fix the number of members to when members and runners-up to the desired. We'll be in // this state almost always. let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); clean::(); // create m members and runners combined. let members_and_runners_up = fill_seats_up_to::(m)?; let bailing = members_and_runners_up[T::DesiredMembers::get() as usize + 1].clone(); assert!(>::is_runner_up(&bailing)); whitelist!(bailing); }: renounce_candidacy(RawOrigin::Signed(bailing.clone()), Renouncing::RunnerUp) verify { #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } // We use the max block weight for this extrinsic for now. See below. remove_member_without_replacement {}: { Err(BenchmarkError::Override( BenchmarkResult::from_weight(T::BlockWeights::get().max_block) ))?; } // -- Root ones #[extra] // this calls into phragmen and consumes a full block for now. remove_member_without_replacement_extra { // worse case is when we remove a member and we have no runner as a replacement. This // triggers phragmen again. The only parameter is how many candidates will compete for the // new slot. let c in 1 .. MAX_CANDIDATES; clean::(); // fill only desired members. no runners-up. let all_members = fill_seats_up_to::(T::DesiredMembers::get())?; assert_eq!(>::members().len() as u32, T::DesiredMembers::get()); // submit a new one to compensate, with self-vote. let replacements = submit_candidates_with_self_vote::(c, "new_candidate")?; // create some voters for these replacements. distribute_voters::(replacements, MAX_VOTERS, MAXIMUM_VOTE)?; let to_remove = as_lookup::(all_members[0].clone()); }: remove_member(RawOrigin::Root, to_remove, false) verify { // must still have the desired number of members members. assert_eq!(>::members().len() as u32, T::DesiredMembers::get()); #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } remove_member_with_replacement { // easy case. We have a runner up. Nothing will have that much of an impact. m will be // number of members and runners. There is always at least one runner. let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); clean::(); let _ = fill_seats_up_to::(m)?; let removing = as_lookup::(>::members_ids()[0].clone()); }: remove_member(RawOrigin::Root, removing, true) verify { // must still have enough members. assert_eq!(>::members().len() as u32, T::DesiredMembers::get()); #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } remove_member_wrong_refund { // The root call by mistake indicated that this will have no replacement, while it has! // this has now consumed a lot of weight and need to refund. let m = T::DesiredMembers::get() + T::DesiredRunnersUp::get(); clean::(); let _ = fill_seats_up_to::(m)?; let removing = as_lookup::(>::members_ids()[0].clone()); let who = T::Lookup::lookup(removing.clone()).expect("member was added above"); let call = Call::::remove_member { who: removing, has_replacement: false }.encode(); }: { assert_eq!( as Decode>::decode(&mut &*call) .expect("call is encoded above, encoding must be correct") .dispatch_bypass_filter(RawOrigin::Root.into()) .unwrap_err() .error, Error::::InvalidReplacement.into(), ); } verify { // must still have enough members. assert_eq!(>::members().len() as u32, T::DesiredMembers::get()); // on fail, `who` must still be a member assert!(>::members_ids().contains(&who)); #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } clean_defunct_voters { // total number of voters. let v in (MAX_VOTERS / 2) .. MAX_VOTERS; // those that are defunct and need removal. let d in 1 .. (MAX_VOTERS / 2); // remove any previous stuff. clean::(); let all_candidates = submit_candidates::(v, "candidates")?; distribute_voters::(all_candidates, v, MAXIMUM_VOTE)?; // all candidates leave. >::kill(); // now everyone is defunct assert!(>::iter().all(|(_, v)| >::is_defunct_voter(&v.votes))); assert_eq!(>::iter().count() as u32, v); let root = RawOrigin::Root; }: _(root, v, d) verify { assert_eq!(>::iter().count() as u32, 0); } election_phragmen { // This is just to focus on phragmen in the context of this module. We always select 20 // members, this is hard-coded in the runtime and cannot be trivially changed at this stage. // Yet, change the number of voters, candidates and edge per voter to see the impact. Note // that we give all candidates a self vote to make sure they are all considered. let c in 1 .. MAX_CANDIDATES; let v in 1 .. MAX_VOTERS; let e in MAX_VOTERS .. MAX_VOTERS * MAXIMUM_VOTE as u32; clean::(); // so we have a situation with v and e. we want e to basically always be in the range of `e // -> e * MAXIMUM_VOTE`, but we cannot express that now with the benchmarks. So what we do // is: when c is being iterated, v, and e are max and fine. when v is being iterated, e is // being set to max and this is a problem. In these cases, we cap e to a lower value, namely // v * MAXIMUM_VOTE. when e is being iterated, v is at max, and again fine. all in all, // votes_per_voter can never be more than MAXIMUM_VOTE. Note that this might cause `v` to be // an overestimate. let votes_per_voter = (e / v).min(MAXIMUM_VOTE as u32); let all_candidates = submit_candidates_with_self_vote::(c, "candidates")?; let _ = distribute_voters::(all_candidates, v, votes_per_voter as usize)?; }: { >::on_initialize(T::TermDuration::get()); } verify { assert_eq!(>::members().len() as u32, T::DesiredMembers::get().min(c)); assert_eq!( >::runners_up().len() as u32, T::DesiredRunnersUp::get().min(c.saturating_sub(T::DesiredMembers::get())), ); #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } #[extra] election_phragmen_c_e { let c in 1 .. MAX_CANDIDATES; let e in MAX_VOTERS .. MAX_VOTERS * MAXIMUM_VOTE as u32; let fixed_v = MAX_VOTERS; clean::(); let votes_per_voter = e / fixed_v; let all_candidates = submit_candidates_with_self_vote::(c, "candidates")?; let _ = distribute_voters::(all_candidates, fixed_v, votes_per_voter as usize)?; }: { >::on_initialize(T::TermDuration::get()); } verify { assert_eq!(>::members().len() as u32, T::DesiredMembers::get().min(c)); assert_eq!( >::runners_up().len() as u32, T::DesiredRunnersUp::get().min(c.saturating_sub(T::DesiredMembers::get())), ); #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } #[extra] election_phragmen_v { let v in 4 .. 16; let fixed_c = MAX_CANDIDATES; let fixed_e = 64; clean::(); let votes_per_voter = fixed_e / v; let all_candidates = submit_candidates_with_self_vote::(fixed_c, "candidates")?; let _ = distribute_voters::(all_candidates, v, votes_per_voter as usize)?; }: { >::on_initialize(T::TermDuration::get()); } verify { assert_eq!(>::members().len() as u32, T::DesiredMembers::get().min(fixed_c)); assert_eq!( >::runners_up().len() as u32, T::DesiredRunnersUp::get().min(fixed_c.saturating_sub(T::DesiredMembers::get())), ); #[cfg(test)] { // reset members in between benchmark tests. use crate::tests::MEMBERS; MEMBERS.with(|m| *m.borrow_mut() = vec![]); } } } impl_benchmark_test_suite!( Elections, crate::tests::ExtBuilder::default().desired_members(13).desired_runners_up(7), crate::tests::Test, exec_name = build_and_execute, );