Skip to content

Commit 8204b8b

Browse files
chained-growth benchmark
1 parent c6a805b commit 8204b8b

4 files changed

Lines changed: 276 additions & 41 deletions

File tree

storage/src/qmdb/benches/bench.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,15 @@
22
33
use criterion::criterion_main;
44

5+
mod chained_growth;
56
mod common;
67
mod generate;
78
mod init;
89
mod merkleize;
910

10-
criterion_main!(generate::benches, init::benches, merkleize::benches);
11+
criterion_main!(
12+
chained_growth::benches,
13+
generate::benches,
14+
init::benches,
15+
merkleize::benches
16+
);
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
//! Benchmark for chained-growth merkleization against Current QMDB variants.
2+
//!
3+
//! Setup (untimed): seed `NUM_KEYS` keys, then grow a chain of `PREBUILT_CHAIN` batches applying
4+
//! each parent while the child is still alive.
5+
//!
6+
//! Timed: do `batches` more merkleize + apply iterations on top of the pre-built chain, with a
7+
//! single random update per batch so each overlay covers a tiny fraction of chunks.
8+
9+
use crate::common::{seed_db, write_random_updates, Digest, WRITE_BUFFER_SIZE};
10+
use commonware_cryptography::Sha256;
11+
use commonware_runtime::{
12+
benchmarks::{context, tokio},
13+
buffer::paged::CacheRef,
14+
tokio::{Config, Context},
15+
BufferPooler, ThreadPooler,
16+
};
17+
use commonware_storage::{
18+
journal::contiguous::fixed::Config as FConfig,
19+
merkle::{self, journaled, mmb::Family as Mmb},
20+
qmdb::{
21+
any::traits::{DbAny, MerkleizedBatch as _, UnmerkleizedBatch as _},
22+
current::{ordered::fixed::Db as OCFixed, unordered::fixed::Db as UCFixed},
23+
},
24+
translator::EightCap,
25+
};
26+
use commonware_utils::{NZUsize, NZU16, NZU64};
27+
use criterion::{criterion_group, Criterion};
28+
use rand::{rngs::StdRng, SeedableRng};
29+
use std::{
30+
hint::black_box,
31+
num::{NonZeroU16, NonZeroU64, NonZeroUsize},
32+
time::{Duration, Instant},
33+
};
34+
35+
// -- Config (mirrors merkleize bench) --
36+
37+
const ITEMS_PER_BLOB: NonZeroU64 = NZU64!(10_000_000);
38+
const THREADS: NonZeroUsize = NZUsize!(8);
39+
const PAGE_SIZE: NonZeroU16 = NZU16!(4096);
40+
const LARGE_PAGE_CACHE_SIZE: NonZeroUsize = NZUsize!(131_072);
41+
const PARTITION: &str = "bench-chained-growth";
42+
43+
const SMALL_CHUNK_SIZE: usize = 32;
44+
const LARGE_CHUNK_SIZE: usize = 256;
45+
46+
type CurUFix32Mmb = UCFixed<Mmb, Context, Digest, Digest, Sha256, EightCap, SMALL_CHUNK_SIZE>;
47+
type CurOFix32Mmb = OCFixed<Mmb, Context, Digest, Digest, Sha256, EightCap, SMALL_CHUNK_SIZE>;
48+
type CurUFix256Mmb = UCFixed<Mmb, Context, Digest, Digest, Sha256, EightCap, LARGE_CHUNK_SIZE>;
49+
type CurOFix256Mmb = OCFixed<Mmb, Context, Digest, Digest, Sha256, EightCap, LARGE_CHUNK_SIZE>;
50+
51+
fn merkle_cfg(ctx: &(impl BufferPooler + ThreadPooler), pc: CacheRef) -> journaled::Config {
52+
journaled::Config {
53+
journal_partition: format!("journal-{PARTITION}"),
54+
metadata_partition: format!("metadata-{PARTITION}"),
55+
items_per_blob: ITEMS_PER_BLOB,
56+
write_buffer: WRITE_BUFFER_SIZE,
57+
thread_pool: Some(ctx.create_thread_pool(THREADS).unwrap()),
58+
page_cache: pc,
59+
}
60+
}
61+
62+
fn fix_log_cfg(pc: CacheRef) -> FConfig {
63+
FConfig {
64+
partition: format!("log-journal-{PARTITION}"),
65+
items_per_blob: ITEMS_PER_BLOB,
66+
page_cache: pc,
67+
write_buffer: WRITE_BUFFER_SIZE,
68+
}
69+
}
70+
71+
fn pc(ctx: &impl BufferPooler) -> CacheRef {
72+
CacheRef::from_pooler(ctx, PAGE_SIZE, LARGE_PAGE_CACHE_SIZE)
73+
}
74+
75+
fn cur_fix_cfg(
76+
ctx: &(impl BufferPooler + ThreadPooler),
77+
) -> commonware_storage::qmdb::current::FixedConfig<EightCap> {
78+
let pc = pc(ctx);
79+
commonware_storage::qmdb::current::FixedConfig {
80+
merkle_config: merkle_cfg(ctx, pc.clone()),
81+
journal_config: fix_log_cfg(pc),
82+
grafted_metadata_partition: format!("grafted-metadata-{PARTITION}"),
83+
translator: EightCap,
84+
}
85+
}
86+
87+
/// Number of pre-populated keys in the seeded database.
88+
const NUM_KEYS: u64 = 1_000_000;
89+
90+
/// Random updates per batch. One update means each batch's chunk overlay covers ~1 / num_chunks
91+
/// of the bitmap, forcing chain reads to walk deep before finding a matching layer.
92+
const UPDATES_PER_BATCH: u64 = 1;
93+
94+
/// Number of batches grown during the untimed seed phase, producing a Db::status chain of this
95+
/// depth that subsequent reads must walk through.
96+
const PREBUILT_CHAIN: u64 = 10_000;
97+
98+
/// Number of additional batches to grow during the timed region.
99+
const GROW_COUNTS: [u64; 1] = [100];
100+
101+
#[derive(Debug, Clone, Copy)]
102+
enum CurrentVariant {
103+
UnorderedFixed32,
104+
OrderedFixed32,
105+
UnorderedFixed256,
106+
OrderedFixed256,
107+
}
108+
109+
impl CurrentVariant {
110+
const fn name(self) -> &'static str {
111+
match self {
112+
Self::UnorderedFixed32 => "current::unordered::fixed::mmb chunk=32",
113+
Self::OrderedFixed32 => "current::ordered::fixed::mmb chunk=32",
114+
Self::UnorderedFixed256 => "current::unordered::fixed::mmb chunk=256",
115+
Self::OrderedFixed256 => "current::ordered::fixed::mmb chunk=256",
116+
}
117+
}
118+
}
119+
120+
const CURRENT_VARIANTS: [CurrentVariant; 4] = [
121+
CurrentVariant::UnorderedFixed32,
122+
CurrentVariant::OrderedFixed32,
123+
CurrentVariant::UnorderedFixed256,
124+
CurrentVariant::OrderedFixed256,
125+
];
126+
127+
/// Construct a Current database for `$variant`, bind it as `$db`, and execute `$body`.
128+
macro_rules! with_current_db {
129+
($ctx:expr, $variant:expr, |mut $db:ident| $body:expr) => {{
130+
macro_rules! init_db {
131+
($DbType:ty) => {{
132+
#[allow(unused_mut)]
133+
let mut $db = <$DbType>::init($ctx.clone(), cur_fix_cfg(&$ctx))
134+
.await
135+
.unwrap();
136+
$body
137+
}};
138+
}
139+
match $variant {
140+
CurrentVariant::UnorderedFixed32 => init_db!(CurUFix32Mmb),
141+
CurrentVariant::OrderedFixed32 => init_db!(CurOFix32Mmb),
142+
CurrentVariant::UnorderedFixed256 => init_db!(CurUFix256Mmb),
143+
CurrentVariant::OrderedFixed256 => init_db!(CurOFix256Mmb),
144+
}
145+
}};
146+
}
147+
148+
/// Run a chained-growth sequence with a pre-built deep chain.
149+
///
150+
/// `fork_child` bridges the generic trait and the concrete `new_batch` method on a merkleized
151+
/// batch.
152+
async fn run_chained_growth<
153+
F: merkle::Family,
154+
C: DbAny<F, Key = Digest, Value = Digest>,
155+
Fork: Fn(&C::Merkleized) -> C::Batch,
156+
>(
157+
mut db: C,
158+
grow: u64,
159+
fork_child: Fork,
160+
) -> Duration {
161+
seed_db(&mut db, NUM_KEYS).await;
162+
let mut rng = StdRng::seed_from_u64(99);
163+
164+
// Pre-build a deep chain (untimed).
165+
let initial = write_random_updates(db.new_batch(), UPDATES_PER_BATCH, NUM_KEYS, &mut rng);
166+
let mut parent = initial.merkleize(&db, None).await.unwrap();
167+
for _ in 0..PREBUILT_CHAIN {
168+
let child_batch =
169+
write_random_updates(fork_child(&parent), UPDATES_PER_BATCH, NUM_KEYS, &mut rng);
170+
let child = child_batch.merkleize(&db, None).await.unwrap();
171+
db.apply_batch(parent).await.unwrap();
172+
parent = child;
173+
}
174+
175+
// Flush buffered data so the timed region doesn't inherit setup fsync cost.
176+
db.commit().await.unwrap();
177+
db.sync().await.unwrap();
178+
179+
// Timed: grow more batches on top of the pre-built chain.
180+
let start = Instant::now();
181+
for _ in 0..grow {
182+
let child_batch =
183+
write_random_updates(fork_child(&parent), UPDATES_PER_BATCH, NUM_KEYS, &mut rng);
184+
let child = child_batch.merkleize(&db, None).await.unwrap();
185+
black_box(child.root());
186+
db.apply_batch(parent).await.unwrap();
187+
parent = child;
188+
}
189+
db.apply_batch(parent).await.unwrap();
190+
let total = start.elapsed();
191+
192+
db.destroy().await.unwrap();
193+
total
194+
}
195+
196+
fn bench_chained_growth(c: &mut Criterion) {
197+
let runner = tokio::Runner::new(Config::default());
198+
for batches in GROW_COUNTS {
199+
for &variant in &CURRENT_VARIANTS {
200+
c.bench_function(
201+
&format!(
202+
"{}/variant={} batches={batches}",
203+
module_path!(),
204+
variant.name()
205+
),
206+
|b| {
207+
b.to_async(&runner).iter_custom(|iters| async move {
208+
let ctx = context::get::<Context>();
209+
let mut total = Duration::ZERO;
210+
for _ in 0..iters {
211+
with_current_db!(ctx.clone(), variant, |mut db| {
212+
total += run_chained_growth(db, batches, |p| p.new_batch()).await;
213+
});
214+
}
215+
total
216+
});
217+
},
218+
);
219+
}
220+
}
221+
}
222+
223+
criterion_group! {
224+
name = benches;
225+
config = Criterion::default().sample_size(10);
226+
targets = bench_chained_growth
227+
}

