Skip to content

Commit 3720be8

Browse files
committed
feat(dursto): benchmark ERC-20 transactions
1 parent d791196 commit 3720be8

File tree

7 files changed

+239
-68
lines changed

7 files changed

+239
-68
lines changed

durable-storage/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ rocksdb.workspace = true
1919
tempfile.workspace = true
2020
thiserror.workspace = true
2121
tokio.workspace = true
22+
rand.workspace = true
2223

2324
[dev-dependencies]
2425
blake3.workspace = true
@@ -33,3 +34,8 @@ bench = []
3334
name = "avl_tree"
3435
harness = false
3536
required-features = ["bench"]
37+
38+
[[bench]]
39+
name = "database"
40+
harness = false
41+
required-features = ["bench"]

durable-storage/benches/avl_tree.rs

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
// SPDX-License-Identifier: MIT
33

44
use std::collections::BTreeMap;
5-
use std::collections::HashSet;
65
use std::time::Duration;
76

87
use bytes::Bytes;
98
use criterion::Criterion;
109
use criterion::criterion_group;
1110
use criterion::criterion_main;
12-
use octez_riscv_durable_storage::merkle_layer::KEY_MAX_SIZE;
1311
use octez_riscv_durable_storage::merkle_layer::Key;
1412
use octez_riscv_durable_storage::merkle_layer::tree::Avl;
13+
use octez_riscv_durable_storage::random::generate_keys;
14+
use octez_riscv_durable_storage::random::generate_random_bytes;
1515
use rand::prelude::*;
1616

1717
const KEY_COUNT: usize = 10_000_000;
@@ -24,31 +24,7 @@ pub enum Operation {
2424
Delete(Key),
2525
}
2626

