use std::path::Path;
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use mothcdc::{CaterpillarChunker, MinCdcHash4, SliceChunker};
const MIN: usize = 4096;
const MAX: usize = 12288;
const SIZE: usize = 8 * 1024 * 1024;
fn xorshift(seed: u64, n: usize) -> Vec<u8> {
let mut s = seed | 1;
(0..n)
.map(|_| {
s ^= s << 13;
s ^= s >> 7;
s ^= s << 17;
(s >> 33) as u8
})
.collect()
}
fn next_rand(s: &mut u64, bound: usize) -> usize {
*s ^= *s << 13;
*s ^= *s >> 7;
*s ^= *s << 17;
(*s >> 16) as usize % bound
}
fn disk_image_like() -> Vec<u8> {
let mut data = vec![0u8; SIZE];
let mut s = 0xD15Cu64;
let mut pos = 0usize;
while pos < SIZE {
pos += 4096 + next_rand(&mut s, 512 * 1024);
if pos >= SIZE {
break;
}
let cluster = 4096 + next_rand(&mut s, 128 * 1024);
let end = (pos + cluster).min(SIZE);
let content = xorshift(s, end - pos);
data[pos..end].copy_from_slice(&content);
pos = end;
}
data
}
fn padded_binary_like() -> Vec<u8> {
let mut data = Vec::with_capacity(SIZE);
let mut s = 0xB1Au64;
while data.len() < SIZE {
let section = 16 * 1024 + next_rand(&mut s, 256 * 1024);
data.extend_from_slice(&xorshift(s, section));
let pad = 4096 + next_rand(&mut s, 60 * 1024);
data.resize((data.len() + pad).min(SIZE), 0);
}
data.truncate(SIZE);
data
}
fn log_like() -> Vec<u8> {
let mut data = Vec::with_capacity(SIZE + 256);
let mut s = 0x106u64;
let mut seq = 0u64;
while data.len() < SIZE {
seq += 1;
let ts = 1_752_000_000_000u64 + seq * 37 + next_rand(&mut s, 25) as u64;
let line = format!(
"{{\"ts\":{ts},\"level\":\"INFO\",\"service\":\"api-gateway\",\"seq\":{seq},\
\"msg\":\"request completed\",\"path\":\"/v1/objects/{:08x}\",\"status\":200,\
\"latency_ms\":{}}}\n",
next_rand(&mut s, u32::MAX as usize),
next_rand(&mut s, 900),
);
data.extend_from_slice(line.as_bytes());
}
data.truncate(SIZE);
data
}
fn datasets() -> Vec<(String, Vec<u8>)> {
if let Ok(dir) = std::env::var("BENCH_CORPUS") {
let mut out: Vec<(String, Vec<u8>)> = Vec::new();
let mut entries: Vec<_> = std::fs::read_dir(Path::new(&dir))
.expect("BENCH_CORPUS must be a readable directory")
.filter_map(|e| e.ok().map(|e| e.path()))
.filter(|p| p.is_file())
.collect();
entries.sort();
for p in entries {
let name = p.file_name().unwrap().to_string_lossy().into_owned();
let bytes = std::fs::read(&p).expect("corpus file must be readable");
if !bytes.is_empty() {
out.push((name, bytes));
}
}
assert!(
!out.is_empty(),
"BENCH_CORPUS directory has no usable files"
);
return out;
}
let mut periodic = Vec::with_capacity(SIZE + 777);
let unit = xorshift(9, 777);
while periodic.len() < SIZE {
periodic.extend_from_slice(&unit);
}
periodic.truncate(SIZE);
vec![
("zeros".into(), vec![0u8; SIZE]),
("periodic-777".into(), periodic),
("disk-image-like".into(), disk_image_like()),
("padded-binary-like".into(), padded_binary_like()),
("log-like".into(), log_like()),
("random".into(), xorshift(1, SIZE)),
]
}
fn caterpillar_scalar_reference(data: &[u8]) -> usize {
let mut records = 0usize;
let mut last: Option<(usize, usize)> = None; for c in SliceChunker::new(data, MIN, MAX, MinCdcHash4::new()) {
match last {
Some((off, len)) if data[off..off + len] == c[..] => {},
_ => {
records += 1;
last = Some((c.offset(), c.len()));
},
}
}
records
}
fn bench_chunking(c: &mut Criterion) {
let corpus_mode = std::env::var("BENCH_CORPUS").is_ok();
let mut g = c.benchmark_group("chunking");
g.sample_size(if corpus_mode { 10 } else { 30 });
for (name, data) in datasets() {
g.throughput(Throughput::Bytes(data.len() as u64));
g.bench_with_input(BenchmarkId::new("plain", &name), &data, |b, d| {
b.iter(|| {
let mut acc = 0usize;
for c in SliceChunker::new(d, MIN, MAX, MinCdcHash4::new()) {
acc ^= c.offset();
}
acc
})
});
g.bench_with_input(
BenchmarkId::new("caterpillar-scalar", &name),
&data,
|b, d| b.iter(|| caterpillar_scalar_reference(d)),
);
g.bench_with_input(
BenchmarkId::new("caterpillar-packed", &name),
&data,
|b, d| {
b.iter(|| {
let mut acc = 0usize;
for s in CaterpillarChunker::new(d, MIN, MAX, MinCdcHash4::new()) {
acc ^= s.offset() ^ s.chunk_count();
}
acc
})
},
);
}
g.finish();
}
criterion_group!(benches, bench_chunking);
criterion_main!(benches);