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 <tao@solana.com>
This commit is contained in:
Maximilian Schneider 2022-11-22 18:55:56 +01:00 committed by GitHub
parent 637e8a937b
commit c8b0c3ede9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 188 additions and 941 deletions

View File

@ -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(),

View File

@ -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::<Box<dyn Fn()>>,
&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,

View File

@ -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<TransactionStatusSender>,
gossip_vote_sender: ReplayVoteSender,
cost_model: Arc<RwLock<CostModel>>,
log_messages_bytes_limit: Option<usize>,
connection_cache: Arc<ConnectionCache>,
bank_forks: Arc<RwLock<BankForks>>,
@ -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<TransactionStatusSender>,
gossip_vote_sender: ReplayVoteSender,
cost_model: Arc<RwLock<CostModel>>,
log_messages_bytes_limit: Option<usize>,
connection_cache: Arc<ConnectionCache>,
bank_forks: Arc<RwLock<BankForks>>,
@ -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<TransactionStatusSender>,
gossip_vote_sender: ReplayVoteSender,
data_budget: &DataBudget,
cost_model: Arc<RwLock<CostModel>>,
log_messages_bytes_limit: Option<usize>,
connection_cache: Arc<ConnectionCache>,
bank_forks: &Arc<RwLock<BankForks>>,
@ -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::<Box<dyn Fn()>>,
&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::<Box<dyn Fn()>>,
&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,

View File

@ -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<Bank>,
},
ExecuteTiming {
execute_timings: Box<ExecuteTimings>,
},
FrozenBank { bank: Arc<Bank> },
}
pub type CostUpdateReceiver = Receiver<CostUpdate>;
@ -69,15 +21,11 @@ pub struct CostUpdateService {
impl CostUpdateService {
#[allow(clippy::new_ret_no_self)]
pub fn new(
blockstore: Arc<Blockstore>,
cost_model: Arc<RwLock<CostModel>>,
cost_update_receiver: CostUpdateReceiver,
) -> Self {
pub fn new(blockstore: Arc<Blockstore>, 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<Blockstore>,
cost_model: Arc<RwLock<CostModel>>,
cost_update_receiver: CostUpdateReceiver,
) {
let mut cost_update_service_timing = CostUpdateServiceTiming::default();
fn service_loop(_blockstore: Arc<Blockstore>, 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<CostModel>,
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)
);
}
}
}

View File

@ -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<RwLock<CostModel>>,
// QosService hosts metrics object and a private reporting thread, as well as sender to
// communicate with thread.
report_sender: Sender<QosMetrics>,
@ -65,7 +61,7 @@ impl Drop for QosService {
}
impl QosService {
pub fn new(cost_model: Arc<RwLock<CostModel>>, 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<TransactionCost>, Vec<transaction::Result<()>>, 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<Item = &'a SanitizedTransaction>,
) -> Vec<TransactionCost> {
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);

View File

@ -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
}

View File

@ -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<BankNotificationSender>,
tpu_coalesce_ms: u64,
cluster_confirmed_slot_sender: GossipDuplicateConfirmedSlotsSender,
cost_model: &Arc<RwLock<CostModel>>,
connection_cache: &Arc<ConnectionCache>,
keypair: &Keypair,
log_messages_bytes_limit: Option<usize>,
@ -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(),

View File

@ -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<MaxSlots>,
cost_model: &Arc<RwLock<CostModel>>,
block_metadata_notifier: Option<BlockMetadataNotifierLock>,
wait_to_vote_slot: Option<Slot>,
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(),

View File

@ -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::<VoteTracker>::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,

View File

@ -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!(

View File

@ -386,7 +386,6 @@ fn execute_batches(
replay_vote_sender: Option<&ReplayVoteSender>,
confirmation_timing: &mut ConfirmationTiming,
cost_capacity_meter: Arc<RwLock<BlockCostCapacityMeter>>,
cost_model: &CostModel,
log_messages_bytes_limit: Option<usize>,
) -> 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 {

View File

@ -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<RwLock<CostModel>> = Arc::new(RwLock::new(CostModel::default()));
let thread_handlers: Vec<JoinHandle<()>> = (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());
}
}

View File

@ -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<Pubkey, u64>,
occurrences: HashMap<Pubkey, (usize, u128)>,
}
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::<u64>() / 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());
}
}

View File

@ -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;

View File

@ -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<Vec<u8>, std::io::Error> {
self.try_to_vec()
}
}