bench(txpool): reordering (#3882)

This commit is contained in:
Roman Krasiuk
2023-07-27 19:36:07 +03:00
committed by GitHub
parent e651a184d1
commit a298756d95
7 changed files with 329 additions and 3 deletions

2
Cargo.lock generated
View File

@@ -5870,10 +5870,12 @@ dependencies = [
"async-trait",
"auto_impl",
"bitflags 1.3.2",
"criterion",
"fnv",
"futures-util",
"parking_lot 0.12.1",
"paste",
"proptest",
"rand 0.8.5",
"reth-interfaces",
"reth-metrics",

View File

@@ -44,12 +44,20 @@ auto_impl = "1.0"
# testing
rand = { workspace = true, optional = true }
paste = { version = "1.0", optional = true }
proptest = { version = "1.0", optional = true }
[dev-dependencies]
paste = "1.0"
rand = "0.8"
proptest = "1.0"
criterion = "0.5"
[features]
default = ["serde"]
serde = ["dep:serde"]
test-utils = ["rand", "paste", "serde"]
arbitrary = ["proptest", "reth-primitives/arbitrary"]
[[bench]]
name = "reorder"
harness = false

View File

@@ -0,0 +1,220 @@
use criterion::{
black_box, criterion_group, criterion_main, measurement::WallTime, BenchmarkGroup, Criterion,
};
use proptest::{
prelude::*,
strategy::{Strategy, ValueTree},
test_runner::TestRunner,
};
use reth_transaction_pool::test_utils::MockTransaction;
/// Transaction Pool trait for benching.
pub trait BenchTxPool: Default {
fn add_transaction(&mut self, tx: MockTransaction);
fn reorder(&mut self, base_fee: u64);
}
pub fn txpool_reordering(c: &mut Criterion) {
let mut group = c.benchmark_group("Transaction Pool Reordering");
for seed_size in [1_000, 10_000, 50_000, 100_000] {
for input_size in [10, 100, 1_000] {
let (txs, new_txs, base_fee) = generate_test_data(seed_size, input_size);
use implementations::*;
// Vanilla sorting of unsorted collection
txpool_reordering_bench::<VecTxPoolSortStable>(
&mut group,
"VecTxPoolSortStable",
txs.clone(),
new_txs.clone(),
base_fee,
);
// Unstable sorting of unsorted collection
txpool_reordering_bench::<VecTxPoolSortUnstable>(
&mut group,
"VecTxPoolSortUnstable",
txs.clone(),
new_txs.clone(),
base_fee,
);
// BinaryHeap that is resorted on each update
txpool_reordering_bench::<BinaryHeapTxPool>(
&mut group,
"BinaryHeapTxPool",
txs,
new_txs,
base_fee,
);
}
}
}
fn txpool_reordering_bench<T: BenchTxPool>(
group: &mut BenchmarkGroup<WallTime>,
description: &str,
seed: Vec<MockTransaction>,
new_txs: Vec<MockTransaction>,
base_fee: u64,
) {
let setup = || {
let mut txpool = T::default();
txpool.reorder(base_fee);
for tx in seed.iter() {
txpool.add_transaction(tx.clone());
}
(txpool, new_txs.clone())
};
let group_id = format!(
"txpool | seed size: {} | input size: {} | {}",
seed.len(),
new_txs.len(),
description
);
group.bench_function(group_id, |b| {
b.iter_with_setup(setup, |(mut txpool, new_txs)| {
black_box({
// Reorder with new base fee
let bigger_base_fee = base_fee.saturating_add(10);
txpool.reorder(bigger_base_fee);
// Reorder with new base fee after adding transactions.
for new_tx in new_txs {
txpool.add_transaction(new_tx);
}
let smaller_base_fee = base_fee.saturating_sub(10);
txpool.reorder(smaller_base_fee);
})
});
});
}
fn generate_test_data(
seed_size: usize,
input_size: usize,
) -> (Vec<MockTransaction>, Vec<MockTransaction>, u64) {
let config = ProptestConfig::default();
let mut runner = TestRunner::new(config);
let txs = prop::collection::vec(any::<MockTransaction>(), seed_size)
.new_tree(&mut runner)
.unwrap()
.current();
let new_txs = prop::collection::vec(any::<MockTransaction>(), input_size)
.new_tree(&mut runner)
.unwrap()
.current();
let base_fee = any::<u64>().new_tree(&mut runner).unwrap().current();
(txs, new_txs, base_fee)
}
mod implementations {
use super::*;
use reth_transaction_pool::PoolTransaction;
use std::collections::BinaryHeap;
/// This implementation appends the transactions and uses [Vec::sort_by] function for sorting.
#[derive(Default)]
pub struct VecTxPoolSortStable {
inner: Vec<MockTransaction>,
}
impl BenchTxPool for VecTxPoolSortStable {
fn add_transaction(&mut self, tx: MockTransaction) {
self.inner.push(tx);
}
fn reorder(&mut self, base_fee: u64) {
self.inner.sort_by(|a, b| {
a.effective_tip_per_gas(base_fee)
.expect("exists")
.cmp(&b.effective_tip_per_gas(base_fee).expect("exists"))
})
}
}
/// This implementation appends the transactions and uses [Vec::sort_unstable_by] function for
/// sorting.
#[derive(Default)]
pub struct VecTxPoolSortUnstable {
inner: Vec<MockTransaction>,
}
impl BenchTxPool for VecTxPoolSortUnstable {
fn add_transaction(&mut self, tx: MockTransaction) {
self.inner.push(tx);
}
fn reorder(&mut self, base_fee: u64) {
self.inner.sort_unstable_by(|a, b| {
a.effective_tip_per_gas(base_fee)
.expect("exists")
.cmp(&b.effective_tip_per_gas(base_fee).expect("exists"))
})
}
}
struct MockTransactionWithPriority {
tx: MockTransaction,
priority: u128,
}
impl PartialEq for MockTransactionWithPriority {
fn eq(&self, other: &Self) -> bool {
self.priority.eq(&other.priority)
}
}
impl Eq for MockTransactionWithPriority {}
impl PartialOrd for MockTransactionWithPriority {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.priority.partial_cmp(&other.priority)
}
}
impl Ord for MockTransactionWithPriority {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.priority.cmp(&other.priority)
}
}
/// This implementation uses BinaryHeap which is drained and reconstructed on each reordering.
#[derive(Default)]
pub struct BinaryHeapTxPool {
inner: BinaryHeap<MockTransactionWithPriority>,
base_fee: Option<u64>,
}
impl BenchTxPool for BinaryHeapTxPool {
fn add_transaction(&mut self, tx: MockTransaction) {
let priority = self
.base_fee
.as_ref()
.map(|bf| tx.effective_tip_per_gas(*bf).expect("set"))
.unwrap_or_default();
self.inner.push(MockTransactionWithPriority { tx, priority });
}
fn reorder(&mut self, base_fee: u64) {
self.base_fee = Some(base_fee);
let drained = self.inner.drain();
self.inner = BinaryHeap::from_iter(drained.map(|mock| {
let priority = mock.tx.effective_tip_per_gas(base_fee).expect("set");
MockTransactionWithPriority { tx: mock.tx, priority }
}));
}
}
}
criterion_group!(reorder, txpool_reordering);
criterion_main!(reorder);

