#![allow(clippy::expect_used, clippy::unwrap_used)]
mod fixture;
mod s7;
use std::hint::black_box;
use std::io::Read;
use std::sync::OnceLock;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use fixture::ObjectBenchFixtures;
use flate2::read::ZlibDecoder;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use grit_lib::objects::hash::{hash_object, hash_objects_parallel};
use grit_lib::objects::odb::Odb;
use grit_lib::objects::pack::{clear_pack_cache, read_object_from_pack, PackIndex};
use grit_lib::objects::unpack_objects::apply_delta;
use grit_lib::objects::{parse_tree, HashAlgo, ObjectKind, TreeRefIter};
fn bench_sha1_throughput(c: &mut Criterion) {
let fx = ObjectBenchFixtures::global();
let mut group = c.benchmark_group("sha1_throughput");
for (size, buf) in &fx.sha1_buffers {
group.throughput(Throughput::Bytes(*size as u64));
group.bench_with_input(BenchmarkId::from_parameter(size), buf, |b, input| {
b.iter(|| {
let mut hasher = HashAlgo::Sha1.hasher();
hasher.update(black_box(input.as_slice()));
hasher.finalize()
});
});
}
group.finish();
}
fn bench_object_id_hash(c: &mut Criterion) {
let fx = ObjectBenchFixtures::global();
let mut group = c.benchmark_group("object_id_hash");
for (label, kind, data) in [
("blob", ObjectKind::Blob, fx.delta_base.as_slice()),
("tree", ObjectKind::Tree, fx.tree_body.as_slice()),
] {
group.throughput(Throughput::Bytes(data.len() as u64));
group.bench_with_input(BenchmarkId::from_parameter(label), &data, |b, input| {
b.iter(|| black_box(HashAlgo::Sha1.hash_object(kind, input).expect("hash")));
});
}
group.finish();
}
fn bench_hash_object(c: &mut Criterion) {
let fx = ObjectBenchFixtures::global();
let mut group = c.benchmark_group("hash_object");
for (label, kind, data) in [
("blob", ObjectKind::Blob, fx.delta_base.as_slice()),
("tree", ObjectKind::Tree, fx.tree_body.as_slice()),
] {
group.throughput(Throughput::Bytes(data.len() as u64));
group.bench_with_input(BenchmarkId::from_parameter(label), &data, |b, input| {
b.iter(|| black_box(hash_object(HashAlgo::Sha1, kind, input).expect("hash")));
});
}
group.finish();
}
fn bench_zlib(c: &mut Criterion) {
let fx = ObjectBenchFixtures::global();
let mut group = c.benchmark_group("zlib_deflate");
for (label, store) in [
("blob", fx.blob_store.as_slice()),
("tree", fx.tree_store.as_slice()),
] {
group.throughput(Throughput::Bytes(store.len() as u64));
group.bench_with_input(BenchmarkId::from_parameter(label), store, |b, input| {
b.iter(|| {
let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
std::io::Write::write_all(&mut enc, black_box(input)).expect("deflate");
enc.finish().expect("finish")
});
});
}
group.finish();
let mut group = c.benchmark_group("zlib_inflate");
for (label, compressed, plain_len) in [
(
"tree_200b",
fx.tree_small_zlib.as_slice(),
fx.tree_small_store.len(),
),
(
"blob_4kib",
fx.blob_4k_zlib.as_slice(),
fx.blob_4k_store.len(),
),
(
"blob_1mib",
fx.blob_1m_zlib.as_slice(),
fx.blob_1m_store.len(),
),
(
"delta_payload",
fx.delta_zlib.as_slice(),
fx.delta_store.len(),
),
("blob_typical", fx.blob_zlib.as_slice(), fx.blob_store.len()),
("tree_typical", fx.tree_zlib.as_slice(), fx.tree_store.len()),
] {
group.throughput(Throughput::Bytes(compressed.len() as u64));
group.bench_with_input(
BenchmarkId::from_parameter(label),
&(compressed, plain_len),
|b, (input, len)| {
b.iter(|| {
let mut decoder = ZlibDecoder::new(black_box(&**input));
let mut out = vec![0u8; *len];
decoder.read_exact(&mut out).expect("inflate");
black_box(out)
});
},
);
}
group.finish();
}
fn bench_loose_read(c: &mut Criterion) {
let fx = ObjectBenchFixtures::global();
c.bench_function("loose_object_read", |b| {
b.iter(|| black_box(fx.odb.read(&fx.loose_blob_oid).expect("read loose")));
});
}
fn bench_packed_read(c: &mut Criterion) {
let fx = ObjectBenchFixtures::global();
let mut group = c.benchmark_group("packed_object_read");
group.bench_function("whole", |b| {
use grit_lib::objects::pack_store::PackStore;
use std::sync::Arc;
PackStore::with_context(Arc::clone(fx.odb.pack_store().expect("pack_store")), || {
b.iter(|| {
black_box(
read_object_from_pack(&fx.packed_whole_idx, &fx.packed_whole_oid)
.expect("read whole"),
)
});
});
});
group.bench_function("deep_delta_chain", |b| {
assert!(
fx.packed_delta_chain_depth >= 50,
"fixture delta depth {}",
fx.packed_delta_chain_depth
);
b.iter(|| {
clear_pack_cache();
black_box(
read_object_from_pack(&fx.packed_delta_idx, &fx.packed_delta_oid)
.expect("read delta chain"),
)
});
});
group.finish();
}
fn bench_idx_lookup(c: &mut Criterion) {
let fx = ObjectBenchFixtures::global();
let mut group = c.benchmark_group("pack_idx_lookup");
fn lookup_hit(idx: &PackIndex, oid: &grit_lib::objects::ObjectId) {
black_box(idx.find_offset(oid));
}
fn lookup_miss(idx: &PackIndex, oid: &grit_lib::objects::ObjectId) {
black_box(idx.find_offset(oid));
}
for (name, idx, hit, miss) in [
("small_hit", &fx.small_idx, &fx.small_hit, &fx.small_miss),
("small_miss", &fx.small_idx, &fx.small_miss, &fx.small_miss),
("large_hit", &fx.large_idx, &fx.large_hit, &fx.large_miss),
("large_miss", &fx.large_idx, &fx.large_miss, &fx.large_miss),
] {
group.bench_with_input(BenchmarkId::from_parameter(name), idx, |b, index| {
b.iter(|| {
if name.ends_with("_hit") {
lookup_hit(index, hit);
} else {
lookup_miss(index, miss);
}
});
});
}
group.finish();
}
fn bench_hash_batch_parallel(c: &mut Criterion) {
const COUNT: usize = 20_000;
const BLOB_LEN: usize = 16 * 1024;
let blob = vec![0x5a_u8; BLOB_LEN];
let items: Vec<(ObjectKind, &[u8])> = (0..COUNT)
.map(|_| (ObjectKind::Blob, blob.as_slice()))
.collect();
let mut group = c.benchmark_group("hash/batch_parallel");
group.throughput(Throughput::Bytes((COUNT * BLOB_LEN) as u64));
for threads in [1_usize, 8] {
let nz = std::num::NonZeroUsize::new(threads).expect("threads");
group.bench_with_input(
BenchmarkId::from_parameter(format!("threads={threads}")),
&nz,
|b, &threads| {
b.iter(|| {
black_box(hash_objects_parallel(
HashAlgo::Sha1,
black_box(&items),
threads,
))
});
},
);
}
group.finish();
}
fn bench_read_info_mib_loose(c: &mut Criterion) {
static CTX: OnceLock<(tempfile::TempDir, Odb, grit_lib::objects::ObjectId)> = OnceLock::new();
let (_tmp, odb, oid) = CTX.get_or_init(|| {
let tmp = tempfile::tempdir().expect("tempdir");
let odb = Odb::new(&tmp.path().join("objects"));
let data = vec![0xABu8; 1024 * 1024];
let oid = odb
.write(ObjectKind::Blob, &data)
.expect("write 1 MiB loose blob");
(tmp, odb, oid)
});
let mut group = c.benchmark_group("read_info_vs_read");
group.throughput(Throughput::Bytes(1024 * 1024));
group.bench_function("loose_read", |b| {
b.iter(|| black_box(odb.read(oid).expect("read")));
});
group.bench_function("loose_read_info", |b| {
b.iter(|| black_box(odb.read_info(oid).expect("read_info")));
});
group.finish();
}
fn bench_exists_local_10k_oids_packed(c: &mut Criterion) {
static FIX: OnceLock<(tempfile::TempDir, Odb, Vec<grit_lib::objects::ObjectId>)> =
OnceLock::new();
let (_dir, odb, oids) = FIX.get_or_init(|| fixture::build_packed_exists_local_fixture(10_000));
c.bench_function("exists_local_10k_oids_packed", |b| {
b.iter(|| {
for oid in oids.iter().take(10_000) {
black_box(odb.exists_local(oid));
}
});
});
}
fn bench_tree_iter(c: &mut Criterion) {
let fx = s7::HistoryBenchFixtures::global();
let odb = fx.repo.odb();
let obj = odb.read(&fx.wide_tree).expect("read wide tree");
let data = obj.data.as_slice();
let oid_len = odb.hash_algo().len();
let mut group = c.benchmark_group("tree_iter");
group.throughput(Throughput::Elements(10_000));
group.bench_function("parse_tree", |b| {
b.iter(|| black_box(parse_tree(black_box(data)).expect("parse")))
});
group.bench_function("tree_ref_iter", |b| {
b.iter(|| {
for entry in TreeRefIter::with_oid_len(black_box(data), oid_len) {
black_box(entry.expect("entry"));
}
})
});
group.finish();
}
fn bench_validated_tree_write(c: &mut Criterion) {
let fx = s7::HistoryBenchFixtures::global();
let objects = fx.repo.objects();
let data = fx
.repo
.odb()
.read(&fx.wide_tree)
.expect("read wide tree")
.data;
let entries = parse_tree(&data).expect("parse");
let mut group = c.benchmark_group("tree_write_10k");
group.throughput(Throughput::Elements(entries.len() as u64));
group.bench_function("write_literally", |b| {
b.iter(|| black_box(objects.write_literally(ObjectKind::Tree, black_box(&data))))
});
group.bench_function("write_fsck_checked", |b| {
b.iter(|| black_box(objects.write(ObjectKind::Tree, black_box(&data))))
});
group.bench_function("write_tree_entries", |b| {
b.iter(|| black_box(objects.write_tree(black_box(entries.clone()))))
});
group.finish();
}
fn bench_delta_apply(c: &mut Criterion) {
let fx = ObjectBenchFixtures::global();
let mut group = c.benchmark_group("delta_apply");
group.throughput(Throughput::Bytes(fx.delta_base.len() as u64));
group.bench_function("apply", |b| {
b.iter(|| {
black_box(
apply_delta(black_box(&fx.delta_base), black_box(&fx.delta_bytes)).expect("apply"),
)
});
});
group.finish();
}
criterion_group!(
objects,
bench_sha1_throughput,
bench_hash_batch_parallel,
bench_object_id_hash,
bench_hash_object,
bench_zlib,
bench_loose_read,
bench_read_info_mib_loose,
bench_packed_read,
bench_idx_lookup,
bench_exists_local_10k_oids_packed,
bench_tree_iter,
bench_validated_tree_write,
bench_delta_apply
);
criterion_main!(objects);