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:
Tao Zhu
2021-11-12 01:04:53 -06:00
committed by GitHub
parent ef29d2d172
commit 11153e1f87
10 changed files with 479 additions and 228 deletions

View File

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

View File

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

View File

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

View File

@ -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());
}
}