Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy

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()
}