From b356db9a762c2b4edd640b63ff9e8a1c688de966 Mon Sep 17 00:00:00 2001 From: Benjamin Gallois <business@gallois.cc> Date: Fri, 24 Nov 2023 16:18:36 +0100 Subject: [PATCH] Quota pallet benchmark (nodes/rust/duniter-v2s!190) * fix process_refund_queue weight * add weight limit tests * benchmark process_refund_queue overhead * add pallet-quota benchmarks --- pallets/quota/src/benchmarking.rs | 117 ++++++++++++++++-- pallets/quota/src/lib.rs | 28 +++-- pallets/quota/src/tests.rs | 131 ++++++++++++++++++++- pallets/quota/src/weights.rs | 16 ++- runtime/common/src/weights/pallet_quota.rs | 117 +++++++++++------- 5 files changed, 337 insertions(+), 72 deletions(-) diff --git a/pallets/quota/src/benchmarking.rs b/pallets/quota/src/benchmarking.rs index 587ff9cc5..4519eea02 100644 --- a/pallets/quota/src/benchmarking.rs +++ b/pallets/quota/src/benchmarking.rs @@ -18,47 +18,138 @@ use super::*; use frame_benchmarking::{account, benchmarks}; +use sp_runtime::traits::One; -// FIXME this is a naïve implementation of benchmarks: -// - without properly prepare data -// - without "verify" blocks -// - without thinking about worst case scenario -// - without writing complexity in the term of refund queue length -// It's there as a seed for benchmark implementation and to use WeightInfo where needed. +fn assert_has_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) { + frame_system::Pallet::<T>::assert_has_event(generic_event.into()); +} benchmarks! { where_clause { where IdtyId<T>: From<u32>, BalanceOf<T>: From<u64>, + T::AccountId: From<[u8; 32]>, } queue_refund { let account: T::AccountId = account("Alice", 1, 1); + let dummy_refund = Refund { + account: account.clone(), + identity: 0u32.into(), + amount: 20u64.into(), + }; let refund = Refund { account, identity: 1u32.into(), amount: 10u64.into(), }; - }: { Pallet::<T>::queue_refund(refund) } + // Complexity is bound to MAX_QUEUD_REFUNDS where an insertion is O(n-1) + for i in 0..MAX_QUEUED_REFUNDS-1 { + Pallet::<T>::queue_refund(dummy_refund.clone()) + } + }: { Pallet::<T>::queue_refund(refund.clone()) } + verify { + assert_eq!(RefundQueue::<T>::get().last(), Some(refund).as_ref()); + assert_eq!(RefundQueue::<T>::get().len() as u32, MAX_QUEUED_REFUNDS); + } spend_quota { - let idty_id = 1u32; - let amount = 1u64; + let idty_id: IdtyId<T> = 1u32.into(); + let amount = 2u64; + let quota_amount = 10u64; + IdtyQuota::<T>::insert( + idty_id, + Quota { + last_use: T::BlockNumber::zero(), + amount: quota_amount.into(), + }, + ); }: { Pallet::<T>::spend_quota(idty_id.into(), amount.into()) } + verify { + let quota_growth = sp_runtime::Perbill::from_rational( + T::BlockNumber::one(), + T::ReloadRate::get(), + ).mul_floor(T::MaxQuota::get()); + assert_eq!(IdtyQuota::<T>::get(idty_id).unwrap().amount, quota_growth +quota_amount.into() - amount.into()); + } try_refund { let account: T::AccountId = account("Alice", 1, 1); + let idty_id: IdtyId<T> = 1u32.into(); + IdtyQuota::<T>::insert( + idty_id, + Quota { + last_use: T::BlockNumber::zero(), + amount: 10u64.into(), + }, + ); + let _ = CurrencyOf::<T>:: make_free_balance_be( + &T::RefundAccount::get(),u32::MAX.into(), + ); + // The worst-case scenario is when the refund fails + // and can only be triggered if the account is dead, + // in this case by having no balance in the account. let refund = Refund { - account, + account: account.clone(), identity: 1u32.into(), amount: 10u64.into(), }; }: { Pallet::<T>::try_refund(refund) } + verify { + assert_has_event::<T>(Event::<T>::RefundFailed ( account ).into()); + } do_refund { let account: T::AccountId = account("Alice", 1, 1); + let _ = CurrencyOf::<T>:: make_free_balance_be( + &T::RefundAccount::get(),u32::MAX.into(), + ); + // The worst-case scenario is when the refund fails + // and can only be triggered if the account is dead, + // in this case by having no balance in the account. let refund = Refund { - account, + account: account.clone(), identity: 1u32.into(), amount: 10u64.into(), }; - let amount = 5u64.into(); - }: { Pallet::<T>::do_refund(refund, amount) } + }: { Pallet::<T>::try_refund(refund) } + verify { + assert_has_event::<T>(Event::<T>::RefundFailed ( account ).into()); + } + // The base weight consumed on processing refund queue when empty. + on_process_refund_queue { + assert_eq!(RefundQueue::<T>::get().len() as u32, 0); + }: { Pallet::<T>::process_refund_queue(Weight::MAX) } + // The weight consumed on processing refund queue with one element. + // Can deduce the process_refund_queue overhead by subtracting try_refund weight. + #[pov_mode = Measured] + on_process_refund_queue_elements { + let i in 1..MAX_QUEUED_REFUNDS; + let account: T::AccountId = account("Alice", 1, 1); + let idty_id: IdtyId<T> = 1u32.into(); + IdtyQuota::<T>::insert( + idty_id, + Quota { + last_use: T::BlockNumber::zero(), + amount: 10u64.into(), + }, + ); + let _ = CurrencyOf::<T>:: make_free_balance_be( + &T::RefundAccount::get(),u32::MAX.into(), + ); + // The worst-case scenario is when the refund fails + // and can only be triggered if the account is dead, + // in this case by having no balance in the account. + let refund = Refund { + account: account.clone(), + identity: 1u32.into(), + amount: 10u64.into(), + }; + for _ in 0..i { + Pallet::<T>::queue_refund(refund.clone()); + } + assert_eq!(RefundQueue::<T>::get().len() as u32, i); + }: { Pallet::<T>::process_refund_queue(Weight::MAX) } + verify { + assert_eq!(RefundQueue::<T>::get().len() as u32, 0); + assert_has_event::<T>(Event::<T>::RefundFailed ( account ).into()); + } + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(crate::mock::QuotaConfig{identities: vec![1, 2]}), crate::mock::Test); } diff --git a/pallets/quota/src/lib.rs b/pallets/quota/src/lib.rs index eff611f21..91df0459e 100644 --- a/pallets/quota/src/lib.rs +++ b/pallets/quota/src/lib.rs @@ -210,19 +210,31 @@ pub mod pallet { /// perform as many refunds as possible within the supplied weight limit pub fn process_refund_queue(weight_limit: Weight) -> Weight { RefundQueue::<T>::mutate(|queue| { + // The weight to process an empty queue + let mut total_weight = <T as pallet::Config>::WeightInfo::on_process_refund_queue(); + // The weight to process one element without the actual try_refund weight + let overhead = + <T as pallet::Config>::WeightInfo::on_process_refund_queue_elements(2) + .saturating_sub( + <T as pallet::Config>::WeightInfo::on_process_refund_queue_elements(1), + ) + .saturating_sub(<T as pallet::Config>::WeightInfo::try_refund()); + + // make sure that we have at least the time to handle one try_refund call if queue.is_empty() { - return Weight::zero(); + return total_weight; } - let mut total_weight = Weight::zero(); - // make sure that we have at least the time to handle one try_refund call - while total_weight.any_lt( - weight_limit.saturating_sub(<T as pallet::Config>::WeightInfo::try_refund()), - ) { + + while total_weight.any_lt(weight_limit.saturating_sub( + <T as pallet::Config>::WeightInfo::try_refund().saturating_add(overhead), + )) { let Some(queued_refund) = queue.pop() else { break; }; let consumed_weight = Self::try_refund(queued_refund); - total_weight = total_weight.saturating_add(consumed_weight); + total_weight = total_weight + .saturating_add(consumed_weight) + .saturating_add(overhead); } total_weight }) @@ -309,8 +321,6 @@ pub mod pallet { // process refund queue if space left on block fn on_idle(_block: T::BlockNumber, remaining_weight: Weight) -> Weight { Self::process_refund_queue(remaining_weight) - // opti: benchmark process_refund_queue overhead and substract this from weight limit - // .saturating_sub(T::WeightInfo::process_refund_queue()) } } } diff --git a/pallets/quota/src/tests.rs b/pallets/quota/src/tests.rs index 14fa890f7..81f65b8bf 100644 --- a/pallets/quota/src/tests.rs +++ b/pallets/quota/src/tests.rs @@ -238,4 +238,133 @@ fn test_not_enough_treasury() { }) } -// TODO implement a mock weight to test if refund queue processing actually stops when reached limit +/// test complete scenario with queue and process refund queue weight with available quotas +#[test] +fn test_process_refund_queue_weight_with_quotas() { + new_test_ext(QuotaConfig { + identities: vec![1, 2, 3], + }) + .execute_with(|| { + run_to_block(15); + // give enough currency to accounts and treasury and double check + Balances::make_free_balance_be(&account(1), 1000); + Balances::make_free_balance_be(&account(2), 1000); + Balances::make_free_balance_be(&account(3), 1000); + Balances::make_free_balance_be( + &<Test as pallet_quota::Config>::RefundAccount::get(), + 10_000, + ); + assert_eq!( + Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()), + 10_000 + ); + // fill in the refund queue + Quota::queue_refund(pallet_quota::Refund { + account: account(1), + identity: 10, + amount: 10, + }); + Quota::queue_refund(pallet_quota::Refund { + account: account(2), + identity: 2, + amount: 500, + }); + Quota::queue_refund(pallet_quota::Refund { + account: account(3), + identity: 3, + amount: 666, + }); + // process it with only no weight + Quota::process_refund_queue(Weight::from(0)); + // after processing, it should be of the same size + assert_eq!(pallet_quota::RefundQueue::<Test>::get().len(), 3); + // process it with only 200 allowed weight + Quota::process_refund_queue(Weight::from_parts(200u64, 0)); + // after processing, it should be of size 1 because total_weight += 25*2 by iteration and + // limit is total_weight < 200-100 so 2 elements can be processed + assert_eq!(pallet_quota::RefundQueue::<Test>::get().len(), 1); + // and we should observe the effects of refund + assert_eq!(Balances::free_balance(account(3)), 1666); // 1000 initial + 666 refunded + assert_eq!(Balances::free_balance(account(2)), 1500); // 1000 initial + 1500 refunded + assert_eq!(Balances::free_balance(account(1)), 1000); // only initial because no available weight to process + assert_eq!( + Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()), + // initial minus refunds + 10_000 - 666 - 500 + ); + // events + System::assert_has_event(RuntimeEvent::Quota(pallet_quota::Event::Refunded { + who: account(3), + identity: 3, + amount: 666, + })); + System::assert_has_event(RuntimeEvent::Quota(pallet_quota::Event::Refunded { + who: account(2), + identity: 2, + amount: 500, + })); + }) +} + +/// test complete scenario with queue and process refund queue weight with limited quotas +#[test] +fn test_process_refund_queue_weight_no_quotas() { + new_test_ext(QuotaConfig { + identities: vec![1, 2], + }) + .execute_with(|| { + run_to_block(15); + // give enough currency to accounts and treasury and double check + Balances::make_free_balance_be(&account(1), 1000); + Balances::make_free_balance_be(&account(2), 1000); + Balances::make_free_balance_be(&account(3), 1000); + Balances::make_free_balance_be( + &<Test as pallet_quota::Config>::RefundAccount::get(), + 10_000, + ); + assert_eq!( + Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()), + 10_000 + ); + // fill in the refund queue + Quota::queue_refund(pallet_quota::Refund { + account: account(1), + identity: 10, + amount: 10, + }); + Quota::queue_refund(pallet_quota::Refund { + account: account(2), + identity: 2, + amount: 500, + }); + Quota::queue_refund(pallet_quota::Refund { + account: account(3), + identity: 3, + amount: 666, + }); + // process it with only no weight + Quota::process_refund_queue(Weight::from(0)); + // after processing, it should be of the same size + assert_eq!(pallet_quota::RefundQueue::<Test>::get().len(), 3); + // process it with only 150 allowed weight + Quota::process_refund_queue(Weight::from_parts(150u64, 0)); + // after processing, it should be of size 2 because try_refund weight is 25 (first in the queue with no quota) then 25*2 for the 2 other elements + // limit is total_weight < 150-100 so 2 elements can be processed + assert_eq!(pallet_quota::RefundQueue::<Test>::get().len(), 1); + // and we should observe the effects of refund + assert_eq!(Balances::free_balance(account(3)), 1000); // 1000 initial only because no quota available + assert_eq!(Balances::free_balance(account(2)), 1500); // 1000 initial + 500 refunded + assert_eq!(Balances::free_balance(account(1)), 1000); // only initial because no available weight to process + assert_eq!( + Balances::free_balance(<Test as pallet_quota::Config>::RefundAccount::get()), + // initial minus refunds + 10_000 - 500 + ); + // events + System::assert_has_event(RuntimeEvent::Quota(pallet_quota::Event::Refunded { + who: account(2), + identity: 2, + amount: 500, + })); + }) +} diff --git a/pallets/quota/src/weights.rs b/pallets/quota/src/weights.rs index c6cadaf10..076daa4b1 100644 --- a/pallets/quota/src/weights.rs +++ b/pallets/quota/src/weights.rs @@ -7,19 +7,27 @@ pub trait WeightInfo { fn spend_quota() -> Weight; fn try_refund() -> Weight; fn do_refund() -> Weight; + fn on_process_refund_queue() -> Weight; + fn on_process_refund_queue_elements(_i: u32) -> Weight; } impl WeightInfo for () { fn queue_refund() -> Weight { - Weight::from_parts(999u64, 0) + Weight::from_parts(100u64, 0) } fn spend_quota() -> Weight { - Weight::from_parts(999u64, 0) + Weight::from_parts(25u64, 0) } fn try_refund() -> Weight { - Weight::from_parts(999u64, 0) + Weight::from_parts(100u64, 0) } fn do_refund() -> Weight { - Weight::from_parts(999u64, 0) + Weight::from_parts(25u64, 0) + } + fn on_process_refund_queue() -> Weight { + Weight::from_parts(1u64, 0) + } + fn on_process_refund_queue_elements(_i: u32) -> Weight { + Weight::from_parts(1u64, 0) } } diff --git a/runtime/common/src/weights/pallet_quota.rs b/runtime/common/src/weights/pallet_quota.rs index eed488194..398fed336 100644 --- a/runtime/common/src/weights/pallet_quota.rs +++ b/runtime/common/src/weights/pallet_quota.rs @@ -1,41 +1,28 @@ -// Copyright 2021-2022 Axiom-Team -// -// This file is part of Duniter-v2S. -// -// Duniter-v2S is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, version 3 of the License. -// -// Duniter-v2S 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 Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with Duniter-v2S. If not, see <https://www.gnu.org/licenses/>. //! Autogenerated weights for `pallet_quota` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-10-26, STEPS: `5`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2023-11-24, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `Albatros`, CPU: `Intel(R) Core(TM) i7-8565U CPU @ 1.80GHz` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("gdev-benchmark"), DB CACHE: 1024 +//! HOSTNAME: `bgallois-ms7d43`, CPU: `12th Gen Intel(R) Core(TM) i3-12100F` +//! EXECUTION: None, WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 1024 // Executed Command: -// ./target/debug/duniter +// ./target/release/duniter // benchmark // pallet -// --chain=gdev-benchmark -// --steps=5 -// --repeat=2 -// --pallet=pallet_quota -// --extrinsic=* -// --execution=wasm +// --chain +// dev // --wasm-execution=compiled -// --heap-pages=4096 -// --output=./ -// --header=./file_header.txt +// --pallet +// pallet-quota +// --extrinsic +// * +// --steps +// 50 +// --repeat +// 20 +// --output=runtime/common/src/weights/ #![cfg_attr(rustfmt, rustfmt_skip)] #![allow(unused_parens)] @@ -52,10 +39,10 @@ impl<T: frame_system::Config> pallet_quota::WeightInfo for WeightInfo<T> { /// Proof: Quota RefundQueue (max_values: Some(1), max_size: Some(11266), added: 11761, mode: MaxEncodedLen) fn queue_refund() -> Weight { // Proof Size summary in bytes: - // Measured: `42` + // Measured: `11288` // Estimated: `12751` - // Minimum execution time: 73_265_000 picoseconds. - Weight::from_parts(77_698_000, 0) + // Minimum execution time: 7_021_000 picoseconds. + Weight::from_parts(7_228_000, 0) .saturating_add(Weight::from_parts(0, 12751)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) @@ -64,32 +51,72 @@ impl<T: frame_system::Config> pallet_quota::WeightInfo for WeightInfo<T> { /// Proof: Quota IdtyQuota (max_values: None, max_size: Some(24), added: 2499, mode: MaxEncodedLen) fn spend_quota() -> Weight { // Proof Size summary in bytes: - // Measured: `137` + // Measured: `139` // Estimated: `3489` - // Minimum execution time: 147_746_000 picoseconds. - Weight::from_parts(165_850_000, 0) + // Minimum execution time: 3_635_000 picoseconds. + Weight::from_parts(3_768_000, 0) .saturating_add(Weight::from_parts(0, 3489)) .saturating_add(T::DbWeight::get().reads(1)) .saturating_add(T::DbWeight::get().writes(1)) } /// Storage: Quota IdtyQuota (r:1 w:1) /// Proof: Quota IdtyQuota (max_values: None, max_size: Some(24), added: 2499, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:0) + /// Proof: System Account (max_values: None, max_size: Some(126), added: 2601, mode: MaxEncodedLen) fn try_refund() -> Weight { // Proof Size summary in bytes: - // Measured: `137` - // Estimated: `3489` - // Minimum execution time: 367_239_000 picoseconds. - Weight::from_parts(392_186_000, 0) - .saturating_add(Weight::from_parts(0, 3489)) - .saturating_add(T::DbWeight::get().reads(1)) + // Measured: `139` + // Estimated: `3591` + // Minimum execution time: 11_415_000 picoseconds. + Weight::from_parts(11_717_000, 0) + .saturating_add(Weight::from_parts(0, 3591)) + .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) } + /// Storage: Quota IdtyQuota (r:1 w:1) + /// Proof: Quota IdtyQuota (max_values: None, max_size: Some(24), added: 2499, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:0) + /// Proof: System Account (max_values: None, max_size: Some(126), added: 2601, mode: MaxEncodedLen) fn do_refund() -> Weight { // Proof Size summary in bytes: - // Measured: `0` - // Estimated: `0` - // Minimum execution time: 356_707_000 picoseconds. - Weight::from_parts(471_930_000, 0) - .saturating_add(Weight::from_parts(0, 0)) + // Measured: `139` + // Estimated: `3591` + // Minimum execution time: 10_849_000 picoseconds. + Weight::from_parts(11_263_000, 0) + .saturating_add(Weight::from_parts(0, 3591)) + .saturating_add(T::DbWeight::get().reads(2)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: Quota RefundQueue (r:1 w:1) + /// Proof: Quota RefundQueue (max_values: Some(1), max_size: Some(11266), added: 11761, mode: MaxEncodedLen) + fn on_process_refund_queue() -> Weight { + // Proof Size summary in bytes: + // Measured: `43` + // Estimated: `12751` + // Minimum execution time: 1_530_000 picoseconds. + Weight::from_parts(1_646_000, 0) + .saturating_add(Weight::from_parts(0, 12751)) + .saturating_add(T::DbWeight::get().reads(1)) + .saturating_add(T::DbWeight::get().writes(1)) + } + /// Storage: Quota RefundQueue (r:1 w:1) + /// Proof: Quota RefundQueue (max_values: Some(1), max_size: Some(11266), added: 11761, mode: Measured) + /// Storage: Quota IdtyQuota (r:1 w:1) + /// Proof: Quota IdtyQuota (max_values: None, max_size: Some(24), added: 2499, mode: Measured) + /// Storage: System Account (r:1 w:0) + /// Proof: System Account (max_values: None, max_size: Some(126), added: 2601, mode: Measured) + /// The range of component `i` is `[1, 256]`. + fn on_process_refund_queue_elements(i: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `164 + i * (44 ±0)` + // Estimated: `3628 + i * (44 ±0)` + // Minimum execution time: 13_465_000 picoseconds. + Weight::from_parts(19_579_671, 0) + .saturating_add(Weight::from_parts(0, 3628)) + // Standard Error: 1_313 + .saturating_add(Weight::from_parts(465_028, 0).saturating_mul(i.into())) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(Weight::from_parts(0, 44).saturating_mul(i.into())) } } -- GitLab