Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ toml_edit = "0.22"
derive-syn-parse = "0.2"
Inflector = "0.11"
cfg-expr = "0.15"
itertools = "0.10"
itertools = { version = "0.10", default-features = false }
macro_magic = { version = "0.5", default-features = false }
frame-support-procedural-tools = { version = "10.0.0", default-features = false }
proc-macro-warning = { version = "1", default-features = false }
Expand Down
70 changes: 68 additions & 2 deletions common/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ pub trait SetLike<T> {
}
}

/// Poll provider seen from the voting pallet's side. Carries the
/// read-only queries plus the tally-update notification fired when a
/// vote moves the tally.
pub trait Polls<AccountId> {
type Index: Parameter + Copy;
type VotingScheme: PartialEq;
Expand All @@ -17,20 +20,83 @@ pub trait Polls<AccountId> {
fn is_ongoing(index: Self::Index) -> bool;
fn voting_scheme_of(index: Self::Index) -> Option<Self::VotingScheme>;
fn voter_set_of(index: Self::Index) -> Option<Self::VoterSet>;

fn on_tally_updated(index: Self::Index, tally: &VoteTally);
/// Worst-case upper bound on `on_tally_updated`'s weight.
fn on_tally_updated_weight() -> Weight;
}

pub trait PollHooks<PollIndex> {
/// Notification fired when a poll is created.
pub trait OnPollCreated<PollIndex> {
fn on_poll_created(poll_index: PollIndex);
/// Returns the worst-case upper bound on `on_poll_created`'s weight.
fn weight() -> Weight;
}

/// Notification fired when a poll reaches a terminal status.
pub trait OnPollCompleted<PollIndex> {
fn on_poll_completed(poll_index: PollIndex);
/// Returns the worst-case upper bound on `on_poll_completed`'s weight.
fn weight() -> Weight;
}

#[impl_trait_for_tuples::impl_for_tuples(10)]
impl<I: Copy> PollHooks<I> for Tuple {
impl<I: Copy> OnPollCreated<I> for Tuple {
fn on_poll_created(poll_index: I) {
for_tuples!( #( Tuple::on_poll_created(poll_index); )* );
}

fn weight() -> Weight {
#[allow(clippy::let_and_return)]
let mut weight = Weight::zero();
for_tuples!( #( weight.saturating_accrue(Tuple::weight()); )* );
weight
}
}

#[impl_trait_for_tuples::impl_for_tuples(10)]
impl<I: Copy> OnPollCompleted<I> for Tuple {
fn on_poll_completed(poll_index: I) {
for_tuples!( #( Tuple::on_poll_completed(poll_index); )* );
}

fn weight() -> Weight {
#[allow(clippy::let_and_return)]
let mut weight = Weight::zero();
for_tuples!( #( weight.saturating_accrue(Tuple::weight()); )* );
weight
}
}

/// Handler for when the members of a collective have changed.
pub trait OnMembersChanged<CollectiveId, AccountId> {
/// A collective's members have changed, `incoming` members have joined and
/// `outgoing` members have left.
fn on_members_changed(
collective_id: CollectiveId,
incoming: &[AccountId],
outgoing: &[AccountId],
);
/// Worst-case upper bound on `on_members_changed`'s weight. The
/// implementation is responsible for bounding its own iteration over
/// `incoming`/`outgoing` against the relevant `MaxMembers` constant.
fn weight() -> Weight;
}

#[impl_trait_for_tuples::impl_for_tuples(10)]
impl<CollectiveId: Clone, AccountId> OnMembersChanged<CollectiveId, AccountId> for Tuple {
fn on_members_changed(
collective_id: CollectiveId,
incoming: &[AccountId],
outgoing: &[AccountId],
) {
for_tuples!( #( Tuple::on_members_changed(collective_id.clone(), incoming, outgoing); )* );
}

fn weight() -> Weight {
#[allow(clippy::let_and_return)]
let mut weight = Weight::zero();
for_tuples!( #( weight.saturating_accrue(Tuple::weight()); )* );
weight
}
}
8 changes: 7 additions & 1 deletion pallets/multi-collective/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { workspace = true, features = ["max-encoded-len"] }
scale-info = { workspace = true, features = ["derive"] }
frame-benchmarking = { workspace = true, optional = true }
frame-system = { workspace = true }
frame-support = { workspace = true }
impl-trait-for-tuples = { workspace = true }
num-traits = { workspace = true }
subtensor-runtime-common = { workspace = true }

[dev-dependencies]
sp-io = { workspace = true, default-features = true }
Expand All @@ -32,14 +34,18 @@ default = ["std"]
std = [
"codec/std",
"scale-info/std",
"frame-benchmarking?/std",
"frame-system/std",
"frame-support/std",
"num-traits/std",
"subtensor-runtime-common/std",
]
runtime-benchmarks = [
"frame-benchmarking/runtime-benchmarks",
"frame-support/runtime-benchmarks",
"frame-system/runtime-benchmarks",
"sp-runtime/runtime-benchmarks"
"sp-runtime/runtime-benchmarks",
"subtensor-runtime-common/runtime-benchmarks",
]
try-runtime = [
"frame-support/try-runtime",
Expand Down
126 changes: 126 additions & 0 deletions pallets/multi-collective/src/benchmarking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//! Benchmarks for `pallet-multi-collective`.
//!
//! Setup is parameterised through [`Config::BenchmarkHelper`]: the runtime
//! supplies a non-rotatable collective whose bounds allow the pallet to
//! fill and drain it freely, plus a separate rotatable collective for
//! `force_rotate`.
#![allow(clippy::unwrap_used, clippy::expect_used)]

use super::*;
use frame_benchmarking::v2::*;
use frame_system::RawOrigin;

/// Stable seed for `frame_benchmarking::account` so accounts generated
/// across benchmark setup steps round-trip the same value.
const SEED: u32 = 0;

/// Pre-fill a collective's `Members` storage with `count` distinct
/// accounts, returning them sorted by `AccountId` (the canonical storage
/// order).
fn fill_members<T: Config>(collective_id: T::CollectiveId, count: u32) -> Vec<T::AccountId> {
let mut members: Vec<T::AccountId> = (0..count)
.map(|i| account::<T::AccountId>("member", i, SEED))
.collect();
members.sort();

// Bypass `add_member` to avoid paying the per-call binary_search cost
// during setup: we know the list is sorted and unique, so we can
// write the storage directly.
let bounded =
BoundedVec::try_from(members.clone()).expect("benchmark fill must respect MaxMembers");
Members::<T>::insert(collective_id, bounded);
members
}

#[benchmarks]
mod benches {
use super::*;

/// Worst case: pre-fill to `MaxMembers - 1` so the binary_search
/// runs at full depth. The new account's insert position depends on
/// its `AccountId` hash — uniformly distributed but deterministic
/// across benchmark runs, and the per-element shift cost is
/// constant-bounded by `MaxMembers × sizeof::<AccountId>`.
#[benchmark]
fn add_member() {
let collective = T::BenchmarkHelper::collective();
let max = T::MaxMembers::get();
let _existing = fill_members::<T>(collective, max.saturating_sub(1));
let new_member = account::<T::AccountId>("new", 0, SEED);

#[extrinsic_call]
add_member(RawOrigin::Root, collective, new_member);

assert_eq!(Members::<T>::get(collective).len(), max as usize);
}

/// Worst case: full collective; binary_search at max depth, remove
/// shifts the maximum number of trailing elements.
#[benchmark]
fn remove_member() {
let collective = T::BenchmarkHelper::collective();
let max = T::MaxMembers::get();
let members = fill_members::<T>(collective, max);
// Remove the head: `remove(0)` shifts every other element.
let to_remove = members[0].clone();

#[extrinsic_call]
remove_member(RawOrigin::Root, collective, to_remove);

assert_eq!(
Members::<T>::get(collective).len(),
(max as usize).saturating_sub(1),
);
}

/// Worst case: full collective; two binary_searches at max depth,
/// then a remove + insert each shifting the maximum trailing slice.
#[benchmark]
fn swap_member() {
let collective = T::BenchmarkHelper::collective();
let max = T::MaxMembers::get();
let members = fill_members::<T>(collective, max);
let to_remove = members[0].clone();
// A fresh account, distinct from the existing set.
let to_add = account::<T::AccountId>("new", 0, SEED);

#[extrinsic_call]
swap_member(RawOrigin::Root, collective, to_remove, to_add);

assert_eq!(Members::<T>::get(collective).len(), max as usize);
}

/// Worst case: replace a fully-populated collective with a
/// completely disjoint set of `MaxMembers` new accounts. Sort, dedup,
/// and the linear merge all run at maximum length.
#[benchmark]
fn set_members() {
let collective = T::BenchmarkHelper::collective();
let max = T::MaxMembers::get();
let _existing = fill_members::<T>(collective, max);

let new_members: Vec<T::AccountId> = (0..max)
.map(|i| account::<T::AccountId>("new", i, SEED))
.collect();

#[extrinsic_call]
set_members(RawOrigin::Root, collective, new_members.clone());

assert_eq!(Members::<T>::get(collective).len(), max as usize);
}

/// `force_rotate` itself does only validation + a hook dispatch;
/// this benchmark measures just the extrinsic-side overhead. The
/// hook's worst-case cost is added separately via
/// `T::OnNewTerm::weight()` in the `#[pallet::weight(...)]`
/// annotation.
#[benchmark]
fn force_rotate() {
let collective = T::BenchmarkHelper::rotatable_collective();

#[extrinsic_call]
force_rotate(RawOrigin::Root, collective);
}

impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test);
}
Loading
Loading