storage/src/qmdb/benches/common.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ use commonware_cryptography::{Hasher, Sha256};
55
use commonware_runtime::{buffer::paged::CacheRef, tokio::Context, BufferPooler, ThreadPooler};
66
use commonware_storage::{
77
journal::contiguous::{fixed::Config as FConfig, variable::Config as VConfig},
8-
merkle::mmr::{journaled::Config as MmrConfig, Family},
8+
merkle::{self, mmr::{journaled::Config as MmrConfig, Family}},
99
qmdb::{
1010
any::{
1111
ordered::{fixed::Db as OFixed, variable::Db as OVariable},
12-
traits::{DbAny, UnmerkleizedBatch as _},
12+
traits::{DbAny, UnmerkleizedBatch},
1313
unordered::{fixed::Db as UFixed, variable::Db as UVariable},
1414
FixedConfig as AnyFixedConfig, VariableConfig as AnyVariableConfig,
1515
},
@@ -543,6 +543,43 @@ pub fn make_fixed_value(rng: &mut StdRng) -> Digest {
543543
Sha256::hash(&rng.next_u32().to_be_bytes())
544544
}
545545

546+
/// Pre-populate the database with `num_keys` unique keys, then commit and sync so that
547+
/// seed-phase buffered writes are flushed before the caller starts timing.
548+
pub async fn seed_db<F: merkle::Family, C: DbAny<F, Key = Digest, Value = Digest>>(
549+
db: &mut C,
550+
num_keys: u64,
551+
) {
552+
let mut rng = StdRng::seed_from_u64(42);
553+
let mut batch = db.new_batch();
554+
for i in 0u64..num_keys {
555+
let k = Sha256::hash(&i.to_be_bytes());
556+
batch = batch.write(k, Some(make_fixed_value(&mut rng)));
557+
}
558+
let merkleized = batch.merkleize(db, None).await.unwrap();
559+
db.apply_batch(merkleized).await.unwrap();
560+
db.commit().await.unwrap();
561+
db.sync().await.unwrap();
562+
}
563+
564+
/// Write `num_updates` random key updates into a batch.
565+
pub fn write_random_updates<B, Db>(
566+
mut batch: B,
567+
num_updates: u64,
568+
num_keys: u64,
569+
rng: &mut StdRng,
570+
) -> B
571+
where
572+
B: UnmerkleizedBatch<Db, K = Digest, V = Digest>,
573+
Db: ?Sized,
574+
{
575+
for _ in 0..num_updates {
576+
let idx = rng.next_u64() % num_keys;
577+
let k = Sha256::hash(&idx.to_be_bytes());
578+
batch = batch.write(k, Some(make_fixed_value(rng)));
579+
}
580+
batch
581+
}
582+
546583
/// Generate a variable-size `Vec<u8>` value (1-256 bytes).
547584
pub fn make_var_value(rng: &mut StdRng) -> Vec<u8> {
548585
let len = (rng.next_u32() as usize) % VARIABLE_VALUE_MAX_LEN + 1;

storage/src/qmdb/benches/merkleize.rs

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
//! initialization time is not included in the benchmark. The page cache is large enough to hold the
77
//! entire active key set to eliminate disk access delays from affecting the results.
88
9-
use crate::common::{make_fixed_value, Digest, CHUNK_SIZE, WRITE_BUFFER_SIZE};
10-
use commonware_cryptography::{Hasher, Sha256};
9+
use crate::common::{seed_db, write_random_updates, Digest, CHUNK_SIZE, WRITE_BUFFER_SIZE};
10+
use commonware_cryptography::Sha256;
1111
use commonware_runtime::{
1212
benchmarks::{context, tokio},
1313
buffer::paged::CacheRef,
@@ -22,7 +22,7 @@ use commonware_storage::{
2222
};
2323
use commonware_utils::{NZUsize, NZU16, NZU64};
2424
use criterion::{criterion_group, Criterion};
25-
use rand::{rngs::StdRng, RngCore, SeedableRng};
25+
use rand::{rngs::StdRng, SeedableRng};
2626
use std::{
2727
hint::black_box,
2828
num::{NonZeroU16, NonZeroU64, NonZeroUsize},
@@ -340,41 +340,6 @@ fn cur_var_cfg(
340340

341341
// -- Benchmark helpers --
342342

343-
/// Pre-populate the database with `num_keys` unique keys, commit, and sync.
344-
async fn seed_db<F: merkle::Family, C: DbAny<F, Key = Digest, Value = Digest>>(
345-
db: &mut C,
346-
num_keys: u64,
347-
) {
348-
let mut rng = StdRng::seed_from_u64(42);
349-
let mut batch = db.new_batch();
350-
for i in 0u64..num_keys {
351-
let k = Sha256::hash(&i.to_be_bytes());
352-
batch = batch.write(k, Some(make_fixed_value(&mut rng)));
353-
}
354-
let merkleized = batch.merkleize(db, None).await.unwrap();
355-
db.apply_batch(merkleized).await.unwrap();
356-
db.commit().await.unwrap();
357-
db.sync().await.unwrap();
358-
}
359-
360-
/// Write `num_updates` random key updates into a batch.
361-
fn write_random_updates<
362-
B: commonware_storage::qmdb::any::traits::UnmerkleizedBatch<Db, K = Digest, V = Digest>,
363-
Db: ?Sized,
364-
>(
365-
mut batch: B,
366-
num_updates: u64,
367-
num_keys: u64,
368-
rng: &mut StdRng,
369-
) -> B {
370-
for _ in 0..num_updates {
371-
let idx = rng.next_u64() % num_keys;
372-
let k = Sha256::hash(&idx.to_be_bytes());
373-
batch = batch.write(k, Some(make_fixed_value(rng)));
374-
}
375-
batch
376-
}
377-
378343
/// Single-batch benchmark: create batch, write updates, merkleize, read root.
379344
async fn run_bench<F: merkle::Family, C: DbAny<F, Key = Digest, Value = Digest>>(
380345
mut db: C,

0 commit comments

Comments
 (0)