mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
In ci when testing our benchmarks use our test profile `release-with-assertions` This avoids an expensive `lto` step and ensures that our tests run with debug asserts. I noticed that our workflow for 'test cargo benches' was very slow and a lot of that time was the build presumably due to lto overheads across so many benchmark binaries. Compare: * cargo benches for this pr: https://github.com/vercel/next.js/runs/79974366863?pr=94538 - build=4m42s - test=1m46s * cargo benches before this pr: https://github.com/vercel/next.js/actions/runs/27097792020/job/79973082715#logs - build=8m23s - test=6m56s Of course doing this revealed a few things * a birthday paradox panic in one of the persistence tests * some 'top level read' errors to address in the benchmarks and in the turbopack-cli server used for some benchmarks. And finally, that the persistence tests are just extremely slow in test mode due to their initialization overhead (populating >1GB dbs), so that harness is rewritten to remove some variants and also to reduce max sizes in test mode
1488 lines
54 KiB
Rust
1488 lines
54 KiB
Rust
use std::{cell::UnsafeCell, path::Path, sync::LazyLock, time::Duration};
|
|
|
|
use anyhow::Result;
|
|
use criterion::{
|
|
BatchSize, Bencher, Criterion, SamplingMode, black_box, criterion_group, criterion_main,
|
|
measurement::{Measurement, WallTime},
|
|
};
|
|
use parking_lot::Mutex;
|
|
use quick_cache::sync::GuardResult;
|
|
use rand::{RngExt, SeedableRng, rngs::SmallRng, seq::SliceRandom};
|
|
use tempfile::TempDir;
|
|
use turbo_persistence::{
|
|
ArcBytes, BlockCache, CompactConfig, DbConfig as TpDbConfig, Entry, EntryValue, FamilyConfig,
|
|
FamilyKind, MetaEntryFlags, SerialScheduler, StaticSortedFile, StaticSortedFileMetaData,
|
|
TurboPersistence, hash_key, write_static_stored_file,
|
|
};
|
|
use turbo_tasks_malloc::TurboMalloc;
|
|
|
|
#[global_allocator]
|
|
static ALLOC: TurboMalloc = TurboMalloc;
|
|
|
|
// =============================================================================
|
|
// Constants
|
|
// =============================================================================
|
|
|
|
const MB: u64 = 1024 * 1024;
|
|
/// Data amount for batch read benchmarks (128 MiB).
|
|
const BATCH_READ_DATA_AMOUNT: usize = 128 * MB as usize;
|
|
/// Maximum memory to use for storing keys during prefill (4 GiB)
|
|
const MAX_KEY_MEMORY: usize = 4 * 1024 * MB as usize;
|
|
|
|
/// Entry-count cap applied to prefill/setup work unless `LARGE_DB` is set, so the default
|
|
/// `cargo bench` / `cargo test --benches` run stays fast.
|
|
const SCALED_MAX_ENTRIES: usize = 100 * 1024;
|
|
|
|
/// True when the `LARGE_DB` env var is set, unlocking the full (large) benchmark sizes.
|
|
/// Defaults to false so both `cargo bench` and `cargo test --benches` run scaled-down; set
|
|
/// `LARGE_DB=1` for a real benchmarking run.
|
|
static LARGE_DB: LazyLock<bool> = LazyLock::new(|| std::env::var_os("LARGE_DB").is_some());
|
|
|
|
/// Cap an entry count to [`SCALED_MAX_ENTRIES`] unless `LARGE_DB` is set; otherwise return it
|
|
/// unchanged. Used only to bound prefill/setup work — benchmark `id` strings keep using the
|
|
/// configured (unscaled) sizes so real-run reports are unaffected.
|
|
fn scaled(full: usize) -> usize {
|
|
if *LARGE_DB {
|
|
full
|
|
} else {
|
|
full.min(SCALED_MAX_ENTRIES)
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Helper Types and Functions
|
|
// =============================================================================
|
|
|
|
/// Format a number with ki, Mi, Gi suffixes for thousands, millions, billions
|
|
fn format_number(n: usize) -> String {
|
|
if n >= 1_024 * 1_024 * 1_024 {
|
|
format!("{:.2}Gi", n as f32 / (1_024 * 1_024 * 1_024) as f32)
|
|
} else if n >= 1_024 * 1_024 {
|
|
format!("{:.2}Mi", n as f32 / (1_024 * 1_024) as f32)
|
|
} else if n >= 1_024 {
|
|
format!("{:.2}Ki", n as f32 / 1_024_f32)
|
|
} else {
|
|
n.to_string()
|
|
}
|
|
}
|
|
|
|
/// Configuration for prefilling a database
|
|
#[derive(Clone, Copy, Debug)]
|
|
struct DbConfig {
|
|
key_size: usize,
|
|
value_size: usize,
|
|
entry_count: usize,
|
|
commit_count: usize,
|
|
compacted: bool,
|
|
}
|
|
|
|
const KEY_SHARED_PERCENTAGE: usize = 33;
|
|
const KEY_COMPRESSIBLE_PERCENTAGE: usize = 33;
|
|
const VALUE_SHARED_PERCENTAGE: usize = 33;
|
|
const VALUE_COMPRESSIBLE_PERCENTAGE: usize = 33;
|
|
|
|
/// Generate a random key of the specified size
|
|
fn random_data(
|
|
rng: &mut SmallRng,
|
|
size: usize,
|
|
compressability_percentage: usize,
|
|
shared_percentage: usize,
|
|
) -> Box<[u8]> {
|
|
let mut data = vec![0u8; size].into_boxed_slice();
|
|
if size <= 8 {
|
|
rng.fill(&mut data[..]);
|
|
} else {
|
|
let pos_shared = size * shared_percentage / 100;
|
|
let pos_compressible = size * (shared_percentage + compressability_percentage) / 100;
|
|
SmallRng::seed_from_u64(999).fill(&mut data[..pos_shared]);
|
|
rng.fill(&mut data[pos_compressible..]);
|
|
}
|
|
data
|
|
}
|
|
|
|
/// Generate a random key of the specified size
|
|
fn random_key(rng: &mut SmallRng, size: usize) -> Box<[u8]> {
|
|
random_data(
|
|
rng,
|
|
size,
|
|
KEY_COMPRESSIBLE_PERCENTAGE,
|
|
KEY_SHARED_PERCENTAGE,
|
|
)
|
|
}
|
|
|
|
/// Generate `count` distinct keys of `size` bytes each, in random order.
|
|
fn unique_keys(rng: &mut SmallRng, size: usize, count: usize) -> Vec<Box<[u8]>> {
|
|
let mut counters: Vec<u64> = (0..count as u64).collect();
|
|
counters.shuffle(rng);
|
|
let counter_len = size.min(size_of::<u64>());
|
|
counters
|
|
.into_iter()
|
|
.map(|counter| {
|
|
let mut data = random_key(rng, size);
|
|
data[..counter_len].copy_from_slice(&counter.to_le_bytes()[..counter_len]);
|
|
data
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Generate a random value of the specified size
|
|
fn random_value(rng: &mut SmallRng, size: usize) -> Box<[u8]> {
|
|
random_data(
|
|
rng,
|
|
size,
|
|
VALUE_COMPRESSIBLE_PERCENTAGE,
|
|
VALUE_SHARED_PERCENTAGE,
|
|
)
|
|
}
|
|
|
|
/// Prefill a database with the given configuration and return the generated keys
|
|
fn prefill_database(path: &Path, config: &DbConfig) -> Result<Vec<Box<[u8]>>> {
|
|
let db = TurboPersistence::<SerialScheduler, 1>::open(path.to_path_buf())?;
|
|
let mut rng = SmallRng::seed_from_u64(42);
|
|
// Bound prefill work unless `LARGE_DB` is set; `LARGE_DB` runs use the full count.
|
|
let entry_count = scaled(config.entry_count);
|
|
let mut keys = Vec::with_capacity(
|
|
entry_count.min(MAX_KEY_MEMORY / (config.key_size + size_of::<Box<[u8]>>())),
|
|
);
|
|
let all_keys = unique_keys(&mut rng, config.key_size, entry_count);
|
|
|
|
let entries_per_commit = entry_count / config.commit_count;
|
|
|
|
for commit_idx in 0..config.commit_count {
|
|
let batch = db.write_batch()?;
|
|
let start = commit_idx * entries_per_commit;
|
|
let end = if commit_idx == config.commit_count - 1 {
|
|
entry_count
|
|
} else {
|
|
start + entries_per_commit
|
|
};
|
|
|
|
for key in &all_keys[start..end] {
|
|
let value = random_value(&mut rng, config.value_size);
|
|
batch.put(0, key.clone(), value.into())?;
|
|
if keys.len() < keys.capacity() {
|
|
keys.push(key.clone());
|
|
} else {
|
|
let replace = rng.random_range(0..keys.len());
|
|
keys[replace] = key.clone();
|
|
}
|
|
}
|
|
db.commit_write_batch(batch)?;
|
|
}
|
|
|
|
if config.compacted {
|
|
// Run compaction multiple times to ensure all levels are fully compacted
|
|
for _ in 0..3 {
|
|
db.full_compact()?;
|
|
}
|
|
}
|
|
|
|
db.shutdown()?;
|
|
keys.shuffle(&mut rng);
|
|
Ok(keys)
|
|
}
|
|
|
|
/// Create a temporary directory with a prefilled database and return the generated keys
|
|
fn setup_prefilled_db(config: &DbConfig, id: &str) -> Result<(TempDir, Vec<Box<[u8]>>)> {
|
|
let tempdir = tempfile::tempdir()?;
|
|
let keys = prefill_database(tempdir.path(), config)?;
|
|
// Measure disk usage of the database and print it for informational purposes.
|
|
let entry_count = scaled(config.entry_count);
|
|
let db_size = tempdir
|
|
.path()
|
|
.read_dir()?
|
|
.filter_map(|entry| entry.ok())
|
|
.filter_map(|entry| entry.metadata().ok())
|
|
.map(|metadata| metadata.len())
|
|
.sum::<u64>();
|
|
println!(
|
|
"\n{id} db size: {}B = {}B per item = {}% of original size",
|
|
format_number(db_size as usize),
|
|
format_number(db_size as usize / entry_count),
|
|
(db_size as usize * 100 / (entry_count * (config.key_size + config.value_size)))
|
|
);
|
|
Ok((tempdir, keys))
|
|
}
|
|
|
|
fn prepare_db_for_benchmarking(db: &TurboPersistence<SerialScheduler, 1>) {
|
|
db.clear_block_caches();
|
|
db.prepare_all_sst_caches();
|
|
}
|
|
|
|
fn iter_batched_with_init<'a, T, O>(
|
|
b: &mut Bencher<'a, WallTime>,
|
|
mut init: impl FnMut(u64),
|
|
mut setup: impl FnMut(u64) -> T,
|
|
mut iter: impl FnMut(T) -> O,
|
|
batch_size: BatchSize,
|
|
) {
|
|
b.iter_custom(|iters| {
|
|
let batch_size = match batch_size {
|
|
BatchSize::SmallInput => iters.div_ceil(10),
|
|
BatchSize::LargeInput => iters.div_ceil(1000),
|
|
BatchSize::PerIteration => 1,
|
|
BatchSize::NumBatches(size) => iters.div_ceil(size),
|
|
BatchSize::NumIterations(size) => size,
|
|
BatchSize::__NonExhaustive => panic!("__NonExhaustive is not a valid BatchSize."),
|
|
};
|
|
assert!(batch_size > 0);
|
|
let measurement = WallTime;
|
|
let mut value = measurement.zero();
|
|
if batch_size == 1 {
|
|
for _ in 0..iters {
|
|
init(1);
|
|
let input = black_box(setup(0));
|
|
|
|
let start = measurement.start();
|
|
let output = iter(input);
|
|
let end = measurement.end(start);
|
|
|
|
value = measurement.add(&value, &end);
|
|
|
|
drop(black_box(output));
|
|
}
|
|
} else {
|
|
let mut iteration_counter = 0;
|
|
|
|
while iteration_counter < iters {
|
|
let batch_size = ::std::cmp::min(batch_size, iters - iteration_counter);
|
|
init(batch_size);
|
|
let inputs = black_box((0..batch_size).map(&mut setup).collect::<Vec<_>>());
|
|
let mut outputs = Vec::with_capacity(batch_size as usize);
|
|
|
|
let start = measurement.start();
|
|
outputs.extend(inputs.into_iter().map(&mut iter));
|
|
let end = measurement.end(start);
|
|
|
|
value = measurement.add(&value, &end);
|
|
|
|
drop(black_box(outputs));
|
|
iteration_counter += batch_size;
|
|
}
|
|
}
|
|
value
|
|
});
|
|
}
|
|
|
|
// =============================================================================
|
|
// Write Benchmarks
|
|
// =============================================================================
|
|
|
|
fn bench_write(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("write");
|
|
// Reduce sample size for slow benchmarks
|
|
group.sample_size(10);
|
|
group.sampling_mode(SamplingMode::Flat);
|
|
|
|
// Key-value sizes to test
|
|
let entry_sizes = [(8, 4), (4, 32 * 1024), (32 * 1024, 4)];
|
|
// Entry counts to test
|
|
let database_sizes = [1024 * 1024, 10 * 1024 * 1024, 100 * 1024 * 1024];
|
|
|
|
for &(key_size, value_size) in &entry_sizes {
|
|
for &database_size in &database_sizes {
|
|
let full_entry_count = database_size / (key_size + value_size);
|
|
let entry_count = scaled(full_entry_count);
|
|
|
|
let id = format!(
|
|
"key_{}/value_{}/entries_{}",
|
|
format_number(key_size),
|
|
format_number(value_size),
|
|
format_number(full_entry_count)
|
|
);
|
|
group.bench_function(&id, |b| {
|
|
b.iter_batched(
|
|
|| {
|
|
// Setup: create temp directory and RNG
|
|
let tempdir = tempfile::tempdir().unwrap();
|
|
let mut rng = SmallRng::seed_from_u64(42);
|
|
let entry_size = key_size + value_size;
|
|
let mut random_data = vec![0u8; entry_count * entry_size];
|
|
rng.fill(&mut random_data[..]);
|
|
// SingleValue families forbid duplicate keys, so overwrite each key region
|
|
// with a distinct, shuffled key (see `unique_keys`).
|
|
for (i, key) in unique_keys(&mut rng, key_size, entry_count)
|
|
.iter()
|
|
.enumerate()
|
|
{
|
|
let key_start = i * entry_size;
|
|
random_data[key_start..key_start + key_size].copy_from_slice(key);
|
|
}
|
|
|
|
(tempdir, random_data)
|
|
},
|
|
|(tempdir, random_data)| {
|
|
// Timed: write entries and commit
|
|
let db = TurboPersistence::<SerialScheduler, 1>::open(
|
|
tempdir.path().to_path_buf(),
|
|
)
|
|
.unwrap();
|
|
{
|
|
let batch = db.write_batch().unwrap();
|
|
let entry_size = key_size + value_size;
|
|
|
|
for i in 0..entry_count {
|
|
let key = &random_data[i * entry_size..i * entry_size + key_size];
|
|
let value =
|
|
&random_data[i * entry_size + key_size..(i + 1) * entry_size];
|
|
batch.put(0, key, value.into()).unwrap();
|
|
}
|
|
|
|
db.commit_write_batch(batch).unwrap();
|
|
}
|
|
db.shutdown().unwrap();
|
|
tempdir
|
|
},
|
|
BatchSize::PerIteration,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// =============================================================================
|
|
// Read Benchmarks - Single Get
|
|
// =============================================================================
|
|
|
|
fn bench_read_get(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("read/get");
|
|
group.measurement_time(Duration::from_secs(10));
|
|
|
|
// Test empty database first
|
|
{
|
|
let tempdir = tempfile::tempdir().unwrap();
|
|
let db =
|
|
TurboPersistence::<SerialScheduler, 1>::open(tempdir.path().to_path_buf()).unwrap();
|
|
|
|
group.bench_function("empty", |b| {
|
|
b.iter(|| {
|
|
let result = db.get(0, &[0u8; 4]).unwrap();
|
|
black_box(result)
|
|
});
|
|
});
|
|
}
|
|
|
|
// Configuration parameters: (key_size, value_size)
|
|
let entry_sizes = [(8, 4), (4, 32 * 1024), (32 * 1024, 4)];
|
|
// Configuration parameters: (database_size, commit_count, compacted)
|
|
let size_commits_compacted = [
|
|
(128 * 1024 * 1024, 1, true),
|
|
(128 * 1024 * 1024, 1, false),
|
|
(128 * 1024 * 1024, 20, false),
|
|
];
|
|
|
|
for &(key_size, value_size) in &entry_sizes {
|
|
for &(database_size, commit_count, compacted) in &size_commits_compacted {
|
|
let entry_count = database_size / (key_size + value_size);
|
|
let config = DbConfig {
|
|
key_size,
|
|
value_size,
|
|
entry_count,
|
|
commit_count,
|
|
compacted,
|
|
};
|
|
|
|
let compacted_str = if compacted {
|
|
"compacted"
|
|
} else {
|
|
"uncompacted"
|
|
};
|
|
let id = format!(
|
|
"key_{}/value_{}/entries_{}/commits_{}/{}",
|
|
format_number(key_size),
|
|
format_number(value_size),
|
|
format_number(entry_count),
|
|
commit_count,
|
|
compacted_str,
|
|
);
|
|
|
|
let db = LazyLock::new(|| {
|
|
let (tempdir, keys) = setup_prefilled_db(&config, &id).unwrap();
|
|
let db = TurboPersistence::<SerialScheduler, 1>::open(tempdir.path().to_path_buf())
|
|
.unwrap();
|
|
let rng = Mutex::new(SmallRng::seed_from_u64(123));
|
|
(tempdir, db, keys, rng)
|
|
});
|
|
|
|
group.bench_function(format!("{id}/hit/uncached"), |b| {
|
|
let (_, db, keys, rng) = &*db;
|
|
let mut rng = rng.lock();
|
|
iter_batched_with_init(
|
|
b,
|
|
|_| prepare_db_for_benchmarking(db),
|
|
|_| {
|
|
let idx = rng.random_range(0..keys.len());
|
|
&keys[idx]
|
|
},
|
|
|key| {
|
|
let result = db.get(0, key).unwrap();
|
|
black_box(result)
|
|
},
|
|
BatchSize::PerIteration,
|
|
);
|
|
});
|
|
|
|
group.bench_function(format!("{id}/hit/cached"), |b| {
|
|
let (_, db, keys, _) = &*db;
|
|
iter_batched_with_init(
|
|
b,
|
|
|_| prepare_db_for_benchmarking(db),
|
|
|i| &keys[i as usize % keys.len()],
|
|
|key| {
|
|
let result = db.get(0, key).unwrap();
|
|
black_box(result)
|
|
},
|
|
BatchSize::NumBatches(1),
|
|
);
|
|
});
|
|
|
|
group.bench_function(format!("{id}/miss/uncached"), |b| {
|
|
let (_, db, _, rng) = &*db;
|
|
let mut rng = rng.lock();
|
|
iter_batched_with_init(
|
|
b,
|
|
|_| prepare_db_for_benchmarking(db),
|
|
|_| random_key(&mut rng, key_size),
|
|
|key| {
|
|
let result = db.get(0, &key).unwrap();
|
|
black_box(result)
|
|
},
|
|
BatchSize::PerIteration,
|
|
);
|
|
});
|
|
|
|
group.bench_function(format!("{id}/miss/cached"), |b| {
|
|
let (_, db, _, rng) = &*db;
|
|
let mut rng = rng.lock();
|
|
let miss_keys = UnsafeCell::new(Vec::new());
|
|
iter_batched_with_init(
|
|
b,
|
|
|batch_size| {
|
|
prepare_db_for_benchmarking(db);
|
|
// SAFETY: We are the only ones mutating miss_keys during this
|
|
// initialization phase
|
|
let miss_keys = unsafe { &mut *miss_keys.get() };
|
|
while miss_keys.len() < batch_size as usize {
|
|
miss_keys.push(random_key(&mut rng, key_size));
|
|
}
|
|
},
|
|
|i| {
|
|
let miss_keys = unsafe { &*miss_keys.get() };
|
|
&miss_keys[i as usize]
|
|
},
|
|
|key| {
|
|
let result = db.get(0, key).unwrap();
|
|
black_box(result)
|
|
},
|
|
BatchSize::NumBatches(1),
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// =============================================================================
|
|
// Read Benchmarks - Batch Get
|
|
// =============================================================================
|
|
|
|
fn bench_read_batch_get(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("read/batch_get");
|
|
group.measurement_time(Duration::from_secs(10));
|
|
group.sample_size(20);
|
|
group.sampling_mode(SamplingMode::Flat);
|
|
|
|
// Configuration parameters: (key_size, value_size)
|
|
let entry_sizes = [(8, 4), (4, 512), (512, 4)];
|
|
// Configuration parameters: (commit_count, compacted)
|
|
let commit_configs = [(1, true), (1, false), (20, false)];
|
|
let batch_sizes = [100, 1024, 10 * 1024, 100 * 1024];
|
|
|
|
for &(key_size, value_size) in &entry_sizes {
|
|
for &(commit_count, compacted) in &commit_configs {
|
|
let entry_count = BATCH_READ_DATA_AMOUNT / (key_size + value_size);
|
|
|
|
let config = DbConfig {
|
|
key_size,
|
|
value_size,
|
|
entry_count,
|
|
commit_count,
|
|
compacted,
|
|
};
|
|
|
|
let compacted_str = if compacted {
|
|
"compacted"
|
|
} else {
|
|
"uncompacted"
|
|
};
|
|
|
|
let id = format!(
|
|
"key_{}/value_{}/entries_{}/commits_{}/{}",
|
|
format_number(key_size),
|
|
format_number(value_size),
|
|
format_number(entry_count),
|
|
commit_count,
|
|
compacted_str,
|
|
);
|
|
|
|
let db = LazyLock::new(|| {
|
|
let (tempdir, stored_keys) = setup_prefilled_db(&config, &id).unwrap();
|
|
let db = TurboPersistence::<SerialScheduler, 1>::open(tempdir.path().to_path_buf())
|
|
.unwrap();
|
|
let rng = Mutex::new(SmallRng::seed_from_u64(456));
|
|
(tempdir, db, stored_keys, rng)
|
|
});
|
|
|
|
for &batch_size in &batch_sizes {
|
|
let id = format!("{id}/batch_{}", format_number(batch_size),);
|
|
|
|
group.bench_function(format!("{id}/hit/cached"), |b| {
|
|
let (_, db, stored_keys, _) = &*db;
|
|
iter_batched_with_init(
|
|
b,
|
|
|_| prepare_db_for_benchmarking(db),
|
|
|i| {
|
|
(0..batch_size)
|
|
.map(|j| {
|
|
&*stored_keys[(i as usize * batch_size + j) % stored_keys.len()]
|
|
})
|
|
.collect::<Vec<_>>()
|
|
},
|
|
|keys| {
|
|
let result = db.batch_get(0, &keys).unwrap();
|
|
black_box(result)
|
|
},
|
|
BatchSize::NumBatches(1),
|
|
);
|
|
});
|
|
|
|
group.bench_function(format!("{id}/miss/cached"), |b| {
|
|
let (_, db, _, rng) = &*db;
|
|
let mut rng = rng.lock();
|
|
let miss_keys = UnsafeCell::new(Vec::new());
|
|
iter_batched_with_init(
|
|
b,
|
|
|iter_batch_size| {
|
|
prepare_db_for_benchmarking(db);
|
|
// SAFETY: We are the only ones mutating miss_keys during this
|
|
// initialization phase
|
|
let miss_keys = unsafe { &mut *miss_keys.get() };
|
|
let needed_keys = iter_batch_size as usize * batch_size;
|
|
while miss_keys.len() < needed_keys {
|
|
miss_keys.push(random_key(&mut rng, key_size));
|
|
}
|
|
},
|
|
|i| {
|
|
let miss_keys = unsafe { &*miss_keys.get() };
|
|
(0..batch_size)
|
|
.map(|j| &*miss_keys[i as usize * batch_size + j])
|
|
.collect::<Vec<_>>()
|
|
},
|
|
|keys| {
|
|
let result = db.batch_get(0, &keys).unwrap();
|
|
black_box(result)
|
|
},
|
|
BatchSize::NumBatches(1),
|
|
);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// =============================================================================
|
|
// Read Benchmarks - Get Multiple
|
|
// =============================================================================
|
|
|
|
/// Configuration for prefilling a multi-value database
|
|
#[derive(Clone, Copy, Debug)]
|
|
struct MultiValueDbConfig {
|
|
key_size: usize,
|
|
value_size: usize,
|
|
/// Total number of entries (key-value pairs) written
|
|
entry_count: usize,
|
|
/// Number of distinct keys. Must be <= entry_count.
|
|
/// When < entry_count, some keys will have multiple values.
|
|
distinct_key_count: usize,
|
|
commit_count: usize,
|
|
compacted: bool,
|
|
}
|
|
|
|
/// Prefill a multi-value database and return the distinct keys
|
|
fn prefill_multi_value_database(
|
|
path: &Path,
|
|
config: &MultiValueDbConfig,
|
|
) -> Result<Vec<Box<[u8]>>> {
|
|
let db_config = TpDbConfig {
|
|
family_configs: [FamilyConfig {
|
|
name: "test",
|
|
kind: FamilyKind::MultiValue,
|
|
}],
|
|
};
|
|
let db =
|
|
TurboPersistence::<SerialScheduler, 1>::open_with_config(path.to_path_buf(), db_config)?;
|
|
let mut rng = SmallRng::seed_from_u64(42);
|
|
|
|
let entry_count = scaled(config.entry_count);
|
|
let distinct_key_count = scaled(config.distinct_key_count);
|
|
|
|
// Generate all distinct keys up front.
|
|
let max_stored_keys =
|
|
distinct_key_count.min(MAX_KEY_MEMORY / (config.key_size + size_of::<Box<[u8]>>()));
|
|
let mut keys = unique_keys(&mut rng, config.key_size, max_stored_keys);
|
|
|
|
let entries_per_commit = entry_count / config.commit_count;
|
|
|
|
for commit_idx in 0..config.commit_count {
|
|
let batch = db.write_batch()?;
|
|
let start = commit_idx * entries_per_commit;
|
|
let end = if commit_idx == config.commit_count - 1 {
|
|
entry_count
|
|
} else {
|
|
start + entries_per_commit
|
|
};
|
|
|
|
for i in start..end {
|
|
// Cycle through keys to create duplicates
|
|
let key = &keys[i % keys.len()];
|
|
let value = random_value(&mut rng, config.value_size);
|
|
batch.put(0, key.clone(), value.into())?;
|
|
}
|
|
db.commit_write_batch(batch)?;
|
|
}
|
|
|
|
if config.compacted {
|
|
for _ in 0..3 {
|
|
db.full_compact()?;
|
|
}
|
|
}
|
|
|
|
db.shutdown()?;
|
|
keys.shuffle(&mut rng);
|
|
Ok(keys)
|
|
}
|
|
|
|
/// Create a temporary directory with a prefilled multi-value database
|
|
fn setup_prefilled_multi_value_db(
|
|
config: &MultiValueDbConfig,
|
|
id: &str,
|
|
) -> Result<(TempDir, Vec<Box<[u8]>>)> {
|
|
let tempdir = tempfile::tempdir()?;
|
|
let keys = prefill_multi_value_database(tempdir.path(), config)?;
|
|
let entry_count = scaled(config.entry_count);
|
|
let db_size = tempdir
|
|
.path()
|
|
.read_dir()?
|
|
.filter_map(|entry| entry.ok())
|
|
.filter_map(|entry| entry.metadata().ok())
|
|
.map(|metadata| metadata.len())
|
|
.sum::<u64>();
|
|
println!(
|
|
"\n{id} db size: {}B = {}B per item = {}% of original size",
|
|
format_number(db_size as usize),
|
|
format_number(db_size as usize / entry_count),
|
|
(db_size as usize * 100 / (entry_count * (config.key_size + config.value_size)))
|
|
);
|
|
Ok((tempdir, keys))
|
|
}
|
|
|
|
fn open_multi_value_db(path: &Path) -> TurboPersistence<SerialScheduler, 1> {
|
|
let db_config = TpDbConfig {
|
|
family_configs: [FamilyConfig {
|
|
name: "test",
|
|
kind: FamilyKind::MultiValue,
|
|
}],
|
|
};
|
|
TurboPersistence::<SerialScheduler, 1>::open_with_config(path.to_path_buf(), db_config).unwrap()
|
|
}
|
|
|
|
fn bench_read_get_multiple(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("read/get_multiple");
|
|
group.measurement_time(Duration::from_secs(10));
|
|
|
|
// Configuration parameters: (key_size, value_size)
|
|
let entry_sizes = [(8, 4), (4, 32 * 1024), (32 * 1024, 4)];
|
|
// Configuration parameters: (database_size, values_per_key, commit_count, compacted)
|
|
let db_configs: &[(usize, usize, usize, bool)] = &[
|
|
// 1 value per key (similar to SingleValue, baseline)
|
|
(128 * 1024 * 1024, 1, 1, true),
|
|
(128 * 1024 * 1024, 1, 1, false),
|
|
// Multiple values per key
|
|
(128 * 1024 * 1024, 4, 1, true),
|
|
(128 * 1024 * 1024, 4, 1, false),
|
|
(128 * 1024 * 1024, 4, 20, false),
|
|
];
|
|
|
|
for &(key_size, value_size) in &entry_sizes {
|
|
for &(database_size, values_per_key, commit_count, compacted) in db_configs {
|
|
let entry_count = database_size / (key_size + value_size);
|
|
let distinct_key_count = entry_count / values_per_key;
|
|
let config = MultiValueDbConfig {
|
|
key_size,
|
|
value_size,
|
|
entry_count,
|
|
distinct_key_count,
|
|
commit_count,
|
|
compacted,
|
|
};
|
|
|
|
let compacted_str = if compacted {
|
|
"compacted"
|
|
} else {
|
|
"uncompacted"
|
|
};
|
|
let id = format!(
|
|
"key_{}/value_{}/entries_{}/vpk_{}/commits_{}/{}",
|
|
format_number(key_size),
|
|
format_number(value_size),
|
|
format_number(entry_count),
|
|
values_per_key,
|
|
commit_count,
|
|
compacted_str,
|
|
);
|
|
|
|
let db = LazyLock::new(|| {
|
|
let (tempdir, keys) = setup_prefilled_multi_value_db(&config, &id).unwrap();
|
|
let db = open_multi_value_db(tempdir.path());
|
|
let rng = Mutex::new(SmallRng::seed_from_u64(123));
|
|
(tempdir, db, keys, rng)
|
|
});
|
|
|
|
group.bench_function(format!("{id}/hit/uncached"), |b| {
|
|
let (_, db, keys, rng) = &*db;
|
|
let mut rng = rng.lock();
|
|
iter_batched_with_init(
|
|
b,
|
|
|_| prepare_db_for_benchmarking(db),
|
|
|_| {
|
|
let idx = rng.random_range(0..keys.len());
|
|
&keys[idx]
|
|
},
|
|
|key| {
|
|
let result = db.get_multiple(0, key).unwrap();
|
|
black_box(result)
|
|
},
|
|
BatchSize::PerIteration,
|
|
);
|
|
});
|
|
|
|
group.bench_function(format!("{id}/hit/cached"), |b| {
|
|
let (_, db, keys, _) = &*db;
|
|
iter_batched_with_init(
|
|
b,
|
|
|_| prepare_db_for_benchmarking(db),
|
|
|i| &keys[i as usize % keys.len()],
|
|
|key| {
|
|
let result = db.get_multiple(0, key).unwrap();
|
|
black_box(result)
|
|
},
|
|
BatchSize::NumBatches(1),
|
|
);
|
|
});
|
|
|
|
group.bench_function(format!("{id}/miss/uncached"), |b| {
|
|
let (_, db, _, rng) = &*db;
|
|
let mut rng = rng.lock();
|
|
iter_batched_with_init(
|
|
b,
|
|
|_| prepare_db_for_benchmarking(db),
|
|
|_| random_key(&mut rng, key_size),
|
|
|key| {
|
|
let result = db.get_multiple(0, &key).unwrap();
|
|
black_box(result)
|
|
},
|
|
BatchSize::PerIteration,
|
|
);
|
|
});
|
|
|
|
group.bench_function(format!("{id}/miss/cached"), |b| {
|
|
let (_, db, _, rng) = &*db;
|
|
let mut rng = rng.lock();
|
|
let miss_keys = UnsafeCell::new(Vec::new());
|
|
iter_batched_with_init(
|
|
b,
|
|
|batch_size| {
|
|
prepare_db_for_benchmarking(db);
|
|
// SAFETY: We are the only ones mutating miss_keys during this
|
|
// initialization phase
|
|
let miss_keys = unsafe { &mut *miss_keys.get() };
|
|
while miss_keys.len() < batch_size as usize {
|
|
miss_keys.push(random_key(&mut rng, key_size));
|
|
}
|
|
},
|
|
|i| {
|
|
let miss_keys = unsafe { &*miss_keys.get() };
|
|
&miss_keys[i as usize]
|
|
},
|
|
|key| {
|
|
let result = db.get_multiple(0, key).unwrap();
|
|
black_box(result)
|
|
},
|
|
BatchSize::NumBatches(1),
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// =============================================================================
|
|
// Compaction Benchmarks
|
|
// =============================================================================
|
|
|
|
fn bench_compaction(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("compaction");
|
|
// Compaction is expensive, reduce sample size
|
|
group.sample_size(10);
|
|
group.sampling_mode(SamplingMode::Flat);
|
|
|
|
// Configuration parameters: (key_size, value_size)
|
|
let entry_sizes = [(8, 4)];
|
|
// Configuration parameters: (entry_count, commit_count)
|
|
let db_configs = [
|
|
(1024 * 1024 * 4, 8),
|
|
(1024 * 1024 * 16, 8),
|
|
(1024 * 1024 * 4, 32),
|
|
(1024 * 1024 * 16, 32),
|
|
(1024 * 1024 * 4, 128),
|
|
(1024 * 1024 * 16, 128),
|
|
];
|
|
|
|
for &(key_size, value_size) in &entry_sizes {
|
|
for &(entry_count, commit_count) in &db_configs {
|
|
let id = format!(
|
|
"key_{}/value_{}/entries_{}/commits_{}",
|
|
format_number(key_size),
|
|
format_number(value_size),
|
|
format_number(entry_count),
|
|
commit_count
|
|
);
|
|
|
|
let setup = || {
|
|
// Setup: create and prefill database (not compacted)
|
|
let config = DbConfig {
|
|
key_size,
|
|
value_size,
|
|
entry_count,
|
|
commit_count,
|
|
compacted: false,
|
|
};
|
|
let (tempdir, _keys) = setup_prefilled_db(&config, &id).unwrap();
|
|
let db = TurboPersistence::<SerialScheduler, 1>::open(tempdir.path().to_path_buf())
|
|
.unwrap();
|
|
(tempdir, db)
|
|
};
|
|
|
|
group.bench_function(&id, |b| {
|
|
b.iter_batched(
|
|
setup,
|
|
|(_tempdir, db)| {
|
|
// Timed: run normal compaction
|
|
db.compact(&CompactConfig {
|
|
min_merge_count: 3,
|
|
optimal_merge_count: 8,
|
|
max_merge_count: 64,
|
|
max_merge_bytes: 512 * MB,
|
|
min_merge_duplication_bytes: 50 * MB,
|
|
optimal_merge_duplication_bytes: 100 * MB,
|
|
max_merge_segment_count: 16,
|
|
})
|
|
.unwrap();
|
|
black_box(db)
|
|
},
|
|
BatchSize::PerIteration,
|
|
);
|
|
});
|
|
group.bench_function(format!("{id}/full"), |b| {
|
|
b.iter_batched(
|
|
setup,
|
|
|(_tempdir, db)| {
|
|
// Timed: run full compaction
|
|
db.full_compact().unwrap();
|
|
black_box(db)
|
|
},
|
|
BatchSize::PerIteration,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// =============================================================================
|
|
// Write Benchmarks - Multi-Value
|
|
// =============================================================================
|
|
|
|
fn bench_write_multi_value(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("write/multi_value");
|
|
group.sample_size(10);
|
|
group.sampling_mode(SamplingMode::Flat);
|
|
|
|
// Key-value sizes to test
|
|
let entry_sizes = [(8, 4), (4, 32 * 1024)];
|
|
// (database_size, values_per_key)
|
|
let configs: &[(usize, usize)] = &[
|
|
(10 * 1024 * 1024, 1), // Baseline: 1 value per key
|
|
(10 * 1024 * 1024, 4), // 4 values per key
|
|
];
|
|
|
|
for &(key_size, value_size) in &entry_sizes {
|
|
for &(database_size, values_per_key) in configs {
|
|
// `id` uses the configured size so real-run reports are unaffected; the scaled
|
|
// counts bound the actual work unless `LARGE_DB` is set.
|
|
let full_entry_count = database_size / (key_size + value_size);
|
|
let entry_count = scaled(full_entry_count);
|
|
let distinct_key_count = (entry_count / values_per_key).max(1);
|
|
|
|
let id = format!(
|
|
"key_{}/value_{}/entries_{}/vpk_{}",
|
|
format_number(key_size),
|
|
format_number(value_size),
|
|
format_number(full_entry_count),
|
|
values_per_key,
|
|
);
|
|
group.bench_function(&id, |b| {
|
|
b.iter_batched(
|
|
|| {
|
|
let tempdir = tempfile::tempdir().unwrap();
|
|
let mut rng = SmallRng::seed_from_u64(42);
|
|
// Generate distinct (unique) keys to keep the true distinct count exact.
|
|
let keys = unique_keys(&mut rng, key_size, distinct_key_count);
|
|
let mut random_data = vec![0u8; entry_count * value_size];
|
|
rng.fill(&mut random_data[..]);
|
|
(tempdir, keys, random_data)
|
|
},
|
|
|(tempdir, keys, random_data)| {
|
|
let db_config = TpDbConfig {
|
|
family_configs: [FamilyConfig {
|
|
name: "test",
|
|
kind: FamilyKind::MultiValue,
|
|
}],
|
|
};
|
|
let db = TurboPersistence::<SerialScheduler, 1>::open_with_config(
|
|
tempdir.path().to_path_buf(),
|
|
db_config,
|
|
)
|
|
.unwrap();
|
|
let batch = db.write_batch().unwrap();
|
|
for i in 0..entry_count {
|
|
let key = &keys[i % distinct_key_count];
|
|
let value = &random_data[i * value_size..(i + 1) * value_size];
|
|
batch.put(0, &**key, value.into()).unwrap();
|
|
}
|
|
db.commit_write_batch(batch).unwrap();
|
|
db.shutdown().unwrap();
|
|
tempdir
|
|
},
|
|
BatchSize::PerIteration,
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// =============================================================================
|
|
// Compaction Benchmarks - Multi-Value
|
|
// =============================================================================
|
|
|
|
fn bench_compaction_multi_value(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("compaction/multi_value");
|
|
group.sample_size(10);
|
|
group.sampling_mode(SamplingMode::Flat);
|
|
|
|
// (entry_count, values_per_key, commit_count)
|
|
let configs = [
|
|
(1024 * 1024 * 4, 1, 8), // Baseline: 1 value/key
|
|
(1024 * 1024 * 4, 4, 8), // 4 values/key
|
|
(1024 * 1024 * 4, 4, 32), // 4 values/key, more commits
|
|
];
|
|
|
|
let key_size = 8;
|
|
let value_size = 4;
|
|
|
|
for &(entry_count, values_per_key, commit_count) in &configs {
|
|
let distinct_key_count = entry_count / values_per_key;
|
|
let config = MultiValueDbConfig {
|
|
key_size,
|
|
value_size,
|
|
entry_count,
|
|
distinct_key_count,
|
|
commit_count,
|
|
compacted: false,
|
|
};
|
|
|
|
let id = format!(
|
|
"entries_{}/vpk_{}/commits_{}",
|
|
format_number(entry_count),
|
|
values_per_key,
|
|
commit_count,
|
|
);
|
|
|
|
let setup = || {
|
|
let (tempdir, _keys) = setup_prefilled_multi_value_db(&config, &id).unwrap();
|
|
let db = open_multi_value_db(tempdir.path());
|
|
(tempdir, db)
|
|
};
|
|
|
|
group.bench_function(format!("{id}/full"), |b| {
|
|
b.iter_batched(
|
|
setup,
|
|
|(_tempdir, db)| {
|
|
db.full_compact().unwrap();
|
|
black_box(db)
|
|
},
|
|
BatchSize::PerIteration,
|
|
);
|
|
});
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// =============================================================================
|
|
// QFilter Benchmarks
|
|
// =============================================================================
|
|
|
|
fn bench_qfilter(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("qfilter");
|
|
group.warm_up_time(Duration::from_secs(5));
|
|
group.measurement_time(Duration::from_secs(10));
|
|
|
|
// Filter sizes to test: 1ki, 10ki, 100ki, 1000ki fingerprints
|
|
let filter_sizes = [1024, 10 * 1024, 100 * 1024, 1000 * 1024];
|
|
// False positive rate matching AMQF_FALSE_POSITIVE_RATE in static_sorted_file_builder.rs
|
|
const FALSE_POSITIVE_RATE: f64 = 0.01;
|
|
|
|
for &size in &filter_sizes {
|
|
// Pre-build filter with random fingerprints
|
|
let filter = LazyLock::new(|| {
|
|
let mut rng = SmallRng::seed_from_u64(42);
|
|
let mut filter = qfilter::Filter::new(size as u64, FALSE_POSITIVE_RATE)
|
|
.expect("Filter construction failed");
|
|
|
|
// Store fingerprints for hit testing
|
|
let mut fingerprints = Vec::with_capacity(size);
|
|
for _ in 0..size {
|
|
let fp: u64 = rng.random();
|
|
filter.insert_fingerprint(false, fp).expect("Insert failed");
|
|
fingerprints.push(fp);
|
|
}
|
|
|
|
let rng = Mutex::new(SmallRng::seed_from_u64(123));
|
|
(filter, fingerprints, rng)
|
|
});
|
|
|
|
let id = format!("entries_{}", format_number(size));
|
|
|
|
// Benchmark hit case (query fingerprints that exist)
|
|
group.bench_function(format!("{id}/hit"), |b| {
|
|
let (filter, fingerprints, rng) = &*filter;
|
|
let mut rng = rng.lock();
|
|
b.iter_batched(
|
|
|| {
|
|
let idx = rng.random_range(0..fingerprints.len());
|
|
fingerprints[idx]
|
|
},
|
|
|fp| {
|
|
let result = filter.contains_fingerprint(fp);
|
|
black_box(result)
|
|
},
|
|
BatchSize::SmallInput,
|
|
);
|
|
});
|
|
|
|
// Benchmark miss case (query random fingerprints not in filter)
|
|
group.bench_function(format!("{id}/miss"), |b| {
|
|
let (filter, _, rng) = &*filter;
|
|
let mut rng = rng.lock();
|
|
b.iter_batched(
|
|
|| {
|
|
// Generate random fingerprint (very unlikely to be in filter)
|
|
rng.random::<u64>()
|
|
},
|
|
|fp| {
|
|
let result = filter.contains_fingerprint(fp);
|
|
black_box(result)
|
|
},
|
|
BatchSize::SmallInput,
|
|
);
|
|
});
|
|
|
|
// Benchmark insert performance (build filter from scratch)
|
|
group.bench_function(format!("{id}/insert"), |b| {
|
|
let (_, fingerprints, _) = &*filter;
|
|
b.iter_batched(
|
|
|| {
|
|
// Setup: create empty filter
|
|
qfilter::Filter::new(size as u64, FALSE_POSITIVE_RATE)
|
|
.expect("Filter construction failed")
|
|
},
|
|
|mut filter| {
|
|
// Timed: insert all fingerprints
|
|
for &fp in fingerprints {
|
|
filter.insert_fingerprint(false, fp).expect("Insert failed");
|
|
}
|
|
black_box(filter)
|
|
},
|
|
BatchSize::LargeInput,
|
|
);
|
|
});
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// =============================================================================
|
|
// StaticSortedFile Lookup Benchmarks
|
|
// =============================================================================
|
|
|
|
/// Entry implementation for benchmarking StaticSortedFile
|
|
struct BenchEntry {
|
|
key: [u8; 8],
|
|
value: [u8; 4],
|
|
hash: u64,
|
|
}
|
|
|
|
impl Entry for BenchEntry {
|
|
fn key_hash(&self) -> u64 {
|
|
self.hash
|
|
}
|
|
|
|
fn key_len(&self) -> usize {
|
|
8
|
|
}
|
|
|
|
fn write_key_to(&self, buf: &mut Vec<u8>) {
|
|
buf.extend_from_slice(&self.key);
|
|
}
|
|
|
|
fn value(&self) -> EntryValue<'_> {
|
|
EntryValue::Small { value: &self.value }
|
|
}
|
|
}
|
|
|
|
fn bench_static_sorted_file_lookup(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("static_sorted_file_lookup");
|
|
group.warm_up_time(Duration::from_secs(5));
|
|
group.measurement_time(Duration::from_secs(10));
|
|
|
|
// Entry counts to test: 1ki, 10ki, 100ki, 1000ki
|
|
let entry_counts = [1024, 10 * 1024, 100 * 1024, 1000 * 1024];
|
|
|
|
for &entry_count in &entry_counts {
|
|
// Pre-build the SST file and related data structures
|
|
let data = LazyLock::new(|| {
|
|
let mut rng = SmallRng::seed_from_u64(42);
|
|
|
|
// Generate random entries
|
|
let mut entries: Vec<BenchEntry> = (0..entry_count)
|
|
.map(|_| {
|
|
let mut key = [0u8; 8];
|
|
let mut value = [0u8; 4];
|
|
rng.fill(&mut key[..]);
|
|
rng.fill(&mut value[..]);
|
|
let hash = hash_key(&key);
|
|
BenchEntry { key, value, hash }
|
|
})
|
|
.collect();
|
|
|
|
// Sort by hash (required by write_static_stored_file)
|
|
entries.sort_by_key(|e| e.hash);
|
|
|
|
// Create temp directory and write SST file
|
|
let tempdir = tempfile::tempdir().unwrap();
|
|
let sst_path = tempdir.path().join("00000001.sst");
|
|
let (meta, _file) =
|
|
write_static_stored_file(&entries, &sst_path, MetaEntryFlags::FRESH).unwrap();
|
|
|
|
// Open the SST file
|
|
let sst_meta = StaticSortedFileMetaData {
|
|
sequence_number: 1,
|
|
block_count: meta.block_count,
|
|
};
|
|
let sst = StaticSortedFile::open(tempdir.path(), sst_meta).unwrap();
|
|
|
|
// Create block caches
|
|
let key_block_cache: BlockCache = BlockCache::with(
|
|
1000,
|
|
64 * 1024 * 1024,
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
);
|
|
let value_block_cache: BlockCache = BlockCache::with(
|
|
1000,
|
|
64 * 1024 * 1024,
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
);
|
|
|
|
// Store keys and hashes for lookup testing
|
|
let keys: Vec<([u8; 8], u64)> = entries.iter().map(|e| (e.key, e.hash)).collect();
|
|
|
|
let rng = Mutex::new(SmallRng::seed_from_u64(123));
|
|
(tempdir, sst, key_block_cache, value_block_cache, keys, rng)
|
|
});
|
|
|
|
let id = format!("entries_{}", format_number(entry_count));
|
|
|
|
// Benchmark hit case (lookup keys that exist)
|
|
group.bench_function(format!("{id}/hit/uncached"), |b| {
|
|
let (_, sst, key_block_cache, value_block_cache, keys, rng) = &*data;
|
|
let mut rng = rng.lock();
|
|
iter_batched_with_init(
|
|
b,
|
|
|_| {
|
|
key_block_cache.clear();
|
|
value_block_cache.clear();
|
|
},
|
|
|_| {
|
|
let idx = rng.random_range(0..keys.len());
|
|
keys[idx]
|
|
},
|
|
|(key, hash)| {
|
|
let result = sst
|
|
.lookup::<_, false>(hash, &key, key_block_cache, value_block_cache)
|
|
.unwrap();
|
|
black_box(result)
|
|
},
|
|
BatchSize::PerIteration,
|
|
);
|
|
});
|
|
|
|
group.bench_function(format!("{id}/hit/cached"), |b| {
|
|
let (_, sst, key_block_cache, value_block_cache, keys, _) = &*data;
|
|
iter_batched_with_init(
|
|
b,
|
|
|_| {
|
|
key_block_cache.clear();
|
|
value_block_cache.clear();
|
|
},
|
|
|i| keys[i as usize % keys.len()],
|
|
|(key, hash)| {
|
|
let result = sst
|
|
.lookup::<_, false>(hash, &key, key_block_cache, value_block_cache)
|
|
.unwrap();
|
|
black_box(result)
|
|
},
|
|
BatchSize::NumBatches(1),
|
|
);
|
|
});
|
|
|
|
// Benchmark miss case (lookup random keys not in file)
|
|
group.bench_function(format!("{id}/miss/uncached"), |b| {
|
|
let (_, sst, key_block_cache, value_block_cache, _, rng) = &*data;
|
|
let mut rng = rng.lock();
|
|
iter_batched_with_init(
|
|
b,
|
|
|_| {
|
|
key_block_cache.clear();
|
|
value_block_cache.clear();
|
|
},
|
|
|_| {
|
|
// Generate random key (very unlikely to be in file)
|
|
let key = random_key(&mut rng, 8);
|
|
let hash = hash_key(&key);
|
|
(key, hash)
|
|
},
|
|
|(key, hash)| {
|
|
let result = sst
|
|
.lookup::<_, false>(hash, &key, key_block_cache, value_block_cache)
|
|
.unwrap();
|
|
black_box(result)
|
|
},
|
|
BatchSize::PerIteration,
|
|
);
|
|
});
|
|
|
|
group.bench_function(format!("{id}/miss/cached"), |b| {
|
|
let (_, sst, key_block_cache, value_block_cache, _, rng) = &*data;
|
|
let mut rng = rng.lock();
|
|
let miss_keys = UnsafeCell::new(Vec::new());
|
|
iter_batched_with_init(
|
|
b,
|
|
|batch_size| {
|
|
key_block_cache.clear();
|
|
value_block_cache.clear();
|
|
|
|
// SAFETY: We are the only ones mutating miss_keys during this initialization
|
|
// phase
|
|
let miss_keys = unsafe { &mut *miss_keys.get() };
|
|
while miss_keys.len() < batch_size as usize {
|
|
let key = random_key(&mut rng, 8);
|
|
let hash = hash_key(&key);
|
|
miss_keys.push((key, hash));
|
|
}
|
|
},
|
|
|i| {
|
|
let miss_keys = unsafe { &*miss_keys.get() };
|
|
&miss_keys[i as usize]
|
|
},
|
|
|(key, hash)| {
|
|
let result = sst
|
|
.lookup::<_, false>(*hash, &key, key_block_cache, value_block_cache)
|
|
.unwrap();
|
|
black_box(result)
|
|
},
|
|
BatchSize::NumBatches(1),
|
|
);
|
|
});
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// =============================================================================
|
|
// BlockCache Benchmarks
|
|
// =============================================================================
|
|
|
|
fn bench_block_cache(c: &mut Criterion) {
|
|
let mut group = c.benchmark_group("block_cache");
|
|
group.warm_up_time(Duration::from_secs(5));
|
|
group.measurement_time(Duration::from_secs(10));
|
|
|
|
// Cache sizes to test: 10, 100, 1000 entries
|
|
let cache_sizes = [10, 100, 1000];
|
|
// Block size (typical decompressed block)
|
|
const BLOCK_SIZE: usize = 4096;
|
|
|
|
for &size in &cache_sizes {
|
|
let data = LazyLock::new(|| {
|
|
let mut rng = SmallRng::seed_from_u64(42);
|
|
|
|
let mut block_data = vec![0u8; BLOCK_SIZE];
|
|
rng.fill(&mut block_data[..]);
|
|
let block = ArcBytes::from(block_data.into_boxed_slice());
|
|
|
|
// Create cache with enough capacity for all entries
|
|
let cache: BlockCache = BlockCache::with(
|
|
size * 16,
|
|
(size * 16 * (BLOCK_SIZE + 8)) as u64,
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
);
|
|
|
|
// Pre-populate cache with random blocks
|
|
let mut keys = Vec::with_capacity(size);
|
|
for i in 0..size {
|
|
let key = (1u32, i as u16);
|
|
|
|
match cache.get_value_or_guard(&key, None) {
|
|
GuardResult::Guard(guard) => {
|
|
let _ = guard.insert(block.clone());
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
keys.push(key);
|
|
}
|
|
|
|
let iteration = Mutex::new(1);
|
|
let rng = Mutex::new(SmallRng::seed_from_u64(123));
|
|
(cache, keys, block, iteration, rng)
|
|
});
|
|
|
|
let id = format!("entries_{}", format_number(size));
|
|
|
|
// Benchmark cache hit
|
|
group.bench_function(format!("{id}/hit"), |b| {
|
|
let (cache, keys, _, _, rng) = &*data;
|
|
let mut rng = rng.lock();
|
|
b.iter_batched(
|
|
|| {
|
|
let idx = rng.random_range(0..keys.len());
|
|
keys[idx]
|
|
},
|
|
|key| match cache.get_value_or_guard(&key, None) {
|
|
GuardResult::Guard(guard) => {
|
|
drop(guard);
|
|
None
|
|
}
|
|
GuardResult::Value(v) => Some(black_box(v)),
|
|
GuardResult::Timeout => {
|
|
unreachable!()
|
|
}
|
|
},
|
|
BatchSize::SmallInput,
|
|
);
|
|
});
|
|
|
|
// Benchmark cache miss (without insert)
|
|
group.bench_function(format!("{id}/miss"), |b| {
|
|
let (cache, _, _, iteration, rng) = &*data;
|
|
let mut rng = rng.lock();
|
|
let mut iteration = iteration.lock();
|
|
b.iter_batched(
|
|
|| {
|
|
// Generate a key that won't be in the cache (different sequence number)
|
|
*iteration += 1;
|
|
(*iteration, rng.random::<u16>())
|
|
},
|
|
|key| match cache.get_value_or_guard(&key, None) {
|
|
GuardResult::Guard(guard) => {
|
|
drop(guard);
|
|
None
|
|
}
|
|
GuardResult::Value(v) => Some(black_box(v)),
|
|
GuardResult::Timeout => {
|
|
unreachable!()
|
|
}
|
|
},
|
|
BatchSize::SmallInput,
|
|
);
|
|
});
|
|
|
|
// Benchmark cache miss + insert (uses existing cache, keys will miss)
|
|
group.bench_function(format!("{id}/miss_insert"), |b| {
|
|
let (cache, _, block, iteration, rng) = &*data;
|
|
let mut rng = rng.lock();
|
|
let mut iteration = iteration.lock();
|
|
*iteration += 1;
|
|
iter_batched_with_init(
|
|
b,
|
|
|_| {
|
|
cache.retain(|(key_prefix, _), _| *key_prefix == 1u32);
|
|
},
|
|
|_| {
|
|
// Generate a key that won't be in the cache (different sequence number)
|
|
let key = (*iteration, rng.random::<u16>());
|
|
(key, block.clone())
|
|
},
|
|
|(key, block)| match cache.get_value_or_guard(&key, None) {
|
|
GuardResult::Guard(guard) => {
|
|
let _ = guard.insert(block);
|
|
}
|
|
GuardResult::Value(v) => {
|
|
black_box(v);
|
|
}
|
|
GuardResult::Timeout => unreachable!(),
|
|
},
|
|
BatchSize::PerIteration,
|
|
);
|
|
});
|
|
}
|
|
|
|
group.finish();
|
|
}
|
|
|
|
// =============================================================================
|
|
// Criterion Setup
|
|
// =============================================================================
|
|
|
|
criterion_group!(
|
|
name = benches;
|
|
config = Criterion::default();
|
|
targets = bench_write, bench_write_multi_value, bench_read_get, bench_read_batch_get, bench_read_get_multiple, bench_compaction, bench_compaction_multi_value, bench_qfilter, bench_static_sorted_file_lookup, bench_block_cache
|
|
);
|
|
criterion_main!(benches);
|