use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use rand::Rng;
use rusqlite::{params, Connection};
use serde_json::json;
use tempfile::TempDir;
use zippy_data::{FastStore, Layout};
const RECORD_COUNTS: &[usize] = &[1000, 10000, 100000];
const RANDOM_LOOKUP_COUNT: usize = 1000;
fn generate_doc(i: usize) -> serde_json::Value {
json!({
"id": format!("record_{:08}", i),
"name": format!("User {}", i),
"email": format!("user{}@example.com", i),
"age": 20 + (i % 60),
"score": (i as f64) * 0.87 + 12.5,
"active": i % 2 == 0,
"tags": ["tag_a", "tag_b", "tag_c"],
"metadata": {
"created": "2025-01-15",
"source": "benchmark"
}
})
}
fn setup_zds(count: usize) -> (TempDir, PathBuf) {
let tmp = TempDir::new().unwrap();
let root = tmp.path().to_path_buf();
Layout::init_root(&root).unwrap();
let mut store = FastStore::open(&root, "bench", 1000).unwrap();
for i in 0..count {
store
.put(format!("record_{:08}", i), generate_doc(i))
.unwrap();
}
store.flush().unwrap();
(tmp, root)
}
fn setup_sqlite(count: usize) -> (TempDir, PathBuf) {
let tmp = TempDir::new().unwrap();
let db_path = tmp.path().join("bench.db");
let conn = Connection::open(&db_path).unwrap();
conn.execute_batch(
"
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
CREATE TABLE docs (id TEXT PRIMARY KEY, data TEXT);
",
)
.unwrap();
{
let mut stmt = conn
.prepare("INSERT INTO docs (id, data) VALUES (?, ?)")
.unwrap();
for i in 0..count {
let id = format!("record_{:08}", i);
let data = serde_json::to_string(&generate_doc(i)).unwrap();
stmt.execute(params![id, data]).unwrap();
}
}
(tmp, db_path)
}
fn setup_sled(count: usize) -> (TempDir, PathBuf) {
let tmp = TempDir::new().unwrap();
let db_path = tmp.path().join("sled_db");
let db = sled::open(&db_path).unwrap();
for i in 0..count {
let id = format!("record_{:08}", i);
let data = serde_json::to_string(&generate_doc(i)).unwrap();
db.insert(id.as_bytes(), data.as_bytes()).unwrap();
}
db.flush().unwrap();
(tmp, db_path)
}
fn bench_write(c: &mut Criterion) {
let mut group = c.benchmark_group("write");
for count in RECORD_COUNTS.iter().take(2) {
group.throughput(Throughput::Elements(*count as u64));
group.bench_with_input(BenchmarkId::new("zds", count), count, |b, &count| {
b.iter_with_setup(
|| {
let tmp = TempDir::new().unwrap();
let root = tmp.path().to_path_buf();
Layout::init_root(&root).unwrap();
(tmp, root)
},
|(_tmp, root)| {
let mut store = FastStore::open(&root, "bench", 1000).unwrap();
for i in 0..count {
store
.put(format!("record_{:08}", i), generate_doc(i))
.unwrap();
}
store.flush().unwrap();
black_box(())
},
);
});
group.bench_with_input(BenchmarkId::new("sqlite", count), count, |b, &count| {
b.iter_with_setup(
|| {
let tmp = TempDir::new().unwrap();
let db_path = tmp.path().join("bench.db");
(tmp, db_path)
},
|(_tmp, db_path)| {
let conn = Connection::open(&db_path).unwrap();
conn.execute_batch(
"
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
CREATE TABLE docs (id TEXT PRIMARY KEY, data TEXT);
",
)
.unwrap();
let mut stmt = conn
.prepare("INSERT INTO docs (id, data) VALUES (?, ?)")
.unwrap();
for i in 0..count {
let id = format!("record_{:08}", i);
let data = serde_json::to_string(&generate_doc(i)).unwrap();
stmt.execute(params![id, data]).unwrap();
}
black_box(())
},
);
});
group.bench_with_input(BenchmarkId::new("sled", count), count, |b, &count| {
b.iter_with_setup(
|| {
let tmp = TempDir::new().unwrap();
let db_path = tmp.path().join("sled_db");
(tmp, db_path)
},
|(_tmp, db_path)| {
let db = sled::open(&db_path).unwrap();
for i in 0..count {
let id = format!("record_{:08}", i);
let data = serde_json::to_string(&generate_doc(i)).unwrap();
db.insert(id.as_bytes(), data.as_bytes()).unwrap();
}
db.flush().unwrap();
black_box(())
},
);
});
}
group.finish();
}
fn bench_read_all(c: &mut Criterion) {
let mut group = c.benchmark_group("read_all");
for count in RECORD_COUNTS.iter() {
group.throughput(Throughput::Elements(*count as u64));
let (zds_tmp, zds_root) = setup_zds(*count);
group.bench_with_input(BenchmarkId::new("zds_cold", count), count, |b, _| {
b.iter(|| {
let store = FastStore::open(&zds_root, "bench", 1000).unwrap();
let docs = store.scan().unwrap();
black_box(docs.len())
});
});
let warm_store = FastStore::open(&zds_root, "bench", 1000).unwrap();
group.bench_with_input(BenchmarkId::new("zds_warm", count), count, |b, _| {
b.iter(|| {
let docs = warm_store.scan().unwrap();
black_box(docs.len())
});
});
drop(warm_store);
drop(zds_tmp);
let (sqlite_tmp, sqlite_path) = setup_sqlite(*count);
group.bench_with_input(BenchmarkId::new("sqlite_cold", count), count, |b, _| {
b.iter(|| {
let conn = Connection::open(&sqlite_path).unwrap();
let mut stmt = conn.prepare("SELECT data FROM docs").unwrap();
let docs: Vec<String> = stmt
.query_map([], |row| row.get(0))
.unwrap()
.filter_map(|r| r.ok())
.collect();
black_box(docs.len())
});
});
let warm_conn = Connection::open(&sqlite_path).unwrap();
group.bench_with_input(BenchmarkId::new("sqlite_warm", count), count, |b, _| {
b.iter(|| {
let mut stmt = warm_conn.prepare_cached("SELECT data FROM docs").unwrap();
let docs: Vec<String> = stmt
.query_map([], |row| row.get(0))
.unwrap()
.filter_map(|r| r.ok())
.collect();
black_box(docs.len())
});
});
drop(warm_conn);
drop(sqlite_tmp);
let (sled_tmp, sled_path) = setup_sled(*count);
group.bench_with_input(BenchmarkId::new("sled_cold", count), count, |b, _| {
b.iter(|| {
let db = sled::open(&sled_path).unwrap();
let docs: Vec<_> = db.iter().filter_map(|r| r.ok()).collect();
black_box(docs.len())
});
});
let warm_db = sled::open(&sled_path).unwrap();
group.bench_with_input(BenchmarkId::new("sled_warm", count), count, |b, _| {
b.iter(|| {
let docs: Vec<_> = warm_db.iter().filter_map(|r| r.ok()).collect();
black_box(docs.len())
});
});
drop(warm_db);
drop(sled_tmp);
}
group.finish();
}
fn bench_random_access(c: &mut Criterion) {
let mut group = c.benchmark_group("random_access");
let count = 10000;
group.throughput(Throughput::Elements(RANDOM_LOOKUP_COUNT as u64));
let mut rng = rand::thread_rng();
let lookup_ids: Vec<String> = (0..RANDOM_LOOKUP_COUNT)
.map(|_| format!("record_{:08}", rng.gen_range(0..count)))
.collect();
let (zds_tmp, zds_root) = setup_zds(count);
let zds_store = FastStore::open(&zds_root, "bench", 1000).unwrap();
group.bench_function("zds", |b| {
b.iter(|| {
for id in &lookup_ids {
let doc = zds_store.get(id).unwrap();
black_box(doc);
}
});
});
drop(zds_store);
drop(zds_tmp);
let (sqlite_tmp, sqlite_path) = setup_sqlite(count);
let sqlite_conn = Connection::open(&sqlite_path).unwrap();
group.bench_function("sqlite", |b| {
b.iter(|| {
for id in &lookup_ids {
let mut stmt = sqlite_conn
.prepare_cached("SELECT data FROM docs WHERE id = ?")
.unwrap();
let doc: String = stmt.query_row(params![id], |row| row.get(0)).unwrap();
black_box(doc);
}
});
});
drop(sqlite_conn);
drop(sqlite_tmp);
let (sled_tmp, sled_path) = setup_sled(count);
let sled_db = sled::open(&sled_path).unwrap();
group.bench_function("sled", |b| {
b.iter(|| {
for id in &lookup_ids {
let doc = sled_db.get(id.as_bytes()).unwrap().unwrap();
black_box(doc);
}
});
});
drop(sled_db);
drop(sled_tmp);
group.finish();
}
criterion_group!(benches, bench_write, bench_read_all, bench_random_access);
criterion_main!(benches);