//! Criterion benchmark suite for `clawhdf5-onion`. //! //! Targets from the Phase 5 PRD: //! //! | Benchmark | Target | //! |----------------------------------------|---------------| //! | Onion write overhead (vs unversioned) | ≤ 10% | //! | open(rev=100) — no snapshot | ≤ 1 ms | //! | open(rev=1000) — with snapshot | ≤ 5 ms | //! | Manifest generation 100 revisions | ≤ 50 ms | //! | BLAKE3 verify throughput | ≥ 5 GB/s | //! | Branch fork | ≤ 100 µs | //! | GC keep_last_1000 on 10K revisions | ≤ 2 s | use clawhdf5_onion::gc::GcPolicy; use clawhdf5_onion::writer::OnionFile; use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main}; use tempfile::NamedTempFile; // ───────────────────────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────────────────────── const PAGE_SIZE: u32 = 4096; fn tmp_onion_with_n(n: usize) -> (NamedTempFile, std::path::PathBuf, OnionFile, Vec) { let f = NamedTempFile::new().unwrap(); let h5 = f.path().with_extension("h5"); let base = b"\x89HDF\r\n\x1a\n".to_vec(); std::fs::write(&h5, &base).unwrap(); let mut onion = OnionFile::create(&h5, PAGE_SIZE).unwrap(); for i in 0..n { let mut s = onion.begin_session(None).unwrap(); s.record_page(0, &vec![(i % 256) as u8; PAGE_SIZE as usize]); onion.commit_session(s, None).unwrap(); } (f, h5, onion, base) } // ───────────────────────────────────────────────────────────────────────────── // Benchmarks // ───────────────────────────────────────────────────────────────────────────── /// Benchmark: single revision write (commit_session overhead). fn bench_write_one_revision(c: &mut Criterion) { let mut group = c.benchmark_group("write"); group.throughput(Throughput::Elements(1)); group.bench_function("commit_session/1_page", |b| { let f = NamedTempFile::new().unwrap(); let h5 = f.path().with_extension("h5"); std::fs::write(&h5, b"\x89HDF\r\n\x1a\n").unwrap(); let mut onion = OnionFile::create(&h5, PAGE_SIZE).unwrap(); b.iter(|| { let mut s = onion.begin_session(None).unwrap(); s.record_page(0, black_box(&vec![0xABu8; PAGE_SIZE as usize])); black_box(onion.commit_session(s, None).unwrap()); }); }); group.bench_function("commit_session/4_pages", |b| { let f = NamedTempFile::new().unwrap(); let h5 = f.path().with_extension("h5"); std::fs::write(&h5, b"\x89HDF\r\n\x1a\n").unwrap(); let mut onion = OnionFile::create(&h5, PAGE_SIZE).unwrap(); b.iter(|| { let mut s = onion.begin_session(None).unwrap(); for i in 0u64..4 { s.record_page( i * PAGE_SIZE as u64, black_box(&vec![0xCDu8; PAGE_SIZE as usize]), ); } black_box(onion.commit_session(s, None).unwrap()); }); }); group.finish(); } /// Benchmark: reconstruct_revision at various depths. fn bench_reconstruct_revision(c: &mut Criterion) { let mut group = c.benchmark_group("reconstruct"); for &depth in &[10usize, 50, 100] { let (_f, _h5, onion, base) = tmp_onion_with_n(depth); let target_rev = (depth - 1) as u64; group.bench_with_input(BenchmarkId::new("no_snapshot", depth), &depth, |b, _| { b.iter(|| { black_box( onion .reconstruct_revision(target_rev, black_box(&base)) .unwrap(), ); }); }); } // With snapshot: create a real snapshot at midpoint via create_snapshot() for &depth in &[100usize, 200] { let (_f, _h5, mut onion, base) = tmp_onion_with_n(depth); // Insert a snapshot halfway through onion.create_snapshot(&base, Some("mid-snap")).unwrap(); // Add a few more revisions after the snapshot for i in 0u8..5 { let mut s = onion.begin_session(None).unwrap(); s.record_page(0, &vec![i; PAGE_SIZE as usize]); onion.commit_session(s, None).unwrap(); } let target_rev = onion.revision_count() - 1; group.bench_with_input(BenchmarkId::new("with_snapshot", depth), &depth, |b, _| { b.iter(|| { black_box( onion .reconstruct_revision(target_rev, black_box(&base)) .unwrap(), ); }); }); } group.finish(); } /// Benchmark: list_revisions (manifest generation). fn bench_manifest_generation(c: &mut Criterion) { let mut group = c.benchmark_group("manifest"); for &n in &[10usize, 100, 500] { let (_f, _h5, onion, _base) = tmp_onion_with_n(n); group.bench_with_input(BenchmarkId::new("list_revisions", n), &n, |b, _| { b.iter(|| { black_box(onion.list_revisions()); }); }); } group.finish(); } /// Benchmark: BLAKE3 hashing throughput — single-threaded vs Rayon. fn bench_blake3_throughput(c: &mut Criterion) { let mut group = c.benchmark_group("blake3"); // Single-threaded path (best for small pages — dispatch overhead dominates above). for &size_kb in &[4usize, 64, 1024, 4096] { let data = vec![0xAAu8; size_kb * 1024]; group.throughput(Throughput::Bytes(data.len() as u64)); group.bench_with_input( BenchmarkId::new("single", format!("{size_kb}KB")), &data, |b, d| { b.iter(|| { black_box(clawsync_core::checksum::blake3_hash(black_box(d))); }); }, ); } // Rayon tree-parallel path (best for large inputs >= 128 KB). for &size_kb in &[128usize, 512, 1024, 4096] { let data = vec![0xAAu8; size_kb * 1024]; group.throughput(Throughput::Bytes(data.len() as u64)); group.bench_with_input( BenchmarkId::new("rayon", format!("{size_kb}KB")), &data, |b, d| { b.iter(|| { black_box(clawsync_core::checksum::blake3_hash_large(black_box(d))); }); }, ); } // Batch parallel: hash N independent 4 KB pages simultaneously. for &n_pages in &[16usize, 64, 256] { let pages: Vec> = (0..n_pages).map(|i| vec![(i % 256) as u8; 4096]).collect(); let page_refs: Vec<&[u8]> = pages.iter().map(|p| p.as_slice()).collect(); group.throughput(Throughput::Bytes((n_pages * 4096) as u64)); group.bench_with_input( BenchmarkId::new("batch_pages", format!("{n_pages}x4KB")), &page_refs, |b, refs| { b.iter(|| { black_box(clawsync_core::checksum::blake3_hash_batch(black_box(refs))); }); }, ); } group.finish(); } /// Benchmark: branch fork. fn bench_branch_fork(c: &mut Criterion) { c.bench_function("branch/fork", |b| { let f = NamedTempFile::new().unwrap(); let h5 = f.path().with_extension("h5"); std::fs::write(&h5, b"\x89HDF\r\n\x1a\n").unwrap(); let mut onion = OnionFile::create(&h5, PAGE_SIZE).unwrap(); let mut s = onion.begin_session(None).unwrap(); s.record_page(0, &vec![0u8; PAGE_SIZE as usize]); onion.commit_session(s, None).unwrap(); let mut fork_count = 0usize; b.iter(|| { let name = format!("bench-{fork_count}"); fork_count += 1; black_box(onion.create_branch(&name, "main").unwrap()); }); }); } /// Benchmark: GC keep_last_N. fn bench_gc(c: &mut Criterion) { let mut group = c.benchmark_group("gc"); group.warm_up_time(std::time::Duration::from_millis(200)); group.measurement_time(std::time::Duration::from_secs(3)); for &(total, keep) in &[(200usize, 50usize), (500, 100)] { group.bench_with_input( BenchmarkId::new(format!("keep_last_{keep}"), total), &(total, keep), |b, &(total, keep)| { b.iter_with_setup( || tmp_onion_with_n(total).2, |mut onion| { black_box(onion.gc(GcPolicy::KeepLastN(keep as u64)).unwrap()); }, ); }, ); } group.finish(); } /// Benchmark: snapshot creation overhead. fn bench_snapshot(c: &mut Criterion) { let mut group = c.benchmark_group("snapshot"); for &n_revs in &[5usize, 20, 50] { group.bench_with_input( BenchmarkId::new("create_snapshot", n_revs), &n_revs, |b, &n_revs| { let (_f, _h5, mut onion, base) = tmp_onion_with_n(n_revs); b.iter(|| { black_box(onion.create_snapshot(&base, Some("bench-snap")).unwrap()); }); }, ); } group.finish(); } // ───────────────────────────────────────────────────────────────────────────── // Registration // ───────────────────────────────────────────────────────────────────────────── criterion_group!( benches, bench_write_one_revision, bench_reconstruct_revision, bench_manifest_generation, bench_blake3_throughput, bench_branch_fork, bench_gc, bench_snapshot, ); criterion_main!(benches);