use std::hint::black_box;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::thread;
use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use sha1::{Digest, Sha1};
use tempfile::TempDir;
use zipatch_rs::Platform;
use zipatch_rs::index::{
PartExpected, PartSource, PatchRef, PatchSourceKind, Plan, PlanVerifier, Region, Target,
TargetPath,
};
use zipatch_rs::verify::{ExpectedHash, HashVerifier, Sha1Digest};
const FILE_SIZE: usize = 4 * 1024 * 1024;
const FILE_COUNTS: &[usize] = &[1, 4, 16, 64];
const THREAD_COUNTS: &[usize] = &[2, 4, 8];
fn make_payload(size: usize, seed: u8) -> Vec<u8> {
(0..size)
.map(|i| seed.wrapping_add((i & 0xFF) as u8))
.collect()
}
fn sha1_of(bytes: &[u8]) -> Sha1Digest {
let mut h = Sha1::new();
h.update(bytes);
let out = h.finalize();
let mut arr = [0u8; 20];
arr.copy_from_slice(&out);
Sha1Digest::new(arr)
}
fn setup_hash_files(dir: &Path, count: usize) -> Vec<(PathBuf, ExpectedHash)> {
let mut tasks = Vec::with_capacity(count);
for i in 0..count {
let path = dir.join(format!("f{i}.bin"));
let payload = make_payload(FILE_SIZE, i as u8);
let mut f = std::fs::File::create(&path).expect("create");
f.write_all(&payload).expect("write");
f.sync_all().expect("fsync");
tasks.push((path, ExpectedHash::whole(sha1_of(&payload))));
}
tasks
}
fn run_hash_serial(tasks: &[(PathBuf, ExpectedHash)]) {
let mut b = HashVerifier::new();
for (p, e) in tasks {
b = b.expect(p.clone(), e.clone());
}
let r = b.execute().expect("verify");
debug_assert!(r.is_clean());
black_box(r);
}
fn run_hash_parallel(tasks: &[(PathBuf, ExpectedHash)], threads: usize) {
let chunk = tasks.len().div_ceil(threads).max(1);
thread::scope(|s| {
let mut handles = Vec::new();
for slice in tasks.chunks(chunk) {
handles.push(s.spawn(move || {
let mut b = HashVerifier::new();
for (p, e) in slice {
b = b.expect(p.clone(), e.clone());
}
b.execute().expect("verify")
}));
}
for h in handles {
let r = h.join().expect("thread");
debug_assert!(r.is_clean());
black_box(r);
}
});
}
fn bench_hash_verifier(c: &mut Criterion) {
let mut group = c.benchmark_group("HashVerifier");
group.sample_size(10);
let mut keep_alive: Vec<TempDir> = Vec::new();
for &count in FILE_COUNTS {
let dir = TempDir::new().expect("tempdir");
let tasks = setup_hash_files(dir.path(), count);
let total_bytes = (FILE_SIZE * count) as u64;
group.throughput(Throughput::Bytes(total_bytes));
group.bench_with_input(BenchmarkId::new("serial", count), &tasks, |b, tasks| {
b.iter(|| run_hash_serial(tasks));
});
for &threads in THREAD_COUNTS {
if threads > count {
continue;
}
group.bench_with_input(
BenchmarkId::new(format!("parallel_{threads}t"), count),
&(tasks.clone(), threads),
|b, (tasks, threads)| b.iter(|| run_hash_parallel(tasks, *threads)),
);
}
keep_alive.push(dir);
}
group.finish();
}
fn setup_index_plan(install_root: &Path, target_count: usize) -> Plan {
let mut targets = Vec::with_capacity(target_count);
for i in 0..target_count {
let rel = format!("data/f{i}.bin");
let abs = install_root.join(&rel);
std::fs::create_dir_all(abs.parent().expect("parent")).expect("mkdir");
let payload = make_payload(FILE_SIZE, i as u8);
let mut f = std::fs::File::create(&abs).expect("create");
f.write_all(&payload).expect("write");
f.sync_all().expect("fsync");
let crc = crc32fast::hash(&payload);
let region = Region::new(
0,
FILE_SIZE as u32,
PartSource::Patch {
patch_idx: zipatch_rs::newtypes::PatchIndex::new(0),
offset: 0,
kind: PatchSourceKind::Raw {
len: FILE_SIZE as u32,
},
decoded_skip: 0,
},
PartExpected::Crc32(crc),
);
targets.push(Target::new(
TargetPath::Generic(rel),
FILE_SIZE as u64,
vec![region],
));
}
Plan::new(
Platform::Win32,
vec![PatchRef::new("synthetic", None)],
targets,
vec![],
)
}
fn run_index_serial(plan: &Plan, install_root: &Path) {
let m = PlanVerifier::new(install_root)
.execute(plan)
.expect("verify_plan");
debug_assert!(m.is_clean());
black_box(m);
}
fn run_index_parallel(plan: &Plan, install_root: &Path, threads: usize) {
let chunk = plan.targets.len().div_ceil(threads).max(1);
thread::scope(|s| {
let mut handles = Vec::new();
for slice in plan.targets.chunks(chunk) {
let sub_plan = Plan::new(
plan.platform,
plan.patches.clone(),
slice.to_vec(),
plan.fs_ops.clone(),
);
let root = install_root.to_path_buf();
handles.push(s.spawn(move || {
PlanVerifier::new(root)
.execute(&sub_plan)
.expect("verify_plan")
}));
}
for h in handles {
let m = h.join().expect("thread");
debug_assert!(m.is_clean());
black_box(m);
}
});
}
fn bench_index_verifier(c: &mut Criterion) {
let mut group = c.benchmark_group("index::PlanVerifier");
group.sample_size(10);
let mut keep_alive: Vec<TempDir> = Vec::new();
for &count in FILE_COUNTS {
let dir = TempDir::new().expect("tempdir");
let plan = setup_index_plan(dir.path(), count);
let total_bytes = (FILE_SIZE * count) as u64;
group.throughput(Throughput::Bytes(total_bytes));
let install_root = dir.path().to_path_buf();
group.bench_with_input(
BenchmarkId::new("serial", count),
&(plan.clone(), install_root.clone()),
|b, (plan, root)| b.iter(|| run_index_serial(plan, root)),
);
for &threads in THREAD_COUNTS {
if threads > count {
continue;
}
group.bench_with_input(
BenchmarkId::new(format!("parallel_{threads}t"), count),
&(plan.clone(), install_root.clone(), threads),
|b, (plan, root, threads)| b.iter(|| run_index_parallel(plan, root, *threads)),
);
}
keep_alive.push(dir);
}
group.finish();
}
criterion_group!(benches, bench_hash_verifier, bench_index_verifier);
criterion_main!(benches);