Skip to content

Commit

Permalink
perf(sequencer): add benchmark for prepare_proposal (ENG-660) (astria…
Browse files Browse the repository at this point in the history
…org#1337)

## Summary
Addition of a new benchmark target to assess the performance of the
prepare_proposal method in sequencer's `App`.

## Background
Previous perf work has indicated this is a bottleneck. However, making
that determination was done via spamoor which is slightly convoluted to
run. Before working to improve the performance, we want to have a faster
feedback loop on the effects of updates, hence the need for a benchmark
which is easy to run and which isolates the slow function.

## Changes
- Added benchmark to `app` module. Currently this has only one case: a
mempool filled with transactions containing exclusively transfers. This
matches the shape of the data being sent when using spamoor.
- Added `benchmark` feature to enable sharing some of the existing test
utils.

## Testing
This is a new test.

Example of running `cargo bench --features=benchmark -qp
astria-sequencer app` on my Ryzen 7900X:
```
Timer precision: 10 ns
astria_sequencer                                fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ app                                                        │               │               │               │         │
   ╰─ benchmarks                                              │               │               │               │         │
      ╰─ execute_transactions_prepare_proposal  11.63 s       │ 14.68 s       │ 12.74 s       │ 12.88 s       │ 10      │ 10
```

Since rebasing after astriaorg#1317 has merged, the same run shows (as expected)
a slowdown:
```
astria_sequencer                                fastest       │ slowest       │ median        │ mean          │ samples │ iters
╰─ app                                                        │               │               │               │         │
   ╰─ benchmarks                                              │               │               │               │         │
      ╰─ execute_transactions_prepare_proposal  14.49 s       │ 17 s          │ 16.52 s       │ 15.98 s       │ 8       │ 8
```

## Related Issues
Closes astriaorg#1314.
  • Loading branch information
Fraser999 committed Aug 23, 2024
1 parent 7cf659b commit ab8b80b
Show file tree
Hide file tree
Showing 9 changed files with 288 additions and 98 deletions.
5 changes: 3 additions & 2 deletions crates/astria-sequencer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ homepage = "https://astria.org"
name = "astria-sequencer"

[features]
default = []
benchmark = ["divan"]

[dependencies]
astria-core = { path = "../astria-core", features = ["server", "serde", "borsh"] }
Expand All @@ -38,7 +38,7 @@ tower-http = { version = "0.4", features = ["cors"] }
async-trait = { workspace = true }
borsh = { workspace = true }
bytes = { workspace = true }
divan = { workspace = true }
divan = { workspace = true, optional = true }
futures = { workspace = true }
hex = { workspace = true, features = ["serde"] }
ibc-types = { workspace = true, features = ["with_serde"] }
Expand Down Expand Up @@ -79,3 +79,4 @@ astria-build-info = { path = "../astria-build-info", features = ["build"] }
[[bench]]
name = "benchmark"
harness = false
required-features = ["benchmark"]
113 changes: 113 additions & 0 deletions crates/astria-sequencer/src/app/benchmarks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//! To run the benchmark, from the root of the monorepo, run:
//! ```sh
//! cargo bench --features=benchmark -qp astria-sequencer app
//! ```
use std::time::Duration;

use astria_core::sequencer::{
Account,
AddressPrefixes,
GenesisState,
UncheckedGenesisState,
};
use cnidarium::Storage;
use penumbra_ibc::params::IBCParameters;

use crate::{
app::{
test_utils,
App,
},
benchmark_utils::{
self,
TxTypes,
SIGNER_COUNT,
},
proposal::block_size_constraints::BlockSizeConstraints,
test_utils::{
astria_address,
nria,
ASTRIA_PREFIX,
},
};

/// The max time for any benchmark.
const MAX_TIME: Duration = Duration::from_secs(120);
/// The value provided to `BlockSizeConstraints::new` to constrain block sizes.
///
/// Taken from the actual value seen in `prepare_proposal.max_tx_bytes` when handling
/// `prepare_proposal` during stress testing using spamoor.
const COMETBFT_MAX_TX_BYTES: usize = 22_019_254;

struct Fixture {
app: App,
_storage: Storage,
}

impl Fixture {
/// Initializes a new `App` instance with the genesis accounts derived from the secret keys of
/// `benchmark_utils::signing_keys()`, and inserts transactions into the app mempool.
async fn new() -> Fixture {
let accounts = benchmark_utils::signing_keys()
.enumerate()
.take(usize::from(SIGNER_COUNT))
.map(|(index, signing_key)| Account {
address: astria_address(&signing_key.address_bytes()),
balance: 10u128
.pow(19)
.saturating_add(u128::try_from(index).unwrap()),
})
.collect::<Vec<_>>();
let address_prefixes = AddressPrefixes {
base: ASTRIA_PREFIX.into(),
};
let first_address = accounts.first().unwrap().address;
let unchecked_genesis_state = UncheckedGenesisState {
accounts,
address_prefixes,
authority_sudo_address: first_address,
ibc_sudo_address: first_address,
ibc_relayer_addresses: vec![],
native_asset_base_denomination: nria(),
ibc_params: IBCParameters::default(),
allowed_fee_assets: vec![nria().into()],
fees: test_utils::default_fees(),
};
let genesis_state = GenesisState::try_from(unchecked_genesis_state).unwrap();

let (app, storage) =
test_utils::initialize_app_with_storage(Some(genesis_state), vec![]).await;

for tx in benchmark_utils::transactions(TxTypes::AllTransfers) {
app.mempool.insert(tx.clone(), 0).await.unwrap();
}
Fixture {
app,
_storage: storage,
}
}
}

#[divan::bench(max_time = MAX_TIME)]
fn execute_transactions_prepare_proposal(bencher: divan::Bencher) {
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap();
let mut fixture = runtime.block_on(async { Fixture::new().await });
bencher
.with_inputs(|| BlockSizeConstraints::new(COMETBFT_MAX_TX_BYTES).unwrap())
.bench_local_refs(|constraints| {
let (_tx_bytes, included_txs) = runtime.block_on(async {
fixture
.app
.execute_transactions_prepare_proposal(constraints)
.await
.unwrap()
});
// Ensure we actually processed some txs. This will trip if execution fails for all
// txs, or more likely, if the mempool becomes exhausted of txs.
assert!(!included_txs.is_empty());
});
}
10 changes: 6 additions & 4 deletions crates/astria-sequencer/src/app/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#[cfg(test)]
#[cfg(feature = "benchmark")]
mod benchmarks;
#[cfg(any(test, feature = "benchmark"))]
pub(crate) mod test_utils;
#[cfg(test)]
mod tests_app;
Expand Down Expand Up @@ -924,7 +926,7 @@ impl App {
}

#[instrument(name = "App::begin_block", skip_all)]
pub(crate) async fn begin_block(
async fn begin_block(
&mut self,
begin_block: &abci::request::BeginBlock,
) -> anyhow::Result<Vec<abci::Event>> {
Expand Down Expand Up @@ -957,7 +959,7 @@ impl App {

/// Executes a signed transaction.
#[instrument(name = "App::execute_transaction", skip_all)]
pub(crate) async fn execute_transaction(
async fn execute_transaction(
&mut self,
signed_tx: Arc<SignedTransaction>,
) -> anyhow::Result<Vec<Event>> {
Expand All @@ -975,7 +977,7 @@ impl App {
}

#[instrument(name = "App::end_block", skip_all)]
pub(crate) async fn end_block(
async fn end_block(
&mut self,
height: u64,
fee_recipient: [u8; 20],
Expand Down
6 changes: 6 additions & 0 deletions crates/astria-sequencer/src/app/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub(crate) const CAROL_ADDRESS: &str = "60709e2d391864b732b4f0f51e387abb76743871
pub(crate) const JUDY_ADDRESS: &str = "bc5b91da07778eeaf622d0dcf4d7b4233525998d";
pub(crate) const TED_ADDRESS: &str = "4c4f91d8a918357ab5f6f19c1e179968fc39bb44";

#[cfg_attr(feature = "benchmark", allow(dead_code))]
pub(crate) fn get_alice_signing_key() -> SigningKey {
// this secret key corresponds to ALICE_ADDRESS
let alice_secret_bytes: [u8; 32] =
Expand All @@ -47,6 +48,7 @@ pub(crate) fn get_alice_signing_key() -> SigningKey {
SigningKey::from(alice_secret_bytes)
}

#[cfg_attr(feature = "benchmark", allow(dead_code))]
pub(crate) fn get_bridge_signing_key() -> SigningKey {
let bridge_secret_bytes: [u8; 32] =
hex::decode("db4982e01f3eba9e74ac35422fcd49aa2b47c3c535345c7e7da5220fe3a0ce79")
Expand All @@ -56,6 +58,7 @@ pub(crate) fn get_bridge_signing_key() -> SigningKey {
SigningKey::from(bridge_secret_bytes)
}

#[cfg_attr(feature = "benchmark", allow(dead_code))]
pub(crate) fn default_genesis_accounts() -> Vec<Account> {
vec![
Account {
Expand All @@ -73,6 +76,7 @@ pub(crate) fn default_genesis_accounts() -> Vec<Account> {
]
}

#[cfg_attr(feature = "benchmark", allow(dead_code))]
pub(crate) fn default_fees() -> Fees {
Fees {
transfer_base_fee: 12,
Expand Down Expand Up @@ -130,6 +134,7 @@ pub(crate) async fn initialize_app_with_storage(
(app, storage.clone())
}

#[cfg_attr(feature = "benchmark", allow(dead_code))]
pub(crate) async fn initialize_app(
genesis_state: Option<GenesisState>,
genesis_validators: Vec<ValidatorUpdate>,
Expand All @@ -138,6 +143,7 @@ pub(crate) async fn initialize_app(
app
}

#[cfg_attr(feature = "benchmark", allow(dead_code))]
pub(crate) fn mock_tx(
nonce: u32,
signer: &SigningKey,
Expand Down
132 changes: 132 additions & 0 deletions crates/astria-sequencer/src/benchmark_utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use std::{
collections::HashMap,
sync::{
Arc,
OnceLock,
},
};

use astria_core::{
crypto::SigningKey,
primitive::v1::{
asset::{
Denom,
IbcPrefixed,
},
RollupId,
},
protocol::transaction::v1alpha1::{
action::{
Action,
SequenceAction,
TransferAction,
},
SignedTransaction,
TransactionParams,
UnsignedTransaction,
},
};

use crate::test_utils::{
astria_address,
nria,
};

/// The number of different signers of transactions, and also the number of different chain IDs.
pub(crate) const SIGNER_COUNT: u8 = 10;
/// The number of transfers per transaction.
///
/// 2866 chosen after experimentation of spamming composer.
pub(crate) const TRANSFERS_PER_TX: usize = 2866;

const SEQUENCE_ACTION_TX_COUNT: usize = 100_001;
const TRANSFERS_TX_COUNT: usize = 10_000;

#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
pub(crate) enum TxTypes {
AllSequenceActions,
AllTransfers,
}

/// Returns an endlessly-repeating iterator over `SIGNER_COUNT` separate signing keys.
pub(crate) fn signing_keys() -> impl Iterator<Item = &'static SigningKey> {
static SIGNING_KEYS: OnceLock<Vec<SigningKey>> = OnceLock::new();
SIGNING_KEYS
.get_or_init(|| {
(0..SIGNER_COUNT)
.map(|i| SigningKey::from([i; 32]))
.collect()
})
.iter()
.cycle()
}

/// Returns a static ref to a collection of `MAX_INITIAL_TXS + 1` transactions.
pub(crate) fn transactions(tx_types: TxTypes) -> &'static Vec<Arc<SignedTransaction>> {
static TXS: OnceLock<HashMap<TxTypes, Vec<Arc<SignedTransaction>>>> = OnceLock::new();
TXS.get_or_init(|| {
let mut map = HashMap::new();
map.insert(TxTypes::AllSequenceActions, sequence_actions());
map.insert(TxTypes::AllTransfers, transfers());
map
})
.get(&tx_types)
.unwrap()
}

fn sequence_actions() -> Vec<Arc<SignedTransaction>> {
let mut nonces_and_chain_ids = HashMap::new();
signing_keys()
.map(move |signing_key| {
let verification_key = signing_key.verification_key();
let (nonce, chain_id) = nonces_and_chain_ids
.entry(verification_key)
.or_insert_with(|| (0_u32, format!("chain-{}", signing_key.verification_key())));
*nonce = (*nonce).wrapping_add(1);
let params = TransactionParams::builder()
.nonce(*nonce)
.chain_id(chain_id.as_str())
.build();
let sequence_action = SequenceAction {
rollup_id: RollupId::new([1; 32]),
data: vec![2; 1000].into(),
fee_asset: Denom::IbcPrefixed(IbcPrefixed::new([3; 32])),
};
let tx = UnsignedTransaction {
actions: vec![Action::Sequence(sequence_action)],
params,
}
.into_signed(signing_key);
Arc::new(tx)
})
.take(SEQUENCE_ACTION_TX_COUNT)
.collect()
}

fn transfers() -> Vec<Arc<SignedTransaction>> {
let sender = signing_keys().next().unwrap();
let receiver = signing_keys().nth(1).unwrap();
let to = astria_address(&receiver.address_bytes());
let action = Action::from(TransferAction {
to,
amount: 1,
asset: nria().into(),
fee_asset: nria().into(),
});
(0..TRANSFERS_TX_COUNT)
.map(|nonce| {
let params = TransactionParams::builder()
.nonce(u32::try_from(nonce).unwrap())
.chain_id("test")
.build();
let tx = UnsignedTransaction {
actions: std::iter::repeat(action.clone())
.take(TRANSFERS_PER_TX)
.collect(),
params,
}
.into_signed(sender);
Arc::new(tx)
})
.collect()
}
4 changes: 3 additions & 1 deletion crates/astria-sequencer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ mod api_state_ext;
pub(crate) mod app;
pub(crate) mod assets;
pub(crate) mod authority;
#[cfg(feature = "benchmark")]
pub(crate) mod benchmark_utils;
pub(crate) mod bridge;
mod build_info;
pub(crate) mod component;
Expand All @@ -20,7 +22,7 @@ pub(crate) mod service;
pub(crate) mod state_ext;
pub(crate) mod storage;
pub(crate) mod storage_keys;
#[cfg(test)]
#[cfg(any(test, feature = "benchmark"))]
pub(crate) mod test_utils;
pub(crate) mod transaction;
mod utils;
Expand Down
Loading

0 comments on commit ab8b80b

Please sign in to comment.