Initial commit: ClawSync v0.1.0

8-crate pure-Rust workspace for revision-aware HDF5 sync.

## Crates
- clawhdf5-onion: ClawOnion VFD — page-level versioned HDF5 storage,
  binary format, writer/reader, branch DAG, GC, snapshots, provenance
- clawsync-core: BLAKE3, xxHash3, FastCDC (+ SIMD NEON), zstd/lz4
- clawsync-onion: IBLT sketch, Merkle tree differ, packet differ/merger,
  ClawSyncManifest, SyncSelector
- clawsync-hdf5: dataset-level manifest, differ, patcher, wire payload
  reconstruction (apply_received_payloads)
- clawsync-transport: TCP, QUIC (quinn 0.11/TLS 1.3), SyncPeer abstraction,
  length-prefixed rkyv wire protocol (21 SyncMessage variants)
- clawsync-agent: OnionMemory, SyncScheduler, TcpSyncBackend,
  PeerCapabilities negotiation
- clawsync-fs: CDC-based delta sync for any file type; FsSyncClient/Server,
  W=16 pipelining, atomic writes
- clawsync-cli: push/pull/serve/hdf5-sync/serve-hdf5/sync/serve-fs +
  all local management commands; --quic on all network commands

## Key features
- IBLT pre-flight: O(revision count) vs rsync's O(file size)
- W=16 sliding-window push: 13–15x speedup over stop-and-wait at WAN RTT
- Dataset-granular HDF5 sync: only modified datasets transferred
- CDC delta for any file type: insertion-stable chunk boundaries
- Full revision DAG: branch, merge, rollback, export, snapshot, GC
- QUIC transport: TLS 1.3, per-message streams via quinn 0.11

## Tests
~573 passing (default features); ~589 with --features simd-cdc

## Performance (Apple Silicon)
- Reconstruct rev=100: 68 µs (target ≤ 1 ms)
- BLAKE3 Rayon 1 MB: 10.3 GiB/s (target ≥ 5 GB/s)
- GC 500 revisions: 20.6 µs (target ≤ 2 s)
- W=16 vs W=1 at 5 ms RTT: 14.8x speedup
- No-op pre-flight at 16 MB: 4 ms vs rsync 35 ms (7.8x)

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
osobh
2026-04-04 18:41:22 -05:00
co-authored by Claude Sonnet 4.6
commit 260e15f5b6
102 changed files with 30098 additions and 0 deletions
@@ -0,0 +1,276 @@
//! 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);