- update const cost values with data collected by #19627 (#20314)

- update cost calculation to closely proposed fee schedule #16984
This commit is contained in:
Tao Zhu 2021-10-08 14:48:50 -05:00 committed by GitHub
parent 17314f4a95
commit 675fa6993b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 167 additions and 111 deletions

View File

@ -1,10 +1,7 @@
//! 'cost_model` provides service to estimate a transaction's cost
//! It does so by analyzing accounts the transaction touches, and instructions
//! it includes. Using historical data as guideline, it estimates cost of
//! reading/writing account, the sum of that comes up to "account access cost";
//! Instructions take time to execute, both historical and runtime data are
//! used to determine each instruction's execution time, the sum of that
//! is transaction's "execution cost"
//! following proposed fee schedule #16984; Relevant cluster cost
//! measuring is described by #19627
//!
//! The main function is `calculate_cost` which returns &TransactionCost.
//!
use crate::execute_cost_table::ExecuteCostTable;
@ -28,16 +25,12 @@ pub enum CostModelError {
WouldExceedAccountMaxLimit,
}
// cost of transaction is made of account_access_cost and instruction execution_cost
// where
// account_access_cost is the sum of read/write/sign all accounts included in the transaction
// read is cheaper than write.
// execution_cost is the sum of all instructions execution cost, which is
// observed during runtime and feedback by Replay
#[derive(Default, Debug)]
pub struct TransactionCost {
pub writable_accounts: Vec<Pubkey>,
pub account_access_cost: u64,
pub signature_cost: u64,
pub write_lock_cost: u64,
pub data_bytes_cost: u64,
pub execution_cost: u64,
}
@ -51,9 +44,15 @@ impl TransactionCost {
pub fn reset(&mut self) {
self.writable_accounts.clear();
self.account_access_cost = 0;
self.signature_cost = 0;
self.write_lock_cost = 0;
self.data_bytes_cost = 0;
self.execution_cost = 0;
}
pub fn sum(&self) -> u64 {
self.signature_cost + self.write_lock_cost + self.data_bytes_cost + self.execution_cost
}
}
#[derive(Debug)]
@ -68,7 +67,7 @@ pub struct CostModel {
impl Default for CostModel {
fn default() -> Self {
CostModel::new(ACCOUNT_COST_MAX, BLOCK_COST_MAX)
CostModel::new(MAX_WRITABLE_ACCOUNT_UNITS, MAX_BLOCK_UNITS)
}
}
@ -91,22 +90,29 @@ impl CostModel {
}
pub fn initialize_cost_table(&mut self, cost_table: &[(Pubkey, u64)]) {
for (program_id, cost) in cost_table {
match self.upsert_instruction_cost(program_id, *cost) {
Ok(c) => {
debug!(
"initiating cost table, instruction {:?} has cost {}",
program_id, c
);
cost_table
.iter()
.map(|(key, cost)| (key, cost))
.chain(BUILT_IN_INSTRUCTION_COSTS.iter())
.for_each(|(program_id, cost)| {
match self
.instruction_execution_cost_table
.upsert(program_id, *cost)
{
Some(c) => {
debug!(
"initiating cost table, instruction {:?} has cost {}",
program_id, c
);
}
None => {
debug!(
"initiating cost table, failed for instruction {:?}",
program_id
);
}
}
Err(err) => {
debug!(
"initiating cost table, failed for instruction {:?}, err: {}",
program_id, err
);
}
}
}
});
debug!(
"restored cost model instruction cost table from blockstore, current values: {:?}",
self.get_instruction_cost_table()
@ -120,21 +126,11 @@ impl CostModel {
) -> &TransactionCost {
self.transaction_cost.reset();
// calculate transaction exeution cost
self.transaction_cost.execution_cost = self.find_transaction_cost(transaction);
self.transaction_cost.signature_cost = self.get_signature_cost(transaction);
self.get_write_lock_cost(transaction, demote_program_write_locks);
self.transaction_cost.data_bytes_cost = self.get_data_bytes_cost(transaction);
self.transaction_cost.execution_cost = self.get_transaction_cost(transaction);
// calculate account access cost
let message = transaction.message();
message.account_keys_iter().enumerate().for_each(|(i, k)| {
let is_writable = message.is_writable(i, demote_program_write_locks);
if is_writable {
self.transaction_cost.writable_accounts.push(*k);
self.transaction_cost.account_access_cost += ACCOUNT_WRITE_COST;
} else {
self.transaction_cost.account_access_cost += ACCOUNT_READ_COST;
}
});
debug!(
"transaction {:?} has cost {:?}",
transaction, self.transaction_cost
@ -142,7 +138,6 @@ impl CostModel {
&self.transaction_cost
}
// To update or insert instruction cost to table.
pub fn upsert_instruction_cost(
&mut self,
program_key: &Pubkey,
@ -160,6 +155,52 @@ impl CostModel {
self.instruction_execution_cost_table.get_cost_table()
}
fn get_signature_cost(&self, transaction: &SanitizedTransaction) -> u64 {
transaction.signatures().len() as u64 * SIGNATURE_COST
}
fn get_write_lock_cost(
&mut self,
transaction: &SanitizedTransaction,
demote_program_write_locks: bool,
) {
let message = transaction.message();
message.account_keys_iter().enumerate().for_each(|(i, k)| {
let is_writable = message.is_writable(i, demote_program_write_locks);
if is_writable {
self.transaction_cost.writable_accounts.push(*k);
self.transaction_cost.write_lock_cost += WRITE_LOCK_UNITS;
}
});
}
fn get_data_bytes_cost(&self, transaction: &SanitizedTransaction) -> u64 {
let mut data_bytes_cost: u64 = 0;
transaction
.message()
.program_instructions_iter()
.for_each(|(_, ix)| {
data_bytes_cost += ix.data.len() as u64 / DATA_BYTES_UNITS;
});
data_bytes_cost
}
fn get_transaction_cost(&self, transaction: &SanitizedTransaction) -> u64 {
let mut cost: u64 = 0;
for (program_id, instruction) in transaction.message().program_instructions_iter() {
let instruction_cost = self.find_instruction_cost(program_id);
trace!(
"instruction {:?} has cost of {}",
instruction,
instruction_cost
);
cost = cost.saturating_add(instruction_cost);
}
cost
}
fn find_instruction_cost(&self, program_key: &Pubkey) -> u64 {
match self.instruction_execution_cost_table.get_cost(program_key) {
Some(cost) => *cost,
@ -173,21 +214,6 @@ impl CostModel {
}
}
}
fn find_transaction_cost(&self, transaction: &SanitizedTransaction) -> u64 {
let mut cost: u64 = 0;
for (program_id, instruction) in transaction.message().program_instructions_iter() {
let instruction_cost = self.find_instruction_cost(program_id);
trace!(
"instruction {:?} has cost of {}",
instruction,
instruction_cost
);
cost += instruction_cost;
}
cost
}
}
#[cfg(test)]
@ -272,7 +298,7 @@ mod tests {
.unwrap();
assert_eq!(
expected_cost,
testee.find_transaction_cost(&simple_transaction)
testee.get_transaction_cost(&simple_transaction)
);
}
@ -298,7 +324,7 @@ mod tests {
testee
.upsert_instruction_cost(&system_program::id(), program_cost)
.unwrap();
assert_eq!(expected_cost, testee.find_transaction_cost(&tx));
assert_eq!(expected_cost, testee.get_transaction_cost(&tx));
}
#[test]
@ -326,7 +352,7 @@ mod tests {
debug!("many random transaction {:?}", tx);
let testee = CostModel::default();
let result = testee.find_transaction_cost(&tx);
let result = testee.get_transaction_cost(&tx);
// expected cost for two random/unknown program is
let expected_cost = testee.instruction_execution_cost_table.get_mode() * 2;
@ -392,7 +418,7 @@ mod tests {
.try_into()
.unwrap();
let expected_account_cost = ACCOUNT_WRITE_COST + ACCOUNT_WRITE_COST + ACCOUNT_READ_COST;
let expected_account_cost = WRITE_LOCK_UNITS * 2;
let expected_execution_cost = 8;
let mut cost_model = CostModel::default();
@ -400,7 +426,7 @@ mod tests {
.upsert_instruction_cost(&system_program::id(), expected_execution_cost)
.unwrap();
let tx_cost = cost_model.calculate_cost(&tx, /*demote_program_write_locks=*/ true);
assert_eq!(expected_account_cost, tx_cost.account_access_cost);
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());
}
@ -447,8 +473,7 @@ mod tests {
);
let number_threads = 10;
let expected_account_cost =
ACCOUNT_WRITE_COST + ACCOUNT_WRITE_COST * 2 + ACCOUNT_READ_COST * 2;
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)
@ -472,7 +497,7 @@ mod tests {
let tx_cost = cost_model
.calculate_cost(&tx, /*demote_program_write_locks=*/ true);
assert_eq!(3, tx_cost.writable_accounts.len());
assert_eq!(expected_account_cost, tx_cost.account_access_cost);
assert_eq!(expected_account_cost, tx_cost.write_lock_cost);
})
}
})
@ -484,7 +509,7 @@ mod tests {
}
#[test]
fn test_cost_model_init_cost_table() {
fn test_initialize_cost_table() {
// build cost table
let cost_table = vec![
(Pubkey::new_unique(), 10),
@ -500,5 +525,15 @@ mod tests {
for (id, cost) in cost_table.iter() {
assert_eq!(*cost, cost_model.find_instruction_cost(id));
}
// verify built-in programs
assert!(cost_model
.instruction_execution_cost_table
.get_cost(&system_program::id())
.is_some());
assert!(cost_model
.instruction_execution_cost_table
.get_cost(&solana_vote_program::id())
.is_some());
}
}

View File

@ -52,11 +52,7 @@ impl CostTracker {
) -> Result<(), CostModelError> {
let mut cost_model = self.cost_model.write().unwrap();
let tx_cost = cost_model.calculate_cost(transaction, demote_program_write_locks);
self.would_fit(
&tx_cost.writable_accounts,
&(tx_cost.account_access_cost + tx_cost.execution_cost),
stats,
)
self.would_fit(&tx_cost.writable_accounts, &tx_cost.sum(), stats)
}
pub fn add_transaction_cost(
@ -67,7 +63,7 @@ impl CostTracker {
) {
let mut cost_model = self.cost_model.write().unwrap();
let tx_cost = cost_model.calculate_cost(transaction, demote_program_write_locks);
let cost = tx_cost.account_access_cost + tx_cost.execution_cost;
let cost = tx_cost.sum();
for account_key in tx_cost.writable_accounts.iter() {
*self
.cost_by_writable_accounts
@ -103,7 +99,7 @@ impl CostTracker {
transaction_cost: &TransactionCost,
stats: &mut CostTrackerStats,
) -> Result<u64, CostModelError> {
let cost = transaction_cost.account_access_cost + transaction_cost.execution_cost;
let cost = transaction_cost.sum();
self.would_fit(&transaction_cost.writable_accounts, &cost, stats)?;
self.add_transaction(&transaction_cost.writable_accounts, &cost);
@ -428,8 +424,8 @@ mod tests {
{
let tx_cost = TransactionCost {
writable_accounts: vec![acct1, acct2, acct3],
account_access_cost: 0,
execution_cost: cost,
..TransactionCost::default()
};
assert!(testee
.try_add(&tx_cost, &mut CostTrackerStats::default())
@ -448,8 +444,8 @@ mod tests {
{
let tx_cost = TransactionCost {
writable_accounts: vec![acct2],
account_access_cost: 0,
execution_cost: cost,
..TransactionCost::default()
};
assert!(testee
.try_add(&tx_cost, &mut CostTrackerStats::default())
@ -470,8 +466,8 @@ mod tests {
{
let tx_cost = TransactionCost {
writable_accounts: vec![acct1, acct2],
account_access_cost: 0,
execution_cost: cost,
..TransactionCost::default()
};
assert!(testee
.try_add(&tx_cost, &mut CostTrackerStats::default())

View File

@ -78,7 +78,7 @@ impl ExecuteCostTable {
self.table.get(key)
}
pub fn upsert(&mut self, key: &Pubkey, value: u64) {
pub fn upsert(&mut self, key: &Pubkey, value: u64) -> Option<u64> {
let need_to_add = self.table.get(key).is_none();
let current_size = self.get_count();
if current_size == self.capacity && need_to_add {
@ -94,6 +94,8 @@ impl ExecuteCostTable {
.or_insert((0, SystemTime::now()));
*count += 1;
*timestamp = SystemTime::now();
Some(*program_cost)
}
// prune the old programs so the table contains `new_size` of records,

View File

@ -1,34 +1,56 @@
//! defines block cost related limits
//!
use lazy_static::lazy_static;
use solana_sdk::{
feature, incinerator, native_loader, pubkey::Pubkey, secp256k1_program, system_program,
};
use std::collections::HashMap;
// see https://github.com/solana-labs/solana/issues/18944
// and https://github.com/solana-labs/solana/pull/18994#issuecomment-896128992
//
pub const MAX_BLOCK_TIME_US: u64 = 400_000; // aiming at 400ms/block max time
pub const AVG_INSTRUCTION_TIME_US: u64 = 1_000; // average instruction execution time
pub const SYSTEM_PARALLELISM: u64 = 10;
pub const MAX_INSTRUCTION_COST: u64 = 200_000;
pub const MAX_NUMBER_BPF_INSTRUCTIONS_PER_ACCOUNT: u64 = 200;
/// Static configurations:
///
/// Number of microseconds replaying a block should take, 400 millisecond block times
/// is curerntly publicly communicated on solana.com
pub const MAX_BLOCK_REPLAY_TIME_US: u64 = 400_000;
/// number of concurrent processes,
pub const MAX_CONCURRENCY: u64 = 10;
// 4_000
pub const MAX_INSTRUCTIONS_PER_BLOCK: u64 =
(MAX_BLOCK_TIME_US / AVG_INSTRUCTION_TIME_US) * SYSTEM_PARALLELISM;
/// Cluster data, method of collecting at https://github.com/solana-labs/solana/issues/19627
///
/// cluster avergaed compute unit to microsec conversion rate
pub const COMPUTE_UNIT_TO_US_RATIO: u64 = 40;
/// Number of compute units for one signature verification.
pub const SIGNATURE_COST: u64 = COMPUTE_UNIT_TO_US_RATIO * 175;
/// Number of compute units for one write lock
pub const WRITE_LOCK_UNITS: u64 = COMPUTE_UNIT_TO_US_RATIO * 20;
/// Number of data bytes per compute units
pub const DATA_BYTES_UNITS: u64 = 220 /*bytes per us*/ / COMPUTE_UNIT_TO_US_RATIO;
// Number of compute units for each built-in programs
lazy_static! {
/// Number of compute units for each built-in programs
pub static ref BUILT_IN_INSTRUCTION_COSTS: HashMap<Pubkey, u64> = [
(feature::id(), COMPUTE_UNIT_TO_US_RATIO * 2),
(incinerator::id(), COMPUTE_UNIT_TO_US_RATIO * 2),
(native_loader::id(), COMPUTE_UNIT_TO_US_RATIO * 2),
(solana_sdk::stake::config::id(), COMPUTE_UNIT_TO_US_RATIO * 2),
(solana_sdk::stake::program::id(), COMPUTE_UNIT_TO_US_RATIO * 50),
(solana_vote_program::id(), COMPUTE_UNIT_TO_US_RATIO * 200),
(secp256k1_program::id(), COMPUTE_UNIT_TO_US_RATIO * 4),
(system_program::id(), COMPUTE_UNIT_TO_US_RATIO * 15),
]
.iter()
.cloned()
.collect();
}
// 8_000_000_000
pub const BLOCK_COST_MAX: u64 = MAX_INSTRUCTION_COST * MAX_INSTRUCTIONS_PER_BLOCK * 10;
// 800_000_000
pub const ACCOUNT_COST_MAX: u64 = MAX_INSTRUCTION_COST * MAX_INSTRUCTIONS_PER_BLOCK;
// 2_000
pub const COMPUTE_UNIT_TO_US_RATIO: u64 =
(MAX_INSTRUCTION_COST / AVG_INSTRUCTION_TIME_US) * SYSTEM_PARALLELISM;
// signature takes average 10us, or 20K CU
pub const SIGNATURE_COST: u64 = COMPUTE_UNIT_TO_US_RATIO * 10;
// read account averages 5us, or 10K CU
pub const ACCOUNT_READ_COST: u64 = COMPUTE_UNIT_TO_US_RATIO * 5;
// write account averages 25us, or 50K CU
pub const ACCOUNT_WRITE_COST: u64 = COMPUTE_UNIT_TO_US_RATIO * 25;
/// Statically computed data:
///
/// Number of compute units that a block is allowed. A block's compute units are
/// accumualted by Transactions added to it; A transaction's compute units are
/// calculated by cost_model, based on transaction's signarures, write locks,
/// data size and built-in and BPF instructinos.
pub const MAX_BLOCK_UNITS: u64 =
MAX_BLOCK_REPLAY_TIME_US * COMPUTE_UNIT_TO_US_RATIO * MAX_CONCURRENCY;
/// Number of compute units that a writable account in a block is allowed. The
/// limit is to prevent too many transactions write to same account, threrefore
/// reduce block's paralellism.
pub const MAX_WRITABLE_ACCOUNT_UNITS: u64 = MAX_BLOCK_REPLAY_TIME_US * COMPUTE_UNIT_TO_US_RATIO;

View File

@ -66,7 +66,7 @@ pub struct BlockCostCapacityMeter {
impl Default for BlockCostCapacityMeter {
fn default() -> Self {
BlockCostCapacityMeter::new(BLOCK_COST_MAX)
BlockCostCapacityMeter::new(MAX_BLOCK_UNITS)
}
}
@ -143,7 +143,8 @@ fn aggregate_total_execution_units(execute_timings: &ExecuteTimings) -> u64 {
if timing.count < 1 {
continue;
}
execute_cost_units += timing.accumulated_units / timing.count as u64;
execute_cost_units =
execute_cost_units.saturating_add(timing.accumulated_units / timing.count as u64);
trace!("aggregated execution cost of {:?} {:?}", program_id, timing);
}
execute_cost_units