solana/accounts-cluster-bench/src/main.rs

1284 lines
45 KiB
Rust

#![allow(clippy::arithmetic_side_effects)]
use {
clap::{crate_description, crate_name, value_t, values_t, values_t_or_exit, App, Arg},
log::*,
rand::{thread_rng, Rng},
rayon::prelude::*,
solana_accounts_db::inline_spl_token,
solana_clap_utils::{
hidden_unless_forced, input_parsers::pubkey_of, input_validators::is_url_or_moniker,
},
solana_cli_config::{ConfigInput, CONFIG_FILE},
solana_client::{rpc_request::TokenAccountsFilter, transaction_executor::TransactionExecutor},
solana_gossip::gossip_service::discover,
solana_measure::measure::Measure,
solana_rpc_client::rpc_client::RpcClient,
solana_sdk::{
commitment_config::CommitmentConfig,
hash::Hash,
instruction::{AccountMeta, Instruction},
message::Message,
pubkey::Pubkey,
signature::{read_keypair_file, Keypair, Signer},
system_instruction, system_program,
transaction::Transaction,
},
solana_streamer::socket::SocketAddrSpace,
std::{
cmp::min,
process::exit,
str::FromStr,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
thread::{sleep, Builder, JoinHandle},
time::{Duration, Instant},
},
};
pub const MAX_RPC_CALL_RETRIES: usize = 5;
pub fn poll_get_latest_blockhash(client: &RpcClient) -> Option<Hash> {
let mut num_retries = MAX_RPC_CALL_RETRIES;
loop {
let response = client.get_latest_blockhash();
if let Ok(blockhash) = response {
return Some(blockhash);
} else {
num_retries -= 1;
warn!(
"get_latest_blockhash failure: {:?}. remaining retries {}",
response, num_retries
);
}
if num_retries == 0 {
panic!("failed to get_latest_blockhash(), rpc node down?")
}
sleep(Duration::from_millis(100));
}
}
pub fn poll_get_fee_for_message(client: &RpcClient, message: &mut Message) -> (Option<u64>, Hash) {
let mut num_retries = MAX_RPC_CALL_RETRIES;
loop {
let response = client.get_fee_for_message(message);
if let Ok(fee) = response {
return (Some(fee), message.recent_blockhash);
} else {
num_retries -= 1;
warn!(
"get_fee_for_message failure: {:?}. remaining retries {}",
response, num_retries
);
let blockhash = poll_get_latest_blockhash(client).expect("blockhash");
message.recent_blockhash = blockhash;
}
if num_retries == 0 {
panic!("failed to get_fee_for_message(), rpc node down?")
}
sleep(Duration::from_millis(100));
}
}
fn airdrop_lamports(client: &RpcClient, id: &Keypair, desired_balance: u64) -> bool {
let starting_balance = client.get_balance(&id.pubkey()).unwrap_or(0);
info!("starting balance {}", starting_balance);
if starting_balance < desired_balance {
let airdrop_amount = desired_balance - starting_balance;
info!(
"Airdropping {:?} lamports from {} for {}",
airdrop_amount,
client.url(),
id.pubkey(),
);
let blockhash = client.get_latest_blockhash().unwrap();
if let Err(err) =
client.request_airdrop_with_blockhash(&id.pubkey(), airdrop_amount, &blockhash)
{
panic!(
"Error requesting airdrop: {err:?} to addr: {0:?} amount: {airdrop_amount}",
id.pubkey()
);
}
let current_balance = client.get_balance(&id.pubkey()).unwrap_or_else(|e| {
panic!("airdrop error {e}");
});
info!("current balance {}...", current_balance);
if current_balance - starting_balance != airdrop_amount {
info!(
"Airdrop failed? {} {} {} {}",
id.pubkey(),
current_balance,
starting_balance,
airdrop_amount,
);
}
}
true
}
struct SeedTracker {
max_created: Arc<AtomicU64>,
max_closed: Arc<AtomicU64>,
}
fn make_create_message(
keypair: &Keypair,
base_keypair: &Keypair,
max_created_seed: Arc<AtomicU64>,
num_instructions: usize,
balance: u64,
maybe_space: Option<u64>,
mint: Option<Pubkey>,
) -> Message {
let space = maybe_space.unwrap_or_else(|| thread_rng().gen_range(0..1000));
let instructions: Vec<_> = (0..num_instructions)
.flat_map(|_| {
let program_id = if mint.is_some() {
inline_spl_token::id()
} else {
system_program::id()
};
let seed = max_created_seed.fetch_add(1, Ordering::Relaxed).to_string();
let to_pubkey =
Pubkey::create_with_seed(&base_keypair.pubkey(), &seed, &program_id).unwrap();
let mut instructions = vec![system_instruction::create_account_with_seed(
&keypair.pubkey(),
&to_pubkey,
&base_keypair.pubkey(),
&seed,
balance,
space,
&program_id,
)];
if let Some(mint_address) = mint {
instructions.push(
spl_token::instruction::initialize_account(
&spl_token::id(),
&to_pubkey,
&mint_address,
&base_keypair.pubkey(),
)
.unwrap(),
);
}
instructions
})
.collect();
Message::new(&instructions, Some(&keypair.pubkey()))
}
fn make_close_message(
keypair: &Keypair,
base_keypair: &Keypair,
max_created: &AtomicU64,
max_closed: &AtomicU64,
num_instructions: usize,
balance: u64,
spl_token: bool,
) -> Message {
let instructions: Vec<_> = (0..num_instructions)
.filter_map(|_| {
let program_id = if spl_token {
inline_spl_token::id()
} else {
system_program::id()
};
let max_created_seed = max_created.load(Ordering::Relaxed);
let max_closed_seed = max_closed.load(Ordering::Relaxed);
if max_closed_seed >= max_created_seed {
return None;
}
let seed = max_closed.fetch_add(1, Ordering::Relaxed).to_string();
let address =
Pubkey::create_with_seed(&base_keypair.pubkey(), &seed, &program_id).unwrap();
if spl_token {
Some(
spl_token::instruction::close_account(
&spl_token::id(),
&address,
&keypair.pubkey(),
&base_keypair.pubkey(),
&[],
)
.unwrap(),
)
} else {
Some(system_instruction::transfer_with_seed(
&address,
&base_keypair.pubkey(),
seed,
&program_id,
&keypair.pubkey(),
balance,
))
}
})
.collect();
Message::new(&instructions, Some(&keypair.pubkey()))
}
#[derive(Clone, Copy, Debug)]
pub enum RpcBench {
Version,
Slot,
MultipleAccounts,
ProgramAccounts,
TokenAccountsByOwner,
}
#[derive(Debug)]
pub enum RpcParseError {
InvalidOption,
}
impl FromStr for RpcBench {
type Err = RpcParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"slot" => Ok(RpcBench::Slot),
"multiple-accounts" => Ok(RpcBench::MultipleAccounts),
"token-accounts-by-owner" => Ok(RpcBench::TokenAccountsByOwner),
"version" => Ok(RpcBench::Version),
_ => Err(RpcParseError::InvalidOption),
}
}
}
fn process_get_multiple_accounts(
max_closed: &AtomicU64,
max_created: &AtomicU64,
stats: &mut RpcBenchStats,
last_error: &mut Instant,
base_keypair_pubkey: &Pubkey,
program_id: &Pubkey,
client: &RpcClient,
) {
let start = max_closed.load(Ordering::Relaxed);
let end = max_created.load(Ordering::Relaxed);
let mut chunk_start = start;
let chunk_size = 10;
while chunk_start < end {
let chunk_end = std::cmp::min(chunk_start + chunk_size, end);
let addresses: Vec<_> = (chunk_start..chunk_end)
.map(|seed| {
Pubkey::create_with_seed(base_keypair_pubkey, &seed.to_string(), program_id)
.unwrap()
})
.collect();
chunk_start = chunk_end;
let mut rpc_time = Measure::start("rpc-get-multiple-accounts");
match client.get_multiple_accounts(&addresses) {
Ok(accounts) => {
rpc_time.stop();
for account in accounts.into_iter().flatten() {
if thread_rng().gen_ratio(1, 10_000) {
info!(
"account: lamports {:?} size: {} owner: {:?}",
account.lamports,
account.data.len(),
account.owner
);
}
}
stats.total_success_time_us += rpc_time.as_us();
stats.success += 1;
}
Err(e) => {
rpc_time.stop();
stats.total_errors_time_us += rpc_time.as_us();
stats.errors += 1;
if last_error.elapsed().as_secs() > 2 {
info!("error: {:?}", e);
*last_error = Instant::now();
}
debug!("error: {:?}", e);
}
}
}
}
#[derive(Default)]
struct RpcBenchStats {
errors: u64,
success: u64,
total_errors_time_us: u64,
total_success_time_us: u64,
}
fn run_rpc_bench_loop(
rpc_bench: RpcBench,
thread: usize,
client: &RpcClient,
base_keypair_pubkey: &Pubkey,
exit: &AtomicBool,
program_id: &Pubkey,
max_closed: &AtomicU64,
max_created: &AtomicU64,
mint: &Option<Pubkey>,
) {
let mut stats = RpcBenchStats::default();
let mut iters = 0;
let mut last_error = Instant::now();
let mut last_print = Instant::now();
loop {
if exit.load(Ordering::Relaxed) {
break;
}
match rpc_bench {
RpcBench::Slot => {
let mut rpc_time = Measure::start("rpc-get-slot");
match client.get_slot() {
Ok(_slot) => {
rpc_time.stop();
stats.success += 1;
stats.total_success_time_us += rpc_time.as_us();
}
Err(e) => {
rpc_time.stop();
stats.total_errors_time_us += rpc_time.as_us();
stats.errors += 1;
if last_error.elapsed().as_secs() > 2 {
info!("get_slot error: {:?}", e);
last_error = Instant::now();
}
}
}
}
RpcBench::MultipleAccounts => {
process_get_multiple_accounts(
max_closed,
max_created,
&mut stats,
&mut last_error,
base_keypair_pubkey,
program_id,
client,
);
}
RpcBench::ProgramAccounts => {
let mut rpc_time = Measure::start("rpc-get-program-accounts");
match client.get_program_accounts(program_id) {
Ok(accounts) => {
rpc_time.stop();
stats.success += 1;
stats.total_success_time_us += rpc_time.as_us();
if thread_rng().gen_ratio(1, 100) {
info!("accounts: {} first: {:?}", accounts.len(), accounts.first());
}
}
Err(e) => {
rpc_time.stop();
stats.errors += 1;
stats.total_errors_time_us += rpc_time.as_us();
if last_error.elapsed().as_secs() > 2 {
info!("get-program-accounts error: {:?}", e);
last_error = Instant::now();
}
}
}
}
RpcBench::TokenAccountsByOwner => {
let mut rpc_time = Measure::start("rpc-get-token-accounts-by-owner");
let filter = TokenAccountsFilter::Mint(*mint.as_ref().unwrap());
match client.get_token_accounts_by_owner(program_id, filter) {
Ok(_accounts) => {
rpc_time.stop();
stats.success += 1;
stats.total_success_time_us += rpc_time.as_us();
}
Err(e) => {
rpc_time.stop();
stats.errors += 1;
stats.total_errors_time_us += rpc_time.as_us();
if last_error.elapsed().as_secs() > 2 {
info!("get-token-accounts error: {:?}", e);
last_error = Instant::now();
}
}
}
}
RpcBench::Version => {
let mut rpc_time = Measure::start("rpc-get-version");
match client.get_version() {
Ok(_r) => {
rpc_time.stop();
stats.success += 1;
stats.total_success_time_us += rpc_time.as_us();
}
Err(_e) => {
rpc_time.stop();
stats.errors += 1;
stats.total_errors_time_us += rpc_time.as_us();
}
}
}
}
if last_print.elapsed().as_secs() > 3 {
info!(
"t({}) rpc({:?}) iters: {} success: {} errors: {}",
thread, rpc_bench, iters, stats.success, stats.errors
);
if stats.success > 0 {
info!(
" t({}) rpc({:?} average success_time: {} us",
thread,
rpc_bench,
stats.total_success_time_us / stats.success
);
}
if stats.errors > 0 {
info!(
" rpc average average errors time: {} us",
stats.total_errors_time_us / stats.errors
);
}
last_print = Instant::now();
stats = RpcBenchStats::default();
}
iters += 1;
}
}
fn make_rpc_bench_threads(
rpc_benches: Vec<RpcBench>,
mint: &Option<Pubkey>,
exit: &Arc<AtomicBool>,
client: &Arc<RpcClient>,
seed_tracker: &SeedTracker,
base_keypair_pubkey: Pubkey,
num_rpc_bench_threads: usize,
) -> Vec<JoinHandle<()>> {
let program_id = if mint.is_some() {
inline_spl_token::id()
} else {
system_program::id()
};
rpc_benches
.into_iter()
.flat_map(|rpc_bench| {
(0..num_rpc_bench_threads).map(move |thread| {
let client = client.clone();
let exit = exit.clone();
let max_closed = seed_tracker.max_closed.clone();
let max_created = seed_tracker.max_created.clone();
let mint = *mint;
Builder::new()
.name(format!("rpc-bench-{}", thread))
.spawn(move || {
run_rpc_bench_loop(
rpc_bench,
thread,
&client,
&base_keypair_pubkey,
&exit,
&program_id,
&max_closed,
&max_created,
&mint,
)
})
.unwrap()
})
})
.collect()
}
#[allow(clippy::too_many_arguments)]
fn run_accounts_bench(
client: Arc<RpcClient>,
payer_keypairs: &[&Keypair],
iterations: usize,
maybe_space: Option<u64>,
batch_size: usize,
close_nth_batch: u64,
maybe_lamports: Option<u64>,
num_instructions: usize,
max_accounts: Option<usize>,
mint: Option<Pubkey>,
reclaim_accounts: bool,
rpc_benches: Option<Vec<RpcBench>>,
num_rpc_bench_threads: usize,
) {
assert!(num_instructions > 0);
info!("Targeting {}", client.url());
let mut latest_blockhash = Instant::now();
let mut last_log = Instant::now();
let mut count = 0;
let mut blockhash = poll_get_latest_blockhash(&client).expect("blockhash");
let mut tx_sent_count = 0;
let mut total_accounts_created = 0;
let mut total_accounts_closed = 0;
let mut balances: Vec<_> = payer_keypairs
.iter()
.map(|keypair| client.get_balance(&keypair.pubkey()).unwrap_or(0))
.collect();
let mut last_balance = Instant::now();
let default_max_lamports = 1000;
let min_balance = maybe_lamports.unwrap_or_else(|| {
let space = maybe_space.unwrap_or(default_max_lamports);
client
.get_minimum_balance_for_rent_exemption(space as usize)
.expect("min balance")
});
let base_keypair = Keypair::new();
let seed_tracker = SeedTracker {
max_created: Arc::new(AtomicU64::default()),
max_closed: Arc::new(AtomicU64::default()),
};
info!("Starting balance(s): {:?}", balances);
let executor = TransactionExecutor::new_with_rpc_client(client.clone());
// Create and close messages both require 2 signatures, fake a 2 signature message to calculate fees
let mut message = Message::new(
&[
Instruction::new_with_bytes(
Pubkey::new_unique(),
&[],
vec![AccountMeta::new(Pubkey::new_unique(), true)],
),
Instruction::new_with_bytes(
Pubkey::new_unique(),
&[],
vec![AccountMeta::new(Pubkey::new_unique(), true)],
),
],
None,
);
let exit = Arc::new(AtomicBool::new(false));
let base_keypair_pubkey = base_keypair.pubkey();
let rpc_bench_threads: Vec<_> = if let Some(rpc_benches) = rpc_benches {
make_rpc_bench_threads(
rpc_benches,
&mint,
&exit,
&client,
&seed_tracker,
base_keypair_pubkey,
num_rpc_bench_threads,
)
} else {
Vec::new()
};
loop {
if latest_blockhash.elapsed().as_millis() > 10_000 {
blockhash = poll_get_latest_blockhash(&client).expect("blockhash");
latest_blockhash = Instant::now();
}
message.recent_blockhash = blockhash;
let (fee, blockhash) = poll_get_fee_for_message(&client, &mut message);
let fee = fee.expect("get_fee_for_message");
let lamports = min_balance + fee;
for (i, balance) in balances.iter_mut().enumerate() {
if *balance < lamports || last_balance.elapsed().as_millis() > 2000 {
if let Ok(b) = client.get_balance(&payer_keypairs[i].pubkey()) {
*balance = b;
}
last_balance = Instant::now();
if *balance < lamports * 2 {
info!(
"Balance {} is less than needed: {}, doing airdrop...",
balance, lamports
);
if !airdrop_lamports(&client, payer_keypairs[i], lamports * 100_000) {
warn!("failed airdrop, exiting");
return;
}
}
}
}
// Create accounts
let sigs_len = executor.num_outstanding();
if sigs_len < batch_size {
let num_to_create = batch_size - sigs_len;
if num_to_create >= payer_keypairs.len() {
info!("creating {} new", num_to_create);
let chunk_size = num_to_create / payer_keypairs.len();
if chunk_size > 0 {
for (i, keypair) in payer_keypairs.iter().enumerate() {
let txs: Vec<_> = (0..chunk_size)
.into_par_iter()
.map(|_| {
let message = make_create_message(
keypair,
&base_keypair,
seed_tracker.max_created.clone(),
num_instructions,
min_balance,
maybe_space,
mint,
);
let signers: Vec<&Keypair> = vec![keypair, &base_keypair];
Transaction::new(&signers, message, blockhash)
})
.collect();
balances[i] = balances[i].saturating_sub(lamports * txs.len() as u64);
info!("txs: {}", txs.len());
let new_ids = executor.push_transactions(txs);
info!("ids: {}", new_ids.len());
tx_sent_count += new_ids.len();
total_accounts_created += num_instructions * new_ids.len();
}
}
}
if close_nth_batch > 0 {
let num_batches_to_close =
total_accounts_created as u64 / (close_nth_batch * batch_size as u64);
let expected_closed = num_batches_to_close * batch_size as u64;
let max_closed_seed = seed_tracker.max_closed.load(Ordering::Relaxed);
// Close every account we've created with seed between max_closed_seed..expected_closed
if max_closed_seed < expected_closed {
let txs: Vec<_> = (0..expected_closed - max_closed_seed)
.into_par_iter()
.map(|_| {
let message = make_close_message(
payer_keypairs[0],
&base_keypair,
&seed_tracker.max_created,
&seed_tracker.max_closed,
1,
min_balance,
mint.is_some(),
);
let signers: Vec<&Keypair> = vec![payer_keypairs[0], &base_keypair];
Transaction::new(&signers, message, blockhash)
})
.collect();
balances[0] = balances[0].saturating_sub(fee * txs.len() as u64);
info!("close txs: {}", txs.len());
let new_ids = executor.push_transactions(txs);
info!("close ids: {}", new_ids.len());
tx_sent_count += new_ids.len();
total_accounts_closed += new_ids.len() as u64;
}
}
} else {
let _ = executor.drain_cleared();
}
count += 1;
let max_accounts_met = if let Some(max_accounts) = max_accounts {
total_accounts_created >= max_accounts
} else {
false
};
if last_log.elapsed().as_millis() > 3000
|| (count >= iterations && iterations != 0)
|| max_accounts_met
{
info!(
"total_accounts_created: {} total_accounts_closed: {} tx_sent_count: {} loop_count: {} balance(s): {:?}",
total_accounts_created, total_accounts_closed, tx_sent_count, count, balances
);
last_log = Instant::now();
}
if iterations != 0 && count >= iterations {
info!("{iterations} iterations reached");
break;
}
if max_accounts_met {
info!(
"Max account limit of {:?} reached",
max_accounts.unwrap_or_default()
);
break;
}
if executor.num_outstanding() >= batch_size {
sleep(Duration::from_millis(500));
}
}
executor.close();
if reclaim_accounts {
let executor = TransactionExecutor::new_with_rpc_client(client.clone());
loop {
let max_closed_seed = seed_tracker.max_closed.load(Ordering::Relaxed);
let max_created_seed = seed_tracker.max_created.load(Ordering::Relaxed);
if latest_blockhash.elapsed().as_millis() > 10_000 {
blockhash = poll_get_latest_blockhash(&client).expect("blockhash");
latest_blockhash = Instant::now();
}
message.recent_blockhash = blockhash;
let (fee, blockhash) = poll_get_fee_for_message(&client, &mut message);
let fee = fee.expect("get_fee_for_message");
let sigs_len = executor.num_outstanding();
if sigs_len < batch_size && max_closed_seed < max_created_seed {
let num_to_close = min(
batch_size - sigs_len,
(max_created_seed - max_closed_seed) as usize,
);
if num_to_close >= payer_keypairs.len() {
info!("closing {} accounts", num_to_close);
let chunk_size = num_to_close / payer_keypairs.len();
info!("{:?} chunk_size", chunk_size);
if chunk_size > 0 {
for (i, keypair) in payer_keypairs.iter().enumerate() {
let txs: Vec<_> = (0..chunk_size)
.into_par_iter()
.filter_map(|_| {
let message = make_close_message(
keypair,
&base_keypair,
&seed_tracker.max_created,
&seed_tracker.max_closed,
num_instructions,
min_balance,
mint.is_some(),
);
if message.instructions.is_empty() {
return None;
}
let signers: Vec<&Keypair> = vec![keypair, &base_keypair];
Some(Transaction::new(&signers, message, blockhash))
})
.collect();
balances[i] = balances[i].saturating_sub(fee * txs.len() as u64);
info!("close txs: {}", txs.len());
let new_ids = executor.push_transactions(txs);
info!("close ids: {}", new_ids.len());
tx_sent_count += new_ids.len();
total_accounts_closed += (num_instructions * new_ids.len()) as u64;
}
}
}
} else {
let _ = executor.drain_cleared();
}
count += 1;
if last_log.elapsed().as_millis() > 3000 || max_closed_seed >= max_created_seed {
info!(
"total_accounts_closed: {} tx_sent_count: {} loop_count: {} balance(s): {:?}",
total_accounts_closed, tx_sent_count, count, balances
);
last_log = Instant::now();
}
if max_closed_seed >= max_created_seed {
break;
}
if executor.num_outstanding() >= batch_size {
sleep(Duration::from_millis(500));
}
}
executor.close();
}
exit.store(true, Ordering::Relaxed);
for t in rpc_bench_threads {
t.join().unwrap();
}
}
fn main() {
solana_logger::setup_with_default("solana=info");
let matches = App::new(crate_name!())
.about(crate_description!())
.version(solana_version::version!())
.arg({
let arg = Arg::with_name("config_file")
.short("C")
.long("config")
.value_name("FILEPATH")
.takes_value(true)
.help("Configuration file to use");
if let Some(ref config_file) = *CONFIG_FILE {
arg.default_value(config_file)
} else {
arg
}
})
.arg(
Arg::with_name("json_rpc_url")
.short("u")
.long("url")
.value_name("URL_OR_MONIKER")
.takes_value(true)
.validator(is_url_or_moniker)
.conflicts_with("entrypoint")
.help(
"URL for Solana's JSON RPC or moniker (or their first letter): \
[mainnet-beta, testnet, devnet, localhost]",
),
)
.arg(
Arg::with_name("entrypoint")
.long("entrypoint")
.takes_value(true)
.value_name("HOST:PORT")
.conflicts_with("json_rpc_url")
.help("RPC entrypoint address. Usually <ip>:8899"),
)
.arg(
Arg::with_name("faucet_addr")
.long("faucet")
.takes_value(true)
.value_name("HOST:PORT")
.hidden(hidden_unless_forced())
.help("Faucet entrypoint address. Usually <ip>:9900"),
)
.arg(
Arg::with_name("space")
.long("space")
.takes_value(true)
.value_name("BYTES")
.help("Size of accounts to create"),
)
.arg(
Arg::with_name("lamports")
.long("lamports")
.takes_value(true)
.value_name("LAMPORTS")
.help("How many lamports to fund each account"),
)
.arg(
Arg::with_name("identity")
.long("identity")
.takes_value(true)
.multiple(true)
.value_name("FILE")
.help("keypair file"),
)
.arg(
Arg::with_name("batch_size")
.long("batch-size")
.takes_value(true)
.value_name("BYTES")
.help("Number of transactions to send per batch"),
)
.arg(
Arg::with_name("close_nth_batch")
.long("close-frequency")
.takes_value(true)
.value_name("BYTES")
.help(
"Every `n` batches, create a batch of close transactions for
the earliest remaining batch of accounts created.
Note: Should be > 1 to avoid situations where the close \
transactions will be submitted before the corresponding \
create transactions have been confirmed",
),
)
.arg(
Arg::with_name("num_instructions")
.long("num-instructions")
.takes_value(true)
.value_name("NUM_INSTRUCTIONS")
.help("Number of accounts to create on each transaction"),
)
.arg(
Arg::with_name("iterations")
.long("iterations")
.takes_value(true)
.value_name("NUM_ITERATIONS")
.help("Number of iterations to make. 0 = unlimited iterations."),
)
.arg(
Arg::with_name("max_accounts")
.long("max-accounts")
.takes_value(true)
.value_name("NUM_ACCOUNTS")
.help("Halt after client has created this number of accounts. Does not count closed accounts."),
)
.arg(
Arg::with_name("check_gossip")
.long("check-gossip")
.help("Just use entrypoint address directly"),
)
.arg(
Arg::with_name("mint")
.long("mint")
.takes_value(true)
.value_name("MINT_ADDRESS")
.help("Mint address to initialize account"),
)
.arg(
Arg::with_name("reclaim_accounts")
.long("reclaim-accounts")
.takes_value(false)
.help("Reclaim accounts after session ends; incompatible with --iterations 0"),
)
.arg(
Arg::with_name("num_rpc_bench_threads")
.long("num-rpc-bench-threads")
.takes_value(true)
.value_name("NUM_THREADS")
.help("Spawn this many RPC benching threads for each type passed by --rpc-bench"),
)
.arg(
Arg::with_name("rpc_bench")
.long("rpc-bench")
.takes_value(true)
.value_name("RPC_BENCH_TYPE(S)")
.multiple(true)
.help("Spawn a thread which calls a specific RPC method in a loop to benchmark it"),
)
.get_matches();
let skip_gossip = !matches.is_present("check_gossip");
let space = value_t!(matches, "space", u64).ok();
let lamports = value_t!(matches, "lamports", u64).ok();
let batch_size = value_t!(matches, "batch_size", usize).unwrap_or(4);
let close_nth_batch = value_t!(matches, "close_nth_batch", u64).unwrap_or(0);
let iterations = value_t!(matches, "iterations", usize).unwrap_or(10);
let max_accounts = value_t!(matches, "max_accounts", usize).ok();
let num_instructions = value_t!(matches, "num_instructions", usize).unwrap_or(1);
if num_instructions == 0 || num_instructions > 500 {
eprintln!("bad num_instructions: {num_instructions}");
exit(1);
}
let rpc_benches = values_t!(matches, "rpc_bench", String)
.map(|benches| {
benches
.into_iter()
.map(|bench| RpcBench::from_str(&bench).unwrap())
.collect()
})
.ok();
let num_rpc_bench_threads = if rpc_benches.is_none() {
0
} else {
value_t!(matches, "num_rpc_bench_threads", usize).unwrap_or(1)
};
let mint = pubkey_of(&matches, "mint");
let payer_keypairs: Vec<_> = values_t_or_exit!(matches, "identity", String)
.iter()
.map(|keypair_string| {
read_keypair_file(keypair_string)
.unwrap_or_else(|_| panic!("bad keypair {keypair_string:?}"))
})
.collect();
let mut payer_keypair_refs: Vec<&Keypair> = vec![];
for keypair in payer_keypairs.iter() {
payer_keypair_refs.push(keypair);
}
let client = if let Some(addr) = matches.value_of("entrypoint") {
let entrypoint_addr = solana_net_utils::parse_host_port(addr).unwrap_or_else(|e| {
eprintln!("failed to parse entrypoint address: {e}");
exit(1)
});
let rpc_addr = if !skip_gossip {
info!("Finding cluster entry: {:?}", entrypoint_addr);
let (gossip_nodes, _validators) = discover(
None, // keypair
Some(&entrypoint_addr),
None, // num_nodes
Duration::from_secs(60), // timeout
None, // find_nodes_by_pubkey
Some(&entrypoint_addr), // find_node_by_gossip_addr
None, // my_gossip_addr
0, // my_shred_version
SocketAddrSpace::Unspecified,
)
.unwrap_or_else(|err| {
eprintln!("Failed to discover {entrypoint_addr} node: {err:?}");
exit(1);
});
info!("done found {} nodes", gossip_nodes.len());
gossip_nodes[0].rpc().unwrap()
} else {
info!("Using {:?} as the RPC address", entrypoint_addr);
entrypoint_addr
};
Arc::new(RpcClient::new_socket_with_commitment(
rpc_addr,
CommitmentConfig::confirmed(),
))
} else {
let config = if let Some(config_file) = matches.value_of("config_file") {
solana_cli_config::Config::load(config_file).unwrap_or_default()
} else {
solana_cli_config::Config::default()
};
let (_, json_rpc_url) = ConfigInput::compute_json_rpc_url_setting(
matches.value_of("json_rpc_url").unwrap_or(""),
&config.json_rpc_url,
);
Arc::new(RpcClient::new_with_commitment(
json_rpc_url,
CommitmentConfig::confirmed(),
))
};
run_accounts_bench(
client,
&payer_keypair_refs,
iterations,
space,
batch_size,
close_nth_batch,
lamports,
num_instructions,
max_accounts,
mint,
matches.is_present("reclaim_accounts"),
rpc_benches,
num_rpc_bench_threads,
);
}
#[cfg(test)]
pub mod test {
use {
super::*,
solana_accounts_db::{
accounts_index::{AccountIndex, AccountSecondaryIndexes},
inline_spl_token,
},
solana_core::validator::ValidatorConfig,
solana_faucet::faucet::run_local_faucet,
solana_local_cluster::{
local_cluster::{ClusterConfig, LocalCluster},
validator_configs::make_identical_validator_configs,
},
solana_measure::measure::Measure,
solana_sdk::{native_token::sol_to_lamports, poh_config::PohConfig},
solana_test_validator::TestValidator,
spl_token::{
solana_program::program_pack::Pack,
state::{Account, Mint},
},
};
fn add_secondary_indexes(indexes: &mut AccountSecondaryIndexes) {
indexes.indexes.insert(AccountIndex::SplTokenOwner);
indexes.indexes.insert(AccountIndex::SplTokenMint);
indexes.indexes.insert(AccountIndex::ProgramId);
}
#[test]
fn test_accounts_cluster_bench() {
solana_logger::setup();
let mut validator_config = ValidatorConfig::default_for_test();
let num_nodes = 1;
add_secondary_indexes(&mut validator_config.account_indexes);
add_secondary_indexes(&mut validator_config.rpc_config.account_indexes);
let mut config = ClusterConfig {
cluster_lamports: 10_000_000,
poh_config: PohConfig::new_sleep(Duration::from_millis(50)),
node_stakes: vec![100; num_nodes],
validator_configs: make_identical_validator_configs(&validator_config, num_nodes),
..ClusterConfig::default()
};
let cluster = LocalCluster::new(&mut config, SocketAddrSpace::Unspecified);
let iterations = 10;
let maybe_space = None;
let batch_size = 100;
let close_nth_batch = 100;
let maybe_lamports = None;
let num_instructions = 2;
let mut start = Measure::start("total accounts run");
let rpc_addr = cluster.entry_point_info.rpc().unwrap();
let client = Arc::new(RpcClient::new_socket_with_commitment(
rpc_addr,
CommitmentConfig::confirmed(),
));
let mint = None;
let reclaim_accounts = false;
let pre_txs = client.get_transaction_count().unwrap();
run_accounts_bench(
client.clone(),
&[&cluster.funding_keypair],
iterations,
maybe_space,
batch_size,
close_nth_batch,
maybe_lamports,
num_instructions,
None,
mint,
reclaim_accounts,
Some(vec![RpcBench::ProgramAccounts]),
1,
);
let post_txs = client.get_transaction_count().unwrap();
start.stop();
info!("{} pre {} post {}", start, pre_txs, post_txs);
}
#[test]
fn test_halt_accounts_creation_at_max() {
solana_logger::setup();
let mut validator_config = ValidatorConfig::default_for_test();
let num_nodes = 1;
add_secondary_indexes(&mut validator_config.account_indexes);
add_secondary_indexes(&mut validator_config.rpc_config.account_indexes);
let mut config = ClusterConfig {
cluster_lamports: 10_000_000,
poh_config: PohConfig::new_sleep(Duration::from_millis(50)),
node_stakes: vec![100; num_nodes],
validator_configs: make_identical_validator_configs(&validator_config, num_nodes),
..ClusterConfig::default()
};
let cluster = LocalCluster::new(&mut config, SocketAddrSpace::Unspecified);
let iterations = 100;
let maybe_space = None;
let batch_size = 20;
let close_nth_batch = 0;
let maybe_lamports = None;
let num_instructions = 2;
let mut start = Measure::start("total accounts run");
let rpc_addr = cluster.entry_point_info.rpc().unwrap();
let client = Arc::new(RpcClient::new_socket_with_commitment(
rpc_addr,
CommitmentConfig::confirmed(),
));
let mint = None;
let reclaim_accounts = false;
let pre_txs = client.get_transaction_count().unwrap();
run_accounts_bench(
client.clone(),
&[&cluster.funding_keypair],
iterations,
maybe_space,
batch_size,
close_nth_batch,
maybe_lamports,
num_instructions,
Some(90),
mint,
reclaim_accounts,
Some(vec![RpcBench::ProgramAccounts]),
1,
);
let post_txs = client.get_transaction_count().unwrap();
start.stop();
info!("{} pre {} post {}", start, pre_txs, post_txs);
}
#[test]
fn test_create_then_reclaim_spl_token_accounts() {
solana_logger::setup();
let mint_keypair = Keypair::new();
let mint_pubkey = mint_keypair.pubkey();
let faucet_addr = run_local_faucet(mint_keypair, None);
let test_validator = TestValidator::with_custom_fees(
mint_pubkey,
1,
Some(faucet_addr),
SocketAddrSpace::Unspecified,
);
let rpc_client = Arc::new(RpcClient::new_with_commitment(
test_validator.rpc_url(),
CommitmentConfig::processed(),
));
// Created funder
let funder = Keypair::new();
let latest_blockhash = rpc_client.get_latest_blockhash().unwrap();
let signature = rpc_client
.request_airdrop_with_blockhash(
&funder.pubkey(),
sol_to_lamports(1.0),
&latest_blockhash,
)
.unwrap();
rpc_client
.confirm_transaction_with_spinner(
&signature,
&latest_blockhash,
CommitmentConfig::confirmed(),
)
.unwrap();
// Create Mint
let spl_mint_keypair = Keypair::new();
let spl_mint_len = Mint::get_packed_len();
let spl_mint_rent = rpc_client
.get_minimum_balance_for_rent_exemption(spl_mint_len)
.unwrap();
let transaction = Transaction::new_signed_with_payer(
&[
system_instruction::create_account(
&funder.pubkey(),
&spl_mint_keypair.pubkey(),
spl_mint_rent,
spl_mint_len as u64,
&inline_spl_token::id(),
),
spl_token::instruction::initialize_mint(
&spl_token::id(),
&spl_mint_keypair.pubkey(),
&spl_mint_keypair.pubkey(),
None,
2,
)
.unwrap(),
],
Some(&funder.pubkey()),
&[&funder, &spl_mint_keypair],
latest_blockhash,
);
let _sig = rpc_client
.send_and_confirm_transaction(&transaction)
.unwrap();
let account_len = Account::get_packed_len();
let minimum_balance = rpc_client
.get_minimum_balance_for_rent_exemption(account_len)
.unwrap();
let iterations = 5;
let batch_size = 100;
let close_nth_batch = 0;
let num_instructions = 4;
let mut start = Measure::start("total accounts run");
let keypair0 = Keypair::new();
let keypair1 = Keypair::new();
let keypair2 = Keypair::new();
run_accounts_bench(
rpc_client,
&[&keypair0, &keypair1, &keypair2],
iterations,
Some(account_len as u64),
batch_size,
close_nth_batch,
Some(minimum_balance),
num_instructions,
None,
Some(spl_mint_keypair.pubkey()),
true,
None,
0,
);
start.stop();
info!("{}", start);
}
}