| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- use std::{
- hint::black_box,
- io::{Cursor, Read},
- };
- #[cfg(feature = "bench-allocations")]
- use std::fmt::Write as _;
- #[cfg(not(feature = "bench-allocations"))]
- use std::time::Duration;
- #[cfg(not(feature = "bench-allocations"))]
- use criterion::{BenchmarkId, SamplingMode, Throughput};
- use criterion::{Criterion, criterion_group, criterion_main};
- use kvdb_overlay::{Database, DatabaseOverlay, Value};
- #[cfg(feature = "fjall-backend")]
- const BACKEND: &str = "fjall";
- #[cfg(feature = "sled-backend")]
- const BACKEND: &str = "sled";
- const TREE: &str = "value_reads";
- const KEY: &[u8] = b"value";
- const SIZES: [(&str, usize); 7] = [
- ("64B", 64),
- ("4KiB", 4096),
- ("1MiB", 1024 * 1024),
- ("32MiB", 32 * 1024 * 1024),
- ("128MiB", 128 * 1024 * 1024),
- ("256MiB", 256 * 1024 * 1024),
- ("512MiB", 512 * 1024 * 1024),
- ];
- // Both decoders use the same generic Read implementation, with no decode heap allocation.
- fn decode(mut reader: impl Read, words: usize) -> u64 {
- let mut checksum = 0u64;
- let mut word = [0u8; 8];
- for _ in 0..words {
- reader.read_exact(&mut word).unwrap();
- checksum = checksum.wrapping_add(u64::from_le_bytes(word));
- }
- checksum
- }
- type ReadValue = fn(&DatabaseOverlay, usize) -> u64;
- const READERS: [(&str, ReadValue); 4] = [
- ("vec_cursor", |overlay, words| {
- let value = overlay.get(TREE, KEY).unwrap().unwrap().into_vec();
- decode(Cursor::new(value), words)
- }),
- ("vec_slice", |overlay, words| {
- let value = overlay.get(TREE, KEY).unwrap().unwrap().into_vec();
- decode(value.as_slice(), words)
- }),
- ("handle_slice", |overlay, words| {
- let value = overlay.get(TREE, KEY).unwrap().unwrap();
- decode(value.as_ref(), words)
- }),
- ("handle_cursor", |overlay, words| {
- let value = overlay.get(TREE, KEY).unwrap().unwrap();
- decode(Cursor::new(value), words)
- }),
- ];
- fn value_reads(_criterion: &mut Criterion) {
- let selection = std::env::var("VALUE_READ_SIZE").unwrap_or_else(|_| "all".into());
- assert!(
- selection == "all"
- || selection == "large"
- || SIZES.iter().any(|(name, _)| *name == selection),
- "VALUE_READ_SIZE must be all, large, 64B, 4KiB, 1MiB, 32MiB, 128MiB, 256MiB, or 512MiB"
- );
- #[cfg(feature = "bench-allocations")]
- let mut report = format!(
- "# Value Read Allocations ({BACKEND})\n\n\
- Generated by `VALUE_READ_SIZE={selection} cargo bench --bench value_reads --no-default-features \
- --features {BACKEND}-backend,bench-allocations`.\n\n\
- Warm repeated get + decode + drop; foreground thread only. \
- Totals are cumulative allocations, not live/peak memory or RSS. \
- Backend worker allocations are excluded. No Criterion timings in this run.\n\n\
- | backend | source | size_bytes | decode | approach | operations | allocations_total | bytes_total | allocations_per_op | bytes_per_op |\n\
- | --- | --- | ---: | --- | --- | ---: | ---: | ---: | ---: | ---: |\n"
- );
- for source in ["backend_fallback", "overlay_cache"] {
- for (size_name, size) in SIZES {
- let large = size >= 32 * 1024 * 1024;
- if selection != "all" && selection != size_name && !(selection == "large" && large) {
- continue;
- }
- // Bound fixed validation/allocation work for large payloads.
- let repeats = (100 * 1024 * 1024 / size).clamp(3, 100);
- // Build deterministic, non-uniform words and independent expected checksums.
- let mut payload = Vec::with_capacity(size);
- let mut expected_full = 0u64;
- let mut expected_header = 0u64;
- for index in 0..size / 8 {
- let word = (index as u64 + 1).wrapping_mul(0x9e3779b97f4a7c15);
- if index == 0 {
- expected_header = word;
- }
- expected_full = expected_full.wrapping_add(word);
- payload.extend_from_slice(&word.to_le_bytes());
- }
- // Keep the directory alive until all database/overlay handles are dropped.
- let directory = tempfile::tempdir().unwrap();
- let database = Database::open_default(directory.path()).unwrap();
- let tree = database
- .open_tree(
- TREE,
- #[cfg(feature = "fjall-backend")]
- || {
- let options = kvdb_overlay::fjall::KeyspaceCreateOptions::default();
- if large {
- // Keep the warm-handle experiment on the same storage path.
- options.max_memtable_size(1024 * 1024 * 1024)
- } else {
- options
- }
- },
- )
- .unwrap();
- tree.insert(KEY, &payload).unwrap();
- database.flush_default_mode().unwrap();
- let mut overlay = DatabaseOverlay::new(&database, vec![]).unwrap();
- overlay.open_tree_default(TREE, false).unwrap();
- if source == "overlay_cache" {
- overlay.insert(TREE, KEY, &payload).unwrap();
- }
- assert_eq!(
- matches!(overlay.get(TREE, KEY).unwrap().unwrap(), Value::Cached(_)),
- source == "overlay_cache"
- );
- drop(payload);
- #[cfg(feature = "fjall-backend")]
- let assert_resident = || {
- assert_eq!(tree.tree().sealed_memtable_count(), 0);
- assert_eq!(tree.tree().table_count(), 0);
- };
- #[cfg(not(feature = "bench-allocations"))]
- let mut group =
- _criterion.benchmark_group(format!("value_reads/{BACKEND}/{source}/{size_name}"));
- #[cfg(not(feature = "bench-allocations"))]
- {
- group.sample_size(25);
- group.measurement_time(Duration::from_secs(1));
- group.warm_up_time(Duration::from_millis(300));
- group.throughput(Throughput::Elements(1));
- if large {
- group.sampling_mode(SamplingMode::Flat);
- group.measurement_time(Duration::from_secs(3));
- }
- }
- for (decode_name, words, expected) in [
- ("header_8B", 1, expected_header),
- ("full_payload", size / 8, expected_full),
- ] {
- for (approach, read_value) in READERS {
- // Validate every approach and warm this exact path before measuring.
- for _ in 0..repeats {
- assert_eq!(read_value(black_box(&overlay), black_box(words)), expected);
- }
- #[cfg(feature = "fjall-backend")]
- assert_resident();
- #[cfg(feature = "bench-allocations")]
- {
- let allocations = allocation_counter::measure(|| {
- for _ in 0..repeats {
- black_box(read_value(black_box(&overlay), black_box(words)));
- }
- });
- writeln!(
- report,
- "| {BACKEND} | {source} | {size} | {decode_name} | {approach} | {repeats} | {} | {} | {:.2} | {:.2} |",
- allocations.count_total,
- allocations.bytes_total,
- allocations.count_total as f64 / repeats as f64,
- allocations.bytes_total as f64 / repeats as f64,
- ).unwrap();
- }
- #[cfg(not(feature = "bench-allocations"))]
- group.bench_function(BenchmarkId::new(decode_name, approach), |bencher| {
- bencher.iter(|| {
- // The owned Vec/handle is dropped inside read_value, before return.
- black_box(read_value(black_box(&overlay), black_box(words)))
- });
- });
- #[cfg(feature = "fjall-backend")]
- assert_resident();
- }
- }
- #[cfg(not(feature = "bench-allocations"))]
- group.finish();
- }
- }
- #[cfg(feature = "bench-allocations")]
- {
- let directory = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("target/criterion");
- std::fs::create_dir_all(&directory).unwrap();
- let suffix = if selection == "all" {
- String::new()
- } else {
- format!("-{selection}")
- };
- let path = directory.join(format!("value-allocations-{BACKEND}{suffix}.md"));
- std::fs::write(&path, &report).unwrap();
- print!("{report}");
- eprintln!("Allocation report: {}", path.display());
- }
- }
- criterion_group!(benches, value_reads);
- criterion_main!(benches);
|