Runs cargo fmt --all; all 573 tests still passing, clippy still clean. No logic changes — formatting only. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
280 lines
10 KiB
Rust
280 lines
10 KiB
Rust
//! 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<u8>) {
|
|
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<Vec<u8>> = (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);
|