Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
47 changes: 45 additions & 2 deletions vote/benches/vote_account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ use {
rand::Rng,
solana_account::AccountSharedData,
solana_pubkey::Pubkey,
solana_vote::vote_account::VoteAccount,
solana_vote::vote_account::{VoteAccount, VoteAccounts},
solana_vote_interface::state::{VoteInit, VoteStateV4, VoteStateVersions},
std::{collections::HashMap, sync::Arc},
};

fn new_rand_vote_account<R: Rng>(
Expand Down Expand Up @@ -56,5 +57,47 @@ fn bench_vote_account_try_from(b: &mut Bencher) {
});
}

benchmark_group!(benches, bench_vote_account_try_from);
fn bench_staked_nodes_compute(b: &mut Bencher) {
let mut rng = rand::thread_rng();
let mut vote_accounts_map = HashMap::new();

// Create realistic scenario with 100 nodes, each with 3-5 vote accounts
// This simulates stake aggregation across multiple vote accounts per node
let node_count = 100;

for _ in 0..node_count {
let node_pubkey = Pubkey::new_unique();
let vote_accounts_per_node = rng.gen_range(3..=5);

for _ in 0..vote_accounts_per_node {
let (account, _) = new_rand_vote_account(&mut rng, Some(node_pubkey));
let vote_account = VoteAccount::try_from(account).unwrap();
let stake: u64 = rng.gen_range(1_000_000..100_000_000);
vote_accounts_map.insert(Pubkey::new_unique(), (stake, vote_account));
}
}

// Add some zero-stake accounts to test filtering
for _ in 0..50 {
let (account, _) = new_rand_vote_account(&mut rng, None);
let vote_account = VoteAccount::try_from(account).unwrap();
vote_accounts_map.insert(Pubkey::new_unique(), (0, vote_account));
}

let vote_accounts_map = Arc::new(vote_accounts_map);

// Benchmark measures only the staked_nodes() computation
// Create new VoteAccounts each iteration to bypass OnceLock cache
b.iter(|| {
let vote_accounts = VoteAccounts::from(Arc::clone(&vote_accounts_map));
let staked_nodes = vote_accounts.staked_nodes();
assert!(!staked_nodes.is_empty());
});
}

benchmark_group!(
benches,
bench_vote_account_try_from,
bench_staked_nodes_compute
);
benchmark_main!(benches);
25 changes: 14 additions & 11 deletions vote/src/vote_account.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use {
crate::vote_state_view::VoteStateView,
itertools::Itertools,
serde::{
de::{MapAccess, Visitor},
ser::Serializer,
Expand Down Expand Up @@ -140,16 +139,20 @@ impl VoteAccounts {
pub fn staked_nodes(&self) -> Arc<HashMap</*node_pubkey:*/ Pubkey, /*stake:*/ u64>> {
self.staked_nodes
.get_or_init(|| {
Arc::new(
self.vote_accounts
.values()
.filter(|(stake, _)| *stake != 0u64)
.map(|(stake, vote_account)| (*vote_account.node_pubkey(), stake))
.into_grouping_map()
.aggregate(|acc, _node_pubkey, stake| {
Some(acc.unwrap_or_default() + stake)
}),
)
// Pre-allocate HashMap with estimated capacity to reduce reallocations
let mut staked_nodes =
HashMap::with_capacity(self.vote_accounts.len().saturating_div(2));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given that this is a hash map of validators, I think we could use PubkeyHasherBuilder instead of the default hasher:

Suggested change
HashMap::with_capacity(self.vote_accounts.len().saturating_div(2));
HashMap::with_capacity_and_hasher(self.vote_accounts.len().saturating_div(2), PubkeyHasherBuilder::default());

That should speed it up even more. 🙂 You'll need to add the PubkeyHasherBuilder generic to the return type in the function signature.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PubkeyHasherBuilder is behind "rand" feature. vote package doesn't enable rand feature.
What's your experience on enable rand feature? Would it be a more broader change and require more testing to ensure no regression?
How about doing it as a separate optimization in a follow-up pr?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, we can do it separately.

Regarding adding the rand feature - in this PR #7307 I added the rand feature to solana-pubkey in solana-accounts-db. The only randomness that comes with it is randomization of 8 byte subslice of the pubkey to be used as a hash map key:

https://github.com/anza-xyz/solana-sdk/blob/a7e12b1d4af8fba2a43d447af31aebdc3dbe8a1d/address/src/lib.rs#L13-L14
https://github.com/anza-xyz/solana-sdk/blob/a7e12b1d4af8fba2a43d447af31aebdc3dbe8a1d/address/src/hasher.rs#L57-L81

There is no other module being pulled by this feature. So I don't think there should be any unexpected impact on the validator.


for (stake, vote_account) in self.vote_accounts.values() {
if *stake != 0 {
staked_nodes
.entry(*vote_account.node_pubkey())
.and_modify(|total_stake| *total_stake += stake)
.or_insert(*stake);
}
}

Arc::new(staked_nodes)
})
.clone()
}
Expand Down
Loading