Avoid RMWs on shared data inside parallel loops: collect_rent_from_accounts() (#25790)

This commit is contained in:
Brooks Prumo 2022-06-06 13:32:22 -05:00 committed by GitHub
parent 67a11ce4b1
commit ec64d5261f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 156 additions and 66 deletions

View File

@ -80,7 +80,7 @@ use {
iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}, iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator},
ThreadPool, ThreadPoolBuilder, ThreadPool, ThreadPoolBuilder,
}, },
solana_measure::measure::Measure, solana_measure::{measure, measure::Measure},
solana_metrics::{inc_new_counter_debug, inc_new_counter_info}, solana_metrics::{inc_new_counter_debug, inc_new_counter_info},
solana_program_runtime::{ solana_program_runtime::{
accounts_data_meter::MAX_ACCOUNTS_DATA_LEN, accounts_data_meter::MAX_ACCOUNTS_DATA_LEN,
@ -5240,31 +5240,39 @@ impl Bank {
} }
} }
/// Collect rent from `accounts`
///
/// This fn is called inside a parallel loop from `collect_rent_in_partition()`. Avoid adding
/// any code that causes contention on shared memory/data (i.e. do not update atomic metrics).
///
/// The return value is a struct of computed values that `collect_rent_in_partition()` will
/// reduce at the end of its parallel loop. If possible, place data/computation that cause
/// contention/take locks in the return struct and process them in
/// `collect_rent_from_partition()` after reducing the parallel loop.
fn collect_rent_from_accounts( fn collect_rent_from_accounts(
&self, &self,
mut accounts: Vec<(Pubkey, AccountSharedData, Slot)>, mut accounts: Vec<(Pubkey, AccountSharedData, Slot)>,
metrics: &RentMetrics,
just_rewrites: bool, just_rewrites: bool,
) { ) -> CollectRentFromAccountsInfo {
let mut rent_debits = RentDebits::default(); let mut rent_debits = RentDebits::default();
let mut total_collected = CollectedInfo::default(); let mut total_rent_collected_info = CollectedInfo::default();
let bank_slot = self.slot(); let bank_slot = self.slot();
let mut rewrites_skipped = Vec::with_capacity(accounts.len()); let mut rewrites_skipped = Vec::with_capacity(accounts.len());
let mut accounts_to_store = let mut accounts_to_store =
Vec::<(&Pubkey, &AccountSharedData)>::with_capacity(accounts.len()); Vec::<(&Pubkey, &AccountSharedData)>::with_capacity(accounts.len());
let mut collect_us = 0; let mut time_collecting_rent_us = 0;
let mut hash_skipped_rewrites_us = 0; let mut time_hashing_skipped_rewrites_us = 0;
let mut time_storing_accounts_us = 0;
let can_skip_rewrites = self.rc.accounts.accounts_db.skip_rewrites || just_rewrites; let can_skip_rewrites = self.rc.accounts.accounts_db.skip_rewrites || just_rewrites;
for (pubkey, account, loaded_slot) in accounts.iter_mut() { for (pubkey, account, loaded_slot) in accounts.iter_mut() {
let old_rent_epoch = account.rent_epoch(); let (rent_collected_info, measure) =
let mut time = Measure::start("collect"); measure!(self.rent_collector.collect_from_existing_account(
let collected = self.rent_collector.collect_from_existing_account( pubkey,
pubkey, account,
account, self.rc.accounts.accounts_db.filler_account_suffix.as_ref(),
self.rc.accounts.accounts_db.filler_account_suffix.as_ref(), ));
); time_collecting_rent_us += measure.as_us();
time.stop();
collect_us += time.as_us();
// only store accounts where we collected rent // only store accounts where we collected rent
// but get the hash for all these accounts even if collected rent is 0 (= not updated). // but get the hash for all these accounts even if collected rent is 0 (= not updated).
// Also, there's another subtle side-effect from this: this // Also, there's another subtle side-effect from this: this
@ -5273,48 +5281,45 @@ impl Bank {
if can_skip_rewrites if can_skip_rewrites
&& Self::skip_rewrite( && Self::skip_rewrite(
bank_slot, bank_slot,
collected.rent_amount, rent_collected_info.rent_amount,
*loaded_slot, *loaded_slot,
old_rent_epoch, account.rent_epoch(),
account, account,
) )
{ {
// this would have been rewritten previously. Now we skip it. // this would have been rewritten previously. Now we skip it.
// calculate the hash that we would have gotten if we did the rewrite. // calculate the hash that we would have gotten if we did the rewrite.
// This will be needed to calculate the bank's hash. // This will be needed to calculate the bank's hash.
let mut time = Measure::start("hash_account"); let (hash, measure) = measure!(crate::accounts_db::AccountsDb::hash_account(
let hash = self.slot(),
crate::accounts_db::AccountsDb::hash_account(self.slot(), account, pubkey); account,
time.stop(); pubkey
hash_skipped_rewrites_us += time.as_us(); ));
time_hashing_skipped_rewrites_us += measure.as_us();
rewrites_skipped.push((*pubkey, hash)); rewrites_skipped.push((*pubkey, hash));
assert_eq!(collected, CollectedInfo::default()); assert_eq!(rent_collected_info, CollectedInfo::default());
} else if !just_rewrites { } else if !just_rewrites {
total_collected += collected; total_rent_collected_info += rent_collected_info;
accounts_to_store.push((pubkey, account)); accounts_to_store.push((pubkey, account));
} }
rent_debits.insert(pubkey, collected.rent_amount, account.lamports()); rent_debits.insert(pubkey, rent_collected_info.rent_amount, account.lamports());
} }
metrics.collect_us.fetch_add(collect_us, Relaxed);
metrics.hash_us.fetch_add(hash_skipped_rewrites_us, Relaxed);
if !accounts_to_store.is_empty() { if !accounts_to_store.is_empty() {
let mut time = Measure::start("store_account"); // TODO: Maybe do not call `store_accounts()` here. Instead return `accounts_to_store`
self.store_accounts(&accounts_to_store); // and have `collect_rent_in_partition()` perform all the stores.
time.stop(); let (_, measure) = measure!(self.store_accounts(&accounts_to_store));
metrics.store_us.fetch_add(time.as_us(), Relaxed); time_storing_accounts_us += measure.as_us();
} }
self.remember_skipped_rewrites(rewrites_skipped); CollectRentFromAccountsInfo {
self.collected_rent rent_collected_info: total_rent_collected_info,
.fetch_add(total_collected.rent_amount, Relaxed); rent_rewards: rent_debits.into_unordered_rewards_iter().collect(),
self.rewards rewrites_skipped,
.write() time_collecting_rent_us,
.unwrap() time_hashing_skipped_rewrites_us,
.extend(rent_debits.into_unordered_rewards_iter()); time_storing_accounts_us,
self.update_accounts_data_size_delta_off_chain( }
-(total_collected.account_data_len_reclaimed as i64),
);
} }
/// load accounts with pubkeys in 'partition' /// load accounts with pubkeys in 'partition'
@ -5348,10 +5353,9 @@ impl Bank {
let end_prefix_inclusive = Self::prefix_from_pubkey(subrange_full.end()); let end_prefix_inclusive = Self::prefix_from_pubkey(subrange_full.end());
let range = end_prefix_inclusive - start_prefix; let range = end_prefix_inclusive - start_prefix;
let increment = range / num_threads; let increment = range / num_threads;
for thread_metrics in (0..num_threads) let mut results = (0..num_threads)
.into_par_iter() .into_par_iter()
.map(|chunk| { .map(|chunk| {
let metrics = RentMetrics::default();
let offset = |chunk| start_prefix + chunk * increment; let offset = |chunk| start_prefix + chunk * increment;
let start = offset(chunk); let start = offset(chunk);
let last = chunk == num_threads - 1; let last = chunk == num_threads - 1;
@ -5360,8 +5364,7 @@ impl Bank {
bound bound
}; };
let start = merge_prefix(start, *subrange_full.start()); let start = merge_prefix(start, *subrange_full.start());
let mut load = Measure::start("load"); let (accounts, measure_load_accounts) = measure!(if last {
let accounts = if last {
let end = *subrange_full.end(); let end = *subrange_full.end();
let subrange = start..=end; // IN-clusive let subrange = start..=end; // IN-clusive
self.rc self.rc
@ -5373,32 +5376,44 @@ impl Bank {
self.rc self.rc
.accounts .accounts
.load_to_collect_rent_eagerly(&self.ancestors, subrange) .load_to_collect_rent_eagerly(&self.ancestors, subrange)
}; });
load.stop(); CollectRentInPartitionInfo::new(
metrics.load_us.fetch_add(load.as_us(), Relaxed); self.collect_rent_from_accounts(accounts, just_rewrites),
Duration::from_nanos(measure_load_accounts.as_ns()),
self.collect_rent_from_accounts(accounts, &metrics, just_rewrites); )
metrics
}) })
.collect::<Vec<_>>() .reduce(
{ CollectRentInPartitionInfo::default,
metrics CollectRentInPartitionInfo::reduce,
.load_us );
.fetch_add(thread_metrics.load_us.load(Relaxed), Relaxed);
metrics
.store_us
.fetch_add(thread_metrics.store_us.load(Relaxed), Relaxed);
metrics
.hash_us
.fetch_add(thread_metrics.hash_us.load(Relaxed), Relaxed);
metrics
.collect_us
.fetch_add(thread_metrics.collect_us.load(Relaxed), Relaxed);
}
self.rc self.rc
.accounts .accounts
.hold_range_in_memory(&subrange_full, false, thread_pool); .hold_range_in_memory(&subrange_full, false, thread_pool);
self.collected_rent
.fetch_add(results.rent_collected, Relaxed);
self.update_accounts_data_size_delta_off_chain(
-(results.accounts_data_size_reclaimed as i64),
);
self.rewards
.write()
.unwrap()
.append(&mut results.rent_rewards);
self.remember_skipped_rewrites(results.rewrites_skipped);
metrics
.load_us
.fetch_add(results.time_loading_accounts_us, Relaxed);
metrics
.collect_us
.fetch_add(results.time_collecting_rent_us, Relaxed);
metrics
.hash_us
.fetch_add(results.time_hashing_skipped_rewrites_us, Relaxed);
metrics
.store_us
.fetch_add(results.time_storing_accounts_us, Relaxed);
}); });
} }
@ -7479,6 +7494,81 @@ impl Bank {
} }
} }
/// Return the computed values from `collect_rent_from_accounts()`
///
/// Since `collect_rent_from_accounts()` is running in parallel, instead of updating the
/// atomics/shared data inside this function, return those values in this struct for the caller to
/// process later.
#[derive(Debug, Default)]
struct CollectRentFromAccountsInfo {
rent_collected_info: CollectedInfo,
rent_rewards: Vec<(Pubkey, RewardInfo)>,
rewrites_skipped: Vec<(Pubkey, Hash)>,
time_collecting_rent_us: u64,
time_hashing_skipped_rewrites_us: u64,
time_storing_accounts_us: u64,
}
/// Return the computed values—of each iteration in the parallel loop inside
/// `collect_rent_in_partition()`—and then perform a reduce on all of them.
#[derive(Debug, Default)]
struct CollectRentInPartitionInfo {
rent_collected: u64,
accounts_data_size_reclaimed: u64,
rent_rewards: Vec<(Pubkey, RewardInfo)>,
rewrites_skipped: Vec<(Pubkey, Hash)>,
time_loading_accounts_us: u64,
time_collecting_rent_us: u64,
time_hashing_skipped_rewrites_us: u64,
time_storing_accounts_us: u64,
}
impl CollectRentInPartitionInfo {
/// Create a new `CollectRentInPartitionInfo` from the results of loading accounts and
/// collecting rent on them.
#[must_use]
fn new(info: CollectRentFromAccountsInfo, time_loading_accounts: Duration) -> Self {
Self {
rent_collected: info.rent_collected_info.rent_amount,
accounts_data_size_reclaimed: info.rent_collected_info.account_data_len_reclaimed,
rent_rewards: info.rent_rewards,
rewrites_skipped: info.rewrites_skipped,
time_loading_accounts_us: time_loading_accounts.as_micros() as u64,
time_collecting_rent_us: info.time_collecting_rent_us,
time_hashing_skipped_rewrites_us: info.time_hashing_skipped_rewrites_us,
time_storing_accounts_us: info.time_storing_accounts_us,
}
}
/// Reduce (i.e. 'combine') two `CollectRentInPartitionInfo`s into one.
///
/// This fn is used by `collect_rent_in_partition()` as the reduce step (of map-reduce) in its
/// parallel loop of rent collection.
#[must_use]
fn reduce(lhs: Self, rhs: Self) -> Self {
Self {
rent_collected: lhs.rent_collected.saturating_add(rhs.rent_collected),
accounts_data_size_reclaimed: lhs
.accounts_data_size_reclaimed
.saturating_add(rhs.accounts_data_size_reclaimed),
rent_rewards: [lhs.rent_rewards, rhs.rent_rewards].concat(),
rewrites_skipped: [lhs.rewrites_skipped, rhs.rewrites_skipped].concat(),
time_loading_accounts_us: lhs
.time_loading_accounts_us
.saturating_add(rhs.time_loading_accounts_us),
time_collecting_rent_us: lhs
.time_collecting_rent_us
.saturating_add(rhs.time_collecting_rent_us),
time_hashing_skipped_rewrites_us: lhs
.time_hashing_skipped_rewrites_us
.saturating_add(rhs.time_hashing_skipped_rewrites_us),
time_storing_accounts_us: lhs
.time_storing_accounts_us
.saturating_add(rhs.time_storing_accounts_us),
}
}
}
/// Struct to collect stats when scanning all accounts in `get_total_accounts_stats()` /// Struct to collect stats when scanning all accounts in `get_total_accounts_stats()`
#[derive(Debug, Default, Copy, Clone)] #[derive(Debug, Default, Copy, Clone)]
pub struct TotalAccountsStats { pub struct TotalAccountsStats {