Skip to content

Commit baa1a55

Browse files
committed
runtime: Collect stake delegations only once during epoch activation
Processing new epoch (`Bank::process_new_epoch`) involves collecting stake delegations twice: 1) In `Bank::compute_new_epoch_caches_and_rewards`, to create a stake history entry and refresh vote accounts. 2) In `Bank::get_epoch_reward_calculate_param_info`, which is then used in `Bank::calculate_stake_vote_rewards` to calculate rewards for stakers and voters. The overall time of crossing the epoch boundary is ~519ms: ``` update_epoch_us=519953i ``` Where the two heaviest operations are `collect()`` calls on stake delegations, each of them taking ~200-220ms. Reduce that to just one collect by passing the vector 1) with freshly computed stake history and vote accounts to `Bank::begin_partitioned_rewards`. This way, we can avoid calling `Bank::get_epoch_reward_calculate_param_info`. The new time of crossing the epoch boundary is ~337ms: ``` update_epoch_us=337371i ``` Making that change possible required several refactors: * Tale `&PointValue` in `Bank::create_epoch_rewards_sysvar`. That makes it easier to operate on references of `PartitionedRewardsCalculation`. Copying integers from `PointValue` is cheap and has no visible performance impact. * Split `Stakes::activate_epoch`, that was performing calculations and mutating the cache at the same time. The calculations got split to `Stakes::calculate_activated_stake` that takes `&self`. * Add `Stakes::stake_delegations_ves` method. Stake delegations are stored as hash array mapped trie (HAMT)[0], which means that inserts, deletions and lookups are average-case O(1) and worst-case O(log n). However, the performance of iterations is poor due to depth-first traversal and jumps. Currently it's also impossible to iterate over it with rayon. That issue is known and handled by converting the HAMT to a vector with `stakes.stake_delegations.iter().collect()`. Move that trick to a dedicated method that describes the performance consequences. * Add `FilteredStakeDelegation` wrapper type, that wraps a vector of stake delegations and acts as a lazy iterator that filters out ones with insufficient stake. * Split the code dealing with rewards calculation and vote rewards distribution into separate methods: * `Bank::calculate_rewards` that takes `&self` and does not acquire any locks. * `Bank::begin_partitioned_rewards` that takes `&mut self`, sets calculation status and creates a sysvar. * `Bank::distribute_vote_rewards` that stores partitioned rewards and increases capitalization. [0] https://en.wikipedia.org/wiki/Hash_array_mapped_trie
1 parent 222485e commit baa1a55

9 files changed

Lines changed: 396 additions & 183 deletions

File tree

runtime/src/bank.rs

Lines changed: 76 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ use {
4747
rent_collector::RentCollector,
4848
runtime_config::RuntimeConfig,
4949
stake_account::StakeAccount,
50-
stake_utils,
50+
stake_history::StakeHistory as CowStakeHistory,
5151
stake_weighted_timestamp::{
5252
calculate_stake_weighted_timestamp, MaxAllowableDrift,
5353
MAX_ALLOWABLE_DRIFT_PERCENTAGE_FAST, MAX_ALLOWABLE_DRIFT_PERCENTAGE_SLOW_V2,
@@ -112,7 +112,6 @@ use {
112112
solana_lattice_hash::lt_hash::LtHash,
113113
solana_measure::{measure::Measure, measure_time, measure_us},
114114
solana_message::{inner_instruction::InnerInstructions, AccountKeys, SanitizedMessage},
115-
solana_native_token::LAMPORTS_PER_SOL,
116115
solana_packet::PACKET_DATA_SIZE,
117116
solana_precompile_error::PrecompileError,
118117
solana_program_runtime::{
@@ -167,7 +166,7 @@ use {
167166
transaction_accounts::KeyedAccountSharedData, TransactionReturnData,
168167
},
169168
solana_transaction_error::{TransactionError, TransactionResult as Result},
170-
solana_vote::vote_account::{VoteAccount, VoteAccountsHashMap},
169+
solana_vote::vote_account::{VoteAccount, VoteAccounts, VoteAccountsHashMap},
171170
std::{
172171
collections::{HashMap, HashSet},
173172
fmt,
@@ -1050,6 +1049,14 @@ impl AtomicBankHashStats {
10501049
}
10511050
}
10521051

1052+
struct NewEpochBundle {
1053+
stake_history: CowStakeHistory,
1054+
vote_accounts: VoteAccounts,
1055+
rewards_calculation: Arc<PartitionedRewardsCalculation>,
1056+
calculate_activated_stake_time_us: u64,
1057+
update_rewards_with_thread_pool_time_us: u64,
1058+
}
1059+
10531060
impl Bank {
10541061
fn default_with_accounts(accounts: Accounts) -> Self {
10551062
let mut bank = Self {
@@ -1591,6 +1598,47 @@ impl Bank {
15911598
.new_warmup_cooldown_rate_epoch(&self.epoch_schedule)
15921599
}
15931600

1601+
/// Returns updated stake history and vote accounts that includes new
1602+
/// activated stake from the last epoch.
1603+
fn compute_new_epoch_caches_and_rewards(
1604+
&self,
1605+
thread_pool: &ThreadPool,
1606+
parent_epoch: Epoch,
1607+
reward_calc_tracer: Option<impl RewardCalcTracer>,
1608+
rewards_metrics: &mut RewardsMetrics,
1609+
) -> NewEpochBundle {
1610+
// Add new entry to stakes.stake_history, set appropriate epoch and
1611+
// update vote accounts with warmed up stakes before saving a
1612+
// snapshot of stakes in epoch stakes
1613+
let stakes = self.stakes_cache.stakes();
1614+
let stake_delegations = stakes.stake_delegations_vec();
1615+
let ((stake_history, vote_accounts), calculate_activated_stake_time_us) =
1616+
measure_us!(stakes.calculate_activated_stake(
1617+
self.epoch(),
1618+
thread_pool,
1619+
self.new_warmup_cooldown_rate_epoch(),
1620+
&stake_delegations
1621+
));
1622+
// Apply stake rewards and commission using new snapshots.
1623+
let (rewards_calculation, update_rewards_with_thread_pool_time_us) = measure_us!(self
1624+
.calculate_rewards(
1625+
&stake_history,
1626+
stake_delegations,
1627+
&vote_accounts,
1628+
parent_epoch,
1629+
reward_calc_tracer,
1630+
thread_pool,
1631+
rewards_metrics,
1632+
));
1633+
NewEpochBundle {
1634+
stake_history,
1635+
vote_accounts,
1636+
rewards_calculation,
1637+
calculate_activated_stake_time_us,
1638+
update_rewards_with_thread_pool_time_us,
1639+
}
1640+
}
1641+
15941642
/// process for the start of a new epoch
15951643
fn process_new_epoch(
15961644
&mut self,
@@ -1610,31 +1658,37 @@ impl Bank {
16101658
thread_pool.install(|| { self.compute_and_apply_new_feature_activations() })
16111659
);
16121660

1613-
// Add new entry to stakes.stake_history, set appropriate epoch and
1614-
// update vote accounts with warmed up stakes before saving a
1615-
// snapshot of stakes in epoch stakes
1616-
let (_, activate_epoch_time_us) = measure_us!(self.stakes_cache.activate_epoch(
1617-
epoch,
1661+
let mut rewards_metrics = RewardsMetrics::default();
1662+
let NewEpochBundle {
1663+
stake_history,
1664+
vote_accounts,
1665+
rewards_calculation,
1666+
calculate_activated_stake_time_us,
1667+
update_rewards_with_thread_pool_time_us,
1668+
} = self.compute_new_epoch_caches_and_rewards(
16181669
&thread_pool,
1619-
self.new_warmup_cooldown_rate_epoch()
1620-
));
1670+
parent_epoch,
1671+
reward_calc_tracer,
1672+
&mut rewards_metrics,
1673+
);
1674+
1675+
self.stakes_cache
1676+
.activate_epoch(epoch, stake_history, vote_accounts);
16211677

16221678
// Save a snapshot of stakes for use in consensus and stake weighted networking
16231679
let leader_schedule_epoch = self.epoch_schedule.get_leader_schedule_epoch(slot);
16241680
let (_, update_epoch_stakes_time_us) =
16251681
measure_us!(self.update_epoch_stakes(leader_schedule_epoch));
16261682

1627-
let mut rewards_metrics = RewardsMetrics::default();
1628-
// After saving a snapshot of stakes, apply stake rewards and commission
1629-
let (_, update_rewards_with_thread_pool_time_us) = measure_us!(self
1630-
.begin_partitioned_rewards(
1631-
reward_calc_tracer,
1632-
&thread_pool,
1633-
parent_epoch,
1634-
parent_slot,
1635-
parent_height,
1636-
&mut rewards_metrics,
1637-
));
1683+
// Distribute rewards commission to vote accounts and cache stake rewards
1684+
// for partitioned distribution in the upcoming slots.
1685+
self.begin_partitioned_rewards(
1686+
parent_slot,
1687+
parent_height,
1688+
parent_epoch,
1689+
&rewards_calculation,
1690+
&rewards_metrics,
1691+
);
16381692

16391693
report_new_epoch_metrics(
16401694
epoch,
@@ -1643,7 +1697,7 @@ impl Bank {
16431697
NewEpochTimings {
16441698
thread_pool_time_us,
16451699
apply_feature_activations_time_us,
1646-
activate_epoch_time_us,
1700+
calculate_activated_stake_time_us,
16471701
update_epoch_stakes_time_us,
16481702
update_rewards_with_thread_pool_time_us,
16491703
},
@@ -2301,41 +2355,6 @@ impl Bank {
23012355
}
23022356
}
23032357

2304-
fn filter_stake_delegations<'a>(
2305-
&self,
2306-
stakes: &'a Stakes<StakeAccount<Delegation>>,
2307-
) -> Vec<(&'a Pubkey, &'a StakeAccount<Delegation>)> {
2308-
if self
2309-
.feature_set
2310-
.is_active(&feature_set::stake_minimum_delegation_for_rewards::id())
2311-
{
2312-
let num_stake_delegations = stakes.stake_delegations().len();
2313-
let min_stake_delegation = stake_utils::get_minimum_delegation(
2314-
self.feature_set
2315-
.is_active(&agave_feature_set::stake_raise_minimum_delegation_to_1_sol::id()),
2316-
)
2317-
.max(LAMPORTS_PER_SOL);
2318-
2319-
let (stake_delegations, filter_time_us) = measure_us!(stakes
2320-
.stake_delegations()
2321-
.iter()
2322-
.filter(|(_stake_pubkey, cached_stake_account)| {
2323-
cached_stake_account.delegation().stake >= min_stake_delegation
2324-
})
2325-
.collect::<Vec<_>>());
2326-
2327-
datapoint_info!(
2328-
"stake_account_filter_time",
2329-
("filter_time_us", filter_time_us, i64),
2330-
("num_stake_delegations_before", num_stake_delegations, i64),
2331-
("num_stake_delegations_after", stake_delegations.len(), i64)
2332-
);
2333-
stake_delegations
2334-
} else {
2335-
stakes.stake_delegations().iter().collect()
2336-
}
2337-
}
2338-
23392358
/// Convert computed VoteRewards to VoteRewardsAccounts for storing.
23402359
///
23412360
/// This function processes vote rewards and consolidates them into a single

runtime/src/bank/metrics.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use {
1111
pub(crate) struct NewEpochTimings {
1212
pub(crate) thread_pool_time_us: u64,
1313
pub(crate) apply_feature_activations_time_us: u64,
14-
pub(crate) activate_epoch_time_us: u64,
14+
pub(crate) calculate_activated_stake_time_us: u64,
1515
pub(crate) update_epoch_stakes_time_us: u64,
1616
pub(crate) update_rewards_with_thread_pool_time_us: u64,
1717
}
@@ -63,7 +63,11 @@ pub(crate) fn report_new_epoch_metrics(
6363
timings.apply_feature_activations_time_us,
6464
i64
6565
),
66-
("activate_epoch_us", timings.activate_epoch_time_us, i64),
66+
(
67+
"calculate_activated_stake_us",
68+
timings.calculate_activated_stake_time_us,
69+
i64
70+
),
6771
(
6872
"update_epoch_stakes_us",
6973
timings.update_epoch_stakes_time_us,

0 commit comments

Comments
 (0)