plugins/native/znippy-plugin-git/examples/push_path_bench.rs
//! Five arms on the same packs, the same machine, the same run.
//!
//! ```text
//! cargo run --release --no-default-features --example push_path_bench
//! cargo run --release --no-default-features --example push_path_bench -- \
//! --only uring --size 524288 --iters 50 # one cell, for strace -c -f
//! ```
//!
//! # What is being compared, and what is NOT the same artifact
//!
//! | arm | writes | index |
//! |---|---|---|
//! | `FastWriter` | znippy Arrow archive, pack verbatim | deferred to the channel |
//! | `SafeWriter` | ditto, + fsynced Arrow IPC journal | deferred to the channel |
//! | `UringWriter` | ditto, journal via one linked io_uring chain | deferred to the channel |
//! | `git index-pack` | `.pack` + `.idx` (+ `.rev`) on the filesystem | **built inline** |
//! | `gix Bundle::write_to_directory` | `.pack` + `.idx` | **built inline** |
//!
//! Durability is **not** uniform across those five and the difference was
//! measured, not assumed. `strace -c -f` on the 8 KiB rung, with the
//! fixture-generation baseline subtracted:
//!
//! | arm | syscalls / pack | fsync / pack |
//! |---|---|---|
//! | `FastWriter` | 1 (`pwrite64`) | 0 |
//! | `SafeWriter` | 4 (`pwrite64`, `write`, 2× `fsync`) | 2 |
//! | `UringWriter` | **1** (`io_uring_enter`, carrying 4 linked ops) | 2, inside the ring |
//! | `git index-pack` | ~667 (a whole fork+exec) | 3 |
//! | `gix Bundle::write` | ~383 | **0** |
//!
//! gix issuing **zero** `fsync` is the surprise, and it moves gix out of the
//! durable column: it persists the `.pack`/`.idx` with `renameat` over a
//! `gix-tempfile` and returns. That is `FastWriter`'s contract, not
//! `SafeWriter`'s, and its throughput has to be read that way.
//!
//! These are **not** the same output. git and gix resolve every object in the
//! pack and emit a real oid→offset index before they return. Our three arms
//! store the bytes and hand an extent to the indexer. Comparing only the ack
//! numbers would flatter us for work we have not done yet, so every one of our
//! arms is reported **twice**:
//!
//! * **ack** — `append` only. No index job is even submitted. This is the pure
//! durability comparison and the number a pushing client actually waits for.
//! * **ack+index** — `push_pack` for every pack, then `wait_caught_up()`, all
//! inside the clock. The index tables exist when the timer stops. This is the
//! number that is comparable with git and gix.
//!
//! Even `ack+index` is not identical work: our index is `pack_id / offset /
//! size / version / object_count / sha1`, six columns over the *pack*, whereas
//! git's and gix's `.idx` is one entry per *object* with CRCs and a fanout
//! table. Ours is cheaper because it indexes less. Stated here rather than left
//! for the reader to discover.
//!
//! # Fixtures
//!
//! Real packfiles, built by `git hash-object -w` + `git pack-objects --stdout`
//! over incompressible random blobs, **one distinct pack per iteration** — so no
//! arm gets to skip work by recognising a pack hash it has already stored, which
//! is what a single repeated pack would have let `git index-pack` do.
//!
//! # Where it runs
//!
//! `~/scratch/...` — `/dev/md1`, raid0 over two NVMe. **Not** `/tmp`,
//! which is a 100 GB tmpfs on this box and would make every `fsync` free and the
//! whole comparison meaningless. `/proc/loadavg` is sampled around every cell and
//! printed with it.
//!
//! # MEASURED on oden, 2026-08-07
//!
//! kernel 7.0.0-29-generic, `/dev/md1`, release build, `--no-default-features`,
//! loadavg 1.27–1.54 throughout (recorded per cell by the run itself; a second
//! run at loadavg 1.66–1.92 agreed to within a few percent). Wall and CPU are
//! **per pack**.
//!
//! | rung | arm | index? | wall | CPU | bytes/s |
//! |---|---|---|---|---|---|
//! | 200 B ×200 | FastWriter | no | **0.8 µs** | 0.8 µs | 247 MiB/s |
//! | | FastWriter | yes | 8.1 µs | 24.2 µs | 23.5 MiB/s |
//! | | SafeWriter | no | 98.5 µs | 31.3 µs | 1.9 MiB/s |
//! | | SafeWriter | yes | 103.5 µs | 72.9 µs | 1.8 MiB/s |
//! | | UringWriter | no | 103.4 µs | 38.3 µs | 1.8 MiB/s |
//! | | UringWriter | yes | 107.2 µs | 78.2 µs | 1.8 MiB/s |
//! | | git index-pack | yes | 2 309.7 µs | 2 073.9 µs | 84.6 KiB/s |
//! | | gix write | yes | 50 445 µs | 2 405.9 µs | 3.9 KiB/s |
//! | | gix write, 1 thread | yes | 50 497 µs | 537.5 µs | 3.9 KiB/s |
//! | 8 KiB ×100 | FastWriter | no | **3.9 µs** | 3.9 µs | 2.0 GiB/s |
//! | | FastWriter | yes | 16.3 µs | 80.4 µs | 479 MiB/s |
//! | | SafeWriter | no | 132.2 µs | 45.1 µs | 59.1 MiB/s |
//! | | SafeWriter | yes | 147.7 µs | 99.5 µs | 52.9 MiB/s |
//! | | UringWriter | no | 138.9 µs | 52.9 µs | 56.3 MiB/s |
//! | | UringWriter | yes | 137.9 µs | 96.5 µs | 56.7 MiB/s |
//! | | git index-pack | yes | 1 992.7 µs | 1 741.7 µs | 3.9 MiB/s |
//! | | gix write | yes | 50 572 µs | 2 500.2 µs | 158 KiB/s |
//! | | gix write, 1 thread | yes | 50 562 µs | 629.1 µs | 158 KiB/s |
//! | 512 KiB ×32 | FastWriter | no | **79.5 µs** | 79.6 µs | 6.1 GiB/s |
//! | | FastWriter | yes | 151.2 µs | 680.3 µs | 3.2 GiB/s |
//! | | SafeWriter | no | 352.2 µs | 154.0 µs | 1.4 GiB/s |
//! | | SafeWriter | yes | 452.8 µs | 582.2 µs | 1.1 GiB/s |
//! | | UringWriter | no | 338.3 µs | 154.0 µs | 1.4 GiB/s |
//! | | UringWriter | yes | 358.0 µs | 586.5 µs | 1.4 GiB/s |
//! | | git index-pack | yes | 5 399 µs | 4 993 µs | 92.6 MiB/s |
//! | | gix write | yes | 52 270 µs | 5 588 µs | 9.6 MiB/s |
//! | | gix write, 1 thread | yes | 52 219 µs | 3 716 µs | 9.6 MiB/s |
//! | 32 MiB ×8 | FastWriter | no | **4 373 µs** | 4 366 µs | 7.1 GiB/s |
//! | | FastWriter | yes | 15 990 µs | 40 058 µs | 2.0 GiB/s |
//! | | SafeWriter | no | 10 657 µs | 5 068 µs | 2.9 GiB/s |
//! | | SafeWriter | yes | 19 930 µs | 38 885 µs | 1.6 GiB/s |
//! | | UringWriter | no | 12 903 µs | 5 136 µs | 2.4 GiB/s |
//! | | UringWriter | yes | 21 401 µs | 38 860 µs | 1.5 GiB/s |
//! | | git index-pack | yes | 224 753 µs | 220 971 µs | 142 MiB/s |
//! | | gix write | yes | 211 252 µs | 204 187 µs | 152 MiB/s |
//! | | gix write, 1 thread | yes | 210 774 µs | 200 481 µs | 152 MiB/s |
//!
//! ## What the numbers say
//!
//! **`FastWriter` wins every rung, and it is supposed to.** It is the ceiling,
//! not a candidate: 0.8 µs for a 200 B pack is one `pwrite64` into page cache
//! and a return. The gap between it and the two durable arms **is the price of
//! durability**, and that is the number this bench exists to produce:
//!
//! * 200 B — 0.8 µs vs 98.5 µs. **123× .** Two `fsync`s on md1 cost ~50 µs each
//! and the pack is irrelevant beside them.
//! * 8 KiB — 3.9 µs vs 132 µs. **34×.**
//! * 512 KiB — 79.5 µs vs 338 µs. **4.3×.**
//! * 32 MiB — 4.4 ms vs 10.7 ms. **2.4×.** By here the bytes dominate the fsyncs.
//!
//! **io_uring did not beat four syscalls, and the syscall count proves why.**
//! `UringWriter` really does collapse the four operations into **one**
//! `io_uring_enter` (measured: 200 `io_uring_enter` and zero `pwrite64`/`fsync`
//! for 200 appends, against `SafeWriter`'s 4 syscalls each). It is 4–7% faster
//! at 8 KiB and 512 KiB and 1–5% *slower* at 200 B and 32 MiB — i.e. inside the
//! noise. The reason is that ~95% of a durable append on this box is the two
//! `fsync`s waiting on the device, and moving *where* the fsync is requested
//! from does not make the device faster. Syscall entry was never the bottleneck.
//! Reported as measured rather than as hoped.
//!
//! **Against git and gix, ours win by one to two orders of magnitude at small
//! and medium sizes, and it is not a fair fight in either direction.** At 8 KiB
//! `SafeWriter` with its index built is 147.7 µs against `git index-pack`'s
//! 1 992.7 µs — **13.5×** — but git forks a process (~667 syscalls/pack) and
//! writes a real per-object `.idx`, while ours is an in-process append plus a
//! six-column per-pack row. At 32 MiB, where the actual bytes dominate and git's
//! fork cost amortises, the margin is 224.8 ms → 19.9 ms, still **11×**, and
//! that one *is* mostly real: git and gix inflate and re-hash every object in
//! the pack, and we store it verbatim. That is the whole design bet.
//!
//! **gix's wall clock is 50 ms of sleep.** Every `Bundle::write_to_directory`
//! call pays a hard-coded 50 ms scheduler poll (`gix-pack-0.73.0`
//! `cache/delta/traverse/mod.rs:196` → `gix-features` `in_parallel.rs:222`),
//! independent of pack size — visible as a flat 50.4/50.6/52.3 ms floor across
//! three rungs spanning 2 600× in size, and as CPU/op of 537 µs against a wall
//! of 50 497 µs single-threaded. **Do not read gix's wall figure as its indexing
//! speed.** Its CPU column is the honest one, and there it is in the same class
//! as git.
//!
//! **gix issues no `fsync` at all** (measured, 0 in 200 calls). It is in
//! `FastWriter`'s durability class, not `SafeWriter`'s.
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::atomic::AtomicBool;
use std::time::{Duration, Instant};
use znippy_plugin_git::archive_write::{ArchiveWrite, FastWriter, SafeWriter};
use znippy_plugin_git::indexer::PushPath;
use znippy_plugin_git::uring_write::UringWriter;
const ACCOUNT: &str = "bench-account";
// ── rusage / loadavg ────────────────────────────────────────────────────────
fn rusage(who: libc::c_int) -> Duration {
let mut ru: libc::rusage = unsafe { std::mem::zeroed() };
unsafe { libc::getrusage(who, &mut ru) };
let d = |t: libc::timeval| {
Duration::from_secs(t.tv_sec as u64) + Duration::from_micros(t.tv_usec as u64)
};
d(ru.ru_utime) + d(ru.ru_stime)
}
fn cpu_self() -> Duration {
rusage(libc::RUSAGE_SELF)
}
fn cpu_children() -> Duration {
rusage(libc::RUSAGE_CHILDREN)
}
fn loadavg() -> String {
fs::read_to_string("/proc/loadavg")
.map(|s| s.split_whitespace().take(3).collect::<Vec<_>>().join(" "))
.unwrap_or_else(|_| "?".into())
}
// ── fixtures ────────────────────────────────────────────────────────────────
/// Deterministic incompressible bytes (xorshift64*), so a pack of `n` bytes of
/// payload really costs `n` bytes and no arm wins on zlib luck.
fn random_bytes(seed: u64, n: usize) -> Vec<u8> {
let mut v = Vec::with_capacity(n + 8);
let mut s = seed | 1;
while v.len() < n {
s ^= s >> 12;
s ^= s << 25;
s ^= s >> 27;
v.extend_from_slice(&s.wrapping_mul(0x2545_F491_4F6C_DD1D).to_le_bytes());
}
v.truncate(n);
v
}
/// `iters` distinct real packfiles, each roughly `target` bytes.
fn make_packs(repo: &Path, target: usize, iters: usize, seed0: u64) -> Vec<Vec<u8>> {
// A one-blob pack costs 12 B header + a few B of entry header + the
// deflated blob + 20 B trailer. Random data does not deflate, so
// payload ≈ target - 45.
let payload = target.saturating_sub(45).max(1);
let mut out = Vec::with_capacity(iters);
for i in 0..iters {
let blob = random_bytes(seed0 ^ (i as u64) << 20 ^ target as u64, payload);
let oid = git_stdin(repo, &["hash-object", "-w", "--stdin"], &blob);
let oid = String::from_utf8(oid).unwrap().trim().to_string();
let pack = git_stdin(
repo,
&["pack-objects", "--stdout", "-q"],
format!("{oid}\n").as_bytes(),
);
out.push(pack);
}
out
}
fn git_stdin(cwd: &Path, args: &[&str], stdin: &[u8]) -> Vec<u8> {
let mut c = Command::new("git")
.args(args)
.current_dir(cwd)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("spawn git");
c.stdin.take().unwrap().write_all(stdin).expect("write stdin");
let out = c.wait_with_output().expect("git");
assert!(out.status.success(), "git {args:?} failed");
out.stdout
}
// ── one measured cell ───────────────────────────────────────────────────────
struct Cell {
arm: String,
/// Nominal rung.
target: usize,
/// Mean actual pack size.
pack_bytes: u64,
iters: usize,
index_built: bool,
wall: Duration,
cpu: Duration,
load_before: String,
load_after: String,
durability: String,
artifact: String,
}
impl Cell {
fn bytes_per_s(&self) -> f64 {
(self.pack_bytes * self.iters as u64) as f64 / self.wall.as_secs_f64()
}
fn per_op_us(&self) -> f64 {
self.wall.as_secs_f64() * 1e6 / self.iters as f64
}
fn cpu_per_op_us(&self) -> f64 {
self.cpu.as_secs_f64() * 1e6 / self.iters as f64
}
}
fn human(b: f64) -> String {
const U: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
let mut v = b;
let mut i = 0;
while v >= 1024.0 && i < 4 {
v /= 1024.0;
i += 1;
}
format!("{v:.1} {}", U[i])
}
fn fresh(dir: &Path) -> PathBuf {
let _ = fs::remove_dir_all(dir);
fs::create_dir_all(dir).expect("mkdir");
dir.to_path_buf()
}
// ── the arms ────────────────────────────────────────────────────────────────
/// Arms 1–3, `append` only: no index job submitted, nothing deferred that is
/// then not paid for. The durability comparison in isolation.
fn run_ack(name: &str, base: &Path, packs: &[Vec<u8>]) -> (Duration, Duration, String) {
let dir = fresh(&base.join(format!("{name}-ack")));
let archive = dir.join("repo.znippy");
let w: Box<dyn ArchiveWrite> = match name {
"FastWriter" => Box::new(FastWriter::create(&archive).unwrap()),
"SafeWriter" => Box::new(SafeWriter::create(&archive).unwrap()),
"UringWriter" => Box::new(UringWriter::create(&archive).unwrap()),
_ => unreachable!(),
};
let d = w.durability().to_string();
let c0 = cpu_self();
let t0 = Instant::now();
for p in packs {
w.append(p).unwrap();
}
(t0.elapsed(), cpu_self() - c0, d)
}
/// Arms 1–3 with the index actually built before the clock stops.
fn run_ack_plus_index(name: &str, base: &Path, packs: &[Vec<u8>]) -> (Duration, Duration) {
let dir = fresh(&base.join(format!("{name}-idx")));
let archive = dir.join("repo.znippy");
let (w, journal): (Box<dyn ArchiveWrite>, Option<PathBuf>) = match name {
"FastWriter" => (Box::new(FastWriter::create(&archive).unwrap()), None),
"SafeWriter" => {
let s = SafeWriter::create(&archive).unwrap();
let j = s.journal_file();
(Box::new(s), Some(j))
}
"UringWriter" => {
let u = UringWriter::create(&archive).unwrap();
let j = u.journal_file().to_path_buf();
(Box::new(u), Some(j))
}
_ => unreachable!(),
};
let path = PushPath::new(w, &archive, journal).unwrap();
let c0 = cpu_self();
let t0 = Instant::now();
for p in packs {
path.push_pack(ACCOUNT, p).unwrap();
}
path.pool().wait_caught_up();
let wall = t0.elapsed();
let cpu = cpu_self() - c0;
// Prove the index is real before reporting the number it cost.
let idx = path.indexer(ACCOUNT);
assert_eq!(
idx.rows(),
packs.len(),
"index did not finish: {} of {} rows",
idx.rows(),
packs.len()
);
(wall, cpu)
}
/// Arm 4 — stock git. `git index-pack --stdin` inside a bare repo is what a git
/// server runs on receive-pack: it consumes the pack on stdin and writes
/// `.pack` + `.idx` (+ `.rev`) into `objects/pack`.
fn run_git(base: &Path, packs: &[Vec<u8>]) -> (Duration, Duration) {
let dir = fresh(&base.join("git"));
let st = Command::new("git")
.args(["init", "-q", "--bare", "."])
.current_dir(&dir)
.status()
.expect("git init");
assert!(st.success());
let c0 = cpu_children();
let t0 = Instant::now();
for p in packs {
let mut c = Command::new("git")
.args(["index-pack", "--stdin"])
.current_dir(&dir)
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn git index-pack");
c.stdin.take().unwrap().write_all(p).expect("feed pack");
let s = c.wait().expect("git index-pack");
assert!(s.success(), "git index-pack failed");
}
let wall = t0.elapsed();
let cpu = cpu_children() - c0;
let n = fs::read_dir(dir.join("objects/pack"))
.unwrap()
.filter(|e| {
e.as_ref()
.map(|e| e.path().extension().map(|x| x == "idx").unwrap_or(false))
.unwrap_or(false)
})
.count();
assert_eq!(n, packs.len(), "git wrote {n} .idx of {}", packs.len());
(wall, cpu)
}
/// Arm 5 — gix's own `index-pack`.
///
/// `threads` is passed straight to `Options::thread_limit`. Both `None` (gix's
/// own default: all cores) and `Some(1)` are measured, because gix-pack 0.73
/// pays a **hard-coded 50 ms scheduler poll per call** —
/// `cache/delta/traverse/mod.rs:196` hands `in_parallel_with_slice` a
/// `periodic` of `Duration::from_millis(50)`, and that watchdog thread is only
/// reaped on its next wake, so the scope cannot close sooner. It is a fixed
/// floor independent of pack size, and it shows up as wall ≫ CPU on the small
/// rungs. Reporting only the wall clock would blame gix's indexer for a sleep.
fn run_gix(base: &Path, packs: &[Vec<u8>], threads: Option<usize>) -> (Duration, Duration) {
use gix_features::progress::Discard;
let dir = fresh(&base.join(match threads {
Some(n) => format!("gix-{n}t"),
None => "gix".to_string(),
}));
let stop = AtomicBool::new(false);
// gix's own defaults: all cores, `Mode::Verify`, index V2, SHA-1,
// `Compression::BEST_SPEED` — i.e. what `gix index-pack` itself would use.
// Not tuned in our favour and not tuned against us.
let opts = gix_pack::bundle::write::Options {
thread_limit: threads,
..Default::default()
};
let c0 = cpu_self();
let t0 = Instant::now();
for p in packs {
let mut cur = std::io::Cursor::new(p.as_slice());
let out = gix_pack::Bundle::write_to_directory(
&mut cur,
Some(&dir),
&mut Discard,
&stop,
None::<gix_object::find::Never>,
gix_hash::Kind::Sha1,
opts.clone(),
)
.expect("gix write_to_directory");
assert!(out.index_path.is_some(), "gix produced no .idx");
}
let wall = t0.elapsed();
let cpu = cpu_self() - c0;
let n = fs::read_dir(&dir)
.unwrap()
.filter(|e| {
e.as_ref()
.map(|e| e.path().extension().map(|x| x == "idx").unwrap_or(false))
.unwrap_or(false)
})
.count();
assert_eq!(n, packs.len(), "gix wrote {n} .idx of {}", packs.len());
(wall, cpu)
}
// ── driver ──────────────────────────────────────────────────────────────────
fn main() {
// S-070: agent sandboxes set PR_SET_THP_DISABLE and children inherit it, so
// every MADV_HUGEPAGE this bench's trees issue is a silent no-op and every
// number here was taken THP-less without saying so. Clearing the flag is a
// per-binary decision (the library must never flip process state); this
// binary wants real numbers, and prints which kind it got.
let thp = znippy_zoomies::stree::thp_enable_for_process();
eprintln!("thp_enabled={}", thp);
let args: Vec<String> = std::env::args().skip(1).collect();
let opt = |k: &str| -> Option<String> {
args.iter()
.position(|a| a == k)
.and_then(|i| args.get(i + 1).cloned())
};
let base = PathBuf::from(
opt("--dir").unwrap_or_else(|| format!("{}/scratch/pushpath-bench", std::env::var("HOME").unwrap_or_default())),
);
fs::create_dir_all(&base).expect("mkdir base");
let only = opt("--only");
// rung -> iterations. 200 B (one commit's worth) to 32 MiB: 5.2 orders of
// magnitude. Iterations fall as size rises so no rung dominates the run.
let mut rungs: Vec<(usize, usize)> = vec![
(200, 200),
(8 * 1024, 100),
(512 * 1024, 32),
(32 * 1024 * 1024, 8),
];
if let (Some(s), Some(n)) = (opt("--size"), opt("--iters")) {
rungs = vec![(s.parse().unwrap(), n.parse().unwrap())];
}
let fixture_repo = fresh(&base.join("fixtures"));
let st = Command::new("git")
.args(["init", "-q", "."])
.current_dir(&fixture_repo)
.status()
.expect("git init fixtures");
assert!(st.success());
println!("# push-path bench — {} arms, one run", if only.is_some() { 1 } else { 6 });
println!();
println!("host: oden, /dev/md1 (raid0, 2× NVMe), kernel {}", kernel());
println!("loadavg at start: {}", loadavg());
println!();
println!(
"| rung | pack | n | arm | index built? | wall/op | CPU/op | bytes/s | loadavg (before → after) |"
);
println!("|---|---|---|---|---|---|---|---|---|");
let mut cells: Vec<Cell> = Vec::new();
for (target, iters) in rungs {
eprintln!("… building {iters} distinct packs of ~{target} B");
let packs = make_packs(&fixture_repo, target, iters, 0x5EED);
let mean: u64 = (packs.iter().map(|p| p.len()).sum::<usize>() / packs.len()) as u64;
let mut push = |arm: &str,
index: &str,
wall: Duration,
cpu: Duration,
lb: String,
la: String,
dur: &str,
art: &str| {
let c = Cell {
arm: arm.into(),
target,
pack_bytes: mean,
iters,
index_built: index.contains("yes"),
wall,
cpu,
load_before: lb,
load_after: la,
durability: dur.into(),
artifact: art.into(),
};
println!(
"| {} | {} | {} | {} | {} | {:.1} µs | {:.1} µs | {}/s | {} → {} |",
human(target as f64),
human(mean as f64),
iters,
c.arm,
index,
c.per_op_us(),
c.cpu_per_op_us(),
human(c.bytes_per_s()),
c.load_before,
c.load_after
);
cells.push(c);
};
for name in ["FastWriter", "SafeWriter", "UringWriter"] {
if only.as_deref().is_some_and(|o| !name.to_lowercase().starts_with(&o.to_lowercase())) {
continue;
}
let lb = loadavg();
let (w, c, dur) = run_ack(name, &base, &packs);
push(name, "no (deferred)", w, c, lb, loadavg(), &dur, "Arrow archive, pack verbatim");
let lb = loadavg();
let (w, c) = run_ack_plus_index(name, &base, &packs);
push(name, "**yes**", w, c, lb, loadavg(), &dur, "Arrow archive + Arrow index tables");
}
if only.as_deref().is_none_or(|o| o == "git") {
let lb = loadavg();
let (w, c) = run_git(&base, &packs);
push(
"git index-pack",
"**yes**",
w,
c,
lb,
loadavg(),
"full — 3 fsync/pack MEASURED (strace -c -f, 8 KiB rung, fixture baseline subtracted)",
".pack + .idx + .rev on the filesystem",
);
}
if only.as_deref().is_none_or(|o| o == "gix") {
for (label, threads) in [
("gix Bundle::write", None),
("gix Bundle::write 1thr", Some(1usize)),
] {
let lb = loadavg();
let (w, c) = run_gix(&base, &packs, threads);
push(
label,
"**yes**",
w,
c,
lb,
loadavg(),
"NONE — 0 fsync in 200 calls MEASURED (strace); persisted by renameat only, so a machine crash after return can lose the pack and the idx. Same class as FastWriter.",
".pack + .idx on the filesystem",
);
}
}
println!("| | | | | | | | | |");
}
println!();
println!("## Who won, per rung");
println!();
println!("Two separate races, because they are not the same work. **ack** is our");
println!("three arms with the index deferred; **index built** is every arm that has a");
println!("usable index when the clock stops — our `ack+index` rows against git and gix.");
println!();
println!("| rung | fastest with index built | runner-up | margin | fastest ack (ours) |");
println!("|---|---|---|---|---|");
let mut rungs_seen: Vec<usize> = Vec::new();
for c in &cells {
if !rungs_seen.contains(&c.target) {
rungs_seen.push(c.target);
}
}
for t in &rungs_seen {
let mut with: Vec<&Cell> = cells
.iter()
.filter(|c| c.target == *t && c.index_built)
.collect();
with.sort_by(|a, b| a.wall.partial_cmp(&b.wall).unwrap());
let mut ack: Vec<&Cell> = cells
.iter()
.filter(|c| c.target == *t && !c.index_built)
.collect();
ack.sort_by(|a, b| a.wall.partial_cmp(&b.wall).unwrap());
let w0 = with[0];
let w1 = with[1];
println!(
"| {} | **{}** {:.1} µs | {} {:.1} µs | **{:.1}×** | {} {:.1} µs |",
human(*t as f64),
w0.arm,
w0.per_op_us(),
w1.arm,
w1.per_op_us(),
w1.per_op_us() / w0.per_op_us(),
ack[0].arm,
ack[0].per_op_us()
);
}
println!();
println!("## What each arm actually bought");
println!();
println!("| arm | durability on return | artifact |");
println!("|---|---|---|");
let mut seen: Vec<&str> = Vec::new();
for c in &cells {
if seen.contains(&c.arm.as_str()) {
continue;
}
seen.push(&c.arm);
println!("| {} | {} | {} |", c.arm, c.durability, c.artifact);
}
println!();
println!("loadavg at end: {}", loadavg());
println!("child CPU total: {:?}", cpu_children());
println!("self CPU total: {:?}", cpu_self());
}
fn kernel() -> String {
fs::read_to_string("/proc/sys/kernel/osrelease")
.map(|s| s.trim().to_string())
.unwrap_or_default()
}