27-
fn generate_byte_vector(rng: &mut impl Rng, length: usize) -> Vec<u8> {
28-
let mut ret = vec![0u8; length];
29-
rng.fill(ret.as_mut_slice());
30-
ret
31-
}
32-
33-
fn generate_keys(rng: &mut impl Rng, length: usize) -> Vec<Key> {
34-
let mut tmp: HashSet<Key> = HashSet::new();
35-
while tmp.len() < length {
36-
let key_length = rng.random_range(1..KEY_MAX_SIZE);
37-
tmp.insert(
38-
Key::new(generate_byte_vector(rng, key_length).as_slice())
39-
.expect("The key should be created"),
40-
);
41-
}
42-
tmp.into_iter().collect()
43-
}
44-
45-
fn get_random_data(rng: &mut impl Rng) -> Bytes {
46-
let length: usize = rng.random_range(1..20);
47-
let bytes = generate_byte_vector(rng, length);
48-
Bytes::from(bytes)
49-
}
50-
51-
fn get_operations_batch(rng: &mut impl Rng, keys: &[Key], batch_size: usize) -> Vec<Operation> {
27+
fn get_operations_batch(mut rng: &mut impl Rng, keys: &[Key], batch_size: usize) -> Vec<Operation> {
5228
(0..batch_size)
5329
.map(|_| {
5430
let key: Key = keys
@@ -57,7 +33,7 @@ fn get_operations_batch(rng: &mut impl Rng, keys: &[Key], batch_size: usize) ->
5733
.clone();
5834
match rng.random_range(0..3) {
5935
0 => Operation::Get(key),
60-
1 => Operation::Upsert(key, get_random_data(rng)),
36+
1 => Operation::Upsert(key, generate_random_bytes(&mut rng, 1..20).into()),
6137
_ => Operation::Delete(key),
6238
}
6339
})
@@ -75,7 +51,7 @@ fn bench_avl_tree_operations(c: &mut Criterion) {
7551
// Setting up the tree
7652
let mut tree = Avl::default();
7753
for key in &keys[..keys.len() / 2] {
78-
tree.set(key, get_random_data(&mut rng));
54+
tree.set(key, generate_random_bytes(&mut rng, 1..20).into());
7955
}
8056

8157
c.bench_function("Bench AVL tree with operations", |b| {
@@ -111,7 +87,7 @@ fn reference(c: &mut Criterion) {
11187

11288
let mut tree = BTreeMap::<Key, Bytes>::new();
11389
for key in &keys[..keys.len() / 2] {
114-
tree.insert(key.clone(), get_random_data(&mut rng));
90+
tree.insert(key.clone(), generate_random_bytes(&mut rng, 1..20).into());
11591
}
11692

11793
c.bench_function("BTreeMap reference", |b| {
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
use std::hint::black_box;
2+
3+
use bytes::Bytes;
4+
use criterion::BatchSize;
5+
use criterion::Criterion;
6+
use criterion::criterion_group;
7+
use criterion::criterion_main;
8+
use octez_riscv_durable_storage::database::Database;
9+
use octez_riscv_durable_storage::merkle_layer::Key;
10+
use octez_riscv_durable_storage::persistence_layer::utils::TestableTmpdir;
11+
use octez_riscv_durable_storage::random::generate_keys;
12+
use octez_riscv_durable_storage::random::generate_random_bytes;
13+
use octez_riscv_durable_storage::repo::DirectoryManager;
14+
use rand::prelude::*;
15+
use rand::rng;
16+
use tokio::runtime::Handle;
17+
18+
struct BenchmarkState {
19+
database: Database,
20+
large_read_buffer: Vec<u8>,
21+
large_read_choices: Vec<usize>,
22+
prepopulated_large_node_keys: Vec<Key>,
23+
prepopulated_node_keys: Vec<Key>,
24+
prepopulated_values: Vec<Bytes>,
25+
small_read_buffer: Vec<u8>,
26+
small_read_choices: Vec<usize>,
27+
small_write_choices: Vec<(usize, usize)>,
28+
}
29+
30+
impl BenchmarkState {
31+
fn clone_with(&self, handle: &Handle, repo: &DirectoryManager) -> Self {
32+
Self {
33+
database: self
34+
.database
35+
.try_clone_with(handle, repo)
36+
.expect("Cloning the database should work"),
37+
large_read_buffer: vec![0u8; LARGE_READ_MIN_SIZE],
38+
large_read_choices: self.large_read_choices.clone(),
39+
prepopulated_large_node_keys: self.prepopulated_large_node_keys.clone(),
40+
prepopulated_node_keys: self.prepopulated_node_keys.clone(),
41+
prepopulated_values: self.prepopulated_values.clone(),
42+
small_read_buffer: vec![0u8; SMALL_READ_SIZE],
43+
small_read_choices: self.small_read_choices.clone(),
44+
small_write_choices: self.small_write_choices.clone(),
45+
}
46+
}
47+
}
48+
49+
const BLOCK_FREQUENCY: usize = 5_000;
50+
const ERC_20_TRANSACTIONS: usize = 10_000;
51+
const LARGE_READ_MIN_SIZE: usize = 48000;
52+
const LARGE_READ_MAX_SIZE: usize = LARGE_READ_MIN_SIZE * 2;
53+
const PREPOPULATED_LARGE_NODE_KEYS_COUNT: usize = 100;
54+
const PREPOPULATED_NODE_KEYS_COUNT: usize = 1_000_000 - PREPOPULATED_LARGE_NODE_KEYS_COUNT;
55+
const SMALL_READS: usize = 19;
56+
const SMALL_READ_SIZE: usize = 32;
57+
const SMALL_WRITES: usize = 20;
58+
59+
fn setup_benchmark_state(handle: &Handle, repo: &DirectoryManager) -> BenchmarkState {
60+
let mut database = Database::try_new(handle, repo).expect("Creating a database should succeed");
61+
let mut rng = rng();
62+
63+
const VALUES_COUNT: usize = 100;
64+
let mut prepopulated_node_keys = Vec::with_capacity(PREPOPULATED_NODE_KEYS_COUNT);
65+
let mut prepopulated_large_node_keys = Vec::with_capacity(PREPOPULATED_LARGE_NODE_KEYS_COUNT);
66+
let mut prepopulated_values = Vec::with_capacity(VALUES_COUNT);
67+
68+
// Pre-populate a small number of values of random small sizes
69+
for _ in 0..VALUES_COUNT {
70+
let value = Bytes::from(generate_random_bytes(&mut rng, 1..32));
71+
prepopulated_values.push(value);
72+
}
73+
74+
// Pre-populate the database with a large number of random keys with small values
75+
let keys = generate_keys(&mut rng, PREPOPULATED_NODE_KEYS_COUNT);
76+
for key in keys {
77+
let value = prepopulated_values
78+
.choose(&mut rng)
79+
.expect("The value should exist");
80+
database.write(key.clone(), 0, value.clone()).ok();
81+
prepopulated_node_keys.push(key);
82+
}
83+
84+
// Pre-populate the database with a small number of random keys with large values
85+
let keys = generate_keys(&mut rng, PREPOPULATED_LARGE_NODE_KEYS_COUNT);
86+
for key in keys {
87+
let value = Bytes::from(generate_random_bytes(
88+
&mut rng,
89+
LARGE_READ_MIN_SIZE..LARGE_READ_MAX_SIZE,
90+
));
91+
database.write(key.clone(), 0, value).ok();
92+
prepopulated_large_node_keys.push(key);
93+
}
94+
95+
let large_read_choices = (0..ERC_20_TRANSACTIONS)
96+
.map(|_| rng.random_range(0..prepopulated_large_node_keys.len()))
97+
.collect();
98+
99+
let small_read_choices = (0..ERC_20_TRANSACTIONS * SMALL_READS)
100+
.map(|_| rng.random_range(0..prepopulated_node_keys.len()))
101+
.collect();
102+
103+
let small_write_choices = (0..ERC_20_TRANSACTIONS * SMALL_WRITES)
104+
.map(|_| {
105+
(
106+
rng.random_range(0..prepopulated_node_keys.len()),
107+
rng.random_range(0..prepopulated_values.len()),
108+
)
109+
})
110+
.collect();
111+
112+
BenchmarkState {
113+
database,
114+
large_read_buffer: vec![0u8; LARGE_READ_MAX_SIZE],
115+
large_read_choices,
116+
prepopulated_large_node_keys,
117+
prepopulated_node_keys,
118+
prepopulated_values,
119+
small_read_buffer: vec![0u8; SMALL_READ_SIZE],
120+
small_read_choices,
121+
small_write_choices,
122+
}
123+
}
124+
125+
#[inline(never)]
126+
fn bench_run(mut state: BenchmarkState) {
127+
let large_node_keys = &state.prepopulated_large_node_keys;
128+
let node_keys = &state.prepopulated_node_keys;
129+
let values = &state.prepopulated_values;
130+
131+
for i in 0..ERC_20_TRANSACTIONS {
132+
let key = &large_node_keys[state.large_read_choices[i]];
133+
state
134+
.database
135+
.read(key, 0, &mut state.large_read_buffer)
136+
.expect("The read should succeed");
137+
138+
for _ in 0..SMALL_READS {
139+
let key = &node_keys[state.small_read_choices[i]];
140+
state
141+
.database
142+
.read(key, 0, &mut state.small_read_buffer)
143+
.expect("The read should succeed");
144+
}
145+
146+
for _ in 0..SMALL_WRITES {
147+
let key = node_keys[state.small_write_choices[i].0].clone();
148+
let value = values[state.small_write_choices[i].1].clone();
149+
state
150+
.database
151+
.write(key, 0, value)
152+
.expect("The read should succeed");
153+
}
154+
155+
if i % BLOCK_FREQUENCY == 0 {
156+
black_box(state.database.hash());
157+
}
158+
}
159+
}
160+
161+
fn database_benchmark(c: &mut Criterion) {
162+
let runtime = tokio::runtime::Builder::new_multi_thread()
163+
.worker_threads(2)
164+
.build()
165+
.expect("Creating a Tokio runtime should succeed");
166+
167+
let tmpdir = TestableTmpdir::new();
168+
let repo = DirectoryManager::new(tmpdir.path()).expect("Failed to create directory manager");
169+
let initial_state = setup_benchmark_state(runtime.handle(), &repo);
170+
171+
let setup = || initial_state.clone_with(runtime.handle(), &repo);
172+
173+
let mut group = c.benchmark_group("ERC-20");
174+
group.sample_size(10);
175+
group.measurement_time(std::time::Duration::from_secs(40));
176+
group.bench_function("10_000-transactions", |b| {
177+
b.iter_batched(setup, bench_run, BatchSize::SmallInput)
178+
});
179+
180+
group.finish();
181+
}
182+
183+
criterion_group!(benches, database_benchmark);
184+
criterion_main!(benches);

durable-storage/src/database.rs

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -65,24 +65,17 @@ impl Database {
6565
}
6666

6767
/// Obtain, and possibly calculate, the root hash of the database>
68-
#[cfg_attr(not(test), expect(dead_code, reason = "Implemented in RV-827"))]
69-
pub(crate) fn hash(&self) -> blake3::Hash {
68+
pub fn hash(&self) -> blake3::Hash {
7069
self.merkle.hash()
7170
}
7271

73-
#[cfg_attr(not(test), expect(dead_code, reason = "Implemented in RV-827"))]
7472
/// Read a portion of the value associated with the provided key. The read data will be written
7573
/// into `data`. `offset` specifies from where in the associated value to start reading.
7674
///
7775
/// Fails if:
7876
/// - The key does not exist.
7977
/// - The offset is larger than the length of the associated value.
80-
pub(crate) fn read(
81-
&self,
82-
key: &Key,
83-
offset: usize,
84-
data: &mut [u8],
85-
) -> Result<usize, DatabaseError> {
78+
pub fn read(&self, key: &Key, offset: usize, data: &mut [u8]) -> Result<usize, DatabaseError> {
8679
let value = self.persistent.get(key.as_ref())?;
8780
let value_ref = value.as_ref();
8881

@@ -98,31 +91,21 @@ impl Database {
9891
Ok(bytes_to_copy)
9992
}
10093

101-
#[cfg_attr(
102-
not(test),
103-
expect(dead_code, reason = "Used in RV-828 and integration of the registry")
104-
)]
10594
/// Try to construct a new Database
10695
pub fn try_new(handle: &Handle, repo: &DirectoryManager) -> Result<Self, DatabaseError> {
10796
let persistent: Arc<PersistenceLayer> = PersistenceLayer::new(repo)?.into();
10897
let merkle = MerkleWorker::new(handle, persistent.clone())?;
10998
Ok(Self { persistent, merkle })
11099
}
111100

112-
#[cfg_attr(not(test), expect(dead_code, reason = "Implemented in RV-827"))]
113101
/// Modify the value associated with the provided key. `offset` specifies from where to start
114102
/// writing within the associated value, appending if it is equal to the length. Non-existent
115103
/// keys have the implicit length 0, so they are writeable.
116104
///
117105
/// Fails if:
118106
/// - The offset is non-zero and the key does not exist.
119107
/// - The offset is larger than the length of the associated value.
120-
pub(crate) fn write(
121-
&mut self,
122-
key: Key,
123-
offset: usize,
124-
data: Bytes,
125-
) -> Result<usize, DatabaseError> {
108+
pub fn write(&mut self, key: Key, offset: usize, data: Bytes) -> Result<usize, DatabaseError> {
126109
if offset != 0 {
127110
let value = None;
128111
// TODO : Implement [`MerkleLayer::node::get_mut`] in RV-827
@@ -135,7 +118,6 @@ impl Database {
135118
}
136119
}
137120

138-
#[expect(dead_code, reason = "Used in RV-828 and integration of the registry")]
139121
/// Try to create a cheap clone of the Database.
140122
pub fn try_clone_with(
141123
&self,

durable-storage/src/lib.rs

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,23 @@
2626
//! - **Persistence layer**: Responsible for actually persisting the basic get, set, delete
2727
//! operations on disk.
2828
29-
mod database;
3029
cfg_if::cfg_if! {
3130
if #[cfg(feature = "bench")] {
31+
pub mod database;
3232
pub mod merkle_layer;
33+
pub mod merkle_worker;
34+
pub mod persistence_layer;
35+
pub mod repo;
36+
pub mod random;
3337
} else {
38+
#[cfg_attr(not(test), expect(dead_code, reason = "Incomplete"))]
39+
mod database;
3440
#[cfg_attr(not(test), expect(dead_code, reason = "Incomplete"))]
3541
mod merkle_layer;
42+
mod merkle_worker;
43+
#[cfg_attr(not(test), expect(dead_code, reason = "Incomplete"))]
44+
pub(crate) mod persistence_layer;
45+
#[cfg_attr(not(test), expect(dead_code, reason = "Incomplete"))]
46+
mod repo;
3647
}
3748
}
38-
mod merkle_worker;
39-
#[cfg_attr(
40-
not(any(test, feature = "bench")),
41-
expect(
42-
dead_code,
43-
reason = "Database will use the persistence layer in RV-808"
44-
)
45-
)]
46-
pub(crate) mod persistence_layer;
47-
#[cfg_attr(
48-
not(any(test, feature = "bench")),
49-
expect(dead_code, reason = "Incomplete")
50-
)]
51-
mod repo;

0 commit comments

Comments
 (0)