refactor cost calculation (#21062)
* - cache calculated transaction cost to allow sharing; - atomic cost tracking op; - only lock accounts for transactions eligible for current block; - moved qos service and stats reporting to its own model; - add cost_weight default to neutral (as 1), vote has zero weight; Co-authored-by: Tyera Eulberg <teulberg@gmail.com> * Update core/src/qos_service.rs Co-authored-by: Tyera Eulberg <teulberg@gmail.com> * Update core/src/qos_service.rs Co-authored-by: Tyera Eulberg <teulberg@gmail.com> Co-authored-by: Tyera Eulberg <teulberg@gmail.com>
This commit is contained in:
@ -932,6 +932,26 @@ impl Accounts {
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[allow(clippy::needless_collect)]
|
||||
pub fn lock_accounts_with_results<'a>(
|
||||
&self,
|
||||
txs: impl Iterator<Item = &'a SanitizedTransaction>,
|
||||
results: impl Iterator<Item = &'a Result<()>>,
|
||||
demote_program_write_locks: bool,
|
||||
) -> Vec<Result<()>> {
|
||||
let keys: Vec<_> = txs
|
||||
.map(|tx| tx.get_account_locks(demote_program_write_locks))
|
||||
.collect();
|
||||
let account_locks = &mut self.account_locks.lock().unwrap();
|
||||
keys.into_iter()
|
||||
.zip(results)
|
||||
.map(|(keys, result)| match result {
|
||||
Ok(()) => self.lock_account(account_locks, keys.writable, keys.readonly),
|
||||
Err(e) => Err(e.clone()),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Once accounts are unlocked, new transactions that modify that state can enter the pipeline
|
||||
#[allow(clippy::needless_collect)]
|
||||
pub fn unlock_accounts<'a>(
|
||||
|
@ -230,7 +230,7 @@ impl ExecuteTimings {
|
||||
}
|
||||
|
||||
type BankStatusCache = StatusCache<Result<()>>;
|
||||
#[frozen_abi(digest = "5Br3PNyyX1L7XoS4jYLt5JTeMXowLSsu7v9LhokC8vnq")]
|
||||
#[frozen_abi(digest = "7bCDimGo11ajw6ZHViBBu8KPfoDZBcwSnumWCU8MMuwr")]
|
||||
pub type BankSlotDelta = SlotDelta<Result<()>>;
|
||||
type TransactionAccountRefCells = Vec<(Pubkey, Rc<RefCell<AccountSharedData>>)>;
|
||||
|
||||
@ -3357,6 +3357,22 @@ impl Bank {
|
||||
TransactionBatch::new(lock_results, self, Cow::Borrowed(txs))
|
||||
}
|
||||
|
||||
/// Prepare a locked transaction batch from a list of sanitized transactions, and their cost
|
||||
/// limited packing status
|
||||
pub fn prepare_sanitized_batch_with_results<'a, 'b>(
|
||||
&'a self,
|
||||
transactions: &'b [SanitizedTransaction],
|
||||
transaction_results: impl Iterator<Item = &'b Result<()>>,
|
||||
) -> TransactionBatch<'a, 'b> {
|
||||
// this lock_results could be: Ok, AccountInUse, WouldExceedBlockMaxLimit or WouldExceedAccountMaxLimit
|
||||
let lock_results = self.rc.accounts.lock_accounts_with_results(
|
||||
transactions.iter(),
|
||||
transaction_results,
|
||||
self.demote_program_write_locks(),
|
||||
);
|
||||
TransactionBatch::new(lock_results, self, Cow::Borrowed(transactions))
|
||||
}
|
||||
|
||||
/// Prepare a transaction batch without locking accounts for transaction simulation.
|
||||
pub(crate) fn prepare_simulation_batch<'a>(
|
||||
&'a self,
|
||||
@ -3772,6 +3788,8 @@ impl Bank {
|
||||
error_counters.account_in_use += 1;
|
||||
Some(index)
|
||||
}
|
||||
Err(TransactionError::WouldExceedMaxBlockCostLimit)
|
||||
| Err(TransactionError::WouldExceedMaxAccountCostLimit) => Some(index),
|
||||
Err(_) => None,
|
||||
Ok(_) => None,
|
||||
})
|
||||
@ -6413,7 +6431,7 @@ pub fn goto_end_of_slot(bank: &mut Bank) {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_simple_vote_transaction(transaction: &SanitizedTransaction) -> bool {
|
||||
pub fn is_simple_vote_transaction(transaction: &SanitizedTransaction) -> bool {
|
||||
if transaction.message().instructions().len() == 1 {
|
||||
let (program_pubkey, instruction) = transaction
|
||||
.message()
|
||||
|
@ -4,7 +4,9 @@
|
||||
//!
|
||||
//! The main function is `calculate_cost` which returns &TransactionCost.
|
||||
//!
|
||||
use crate::{block_cost_limits::*, execute_cost_table::ExecuteCostTable};
|
||||
use crate::{
|
||||
bank::is_simple_vote_transaction, block_cost_limits::*, execute_cost_table::ExecuteCostTable,
|
||||
};
|
||||
use log::*;
|
||||
use solana_sdk::{pubkey::Pubkey, transaction::SanitizedTransaction};
|
||||
use std::collections::HashMap;
|
||||
@ -12,13 +14,31 @@ use std::collections::HashMap;
|
||||
const MAX_WRITABLE_ACCOUNTS: usize = 256;
|
||||
|
||||
// costs are stored in number of 'compute unit's
|
||||
#[derive(AbiExample, Default, Debug)]
|
||||
#[derive(AbiExample, Debug)]
|
||||
pub struct TransactionCost {
|
||||
pub writable_accounts: Vec<Pubkey>,
|
||||
pub signature_cost: u64,
|
||||
pub write_lock_cost: u64,
|
||||
pub data_bytes_cost: u64,
|
||||
pub execution_cost: u64,
|
||||
// `cost_weight` is a multiplier to be applied to tx cost, that
|
||||
// allows to increase/decrease tx cost linearly based on algo.
|
||||
// for example, vote tx could have weight zero to bypass cost
|
||||
// limit checking during block packing.
|
||||
pub cost_weight: u32,
|
||||
}
|
||||
|
||||
impl Default for TransactionCost {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
writable_accounts: Vec::with_capacity(MAX_WRITABLE_ACCOUNTS),
|
||||
signature_cost: 0u64,
|
||||
write_lock_cost: 0u64,
|
||||
data_bytes_cost: 0u64,
|
||||
execution_cost: 0u64,
|
||||
cost_weight: 1u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TransactionCost {
|
||||
@ -35,6 +55,7 @@ impl TransactionCost {
|
||||
self.write_lock_cost = 0;
|
||||
self.data_bytes_cost = 0;
|
||||
self.execution_cost = 0;
|
||||
self.cost_weight = 1;
|
||||
}
|
||||
|
||||
pub fn sum(&self) -> u64 {
|
||||
@ -95,6 +116,7 @@ impl CostModel {
|
||||
self.get_write_lock_cost(&mut tx_cost, transaction, demote_program_write_locks);
|
||||
tx_cost.data_bytes_cost = self.get_data_bytes_cost(transaction);
|
||||
tx_cost.execution_cost = self.get_transaction_cost(transaction);
|
||||
tx_cost.cost_weight = self.calculate_cost_weight(transaction);
|
||||
|
||||
debug!("transaction {:?} has cost {:?}", transaction, tx_cost);
|
||||
tx_cost
|
||||
@ -177,6 +199,15 @@ impl CostModel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_cost_weight(&self, transaction: &SanitizedTransaction) -> u32 {
|
||||
if is_simple_vote_transaction(transaction) {
|
||||
// vote has zero cost weight, so it bypasses block cost limit checking
|
||||
0u32
|
||||
} else {
|
||||
1u32
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -196,6 +227,7 @@ mod tests {
|
||||
system_program, system_transaction,
|
||||
transaction::Transaction,
|
||||
};
|
||||
use solana_vote_program::vote_transaction;
|
||||
use std::{
|
||||
str::FromStr,
|
||||
sync::{Arc, RwLock},
|
||||
@ -394,6 +426,7 @@ mod tests {
|
||||
assert_eq!(expected_account_cost, tx_cost.write_lock_cost);
|
||||
assert_eq!(expected_execution_cost, tx_cost.execution_cost);
|
||||
assert_eq!(2, tx_cost.writable_accounts.len());
|
||||
assert_eq!(1u32, tx_cost.cost_weight);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -500,4 +533,31 @@ mod tests {
|
||||
.get_cost(&solana_vote_program::id())
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_cost_weight() {
|
||||
let (mint_keypair, start_hash) = test_setup();
|
||||
|
||||
let keypair = Keypair::new();
|
||||
let simple_transaction = SanitizedTransaction::from_transaction_for_tests(
|
||||
system_transaction::transfer(&mint_keypair, &keypair.pubkey(), 2, start_hash),
|
||||
);
|
||||
let vote_transaction = SanitizedTransaction::from_transaction_for_tests(
|
||||
vote_transaction::new_vote_transaction(
|
||||
vec![42],
|
||||
Hash::default(),
|
||||
Hash::default(),
|
||||
&keypair,
|
||||
&keypair,
|
||||
&keypair,
|
||||
None,
|
||||
),
|
||||
);
|
||||
|
||||
let testee = CostModel::default();
|
||||
|
||||
// For now, vote has zero weight, everything else is neutral, for now
|
||||
assert_eq!(1u32, testee.calculate_cost_weight(&simple_transaction));
|
||||
assert_eq!(0u32, testee.calculate_cost_weight(&vote_transaction));
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ impl CostTracker {
|
||||
_transaction: &SanitizedTransaction,
|
||||
tx_cost: &TransactionCost,
|
||||
) -> Result<u64, CostTrackerError> {
|
||||
let cost = tx_cost.sum();
|
||||
let cost = tx_cost.sum() * tx_cost.cost_weight as u64;
|
||||
self.would_fit(&tx_cost.writable_accounts, &cost)?;
|
||||
self.add_transaction(&tx_cost.writable_accounts, &cost);
|
||||
Ok(self.block_cost)
|
||||
@ -369,4 +369,26 @@ mod tests {
|
||||
assert_eq!(acct2, costliest_account);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_add_with_cost_weight() {
|
||||
let (mint_keypair, start_hash) = test_setup();
|
||||
let (tx, _keys, _cost) = build_simple_transaction(&mint_keypair, &start_hash);
|
||||
let tx = SanitizedTransaction::from_transaction_for_tests(tx);
|
||||
|
||||
let limit = 100u64;
|
||||
let mut testee = CostTracker::new(limit, limit);
|
||||
|
||||
let mut cost = TransactionCost {
|
||||
execution_cost: limit + 1,
|
||||
..TransactionCost::default()
|
||||
};
|
||||
|
||||
// cost exceed limit by 1, will not fit
|
||||
assert!(testee.try_add(&tx, &cost).is_err());
|
||||
|
||||
cost.cost_weight = 0u32;
|
||||
// setting cost_weight to zero will allow this tx
|
||||
assert!(testee.try_add(&tx, &cost).is_ok());
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user