Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions crates/storage/db-api/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ pub trait DbTxMut: Send + Sync {

/// Put value to database
fn put<T: Table>(&self, key: T::Key, value: T::Value) -> Result<(), DatabaseError>;
/// Append value with the largest key to database. This should have the same
/// outcome as `put`, but databases like MDBX provide dedicated modes to make
/// it much faster, typically from O(logN) down to O(1) thanks to no lookup.
fn append<T: Table>(&self, key: T::Key, value: T::Value) -> Result<(), DatabaseError> {
self.put::<T>(key, value)
}
/// Delete value from database
fn delete<T: Table>(&self, key: T::Key, value: Option<T::Value>)
-> Result<bool, DatabaseError>;
Expand Down
5 changes: 5 additions & 0 deletions crates/storage/db/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,8 @@ harness = false
name = "get"
required-features = ["test-utils"]
harness = false

[[bench]]
name = "put"
required-features = ["test-utils"]
harness = false
44 changes: 44 additions & 0 deletions crates/storage/db/benches/put.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#![allow(missing_docs)]

use alloy_primitives::B256;
use criterion::{criterion_group, criterion_main, Criterion};
use reth_db::{test_utils::create_test_rw_db_with_path, CanonicalHeaders, Database};
use reth_db_api::transaction::DbTxMut;

mod utils;
use utils::BENCH_DB_PATH;

const NUM_BLOCKS: u64 = 1_000_000;

criterion_group! {
name = benches;
config = Criterion::default();
targets = put
}
criterion_main!(benches);

// Small benchmark showing that `append` is much faster than `put` when keys are put in order
fn put(c: &mut Criterion) {
let mut group = c.benchmark_group("Put");

let setup = || {
let _ = std::fs::remove_dir_all(BENCH_DB_PATH);
create_test_rw_db_with_path(BENCH_DB_PATH).tx_mut().expect("tx")
};

group.bench_function("put", |b| {
b.iter_with_setup(setup, |tx| {
for i in 0..NUM_BLOCKS {
tx.put::<CanonicalHeaders>(i, B256::ZERO).unwrap();
}
})
});

group.bench_function("append", |b| {
b.iter_with_setup(setup, |tx| {
for i in 0..NUM_BLOCKS {
tx.append::<CanonicalHeaders>(i, B256::ZERO).unwrap();
}
})
});
}
70 changes: 53 additions & 17 deletions crates/storage/db/src/implementation/mdbx/tx.rs
Original file line number Diff line number Diff line change
Expand Up @@ -340,28 +340,64 @@ impl<K: TransactionKind> DbTx for Tx<K> {
}
}

#[derive(Clone, Copy)]
enum PutKind {
/// Default kind that inserts a new key-value or overwrites an existed key.
Upsert,
/// Append the key-value to the end of the table -- fast path when the new
/// key is the highest so far, like the latest block number.
Append,
}

impl PutKind {
const fn into_operation_and_flags(self) -> (Operation, DatabaseWriteOperation, WriteFlags) {
match self {
Self::Upsert => {
(Operation::PutUpsert, DatabaseWriteOperation::PutUpsert, WriteFlags::UPSERT)
}
Self::Append => {
(Operation::PutAppend, DatabaseWriteOperation::PutAppend, WriteFlags::APPEND)
}
}
}
}

impl Tx<RW> {
/// The inner implementation mapping to `mdbx_put` that supports different
/// put kinds like upserting and appending.
fn put<T: Table>(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add some docs here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 🙏.

&self,
kind: PutKind,
key: T::Key,
value: T::Value,
) -> Result<(), DatabaseError> {
let key = key.encode();
let value = value.compress();
let (operation, write_operation, flags) = kind.into_operation_and_flags();
self.execute_with_operation_metric::<T, _>(operation, Some(value.as_ref().len()), |tx| {
tx.put(self.get_dbi::<T>()?, key.as_ref(), value, flags).map_err(|e| {
DatabaseWriteError {
info: e.into(),
operation: write_operation,
table_name: T::NAME,
key: key.into(),
}
.into()
})
})
}
}

impl DbTxMut for Tx<RW> {
type CursorMut<T: Table> = Cursor<RW, T>;
type DupCursorMut<T: DupSort> = Cursor<RW, T>;

fn put<T: Table>(&self, key: T::Key, value: T::Value) -> Result<(), DatabaseError> {
let key = key.encode();
let value = value.compress();
self.execute_with_operation_metric::<T, _>(
Operation::Put,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how is putupsert different from just put?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's the same, just renamed for clarity. By default with flag 0, put will do an upsert

/** Upsertion by default (without any other flags) */
MDBX_UPSERT = 0,

Some(value.as_ref().len()),
|tx| {
tx.put(self.get_dbi::<T>()?, key.as_ref(), value, WriteFlags::UPSERT).map_err(|e| {
DatabaseWriteError {
info: e.into(),
operation: DatabaseWriteOperation::Put,
table_name: T::NAME,
key: key.into(),
}
.into()
})
},
)
self.put::<T>(PutKind::Upsert, key, value)
}

fn append<T: Table>(&self, key: T::Key, value: T::Value) -> Result<(), DatabaseError> {
self.put::<T>(PutKind::Append, key, value)
}

fn delete<T: Table>(
Expand Down
9 changes: 6 additions & 3 deletions crates/storage/db/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,8 +197,10 @@ impl TransactionOutcome {
pub(crate) enum Operation {
/// Database get operation.
Get,
/// Database put operation.
Put,
/// Database put upsert operation.
PutUpsert,
/// Database put append operation.
PutAppend,
/// Database delete operation.
Delete,
/// Database cursor upsert operation.
Expand All @@ -220,7 +222,8 @@ impl Operation {
pub(crate) const fn as_str(&self) -> &'static str {
match self {
Self::Get => "get",
Self::Put => "put",
Self::PutUpsert => "put-upsert",
Self::PutAppend => "put-append",
Self::Delete => "delete",
Self::CursorUpsert => "cursor-upsert",
Self::CursorInsert => "cursor-insert",
Expand Down
6 changes: 4 additions & 2 deletions crates/storage/errors/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,10 @@ pub enum DatabaseWriteOperation {
CursorInsert,
/// Append duplicate cursor.
CursorAppendDup,
/// Put.
Put,
/// Put upsert.
PutUpsert,
/// Put append.
PutAppend,
Comment on lines +109 to +112
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we add another enum for PutKind then this can replace the bool and we can do kind.into() instead

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Too many Intos are annoying, so I did a simple TxKind::into_operation_and_flags that covers everything.

}

/// Database log level.
Expand Down
Loading