//! Compare the storage adapters on identical workloads. //! //! Three workloads: //! //! - `insert_n`: one transaction, N inserts, commit. Measures the per-row //! write path plus tx commit overhead. //! - `scan_full`: `storage.scan(name)`. Measures materialized scan cost. //! - `scan_iter_drain`: drain `storage.scan_iter(name)` with `.count()`. //! Measures the streaming-or-materialize-then-yield cost. //! //! Adapters: memory always; sqlite, redb, fjall, and lmdb gated on their //! respective features. Geomerge is intentionally excluded: it requires a //! pre-loaded theory and its insert semantics (foreign-key references, law //! validation at commit) differ enough that side-by-side comparison would //! mislead. //! //! Run with `cargo bench -p storage --all-features` for the full table. #![allow(clippy::unwrap_used, clippy::expect_used)] use criterion::{black_box, criterion_group, criterion_main, BatchSize, BenchmarkId, Criterion}; use storage::value::Value; use storage::{MemoryStorage, Storage}; #[cfg(feature = "fjall")] use storage::adapters::fjall::FjallStorage; #[cfg(feature = "lmdb")] use storage::adapters::lmdb::LmdbStorage; #[cfg(feature = "redb")] use storage::adapters::redb::RedbStorage; #[cfg(feature = "sqlite")] use storage::adapters::sqlite::SqliteStorage; const ROW_COUNTS: &[usize] = &[100, 1_000, 10_000]; fn make_row(i: usize) -> Vec { vec![Value::Int(i as i64), Value::Int((i * 2) as i64)] } fn populate(storage: &mut dyn Storage, n: usize) { storage.create_relation("rel", 2).unwrap(); let mut tx = storage.transaction().unwrap(); for i in 0..n { tx.insert("rel", make_row(i)).unwrap(); } tx.commit().unwrap(); } fn run_insert_workload(storage: &mut dyn Storage, n: usize) { storage.create_relation("rel", 2).unwrap(); let mut tx = storage.transaction().unwrap(); for i in 0..n { tx.insert("rel", make_row(i)).unwrap(); } tx.commit().unwrap(); } fn bench_insert_n(c: &mut Criterion) { let mut group = c.benchmark_group("insert_n"); for &n in ROW_COUNTS { group.bench_with_input(BenchmarkId::new("memory", n), &n, |b, &n| { b.iter_batched( MemoryStorage::new, |mut s| { run_insert_workload(&mut s, n); black_box(s); }, BatchSize::SmallInput, ); }); #[cfg(feature = "sqlite")] group.bench_with_input(BenchmarkId::new("sqlite-mem", n), &n, |b, &n| { b.iter_batched( || SqliteStorage::open(":memory:").unwrap(), |mut s| { run_insert_workload(&mut s, n); black_box(s); }, BatchSize::SmallInput, ); }); #[cfg(feature = "redb")] group.bench_with_input(BenchmarkId::new("redb", n), &n, |b, &n| { b.iter_batched( || { let dir = tempfile::tempdir().unwrap(); let storage = RedbStorage::open(dir.path().join("db")).unwrap(); (storage, dir) }, |(mut s, dir)| { run_insert_workload(&mut s, n); black_box((s, dir)); }, BatchSize::SmallInput, ); }); #[cfg(feature = "fjall")] group.bench_with_input(BenchmarkId::new("fjall", n), &n, |b, &n| { b.iter_batched( || { let dir = tempfile::tempdir().unwrap(); let storage = FjallStorage::open(dir.path()).unwrap(); (storage, dir) }, |(mut s, dir)| { run_insert_workload(&mut s, n); black_box((s, dir)); }, BatchSize::SmallInput, ); }); #[cfg(feature = "lmdb")] group.bench_with_input(BenchmarkId::new("lmdb", n), &n, |b, &n| { b.iter_batched( || { let dir = tempfile::tempdir().unwrap(); let storage = LmdbStorage::open(dir.path()).unwrap(); (storage, dir) }, |(mut s, dir)| { run_insert_workload(&mut s, n); black_box((s, dir)); }, BatchSize::SmallInput, ); }); } group.finish(); } fn bench_scan_full(c: &mut Criterion) { let mut group = c.benchmark_group("scan_full"); for &n in ROW_COUNTS { group.bench_with_input(BenchmarkId::new("memory", n), &n, |b, &n| { let mut s = MemoryStorage::new(); populate(&mut s, n); b.iter(|| { let rows = s.scan("rel").unwrap(); black_box(rows); }); }); #[cfg(feature = "sqlite")] group.bench_with_input(BenchmarkId::new("sqlite-mem", n), &n, |b, &n| { let mut s = SqliteStorage::open(":memory:").unwrap(); populate(&mut s, n); b.iter(|| { let rows = s.scan("rel").unwrap(); black_box(rows); }); }); #[cfg(feature = "redb")] group.bench_with_input(BenchmarkId::new("redb", n), &n, |b, &n| { let dir = tempfile::tempdir().unwrap(); let mut s = RedbStorage::open(dir.path().join("db")).unwrap(); populate(&mut s, n); b.iter(|| { let rows = s.scan("rel").unwrap(); black_box(rows); }); drop(dir); }); #[cfg(feature = "fjall")] group.bench_with_input(BenchmarkId::new("fjall", n), &n, |b, &n| { let dir = tempfile::tempdir().unwrap(); let mut s = FjallStorage::open(dir.path()).unwrap(); populate(&mut s, n); b.iter(|| { let rows = s.scan("rel").unwrap(); black_box(rows); }); drop(dir); }); #[cfg(feature = "lmdb")] group.bench_with_input(BenchmarkId::new("lmdb", n), &n, |b, &n| { let dir = tempfile::tempdir().unwrap(); let mut s = LmdbStorage::open(dir.path()).unwrap(); populate(&mut s, n); b.iter(|| { let rows = s.scan("rel").unwrap(); black_box(rows); }); drop(dir); }); } group.finish(); } fn bench_scan_iter_drain(c: &mut Criterion) { let mut group = c.benchmark_group("scan_iter_drain"); for &n in ROW_COUNTS { group.bench_with_input(BenchmarkId::new("memory", n), &n, |b, &n| { let mut s = MemoryStorage::new(); populate(&mut s, n); b.iter(|| { let count = s.scan_iter("rel").unwrap().filter_map(Result::ok).count(); black_box(count); }); }); #[cfg(feature = "sqlite")] group.bench_with_input(BenchmarkId::new("sqlite-mem", n), &n, |b, &n| { let mut s = SqliteStorage::open(":memory:").unwrap(); populate(&mut s, n); b.iter(|| { let count = s.scan_iter("rel").unwrap().filter_map(Result::ok).count(); black_box(count); }); }); #[cfg(feature = "redb")] group.bench_with_input(BenchmarkId::new("redb", n), &n, |b, &n| { let dir = tempfile::tempdir().unwrap(); let mut s = RedbStorage::open(dir.path().join("db")).unwrap(); populate(&mut s, n); b.iter(|| { let count = s.scan_iter("rel").unwrap().filter_map(Result::ok).count(); black_box(count); }); drop(dir); }); #[cfg(feature = "fjall")] group.bench_with_input(BenchmarkId::new("fjall", n), &n, |b, &n| { let dir = tempfile::tempdir().unwrap(); let mut s = FjallStorage::open(dir.path()).unwrap(); populate(&mut s, n); b.iter(|| { let count = s.scan_iter("rel").unwrap().filter_map(Result::ok).count(); black_box(count); }); drop(dir); }); #[cfg(feature = "lmdb")] group.bench_with_input(BenchmarkId::new("lmdb", n), &n, |b, &n| { let dir = tempfile::tempdir().unwrap(); let mut s = LmdbStorage::open(dir.path()).unwrap(); populate(&mut s, n); b.iter(|| { let count = s.scan_iter("rel").unwrap().filter_map(Result::ok).count(); black_box(count); }); drop(dir); }); } group.finish(); } criterion_group!( benches, bench_insert_n, bench_scan_full, bench_scan_iter_drain ); criterion_main!(benches);