use std::env::current_dir;
use std::mem::size_of;
use std::sync::Arc;
use std::{fs, process, thread};
use tempfile::{NamedTempFile, TempDir};
mod common;
use common::*;
use std::time::{Duration, Instant};
const ITERATIONS: usize = 2;
const ELEMENTS: usize = 1_000_000;
const KEY_SIZE: usize = 24;
const VALUE_SIZE: usize = 150;
const RNG_SEED: u64 = 3;
fn fill_slice(slice: &mut [u8], rng: &mut fastrand::Rng) {
let mut i = 0;
while i + size_of::<u128>() < slice.len() {
let tmp = rng.u128(..);
slice[i..(i + size_of::<u128>())].copy_from_slice(&tmp.to_le_bytes());
i += size_of::<u128>()
}
if i + size_of::<u64>() < slice.len() {
let tmp = rng.u64(..);
slice[i..(i + size_of::<u64>())].copy_from_slice(&tmp.to_le_bytes());
i += size_of::<u64>()
}
if i + size_of::<u32>() < slice.len() {
let tmp = rng.u32(..);
slice[i..(i + size_of::<u32>())].copy_from_slice(&tmp.to_le_bytes());
i += size_of::<u32>()
}
if i + size_of::<u16>() < slice.len() {
let tmp = rng.u16(..);
slice[i..(i + size_of::<u16>())].copy_from_slice(&tmp.to_le_bytes());
i += size_of::<u16>()
}
if i + size_of::<u8>() < slice.len() {
slice[i] = rng.u8(..);
}
}
fn gen_pair(rng: &mut fastrand::Rng) -> ([u8; KEY_SIZE], Vec<u8>) {
let mut key = [0u8; KEY_SIZE];
fill_slice(&mut key, rng);
let mut value = vec![0u8; VALUE_SIZE];
fill_slice(&mut value, rng);
(key, value)
}
fn make_rng() -> fastrand::Rng {
fastrand::Rng::with_seed(RNG_SEED)
}
fn make_rng_shards(shards: usize, elements: usize) -> Vec<fastrand::Rng> {
let mut rngs = vec![];
let elements_per_shard = elements / shards;
for i in 0..shards {
let mut rng = make_rng();
for _ in 0..(i * elements_per_shard) {
gen_pair(&mut rng);
}
rngs.push(rng);
}
rngs
}
fn benchmark<T: BenchDatabase + Send + Sync>(db: T) -> Vec<(String, Duration)> {
let mut rng = make_rng();
let mut results = Vec::new();
let db = Arc::new(db);
let start = Instant::now();
let mut txn = db.write_transaction();
let mut inserter = txn.get_inserter();
{
for _ in 0..ELEMENTS {
let (key, value) = gen_pair(&mut rng);
inserter.insert(&key, &value).unwrap();
}
}
drop(inserter);
txn.commit().unwrap();
let end = Instant::now();
let duration = end - start;
println!(
"{}: Bulk loaded {} items in {}ms",
T::db_type_name(),
ELEMENTS,
duration.as_millis()
);
results.push(("bulk load".to_string(), duration));
let start = Instant::now();
let writes = 100;
{
for _ in 0..writes {
let mut txn = db.write_transaction();
let mut inserter = txn.get_inserter();
let (key, value) = gen_pair(&mut rng);
inserter.insert(&key, &value).unwrap();
drop(inserter);
txn.commit().unwrap();
}
}
let end = Instant::now();
let duration = end - start;
println!(
"{}: Wrote {} individual items in {}ms",
T::db_type_name(),
writes,
duration.as_millis()
);
results.push(("individual writes".to_string(), duration));
let start = Instant::now();
let batch_size = 1000;
{
for _ in 0..writes {
let mut txn = db.write_transaction();
let mut inserter = txn.get_inserter();
for _ in 0..batch_size {
let (key, value) = gen_pair(&mut rng);
inserter.insert(&key, &value).unwrap();
}
drop(inserter);
txn.commit().unwrap();
}
}
let end = Instant::now();
let duration = end - start;
println!(
"{}: Wrote {} x {} items in {}ms",
T::db_type_name(),
writes,
batch_size,
duration.as_millis()
);
results.push(("batch writes".to_string(), duration));
let txn = db.read_transaction();
{
for _ in 0..ITERATIONS {
let mut rng = make_rng();
let start = Instant::now();
let mut checksum = 0u64;
let mut expected_checksum = 0u64;
let reader = txn.get_reader();
for _ in 0..ELEMENTS {
let (key, value) = gen_pair(&mut rng);
let result = reader.get(&key).unwrap();
checksum += result.as_ref()[0] as u64;
expected_checksum += value[0] as u64;
}
assert_eq!(checksum, expected_checksum);
let end = Instant::now();
let duration = end - start;
println!(
"{}: Random read {} items in {}ms",
T::db_type_name(),
ELEMENTS,
duration.as_millis()
);
results.push(("random reads".to_string(), duration));
}
for _ in 0..ITERATIONS {
let mut rng = make_rng();
let start = Instant::now();
let reader = txn.get_reader();
let mut value_sum = 0;
let num_scan = 10;
for _ in 0..ELEMENTS {
let (key, _value) = gen_pair(&mut rng);
let mut iter = reader.range_from(&key);
for _ in 0..num_scan {
if let Some((_, value)) = iter.next() {
value_sum += value.as_ref()[0];
} else {
break;
}
}
}
assert!(value_sum > 0);
let end = Instant::now();
let duration = end - start;
println!(
"{}: Random range read {} elements in {}ms",
T::db_type_name(),
ELEMENTS * num_scan,
duration.as_millis()
);
results.push(("random range reads".to_string(), duration));
}
}
drop(txn);
for num_threads in [4, 8, 16, 32] {
let mut rngs = make_rng_shards(num_threads, ELEMENTS);
let start = Instant::now();
thread::scope(|s| {
for _ in 0..num_threads {
let db2 = db.clone();
let mut rng = rngs.pop().unwrap();
s.spawn(move || {
let txn = db2.read_transaction();
let mut checksum = 0u64;
let mut expected_checksum = 0u64;
let reader = txn.get_reader();
for _ in 0..(ELEMENTS / num_threads) {
let (key, value) = gen_pair(&mut rng);
let result = reader.get(&key).unwrap();
checksum += result.as_ref()[0] as u64;
expected_checksum += value[0] as u64;
}
assert_eq!(checksum, expected_checksum);
});
}
});
let end = Instant::now();
let duration = end - start;
println!(
"{}: Random read ({} threads) {} items in {}ms",
T::db_type_name(),
num_threads,
ELEMENTS,
duration.as_millis()
);
results.push((format!("random reads ({num_threads} threads)"), duration));
}
let start = Instant::now();
let deletes = ELEMENTS / 2;
{
let mut rng = make_rng();
let mut txn = db.write_transaction();
let mut inserter = txn.get_inserter();
for _ in 0..deletes {
let (key, _value) = gen_pair(&mut rng);
inserter.remove(&key).unwrap();
}
drop(inserter);
txn.commit().unwrap();
}
let end = Instant::now();
let duration = end - start;
println!(
"{}: Removed {} items in {}ms",
T::db_type_name(),
deletes,
duration.as_millis()
);
results.push(("removals".to_string(), duration));
results
}
fn main() {
let tmpdir = current_dir().unwrap().join(".benchmark");
fs::create_dir(&tmpdir).unwrap();
let tmpdir2 = tmpdir.clone();
ctrlc::set_handler(move || {
fs::remove_dir_all(&tmpdir2).unwrap();
process::exit(1);
})
.unwrap();
let redb_latency_results = {
let tmpfile: NamedTempFile = NamedTempFile::new_in(&tmpdir).unwrap();
let db = redb::Database::builder()
.set_cache_size(4 * 1024 * 1024 * 1024)
.create(tmpfile.path())
.unwrap();
let table = RedbBenchDatabase::new(&db);
benchmark(table)
};
let lmdb_results = {
let tmpfile: TempDir = tempfile::tempdir_in(&tmpdir).unwrap();
let env = lmdb::Environment::new().open(tmpfile.path()).unwrap();
env.set_map_size(4096 * 1024 * 1024).unwrap();
let table = LmdbRkvBenchDatabase::new(&env);
benchmark(table)
};
let rocksdb_results = {
let tmpfile: TempDir = tempfile::tempdir_in(&tmpdir).unwrap();
let db = rocksdb::TransactionDB::open_default(tmpfile.path()).unwrap();
let table = RocksdbBenchDatabase::new(&db);
benchmark(table)
};
let sled_results = {
let tmpfile: TempDir = tempfile::tempdir_in(&tmpdir).unwrap();
let db = sled::Config::new().path(tmpfile.path()).open().unwrap();
let table = SledBenchDatabase::new(&db, tmpfile.path());
benchmark(table)
};
let sanakirja_results = {
let tmpfile: NamedTempFile = NamedTempFile::new_in(&tmpdir).unwrap();
fs::remove_file(tmpfile.path()).unwrap();
let db = sanakirja::Env::new(tmpfile.path(), 4096 * 1024 * 1024, 2).unwrap();
let table = SanakirjaBenchDatabase::new(&db);
benchmark(table)
};
fs::remove_dir_all(&tmpdir).unwrap();
let mut rows = Vec::new();
for (benchmark, _duration) in &redb_latency_results {
rows.push(vec![benchmark.to_string()]);
}
for results in [
redb_latency_results,
lmdb_results,
rocksdb_results,
sled_results,
sanakirja_results,
] {
for (i, (_benchmark, duration)) in results.iter().enumerate() {
rows[i].push(format!("{}ms", duration.as_millis()));
}
}
let mut table = comfy_table::Table::new();
table.set_width(100);
table.set_header(["", "redb", "lmdb", "rocksdb", "sled", "sanakirja"]);
for row in rows {
table.add_row(row);
}
println!();
println!("{table}");
}