2026-06-04 15:35:38 +02:00

264 lines
8.8 KiB
Rust

//! 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::{BatchSize, BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
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<Value> {
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);