bench/examples/pushstage-probe.rs
//! One real push, decomposed into stages — the end-to-end gate on whether the
//! missing delta base cache is worth anything to gunnar in production.
//!
//! The per-object measurements say the `fs` backend's read path pays 1.4–3.5×
//! for having no delta base cache. That is only worth acting on if a **request**
//! moves, and on the serve side it mostly cannot: clone and fetch answer from
//! the bitmap and copy pack entries verbatim, neither of which resolves a chain.
//! The push side is different — `connectivity` walks the pushed commits and
//! their tree closure through `ObjectStore::read`, which is exactly the path
//! with no cache on it.
//!
//! ```text
//! pushstage-probe <source.git> <tip-oid> [runs]
//! ```
//!
//! Reuses [`gunnar_bench::stages::push`] rather than rebuilding a push driver,
//! so this and `gunnar.push_stages` are decomposing the same code.
use anyhow::{ensure, Context, Result};
use gix_hash::ObjectId;
fn main() -> Result<()> {
let mut args = std::env::args_os().skip(1);
let source = std::path::PathBuf::from(
args.next()
.context("usage: pushstage-probe <source.git> <tip> [runs]")?,
);
let tip = ObjectId::from_hex(
args.next()
.context("usage: pushstage-probe <source.git> <tip> [runs]")?
.to_string_lossy()
.trim()
.as_bytes(),
)?;
let runs: usize = args
.next()
.map(|s| s.to_string_lossy().parse())
.transpose()?
.unwrap_or(5);
let scratch = gunnar_bench::expand_home("~/scratch/deltachain/pushtargets");
std::fs::create_dir_all(&scratch)?;
let mut best: Option<gunnar_bench::stages::StageRun> = None;
let mut walls: Vec<f64> = Vec::new();
for i in 0..runs {
let into = scratch.join(format!("t{i}.git"));
// A push into a repository that already has the objects is a different
// request: `connectivity` finds everything present without reading it.
if into.exists() {
std::fs::remove_dir_all(&into)?;
}
let run = gunnar_bench::stages::push(&source, &into, &tip, "refs/heads/master")?;
walls.push(run.wall_ns as f64 / 1e6);
if best.as_ref().is_none_or(|b| run.wall_ns < b.wall_ns) {
best = Some(run);
}
}
let run = best.context("no run completed")?;
ensure!(
run.objects > 0,
"the push indexed zero objects, so every share below is a share of nothing"
);
walls.sort_by(f64::total_cmp);
println!(
"# {} objects, {} runs, wall min {:.1} ms / median {:.1} ms / max {:.1} ms",
run.objects,
runs,
walls[0],
walls[walls.len() / 2],
walls[walls.len() - 1]
);
println!(
"# attributed {:.1}% of process CPU; cpu owner = {}, memory owner = {}",
100.0 * run.attributed_frac(),
run.cpu_owner(),
run.memory_owner()
);
println!(
"{:<22} {:>10} {:>8} {:>8} {:>9} {:>11}",
"stage", "wall_ms", "wall%", "cpu%", "reads", "read_MiB"
);
for r in &run.rows {
println!(
"{:<22} {:>10.2} {:>7.1}% {:>7.1}% {:>9} {:>11.2}",
r.name,
r.tally.excl_wall_ns as f64 / 1e6,
100.0 * r.wall_frac,
100.0 * r.cpu_frac,
r.store_reads,
r.store_read_bytes as f64 / 1_048_576.0
);
}
Ok(())
}