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::() < slice.len() { let tmp = rng.u128(..); slice[i..(i + size_of::())].copy_from_slice(&tmp.to_le_bytes()); i += size_of::() } if i + size_of::() < slice.len() { let tmp = rng.u64(..); slice[i..(i + size_of::())].copy_from_slice(&tmp.to_le_bytes()); i += size_of::() } if i + size_of::() < slice.len() { let tmp = rng.u32(..); slice[i..(i + size_of::())].copy_from_slice(&tmp.to_le_bytes()); i += size_of::() } if i + size_of::() < slice.len() { let tmp = rng.u16(..); slice[i..(i + size_of::())].copy_from_slice(&tmp.to_le_bytes()); i += size_of::() } if i + size_of::() < slice.len() { slice[i] = rng.u8(..); } } /// Returns pairs of key, value fn gen_pair(rng: &mut fastrand::Rng) -> ([u8; KEY_SIZE], Vec) { 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 { 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(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}"); }