From c8b0c3ede9df7354feb964766ada1a06eb0ee019 Mon Sep 17 00:00:00 2001 From: Maximilian Schneider Date: Tue, 22 Nov 2022 18:55:56 +0100 Subject: [PATCH] Update cost model to use requested_cu instead of estimated cu #27608 (#28281) * Update cost model to use requested_cu instead of estimated cu #27608 * remove CostUpdate and CostModel from replay/tvu * revive cost update service to send cost tracker stats * CostModel is now static * remove unused package Co-authored-by: Tao Zhu --- banking-bench/src/main.rs | 3 +- core/benches/banking_stage.rs | 5 +- core/src/banking_stage.rs | 38 ++- core/src/cost_update_service.rs | 307 +------------------------ core/src/qos_service.rs | 56 +++-- core/src/replay_stage.rs | 8 - core/src/tpu.rs | 3 - core/src/tvu.rs | 9 +- core/src/validator.rs | 7 - ledger-tool/src/main.rs | 6 +- ledger/src/blockstore_processor.rs | 7 +- runtime/src/cost_model.rs | 358 ++++++++++------------------- runtime/src/execute_cost_table.rs | 314 ------------------------- runtime/src/lib.rs | 1 - sdk/src/compute_budget.rs | 7 + 15 files changed, 188 insertions(+), 941 deletions(-) delete mode 100644 runtime/src/execute_cost_table.rs diff --git a/banking-bench/src/main.rs b/banking-bench/src/main.rs index 69110916bf..6e0c6a7795 100644 --- a/banking-bench/src/main.rs +++ b/banking-bench/src/main.rs @@ -17,7 +17,7 @@ use { solana_measure::measure::Measure, solana_perf::packet::{to_packet_batches, PacketBatch}, solana_poh::poh_recorder::{create_test_recorder, PohRecorder, WorkingBankEntry}, - solana_runtime::{bank::Bank, bank_forks::BankForks, cost_model::CostModel}, + solana_runtime::{bank::Bank, bank_forks::BankForks}, solana_sdk::{ compute_budget::ComputeBudgetInstruction, hash::Hash, @@ -430,7 +430,6 @@ fn main() { num_banking_threads, None, replay_vote_sender, - Arc::new(RwLock::new(CostModel::default())), None, Arc::new(connection_cache), bank_forks.clone(), diff --git a/core/benches/banking_stage.rs b/core/benches/banking_stage.rs index 0f6be0c3e9..97cf22dca5 100644 --- a/core/benches/banking_stage.rs +++ b/core/benches/banking_stage.rs @@ -26,7 +26,7 @@ use { }, solana_perf::{packet::to_packet_batches, test_tx::test_tx}, solana_poh::poh_recorder::{create_test_recorder, WorkingBankEntry}, - solana_runtime::{bank::Bank, bank_forks::BankForks, cost_model::CostModel}, + solana_runtime::{bank::Bank, bank_forks::BankForks}, solana_sdk::{ genesis_config::GenesisConfig, hash::Hash, @@ -100,7 +100,7 @@ fn bench_consume_buffered(bencher: &mut Bencher) { None::>, &BankingStageStats::default(), &recorder, - &QosService::new(Arc::new(RwLock::new(CostModel::default())), 1), + &QosService::new(1), &mut LeaderSlotMetricsTracker::new(0), None, ); @@ -283,7 +283,6 @@ fn bench_banking(bencher: &mut Bencher, tx_type: TransactionType) { vote_receiver, None, s, - Arc::new(RwLock::new(CostModel::default())), None, Arc::new(ConnectionCache::default()), bank_forks, diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index 1dce0dd005..db31f18d0f 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -49,7 +49,6 @@ use { }, bank_forks::BankForks, bank_utils, - cost_model::CostModel, transaction_batch::TransactionBatch, transaction_error_metrics::TransactionErrorMetrics, vote_sender_types::ReplayVoteSender, @@ -387,7 +386,6 @@ impl BankingStage { verified_vote_receiver: BankingPacketReceiver, transaction_status_sender: Option, gossip_vote_sender: ReplayVoteSender, - cost_model: Arc>, log_messages_bytes_limit: Option, connection_cache: Arc, bank_forks: Arc>, @@ -401,7 +399,6 @@ impl BankingStage { Self::num_threads(), transaction_status_sender, gossip_vote_sender, - cost_model, log_messages_bytes_limit, connection_cache, bank_forks, @@ -418,7 +415,6 @@ impl BankingStage { num_threads: u32, transaction_status_sender: Option, gossip_vote_sender: ReplayVoteSender, - cost_model: Arc>, log_messages_bytes_limit: Option, connection_cache: Arc, bank_forks: Arc>, @@ -489,7 +485,6 @@ impl BankingStage { let transaction_status_sender = transaction_status_sender.clone(); let gossip_vote_sender = gossip_vote_sender.clone(); let data_budget = data_budget.clone(); - let cost_model = cost_model.clone(); let connection_cache = connection_cache.clone(); let bank_forks = bank_forks.clone(); Builder::new() @@ -504,7 +499,6 @@ impl BankingStage { transaction_status_sender, gossip_vote_sender, &data_budget, - cost_model, log_messages_bytes_limit, connection_cache, &bank_forks, @@ -1059,7 +1053,6 @@ impl BankingStage { transaction_status_sender: Option, gossip_vote_sender: ReplayVoteSender, data_budget: &DataBudget, - cost_model: Arc>, log_messages_bytes_limit: Option, connection_cache: Arc, bank_forks: &Arc>, @@ -1069,7 +1062,7 @@ impl BankingStage { let socket = UdpSocket::bind("0.0.0.0:0").unwrap(); let mut banking_stage_stats = BankingStageStats::new(id); let mut tracer_packet_stats = TracerPacketStats::new(id); - let qos_service = QosService::new(cost_model, id); + let qos_service = QosService::new(id); let mut slot_metrics_tracker = LeaderSlotMetricsTracker::new(id); let mut last_metrics_update = Instant::now(); @@ -2074,7 +2067,6 @@ mod tests { gossip_verified_vote_receiver, None, gossip_vote_sender, - Arc::new(RwLock::new(CostModel::default())), None, Arc::new(ConnectionCache::default()), bank_forks, @@ -2128,7 +2120,6 @@ mod tests { verified_gossip_vote_receiver, None, gossip_vote_sender, - Arc::new(RwLock::new(CostModel::default())), None, Arc::new(ConnectionCache::default()), bank_forks, @@ -2207,7 +2198,6 @@ mod tests { gossip_verified_vote_receiver, None, gossip_vote_sender, - Arc::new(RwLock::new(CostModel::default())), None, Arc::new(ConnectionCache::default()), bank_forks, @@ -2363,7 +2353,6 @@ mod tests { 3, None, gossip_vote_sender, - Arc::new(RwLock::new(CostModel::default())), None, Arc::new(ConnectionCache::default()), bank_forks, @@ -2667,7 +2656,7 @@ mod tests { 0, &None, &gossip_vote_sender, - &QosService::new(Arc::new(RwLock::new(CostModel::default())), 1), + &QosService::new(1), None, ); @@ -2720,7 +2709,7 @@ mod tests { 0, &None, &gossip_vote_sender, - &QosService::new(Arc::new(RwLock::new(CostModel::default())), 1), + &QosService::new(1), None, ); @@ -2804,7 +2793,7 @@ mod tests { 0, &None, &gossip_vote_sender, - &QosService::new(Arc::new(RwLock::new(CostModel::default())), 1), + &QosService::new(1), None, ); @@ -2871,7 +2860,7 @@ mod tests { poh_recorder.write().unwrap().set_bank(&bank, false); let (gossip_vote_sender, _gossip_vote_receiver) = unbounded(); - let qos_service = QosService::new(Arc::new(RwLock::new(CostModel::default())), 1); + let qos_service = QosService::new(1); let get_block_cost = || bank.read_cost_tracker().unwrap().block_cost(); let get_tx_count = || bank.read_cost_tracker().unwrap().transaction_count(); @@ -3033,7 +3022,7 @@ mod tests { 0, &None, &gossip_vote_sender, - &QosService::new(Arc::new(RwLock::new(CostModel::default())), 1), + &QosService::new(1), None, ); @@ -3111,7 +3100,7 @@ mod tests { &recorder, &None, &gossip_vote_sender, - &QosService::new(Arc::new(RwLock::new(CostModel::default())), 1), + &QosService::new(1), None, ); @@ -3178,7 +3167,7 @@ mod tests { &recorder, &None, &gossip_vote_sender, - &QosService::new(Arc::new(RwLock::new(CostModel::default())), 1), + &QosService::new(1), None, ); @@ -3408,7 +3397,7 @@ mod tests { sender: transaction_status_sender, }), &gossip_vote_sender, - &QosService::new(Arc::new(RwLock::new(CostModel::default())), 1), + &QosService::new(1), None, ); @@ -3577,7 +3566,7 @@ mod tests { sender: transaction_status_sender, }), &gossip_vote_sender, - &QosService::new(Arc::new(RwLock::new(CostModel::default())), 1), + &QosService::new(1), None, ); @@ -3701,7 +3690,7 @@ mod tests { None::>, &BankingStageStats::default(), &recorder, - &QosService::new(Arc::new(RwLock::new(CostModel::default())), 1), + &QosService::new(1), &mut LeaderSlotMetricsTracker::new(0), None, ); @@ -3718,7 +3707,7 @@ mod tests { None::>, &BankingStageStats::default(), &recorder, - &QosService::new(Arc::new(RwLock::new(CostModel::default())), 1), + &QosService::new(1), &mut LeaderSlotMetricsTracker::new(0), None, ); @@ -3780,7 +3769,7 @@ mod tests { test_fn, &BankingStageStats::default(), &recorder, - &QosService::new(Arc::new(RwLock::new(CostModel::default())), 1), + &QosService::new(1), &mut LeaderSlotMetricsTracker::new(0), None, ); @@ -4089,7 +4078,6 @@ mod tests { gossip_verified_vote_receiver, None, gossip_vote_sender, - Arc::new(RwLock::new(CostModel::default())), None, Arc::new(ConnectionCache::default()), bank_forks, diff --git a/core/src/cost_update_service.rs b/core/src/cost_update_service.rs index 8f5038c0c9..8565a4c1bb 100644 --- a/core/src/cost_update_service.rs +++ b/core/src/cost_update_service.rs @@ -1,64 +1,16 @@ -//! this service receives instruction ExecuteTimings from replay_stage, -//! update cost_model which is shared with banking_stage to optimize -//! packing transactions into block; it also triggers persisting cost -//! table to blockstore. +//! this service asynchronously reports CostTracker stats use { crossbeam_channel::Receiver, solana_ledger::blockstore::Blockstore, - solana_measure::measure, - solana_program_runtime::timings::ExecuteTimings, - solana_runtime::{bank::Bank, cost_model::CostModel}, - solana_sdk::timing::timestamp, + solana_runtime::bank::Bank, std::{ - sync::{Arc, RwLock}, + sync::Arc, thread::{self, Builder, JoinHandle}, }, }; - -#[derive(Default)] -pub struct CostUpdateServiceTiming { - last_print: u64, - update_cost_model_count: u64, - update_cost_model_elapsed: u64, -} - -impl CostUpdateServiceTiming { - fn update(&mut self, update_cost_model_count: u64, update_cost_model_elapsed: u64) { - self.update_cost_model_count += update_cost_model_count; - self.update_cost_model_elapsed += update_cost_model_elapsed; - - let now = timestamp(); - let elapsed_ms = now - self.last_print; - if elapsed_ms > 1000 { - datapoint_info!( - "cost-update-service-stats", - ("total_elapsed_us", elapsed_ms * 1000, i64), - ( - "update_cost_model_count", - self.update_cost_model_count as i64, - i64 - ), - ( - "update_cost_model_elapsed", - self.update_cost_model_elapsed as i64, - i64 - ), - ); - - *self = CostUpdateServiceTiming::default(); - self.last_print = now; - } - } -} - pub enum CostUpdate { - FrozenBank { - bank: Arc, - }, - ExecuteTiming { - execute_timings: Box, - }, + FrozenBank { bank: Arc }, } pub type CostUpdateReceiver = Receiver; @@ -69,15 +21,11 @@ pub struct CostUpdateService { impl CostUpdateService { #[allow(clippy::new_ret_no_self)] - pub fn new( - blockstore: Arc, - cost_model: Arc>, - cost_update_receiver: CostUpdateReceiver, - ) -> Self { + pub fn new(blockstore: Arc, cost_update_receiver: CostUpdateReceiver) -> Self { let thread_hdl = Builder::new() .name("solCostUpdtSvc".to_string()) .spawn(move || { - Self::service_loop(blockstore, cost_model, cost_update_receiver); + Self::service_loop(blockstore, cost_update_receiver); }) .unwrap(); @@ -88,254 +36,13 @@ impl CostUpdateService { self.thread_hdl.join() } - fn service_loop( - _blockstore: Arc, - cost_model: Arc>, - cost_update_receiver: CostUpdateReceiver, - ) { - let mut cost_update_service_timing = CostUpdateServiceTiming::default(); + fn service_loop(_blockstore: Arc, cost_update_receiver: CostUpdateReceiver) { for cost_update in cost_update_receiver.iter() { match cost_update { CostUpdate::FrozenBank { bank } => { bank.read_cost_tracker().unwrap().report_stats(bank.slot()); } - CostUpdate::ExecuteTiming { - mut execute_timings, - } => { - let (update_count, update_cost_model_time) = measure!( - Self::update_cost_model(&cost_model, &mut execute_timings), - "update_cost_model_time", - ); - cost_update_service_timing.update(update_count, update_cost_model_time.as_us()); - } } } } - - fn update_cost_model( - cost_model: &RwLock, - execute_timings: &mut ExecuteTimings, - ) -> u64 { - let mut update_count = 0_u64; - for (program_id, program_timings) in &mut execute_timings.details.per_program_timings { - let current_estimated_program_cost = - cost_model.read().unwrap().find_instruction_cost(program_id); - program_timings.coalesce_error_timings(current_estimated_program_cost); - - if program_timings.count < 1 { - continue; - } - - let units = program_timings.accumulated_units / program_timings.count as u64; - cost_model - .write() - .unwrap() - .upsert_instruction_cost(program_id, units); - update_count += 1; - } - update_count - } -} - -#[cfg(test)] -mod tests { - use {super::*, solana_program_runtime::timings::ProgramTiming, solana_sdk::pubkey::Pubkey}; - - #[test] - fn test_update_cost_model_with_empty_execute_timings() { - let cost_model = Arc::new(RwLock::new(CostModel::default())); - let mut empty_execute_timings = ExecuteTimings::default(); - - assert_eq!( - 0, - CostUpdateService::update_cost_model(&cost_model, &mut empty_execute_timings), - ); - } - - #[test] - fn test_update_cost_model_with_execute_timings() { - let cost_model = Arc::new(RwLock::new(CostModel::default())); - let mut execute_timings = ExecuteTimings::default(); - - let program_key_1 = Pubkey::new_unique(); - let mut expected_cost: u64; - - // add new program - { - let accumulated_us: u64 = 1000; - let accumulated_units: u64 = 100; - let total_errored_units = 0; - let count: u32 = 10; - expected_cost = accumulated_units / count as u64; - - execute_timings.details.per_program_timings.insert( - program_key_1, - ProgramTiming { - accumulated_us, - accumulated_units, - count, - errored_txs_compute_consumed: vec![], - total_errored_units, - }, - ); - assert_eq!( - 1, - CostUpdateService::update_cost_model(&cost_model, &mut execute_timings), - ); - assert_eq!( - expected_cost, - cost_model - .read() - .unwrap() - .find_instruction_cost(&program_key_1) - ); - } - - // update program - { - let accumulated_us: u64 = 2000; - let accumulated_units: u64 = 200; - let count: u32 = 10; - // to expect new cost is Average(new_value, existing_value) - expected_cost = ((accumulated_units / count as u64) + expected_cost) / 2; - - execute_timings.details.per_program_timings.insert( - program_key_1, - ProgramTiming { - accumulated_us, - accumulated_units, - count, - errored_txs_compute_consumed: vec![], - total_errored_units: 0, - }, - ); - assert_eq!( - 1, - CostUpdateService::update_cost_model(&cost_model, &mut execute_timings), - ); - assert_eq!( - expected_cost, - cost_model - .read() - .unwrap() - .find_instruction_cost(&program_key_1) - ); - } - } - - #[test] - fn test_update_cost_model_with_error_execute_timings() { - let cost_model = Arc::new(RwLock::new(CostModel::default())); - let mut execute_timings = ExecuteTimings::default(); - let program_key_1 = Pubkey::new_unique(); - - // Test updating cost model with a `ProgramTiming` with no compute units accumulated, i.e. - // `accumulated_units` == 0 - { - execute_timings.details.per_program_timings.insert( - program_key_1, - ProgramTiming { - accumulated_us: 1000, - accumulated_units: 0, - count: 0, - errored_txs_compute_consumed: vec![], - total_errored_units: 0, - }, - ); - // If both the `errored_txs_compute_consumed` is empty and `count == 0`, then - // nothing should be inserted into the cost model - assert_eq!( - CostUpdateService::update_cost_model(&cost_model, &mut execute_timings), - 0 - ); - } - - // set up current instruction cost to 100 - let current_program_cost = 100; - { - execute_timings.details.per_program_timings.insert( - program_key_1, - ProgramTiming { - accumulated_us: 1000, - accumulated_units: current_program_cost, - count: 1, - errored_txs_compute_consumed: vec![], - total_errored_units: 0, - }, - ); - assert_eq!( - CostUpdateService::update_cost_model(&cost_model, &mut execute_timings), - 1 - ); - assert_eq!( - current_program_cost, - cost_model - .read() - .unwrap() - .find_instruction_cost(&program_key_1) - ); - } - - // Test updating cost model with only erroring compute costs where the `cost_per_error` is - // greater than the current instruction cost for the program. Should update with the - // new erroring compute costs - let cost_per_error = 1000; - // the expect cost is (previous_cost + new_cost)/2 = (100 + 1000)/2 = 550 - let expected_units = 550; - { - let errored_txs_compute_consumed = vec![cost_per_error; 3]; - let total_errored_units = errored_txs_compute_consumed.iter().sum(); - execute_timings.details.per_program_timings.insert( - program_key_1, - ProgramTiming { - accumulated_us: 1000, - accumulated_units: 0, - count: 0, - errored_txs_compute_consumed, - total_errored_units, - }, - ); - assert_eq!( - CostUpdateService::update_cost_model(&cost_model, &mut execute_timings), - 1 - ); - assert_eq!( - expected_units, - cost_model - .read() - .unwrap() - .find_instruction_cost(&program_key_1) - ); - } - - // Test updating cost model with only erroring compute costs where the error cost is - // `smaller_cost_per_error`, less than the current instruction cost for the program. - // The cost should not decrease for these new lesser errors - let smaller_cost_per_error = expected_units - 10; - { - let errored_txs_compute_consumed = vec![smaller_cost_per_error; 3]; - let total_errored_units = errored_txs_compute_consumed.iter().sum(); - execute_timings.details.per_program_timings.insert( - program_key_1, - ProgramTiming { - accumulated_us: 1000, - accumulated_units: 0, - count: 0, - errored_txs_compute_consumed, - total_errored_units, - }, - ); - assert_eq!( - CostUpdateService::update_cost_model(&cost_model, &mut execute_timings), - 1 - ); - assert_eq!( - expected_units, - cost_model - .read() - .unwrap() - .find_instruction_cost(&program_key_1) - ); - } - } } diff --git a/core/src/qos_service.rs b/core/src/qos_service.rs index 2f60399f46..05f68f83dd 100644 --- a/core/src/qos_service.rs +++ b/core/src/qos_service.rs @@ -14,13 +14,14 @@ use { }, solana_sdk::{ clock::Slot, + feature_set::FeatureSet, saturating_add_assign, transaction::{self, SanitizedTransaction, TransactionError}, }, std::{ sync::{ atomic::{AtomicBool, AtomicU64, Ordering}, - Arc, RwLock, + Arc, }, thread::{self, Builder, JoinHandle}, time::Duration, @@ -39,11 +40,6 @@ pub enum QosMetrics { // reported if new bank slot has changed. // pub struct QosService { - // cost_model instance is owned by validator, shared between replay_stage and - // banking_stage. replay_stage writes the latest on-chain program timings to - // it; banking_stage's qos_service reads that information to calculate - // transaction cost, hence RwLock wrapped. - cost_model: Arc>, // QosService hosts metrics object and a private reporting thread, as well as sender to // communicate with thread. report_sender: Sender, @@ -65,7 +61,7 @@ impl Drop for QosService { } impl QosService { - pub fn new(cost_model: Arc>, id: u32) -> Self { + pub fn new(id: u32) -> Self { let (report_sender, report_receiver) = unbounded(); let running_flag = Arc::new(AtomicBool::new(true)); let metrics = Arc::new(QosServiceMetrics::new(id)); @@ -82,7 +78,6 @@ impl QosService { ); Self { - cost_model, metrics, reporting_thread, running_flag, @@ -99,7 +94,8 @@ impl QosService { bank: &Bank, transactions: &[SanitizedTransaction], ) -> (Vec, Vec>, usize) { - let transaction_costs = self.compute_transaction_costs(transactions.iter()); + let transaction_costs = + self.compute_transaction_costs(&bank.feature_set, transactions.iter()); let (transactions_qos_results, num_included) = self.select_transactions_per_cost(transactions.iter(), transaction_costs.iter(), bank); self.accumulate_estimated_transaction_costs(&Self::accumulate_batched_transaction_costs( @@ -119,13 +115,13 @@ impl QosService { // invoke cost_model to calculate cost for the given list of transactions fn compute_transaction_costs<'a>( &self, + feature_set: &FeatureSet, transactions: impl Iterator, ) -> Vec { let mut compute_cost_time = Measure::start("compute_cost_time"); - let cost_model = self.cost_model.read().unwrap(); let txs_costs: Vec<_> = transactions .map(|tx| { - let cost = cost_model.calculate_cost(tx); + let cost = CostModel::calculate_cost(tx, feature_set); debug!( "transaction {:?}, cost {:?}, cost sum {}", tx, @@ -691,9 +687,9 @@ mod tests { ); let txs = vec![transfer_tx.clone(), vote_tx.clone(), vote_tx, transfer_tx]; - let cost_model = Arc::new(RwLock::new(CostModel::default())); - let qos_service = QosService::new(cost_model.clone(), 1); - let txs_costs = qos_service.compute_transaction_costs(txs.iter()); + let qos_service = QosService::new(1); + let txs_costs = + qos_service.compute_transaction_costs(&FeatureSet::all_enabled(), txs.iter()); // verify the size of txs_costs and its contents assert_eq!(txs_costs.len(), txs.len()); @@ -703,7 +699,7 @@ mod tests { .map(|(index, cost)| { assert_eq!( cost.sum(), - cost_model.read().unwrap().calculate_cost(&txs[index]).sum() + CostModel::calculate_cost(&txs[index], &FeatureSet::all_enabled()).sum() ); }) .collect_vec(); @@ -714,7 +710,6 @@ mod tests { solana_logger::setup(); let GenesisConfigInfo { genesis_config, .. } = create_genesis_config(10); let bank = Arc::new(Bank::new_for_tests(&genesis_config)); - let cost_model = Arc::new(RwLock::new(CostModel::default())); let keypair = Keypair::new(); let transfer_tx = SanitizedTransaction::from_transaction_for_tests( @@ -731,18 +726,16 @@ mod tests { None, ), ); - let transfer_tx_cost = cost_model - .read() - .unwrap() - .calculate_cost(&transfer_tx) - .sum(); - let vote_tx_cost = cost_model.read().unwrap().calculate_cost(&vote_tx).sum(); + let transfer_tx_cost = + CostModel::calculate_cost(&transfer_tx, &FeatureSet::all_enabled()).sum(); + let vote_tx_cost = CostModel::calculate_cost(&vote_tx, &FeatureSet::all_enabled()).sum(); // make a vec of txs let txs = vec![transfer_tx.clone(), vote_tx.clone(), transfer_tx, vote_tx]; - let qos_service = QosService::new(cost_model, 1); - let txs_costs = qos_service.compute_transaction_costs(txs.iter()); + let qos_service = QosService::new(1); + let txs_costs = + qos_service.compute_transaction_costs(&FeatureSet::all_enabled(), txs.iter()); // set cost tracker limit to fit 1 transfer tx and 1 vote tx let cost_limit = transfer_tx_cost + vote_tx_cost; @@ -781,8 +774,9 @@ mod tests { // assert all tx_costs should be applied to cost_tracker if all execution_results are all committed { - let qos_service = QosService::new(Arc::new(RwLock::new(CostModel::default())), 1); - let txs_costs = qos_service.compute_transaction_costs(txs.iter()); + let qos_service = QosService::new(1); + let txs_costs = + qos_service.compute_transaction_costs(&FeatureSet::all_enabled(), txs.iter()); let total_txs_cost: u64 = txs_costs.iter().map(|cost| cost.sum()).sum(); let (qos_results, _num_included) = qos_service.select_transactions_per_cost(txs.iter(), txs_costs.iter(), &bank); @@ -834,8 +828,9 @@ mod tests { // assert all tx_costs should be removed from cost_tracker if all execution_results are all Not Committed { - let qos_service = QosService::new(Arc::new(RwLock::new(CostModel::default())), 1); - let txs_costs = qos_service.compute_transaction_costs(txs.iter()); + let qos_service = QosService::new(1); + let txs_costs = + qos_service.compute_transaction_costs(&FeatureSet::all_enabled(), txs.iter()); let total_txs_cost: u64 = txs_costs.iter().map(|cost| cost.sum()).sum(); let (qos_results, _num_included) = qos_service.select_transactions_per_cost(txs.iter(), txs_costs.iter(), &bank); @@ -874,8 +869,9 @@ mod tests { // assert only commited tx_costs are applied cost_tracker { - let qos_service = QosService::new(Arc::new(RwLock::new(CostModel::default())), 1); - let txs_costs = qos_service.compute_transaction_costs(txs.iter()); + let qos_service = QosService::new(1); + let txs_costs = + qos_service.compute_transaction_costs(&FeatureSet::all_enabled(), txs.iter()); let total_txs_cost: u64 = txs_costs.iter().map(|cost| cost.sum()).sum(); let (qos_results, _num_included) = qos_service.select_transactions_per_cost(txs.iter(), txs_costs.iter(), &bank); diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 84e87d71db..1d7f130a9d 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -2605,14 +2605,6 @@ impl ReplayStage { } } - // Send accumulated execute-timings to cost_update_service. - if !execute_timings.details.per_program_timings.is_empty() { - cost_update_sender - .send(CostUpdate::ExecuteTiming { - execute_timings: Box::new(execute_timings), - }) - .unwrap_or_else(|err| warn!("cost_update_sender failed: {:?}", err)); - } inc_new_counter_info!("replay_stage-replay_transactions", tx_count); did_complete_bank } diff --git a/core/src/tpu.rs b/core/src/tpu.rs index a03195f74e..4c816d263b 100644 --- a/core/src/tpu.rs +++ b/core/src/tpu.rs @@ -26,7 +26,6 @@ use { }, solana_runtime::{ bank_forks::BankForks, - cost_model::CostModel, vote_sender_types::{ReplayVoteReceiver, ReplayVoteSender}, }, solana_sdk::{pubkey::Pubkey, signature::Keypair}, @@ -94,7 +93,6 @@ impl Tpu { bank_notification_sender: Option, tpu_coalesce_ms: u64, cluster_confirmed_slot_sender: GossipDuplicateConfirmedSlotsSender, - cost_model: &Arc>, connection_cache: &Arc, keypair: &Keypair, log_messages_bytes_limit: Option, @@ -232,7 +230,6 @@ impl Tpu { verified_gossip_vote_packets_receiver, transaction_status_sender, replay_vote_sender, - cost_model.clone(), log_messages_bytes_limit, connection_cache.clone(), bank_forks.clone(), diff --git a/core/src/tvu.rs b/core/src/tvu.rs index e75c174c78..8bdfcaa249 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -42,8 +42,8 @@ use { }, solana_runtime::{ accounts_background_service::AbsRequestSender, bank_forks::BankForks, - commitment::BlockCommitmentCache, cost_model::CostModel, - prioritization_fee_cache::PrioritizationFeeCache, vote_sender_types::ReplayVoteSender, + commitment::BlockCommitmentCache, prioritization_fee_cache::PrioritizationFeeCache, + vote_sender_types::ReplayVoteSender, }, solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Keypair}, std::{ @@ -122,7 +122,6 @@ impl Tvu { gossip_confirmed_slots_receiver: GossipDuplicateConfirmedSlotsReceiver, tvu_config: TvuConfig, max_slots: &Arc, - cost_model: &Arc>, block_metadata_notifier: Option, wait_to_vote_slot: Option, accounts_background_request_sender: AbsRequestSender, @@ -260,8 +259,7 @@ impl Tvu { None }; let (cost_update_sender, cost_update_receiver) = unbounded(); - let cost_update_service = - CostUpdateService::new(blockstore.clone(), cost_model.clone(), cost_update_receiver); + let cost_update_service = CostUpdateService::new(blockstore.clone(), cost_update_receiver); let (drop_bank_sender, drop_bank_receiver) = unbounded(); @@ -445,7 +443,6 @@ pub mod tests { gossip_confirmed_slots_receiver, TvuConfig::default(), &Arc::new(MaxSlots::default()), - &Arc::new(RwLock::new(CostModel::default())), None, None, AbsRequestSender::default(), diff --git a/core/src/validator.rs b/core/src/validator.rs index d753755d7b..36a5cf334f 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -77,7 +77,6 @@ use { bank::Bank, bank_forks::BankForks, commitment::BlockCommitmentCache, - cost_model::CostModel, hardened_unpack::{open_genesis_config, MAX_GENESIS_ARCHIVE_UNPACKED_SIZE}, prioritization_fee_cache::PrioritizationFeeCache, runtime_config::RuntimeConfig, @@ -922,10 +921,6 @@ impl Validator { ); let vote_tracker = Arc::::default(); - let mut cost_model = CostModel::default(); - // initialize cost model with built-in instruction costs only - cost_model.initialize_cost_table(&[]); - let cost_model = Arc::new(RwLock::new(cost_model)); let (retransmit_slots_sender, retransmit_slots_receiver) = unbounded(); let (verified_vote_sender, verified_vote_receiver) = unbounded(); @@ -980,7 +975,6 @@ impl Validator { replay_slots_concurrently: config.replay_slots_concurrently, }, &max_slots, - &cost_model, block_metadata_notifier, config.wait_to_vote_slot, accounts_background_request_sender, @@ -1017,7 +1011,6 @@ impl Validator { bank_notification_sender, config.tpu_coalesce_ms, cluster_confirmed_slot_sender, - &cost_model, &connection_cache, &identity_keypair, config.runtime_config.log_messages_bytes_limit, diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index a2b46b100a..0da06d7734 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -71,7 +71,7 @@ use { account_utils::StateMut, clock::{Epoch, Slot}, feature::{self, Feature}, - feature_set, + feature_set::{self, FeatureSet}, genesis_config::{ClusterType, GenesisConfig}, hash::Hash, inflation::Inflation, @@ -1201,8 +1201,6 @@ fn compute_slot_cost(blockstore: &Blockstore, slot: Slot) -> Result<(), String> let mut num_programs = 0; let mut program_ids = HashMap::new(); - let mut cost_model = CostModel::default(); - cost_model.initialize_cost_table(&blockstore.read_program_costs().unwrap()); let mut cost_tracker = CostTracker::default(); for entry in entries { @@ -1226,7 +1224,7 @@ fn compute_slot_cost(blockstore: &Blockstore, slot: Slot) -> Result<(), String> .for_each(|transaction| { num_programs += transaction.message().instructions().len(); - let tx_cost = cost_model.calculate_cost(&transaction); + let tx_cost = CostModel::calculate_cost(&transaction, &FeatureSet::all_enabled()); let result = cost_tracker.try_add(&tx_cost); if result.is_err() { println!( diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index ebc5b9c716..127ae3ad9b 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -386,7 +386,6 @@ fn execute_batches( replay_vote_sender: Option<&ReplayVoteSender>, confirmation_timing: &mut ConfirmationTiming, cost_capacity_meter: Arc>, - cost_model: &CostModel, log_messages_bytes_limit: Option, ) -> Result<()> { if batches.is_empty() { @@ -416,7 +415,7 @@ fn execute_batches( let tx_costs = sanitized_txs .iter() .map(|tx| { - let tx_cost = cost_model.calculate_cost(tx); + let tx_cost = CostModel::calculate_cost(tx, &bank.feature_set); let cost = tx_cost.sum(); let cost_without_bpf = tx_cost.sum_without_bpf(); minimal_tx_cost = std::cmp::min(minimal_tx_cost, cost); @@ -557,7 +556,6 @@ fn process_entries_with_callback( let mut batches = vec![]; let mut tick_hashes = vec![]; let mut rng = thread_rng(); - let cost_model = CostModel::new(); for ReplayEntry { entry, @@ -579,7 +577,6 @@ fn process_entries_with_callback( replay_vote_sender, confirmation_timing, cost_capacity_meter.clone(), - &cost_model, log_messages_bytes_limit, )?; batches.clear(); @@ -648,7 +645,6 @@ fn process_entries_with_callback( replay_vote_sender, confirmation_timing, cost_capacity_meter.clone(), - &cost_model, log_messages_bytes_limit, )?; batches.clear(); @@ -665,7 +661,6 @@ fn process_entries_with_callback( replay_vote_sender, confirmation_timing, cost_capacity_meter, - &cost_model, log_messages_bytes_limit, )?; for hash in tick_hashes { diff --git a/runtime/src/cost_model.rs b/runtime/src/cost_model.rs index ea854787dd..0d181ae591 100644 --- a/runtime/src/cost_model.rs +++ b/runtime/src/cost_model.rs @@ -4,12 +4,25 @@ //! //! The main function is `calculate_cost` which returns &TransactionCost. //! + use { - crate::{block_cost_limits::*, execute_cost_table::ExecuteCostTable}, + crate::{bank::Bank, block_cost_limits::*}, log::*, + solana_program_runtime::compute_budget::{ + ComputeBudget, DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, + }, solana_sdk::{ - instruction::CompiledInstruction, program_utils::limited_deserialize, pubkey::Pubkey, - system_instruction::SystemInstruction, system_program, transaction::SanitizedTransaction, + compute_budget, + feature_set::{ + cap_transaction_accounts_data_size, remove_deprecated_request_unit_ix, + use_default_units_in_fee_calculation, FeatureSet, + }, + instruction::CompiledInstruction, + program_utils::limited_deserialize, + pubkey::Pubkey, + system_instruction::SystemInstruction, + system_program, + transaction::SanitizedTransaction, }, }; @@ -74,70 +87,30 @@ impl TransactionCost { } } -#[derive(Debug, Default)] -pub struct CostModel { - instruction_execution_cost_table: ExecuteCostTable, -} +pub struct CostModel; impl CostModel { - pub fn new() -> Self { - Self { - instruction_execution_cost_table: ExecuteCostTable::default(), - } - } - - pub fn initialize_cost_table(&mut self, cost_table: &[(Pubkey, u64)]) { - cost_table - .iter() - .map(|(key, cost)| (key, cost)) - .for_each(|(program_id, cost)| { - self.upsert_instruction_cost(program_id, *cost); - }); - } - - pub fn calculate_cost(&self, transaction: &SanitizedTransaction) -> TransactionCost { + pub fn calculate_cost( + transaction: &SanitizedTransaction, + feature_set: &FeatureSet, + ) -> TransactionCost { let mut tx_cost = TransactionCost::new_with_capacity(MAX_WRITABLE_ACCOUNTS); - tx_cost.signature_cost = self.get_signature_cost(transaction); - self.get_write_lock_cost(&mut tx_cost, transaction); - self.get_transaction_cost(&mut tx_cost, transaction); - tx_cost.account_data_size = self.calculate_account_data_size(transaction); + tx_cost.signature_cost = Self::get_signature_cost(transaction); + Self::get_write_lock_cost(&mut tx_cost, transaction); + Self::get_transaction_cost(&mut tx_cost, transaction, feature_set); + tx_cost.account_data_size = Self::calculate_account_data_size(transaction); tx_cost.is_simple_vote = transaction.is_simple_vote_transaction(); debug!("transaction {:?} has cost {:?}", transaction, tx_cost); tx_cost } - pub fn upsert_instruction_cost(&mut self, program_key: &Pubkey, cost: u64) { - self.instruction_execution_cost_table - .upsert(program_key, cost); - } - - pub fn find_instruction_cost(&self, program_key: &Pubkey) -> u64 { - match self.instruction_execution_cost_table.get_cost(program_key) { - Some(cost) => *cost, - None => { - let default_value = self - .instruction_execution_cost_table - .get_default_compute_unit_limit(); - debug!( - "Program {:?} does not have aggregated cost, using default value {}", - program_key, default_value - ); - default_value - } - } - } - - fn get_signature_cost(&self, transaction: &SanitizedTransaction) -> u64 { + fn get_signature_cost(transaction: &SanitizedTransaction) -> u64 { transaction.signatures().len() as u64 * SIGNATURE_COST } - fn get_write_lock_cost( - &self, - tx_cost: &mut TransactionCost, - transaction: &SanitizedTransaction, - ) { + fn get_write_lock_cost(tx_cost: &mut TransactionCost, transaction: &SanitizedTransaction) { let message = transaction.message(); message .account_keys() @@ -154,9 +127,9 @@ impl CostModel { } fn get_transaction_cost( - &self, tx_cost: &mut TransactionCost, transaction: &SanitizedTransaction, + feature_set: &FeatureSet, ) { let mut builtin_costs = 0u64; let mut bpf_costs = 0u64; @@ -166,18 +139,28 @@ impl CostModel { // to keep the same behavior, look for builtin first if let Some(builtin_cost) = BUILT_IN_INSTRUCTION_COSTS.get(program_id) { builtin_costs = builtin_costs.saturating_add(*builtin_cost); - } else { - let instruction_cost = self.find_instruction_cost(program_id); - trace!( - "instruction {:?} has cost of {}", - instruction, - instruction_cost - ); - bpf_costs = bpf_costs.saturating_add(instruction_cost); + } else if !compute_budget::check_id(program_id) { + bpf_costs = bpf_costs.saturating_add(DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT.into()); } data_bytes_len_total = data_bytes_len_total.saturating_add(instruction.data.len() as u64); } + + // calculate bpf cost based on compute budget instructions + let mut budget = ComputeBudget::default(); + let result = budget.process_instructions( + transaction.message().program_instructions_iter(), + feature_set.is_active(&use_default_units_in_fee_calculation::id()), + !feature_set.is_active(&remove_deprecated_request_unit_ix::id()), + feature_set.is_active(&cap_transaction_accounts_data_size::id()), + Bank::get_loaded_accounts_data_limit_type(feature_set), + ); + + // if tx contained user-space instructions and a more accurate estimate available correct it + if bpf_costs > 0 && result.is_ok() { + bpf_costs = budget.compute_unit_limit + } + tx_cost.builtins_execution_cost = builtin_costs; tx_cost.bpf_execution_cost = bpf_costs; tx_cost.data_bytes_cost = data_bytes_len_total / INSTRUCTION_DATA_BYTES_COST; @@ -226,7 +209,7 @@ impl CostModel { /// eventually, potentially determine account data size of all writable accounts /// at the moment, calculate account data size of account creation - fn calculate_account_data_size(&self, transaction: &SanitizedTransaction) -> u64 { + fn calculate_account_data_size(transaction: &SanitizedTransaction) -> u64 { transaction .message() .program_instructions_iter() @@ -244,9 +227,10 @@ mod tests { crate::{ bank::Bank, genesis_utils::{create_genesis_config, GenesisConfigInfo}, + inline_spl_token, }, solana_sdk::{ - bpf_loader, + compute_budget::{self, ComputeBudgetInstruction}, hash::Hash, instruction::CompiledInstruction, message::Message, @@ -255,11 +239,7 @@ mod tests { system_program, system_transaction, transaction::Transaction, }, - std::{ - str::FromStr, - sync::{Arc, RwLock}, - thread::{self, JoinHandle}, - }, + std::sync::Arc, }; fn test_setup() -> (Keypair, Hash) { @@ -274,29 +254,6 @@ mod tests { (mint_keypair, start_hash) } - #[test] - fn test_cost_model_instruction_cost() { - let mut testee = CostModel::default(); - - let known_key = Pubkey::from_str("known11111111111111111111111111111111111111").unwrap(); - testee.upsert_instruction_cost(&known_key, 100); - // find cost for known programs - assert_eq!(100, testee.find_instruction_cost(&known_key)); - - testee.upsert_instruction_cost(&bpf_loader::id(), 1999); - assert_eq!(1999, testee.find_instruction_cost(&bpf_loader::id())); - - // unknown program is assigned with default cost - assert_eq!( - testee - .instruction_execution_cost_table - .get_default_compute_unit_limit(), - testee.find_instruction_cost( - &Pubkey::from_str("unknown111111111111111111111111111111111111").unwrap() - ) - ); - } - #[test] fn test_cost_model_data_len_cost() { let lamports = 0; @@ -362,14 +319,83 @@ mod tests { .get(&system_program::id()) .unwrap(); - let testee = CostModel::default(); let mut tx_cost = TransactionCost::default(); - testee.get_transaction_cost(&mut tx_cost, &simple_transaction); + CostModel::get_transaction_cost( + &mut tx_cost, + &simple_transaction, + &FeatureSet::all_enabled(), + ); assert_eq!(*expected_execution_cost, tx_cost.builtins_execution_cost); assert_eq!(0, tx_cost.bpf_execution_cost); assert_eq!(3, tx_cost.data_bytes_cost); } + #[test] + fn test_cost_model_token_transaction() { + let (mint_keypair, start_hash) = test_setup(); + + let instructions = vec![CompiledInstruction::new(3, &(), vec![1, 2, 0])]; + let tx = Transaction::new_with_compiled_instructions( + &[&mint_keypair], + &[ + solana_sdk::pubkey::new_rand(), + solana_sdk::pubkey::new_rand(), + ], + start_hash, + vec![inline_spl_token::id()], + instructions, + ); + let token_transaction = SanitizedTransaction::from_transaction_for_tests(tx); + debug!("token_transaction {:?}", token_transaction); + + let mut tx_cost = TransactionCost::default(); + CostModel::get_transaction_cost( + &mut tx_cost, + &token_transaction, + &FeatureSet::all_enabled(), + ); + assert_eq!(0, tx_cost.builtins_execution_cost); + assert_eq!(200_000, tx_cost.bpf_execution_cost); + assert_eq!(0, tx_cost.data_bytes_cost); + } + + #[test] + fn test_cost_model_compute_budget_transaction() { + let (mint_keypair, start_hash) = test_setup(); + + let instructions = vec![ + CompiledInstruction::new(3, &(), vec![1, 2, 0]), + CompiledInstruction::new_from_raw_parts( + 4, + ComputeBudgetInstruction::SetComputeUnitLimit(12_345) + .pack() + .unwrap(), + vec![], + ), + ]; + let tx = Transaction::new_with_compiled_instructions( + &[&mint_keypair], + &[ + solana_sdk::pubkey::new_rand(), + solana_sdk::pubkey::new_rand(), + ], + start_hash, + vec![inline_spl_token::id(), compute_budget::id()], + instructions, + ); + let token_transaction = SanitizedTransaction::from_transaction_for_tests(tx); + + let mut tx_cost = TransactionCost::default(); + CostModel::get_transaction_cost( + &mut tx_cost, + &token_transaction, + &FeatureSet::all_enabled(), + ); + assert_eq!(0, tx_cost.builtins_execution_cost); + assert_eq!(12_345, tx_cost.bpf_execution_cost); + assert_eq!(1, tx_cost.data_bytes_cost); + } + #[test] fn test_cost_model_transaction_many_transfer_instructions() { let (mint_keypair, start_hash) = test_setup(); @@ -392,9 +418,8 @@ mod tests { .unwrap(); let expected_cost = program_cost * 2; - let testee = CostModel::default(); let mut tx_cost = TransactionCost::default(); - testee.get_transaction_cost(&mut tx_cost, &tx); + CostModel::get_transaction_cost(&mut tx_cost, &tx, &FeatureSet::all_enabled()); assert_eq!(expected_cost, tx_cost.builtins_execution_cost); assert_eq!(0, tx_cost.bpf_execution_cost); assert_eq!(6, tx_cost.data_bytes_cost); @@ -424,13 +449,9 @@ mod tests { ); debug!("many random transaction {:?}", tx); - let testee = CostModel::default(); - let expected_cost = testee - .instruction_execution_cost_table - .get_default_compute_unit_limit() - * 2; + let expected_cost = DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64 * 2; let mut tx_cost = TransactionCost::default(); - testee.get_transaction_cost(&mut tx_cost, &tx); + CostModel::get_transaction_cost(&mut tx_cost, &tx, &FeatureSet::all_enabled()); assert_eq!(0, tx_cost.builtins_execution_cost); assert_eq!(expected_cost, tx_cost.bpf_execution_cost); assert_eq!(0, tx_cost.data_bytes_cost); @@ -459,8 +480,7 @@ mod tests { ), ); - let cost_model = CostModel::default(); - let tx_cost = cost_model.calculate_cost(&tx); + let tx_cost = CostModel::calculate_cost(&tx, &FeatureSet::all_enabled()); assert_eq!(2 + 2, tx_cost.writable_accounts.len()); assert_eq!(signer1.pubkey(), tx_cost.writable_accounts[0]); assert_eq!(signer2.pubkey(), tx_cost.writable_accounts[1]); @@ -468,27 +488,6 @@ mod tests { assert_eq!(key2, tx_cost.writable_accounts[3]); } - #[test] - fn test_cost_model_insert_instruction_cost() { - let key1 = Pubkey::new_unique(); - let cost1 = 100; - - let mut cost_model = CostModel::default(); - // Using default cost for unknown instruction - assert_eq!( - cost_model - .instruction_execution_cost_table - .get_default_compute_unit_limit(), - cost_model.find_instruction_cost(&key1) - ); - - // insert instruction cost to table - cost_model.upsert_instruction_cost(&key1, cost1); - - // now it is known instruction with known cost - assert_eq!(cost1, cost_model.find_instruction_cost(&key1)); - } - #[test] fn test_cost_model_calculate_cost() { let (mint_keypair, start_hash) = test_setup(); @@ -504,114 +503,9 @@ mod tests { .get(&system_program::id()) .unwrap(); - let cost_model = CostModel::default(); - let tx_cost = cost_model.calculate_cost(&tx); + let tx_cost = CostModel::calculate_cost(&tx, &FeatureSet::all_enabled()); assert_eq!(expected_account_cost, tx_cost.write_lock_cost); assert_eq!(*expected_execution_cost, tx_cost.builtins_execution_cost); assert_eq!(2, tx_cost.writable_accounts.len()); } - - #[test] - fn test_cost_model_update_instruction_cost() { - let key1 = Pubkey::new_unique(); - let cost1 = 100; - let cost2 = 200; - let updated_cost = (cost1 + cost2) / 2; - - let mut cost_model = CostModel::default(); - - // insert instruction cost to table - cost_model.upsert_instruction_cost(&key1, cost1); - assert_eq!(cost1, cost_model.find_instruction_cost(&key1)); - - // update instruction cost - cost_model.upsert_instruction_cost(&key1, cost2); - assert_eq!(updated_cost, cost_model.find_instruction_cost(&key1)); - } - - #[test] - fn test_cost_model_can_be_shared_concurrently_with_rwlock() { - let (mint_keypair, start_hash) = test_setup(); - // construct a transaction with multiple random instructions - let key1 = solana_sdk::pubkey::new_rand(); - let key2 = solana_sdk::pubkey::new_rand(); - let prog1 = solana_sdk::pubkey::new_rand(); - let prog2 = solana_sdk::pubkey::new_rand(); - let instructions = vec![ - CompiledInstruction::new(3, &(), vec![0, 1]), - CompiledInstruction::new(4, &(), vec![0, 2]), - ]; - let tx = Arc::new(SanitizedTransaction::from_transaction_for_tests( - Transaction::new_with_compiled_instructions( - &[&mint_keypair], - &[key1, key2], - start_hash, - vec![prog1, prog2], - instructions, - ), - )); - - let number_threads = 10; - let expected_account_cost = WRITE_LOCK_UNITS * 3; - let cost1 = 100; - let cost2 = 200; - // execution cost can be either 2 * Default (before write) or cost1+cost2 (after write) - - let cost_model: Arc> = Arc::new(RwLock::new(CostModel::default())); - - let thread_handlers: Vec> = (0..number_threads) - .map(|i| { - let cost_model = cost_model.clone(); - let tx = tx.clone(); - - if i == 5 { - thread::spawn(move || { - let mut cost_model = cost_model.write().unwrap(); - cost_model.upsert_instruction_cost(&prog1, cost1); - cost_model.upsert_instruction_cost(&prog2, cost2); - }) - } else { - thread::spawn(move || { - let cost_model = cost_model.write().unwrap(); - let tx_cost = cost_model.calculate_cost(&tx); - assert_eq!(3, tx_cost.writable_accounts.len()); - assert_eq!(expected_account_cost, tx_cost.write_lock_cost); - }) - } - }) - .collect(); - - for th in thread_handlers { - th.join().unwrap(); - } - } - - #[test] - fn test_initialize_cost_table() { - // build cost table - let cost_table = vec![ - (Pubkey::new_unique(), 10), - (Pubkey::new_unique(), 20), - (Pubkey::new_unique(), 30), - ]; - - // init cost model - let mut cost_model = CostModel::default(); - cost_model.initialize_cost_table(&cost_table); - - // verify - for (id, cost) in cost_table.iter() { - assert_eq!(*cost, cost_model.find_instruction_cost(id)); - } - - // verify built-in programs are not in bpf_costs - assert!(cost_model - .instruction_execution_cost_table - .get_cost(&system_program::id()) - .is_none()); - assert!(cost_model - .instruction_execution_cost_table - .get_cost(&solana_vote_program::id()) - .is_none()); - } } diff --git a/runtime/src/execute_cost_table.rs b/runtime/src/execute_cost_table.rs deleted file mode 100644 index 2888007604..0000000000 --- a/runtime/src/execute_cost_table.rs +++ /dev/null @@ -1,314 +0,0 @@ -/// ExecuteCostTable is aggregated by Cost Model, it keeps each program's -/// average cost in its HashMap, with fixed capacity to avoid from growing -/// unchecked. -/// When its capacity limit is reached, it prunes old and less-used programs -/// to make room for new ones. -use { - log::*, solana_program_runtime::compute_budget::DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, - solana_sdk::pubkey::Pubkey, std::collections::HashMap, -}; - -// prune is rather expensive op, free up bulk space in each operation -// would be more efficient. PRUNE_RATIO defines that after prune, table -// size will be original_size * PRUNE_RATIO. The value is defined in -// scale of 100. -const PRUNE_RATIO: usize = 75; -// with 50_000 TPS as norm, weights occurrences '100' per microsec -const OCCURRENCES_WEIGHT: i64 = 100; - -const DEFAULT_CAPACITY: usize = 1024; - -#[derive(AbiExample, Debug)] -pub struct ExecuteCostTable { - capacity: usize, - table: HashMap, - occurrences: HashMap, -} - -impl Default for ExecuteCostTable { - fn default() -> Self { - ExecuteCostTable::new(DEFAULT_CAPACITY) - } -} - -impl ExecuteCostTable { - pub fn new(cap: usize) -> Self { - Self { - capacity: cap, - table: HashMap::with_capacity(cap), - occurrences: HashMap::with_capacity(cap), - } - } - - pub fn get_count(&self) -> usize { - self.table.len() - } - - pub fn get_default_compute_unit_limit(&self) -> u64 { - DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT as u64 - } - - /// average cost of all recorded programs - pub fn get_global_average_program_cost(&self) -> u64 { - if self.table.is_empty() { - self.get_default_compute_unit_limit() - } else { - self.table.values().sum::() / self.get_count() as u64 - } - } - - /// the most frequently occurring program's cost - pub fn get_statistical_mode_program_cost(&self) -> u64 { - if self.occurrences.is_empty() { - self.get_default_compute_unit_limit() - } else { - let key = self - .occurrences - .iter() - .max_by_key(|&(_, count)| count) - .map(|(key, _)| key) - .expect("cannot find mode from cost table"); - - *self.table.get(key).unwrap() - } - } - - /// returns None if program doesn't exist in table. In this case, - /// `get_default_compute_unit_limit()`, `get_global_average_program_cost()` - /// or `get_statistical_mode_program_cost()` can be used to assign a value - /// to new program. - pub fn get_cost(&self, key: &Pubkey) -> Option<&u64> { - self.table.get(key) - } - - /// update-or-insert should be infallible. Query the result of upsert, - /// often requires additional calculation, should be lazy. - pub fn upsert(&mut self, key: &Pubkey, value: u64) { - let need_to_add = !self.table.contains_key(key); - let current_size = self.get_count(); - if current_size >= self.capacity && need_to_add { - let prune_to_size = current_size - .checked_mul(PRUNE_RATIO) - .and_then(|v| v.checked_div(100)) - .unwrap_or(self.capacity); - self.prune_to(&prune_to_size); - } - - let program_cost = self.table.entry(*key).or_insert(value); - *program_cost = (*program_cost + value) / 2; - - let (count, timestamp) = self - .occurrences - .entry(*key) - .or_insert((0, Self::micros_since_epoch())); - *count += 1; - *timestamp = Self::micros_since_epoch(); - } - - /// prune the old programs so the table contains `new_size` of records, - /// where `old` is defined as weighted age, which is negatively correlated - /// with program's age and how frequently the program is occurrenced. - fn prune_to(&mut self, new_size: &usize) { - debug!( - "prune cost table, current size {}, new size {}", - self.get_count(), - new_size - ); - - if *new_size == self.get_count() { - return; - } - - if *new_size == 0 { - self.table.clear(); - self.occurrences.clear(); - return; - } - - let now = Self::micros_since_epoch(); - let mut sorted_by_weighted_age: Vec<_> = self - .occurrences - .iter() - .map(|(key, (count, timestamp))| { - let age = now - timestamp; - let weighted_age = *count as i64 * OCCURRENCES_WEIGHT + -(age as i64); - (weighted_age, *key) - }) - .collect(); - sorted_by_weighted_age.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap()); - - for i in sorted_by_weighted_age.iter() { - self.table.remove(&i.1); - self.occurrences.remove(&i.1); - if *new_size == self.get_count() { - break; - } - } - } - - fn micros_since_epoch() -> u128 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_micros() - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_execute_cost_table_prune_simple_table() { - solana_logger::setup(); - let capacity: usize = 3; - let mut testee = ExecuteCostTable::new(capacity); - - let key1 = Pubkey::new_unique(); - let key2 = Pubkey::new_unique(); - let key3 = Pubkey::new_unique(); - - testee.upsert(&key1, 1); - testee.upsert(&key2, 2); - testee.upsert(&key3, 3); - - testee.prune_to(&(capacity - 1)); - - // the oldest, key1, should be pruned - assert!(testee.get_cost(&key1).is_none()); - assert!(testee.get_cost(&key2).is_some()); - assert!(testee.get_cost(&key2).is_some()); - } - - #[test] - fn test_execute_cost_table_prune_weighted_table() { - solana_logger::setup(); - let capacity: usize = 3; - let mut testee = ExecuteCostTable::new(capacity); - - let key1 = Pubkey::new_unique(); - let key2 = Pubkey::new_unique(); - let key3 = Pubkey::new_unique(); - - // simulate a lot of occurrences to key1, so even there're longer than - // usual delay between upsert(key1..) and upsert(key2, ..), test - // would still satisfy as key1 has enough occurrences to compensate - // its age. - for i in 0..1000 { - testee.upsert(&key1, i); - } - testee.upsert(&key2, 2); - testee.upsert(&key3, 3); - - testee.prune_to(&(capacity - 1)); - - // the oldest, key1, has many counts; 2nd oldest Key2 has 1 count; - // expect key2 to be pruned. - assert!(testee.get_cost(&key1).is_some()); - assert!(testee.get_cost(&key2).is_none()); - assert!(testee.get_cost(&key3).is_some()); - } - - #[test] - fn test_execute_cost_table_upsert_within_capacity() { - solana_logger::setup(); - let mut testee = ExecuteCostTable::default(); - - let key1 = Pubkey::new_unique(); - let key2 = Pubkey::new_unique(); - let cost1: u64 = 100; - let cost2: u64 = 110; - - // query empty table - assert!(testee.get_cost(&key1).is_none()); - - // insert one record - testee.upsert(&key1, cost1); - assert_eq!(1, testee.get_count()); - assert_eq!(cost1, testee.get_global_average_program_cost()); - assert_eq!(cost1, testee.get_statistical_mode_program_cost()); - assert_eq!(&cost1, testee.get_cost(&key1).unwrap()); - - // insert 2nd record - testee.upsert(&key2, cost2); - assert_eq!(2, testee.get_count()); - assert_eq!( - (cost1 + cost2) / 2_u64, - testee.get_global_average_program_cost() - ); - assert_eq!(cost2, testee.get_statistical_mode_program_cost()); - assert_eq!(&cost1, testee.get_cost(&key1).unwrap()); - assert_eq!(&cost2, testee.get_cost(&key2).unwrap()); - - // update 1st record - testee.upsert(&key1, cost2); - assert_eq!(2, testee.get_count()); - assert_eq!( - ((cost1 + cost2) / 2 + cost2) / 2_u64, - testee.get_global_average_program_cost() - ); - assert_eq!( - (cost1 + cost2) / 2, - testee.get_statistical_mode_program_cost() - ); - assert_eq!(&((cost1 + cost2) / 2), testee.get_cost(&key1).unwrap()); - assert_eq!(&cost2, testee.get_cost(&key2).unwrap()); - } - - #[test] - fn test_execute_cost_table_upsert_exceeds_capacity() { - solana_logger::setup(); - let capacity: usize = 2; - let mut testee = ExecuteCostTable::new(capacity); - - let key1 = Pubkey::new_unique(); - let key2 = Pubkey::new_unique(); - let key3 = Pubkey::new_unique(); - let key4 = Pubkey::new_unique(); - let cost1: u64 = 100; - let cost2: u64 = 110; - let cost3: u64 = 120; - let cost4: u64 = 130; - - // insert one record - testee.upsert(&key1, cost1); - assert_eq!(1, testee.get_count()); - assert_eq!(&cost1, testee.get_cost(&key1).unwrap()); - - // insert 2nd record - testee.upsert(&key2, cost2); - assert_eq!(2, testee.get_count()); - assert_eq!(&cost1, testee.get_cost(&key1).unwrap()); - assert_eq!(&cost2, testee.get_cost(&key2).unwrap()); - - // insert 3rd record, pushes out the oldest (eg 1st) record - testee.upsert(&key3, cost3); - assert_eq!(2, testee.get_count()); - assert_eq!( - (cost2 + cost3) / 2_u64, - testee.get_global_average_program_cost() - ); - assert_eq!(cost3, testee.get_statistical_mode_program_cost()); - assert!(testee.get_cost(&key1).is_none()); - assert_eq!(&cost2, testee.get_cost(&key2).unwrap()); - assert_eq!(&cost3, testee.get_cost(&key3).unwrap()); - - // update 2nd record, so the 3rd becomes the oldest - // add 4th record, pushes out 3rd key - testee.upsert(&key2, cost1); - testee.upsert(&key4, cost4); - assert_eq!( - ((cost1 + cost2) / 2 + cost4) / 2_u64, - testee.get_global_average_program_cost() - ); - assert_eq!( - (cost1 + cost2) / 2, - testee.get_statistical_mode_program_cost() - ); - assert_eq!(2, testee.get_count()); - assert!(testee.get_cost(&key1).is_none()); - assert_eq!(&((cost1 + cost2) / 2), testee.get_cost(&key2).unwrap()); - assert!(testee.get_cost(&key3).is_none()); - assert_eq!(&cost4, testee.get_cost(&key4).unwrap()); - } -} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 03c3f423f2..b6369d90f6 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -36,7 +36,6 @@ pub mod cost_model; pub mod cost_tracker; pub mod epoch_accounts_hash; pub mod epoch_stakes; -pub mod execute_cost_table; pub mod genesis_utils; pub mod hardened_unpack; pub mod in_mem_accounts_index; diff --git a/sdk/src/compute_budget.rs b/sdk/src/compute_budget.rs index 632cf6363c..94a9f091e8 100644 --- a/sdk/src/compute_budget.rs +++ b/sdk/src/compute_budget.rs @@ -63,4 +63,11 @@ impl ComputeBudgetInstruction { pub fn set_accounts_data_size_limit(bytes: u32) -> Instruction { Instruction::new_with_borsh(id(), &Self::SetAccountsDataSizeLimit(bytes), vec![]) } + + /// Serialize Instruction using borsh, this is only used in runtime::cost_model::tests but compilation + /// can't be restricted as it's used across packages + // #[cfg(test)] + pub fn pack(self) -> Result, std::io::Error> { + self.try_to_vec() + } }