View File

@@ -10,8 +10,7 @@
rust_2018_idioms,
unreachable_pub,
missing_debug_implementations,
rustdoc::broken_intra_doc_links,
unused_crate_dependencies
rustdoc::broken_intra_doc_links
)]
#![doc(test(
no_crate_inject,

View File

@@ -14,7 +14,7 @@ use rand::{
use reth_primitives::{
constants::MIN_PROTOCOL_BASE_FEE, hex, Address, FromRecoveredTransaction,
IntoRecoveredTransaction, Signature, Transaction, TransactionKind, TransactionSigned,
TransactionSignedEcRecovered, TxEip1559, TxHash, TxLegacy, TxType, H256, U128, U256,
TransactionSignedEcRecovered, TxEip1559, TxEip2930, TxHash, TxLegacy, TxType, H256, U128, U256,
};
use std::{ops::Range, sync::Arc, time::Instant};
@@ -360,6 +360,21 @@ impl PoolTransaction for MockTransaction {
}
}
fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
let base_fee = base_fee as u128;
let max_fee_per_gas = self.max_fee_per_gas();
if max_fee_per_gas < base_fee {
return None
}
let fee = max_fee_per_gas - base_fee;
if let Some(priority_fee) = self.max_priority_fee_per_gas() {
return Some(fee.min(priority_fee))
}
Some(fee)
}
fn kind(&self) -> &TransactionKind {
match self {
MockTransaction::Legacy { to, .. } => to,
@@ -461,6 +476,66 @@ impl IntoRecoveredTransaction for MockTransaction {
}
}
#[cfg(any(test, feature = "arbitrary"))]
impl proptest::arbitrary::Arbitrary for MockTransaction {
type Parameters = ();
type Strategy = proptest::strategy::BoxedStrategy<MockTransaction>;
fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
use proptest::prelude::{any, Strategy};
any::<(Transaction, Address, H256)>()
.prop_map(|(tx, sender, tx_hash)| match &tx {
Transaction::Legacy(TxLegacy {
nonce,
gas_price,
gas_limit,
to,
value,
input,
..
}) |
Transaction::Eip2930(TxEip2930 {
nonce,
gas_price,
gas_limit,
to,
value,
input,
..
}) => MockTransaction::Legacy {
sender,
hash: tx_hash,
nonce: *nonce,
gas_price: *gas_price,
gas_limit: *gas_limit,
to: *to,
value: U256::from(*value),
},
Transaction::Eip1559(TxEip1559 {
nonce,
gas_limit,
max_fee_per_gas,
max_priority_fee_per_gas,
to,
value,
input,
..
}) => MockTransaction::Eip1559 {
sender,
hash: tx_hash,
nonce: *nonce,
max_fee_per_gas: *max_fee_per_gas,
max_priority_fee_per_gas: *max_priority_fee_per_gas,
gas_limit: *gas_limit,
to: *to,
value: U256::from(*value),
},
})
.boxed()
}
}
#[derive(Default)]
pub struct MockTransactionFactory {
pub(crate) ids: SenderIdentifiers,

View File

@@ -491,6 +491,12 @@ pub trait PoolTransaction:
/// This will return `None` for non-EIP1559 transactions
fn max_priority_fee_per_gas(&self) -> Option<u128>;
/// Returns the effective tip for this transaction.
///
/// For EIP-1559 transactions: `min(max_fee_per_gas - base_fee, max_priority_fee_per_gas)`.
/// For legacy transactions: `gas_price - base_fee`.
fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128>;
/// Returns the transaction's [`TransactionKind`], which is the address of the recipient or
/// [`TransactionKind::Create`] if the transaction is a contract creation.
fn kind(&self) -> &TransactionKind;
@@ -599,6 +605,14 @@ impl PoolTransaction for PooledTransaction {
}
}
/// Returns the effective tip for this transaction.
///
/// For EIP-1559 transactions: `min(max_fee_per_gas - base_fee, max_priority_fee_per_gas)`.
/// For legacy transactions: `gas_price - base_fee`.
fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
self.transaction.effective_tip_per_gas(base_fee)
}
/// Returns the transaction's [`TransactionKind`], which is the address of the recipient or
/// [`TransactionKind::Create`] if the transaction is a contract creation.
fn kind(&self) -> &TransactionKind {

View File

@@ -179,6 +179,14 @@ impl<T: PoolTransaction> ValidPoolTransaction<T> {
self.transaction.max_fee_per_gas()
}
/// Returns the effective tip for this transaction.
///
/// For EIP-1559 transactions: `min(max_fee_per_gas - base_fee, max_priority_fee_per_gas)`.
/// For legacy transactions: `gas_price - base_fee`.
pub fn effective_tip_per_gas(&self, base_fee: u64) -> Option<u128> {
self.transaction.effective_tip_per_gas(base_fee)
}
/// Maximum amount of gas that the transaction is allowed to consume.
pub fn gas_limit(&self) -> u64 {
self.transaction.gas_limit()