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

nordisk/gunnar

crates/gunnar-perfbench/src/main.rs

//! **gunnar-perfbench**: Gunnar end to end, with the real clients (lane GUN-PERF, 2026-10-31).
//!
//! One run starts a release `gunnar serve` per engine (`GUNNAR_ENGINE=znippy`, then `gix`, from
//! one binary built with `--features engine-gix`) on a fresh data root, seeds it over SSH, and
//! measures:
//!
//! | arm | what |
//! |---|---|
//! | `push` | a small repository, a 256 MiB incompressible pack and git.git's history, over SSH and HTTP, into fresh repositories |
//! | `clone` | small, a 10 000-file tree and git.git (bare clones), over SSH and HTTP |
//! | `fetch_noop`, `fetch_inc` | an up-to-date fetch, and a fetch of one new commit on top of git.git |
//! | `ls_remote` | `git ls-remote` over SSH and HTTP (process start included) |
//! | `info_refs_v0/v2`, `ls_refs` | the raw requests on one keep-alive connection, c=1 latency and c=16 req/s |
//! | `lfs_upload`, `lfs_download` | `git lfs push --all` and `git lfs fetch --all` over HTTP, MB/s |
//! | `console_*`, `prerender` | the console's repository-page API as a guest, and the public-site prerendered page |
//! | `index_build_first_fetch` (family `index_build`), `clone_cold`, `fetch_cold` (family `cold`) | what a query costs to get there on the real repository: a restarted server's first clone, and its first fetch of a new commit (the reach-index build) |
//!
//! Every arm records p50/p99/mean wall time, MB/s where it moves bytes, the server's CPU time per
//! operation (utime + stime from `/proc/<pid>/stat`) and its peak RSS during the arm (`VmHWM`,
//! reset with `clear_refs` before each arm). Results go to `$GUNPERF_OUT` as JSON lines and to
//! stdout as a table.
//!
//! ```text
//! GUNPERF_GUNNAR=<target>/release/gunnar      the server (build it with --features engine-gix)
//! GUNPERF_UI=<target>/release/gunnar-ui       optional: the console (--features public-site)
//! GUNPERF_HISTORY=<path to a bare git.git>    optional: the big-history repository
//! GUNPERF_WORK=<dir>                          scratch (wiped per engine)
//! GUNPERF_ENGINES=znippy,gix                  which engines
//! GUNPERF_ONLY=clone,info_refs                optional: only these arm families
//! GUNPERF_REPS=1.0                            repetition multiplier
//! GUNPERF_OUT=<file.jsonl>                    results
//! GUNPERF_HOLD=1                              print the server pid and wait for ENTER before the arms (perf attach)
//! GUNPERF_HISTORY_NAME=git.git                the real repository's name in the result (default git.git)
//! GUNPERF_RESULT=<file.json>                  optional: the machine-readable result (schema nordisk-bench-result/1)
//! GUNPERF_MACHINE=oden                        the machine's name in the result (default /etc/hostname)
//! GUNPERF_SOURCE=<commit>                     the gunnar commit the binary was built from
//! GUNPERF_BENCH=gunnar-perfbench             the bench's name in the result (one README table per bench)
//! GUNPERF_PORT_BASE=23000                     the first of the ports (engine i: base + 10 i .. + 3)
//! ```
//!
//! `GUNPERF_RESULT` is what the README's measured table is rendered from (README-BENCH:
//! `cargo xtask bench-readme run` sets all of these, `cargo xtask bench-readme write` renders
//! the table; xtask/src/bench_readme.rs has the schema). Every row of it names its dataset,
//! whose facts (objects, refs, pack MB, commits) the bench counts itself, and the load1 of the
//! machine at that step.
//!
//! It needs `git` (2.4x), `git-lfs` and `ssh` on PATH; the ci4 image carries all three.

use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::fs;
use std::io::{self, BufRead, BufReader, Read, Write};
use std::net::TcpStream;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};

// ───────────────────────────── configuration ─────────────────────────────

struct Cfg {
    gunnar: PathBuf,
    ui: Option<PathBuf>,
    history: Option<PathBuf>,
    work: PathBuf,
    engines: Vec<String>,
    only: Option<Vec<String>>,
    reps: f64,
    out: PathBuf,
    hold: bool,
    lfs_mib: usize,
    history_name: String,
    result: Option<PathBuf>,
    machine: String,
    source: String,
    bench: String,
}

fn env_path(k: &str) -> Option<PathBuf> {
    std::env::var_os(k)
        .filter(|v| !v.is_empty())
        .map(PathBuf::from)
}

impl Cfg {
    fn from_env() -> Cfg {
        let work = env_path("GUNPERF_WORK").unwrap_or_else(|| std::env::temp_dir().join("gunperf"));
        Cfg {
            gunnar: env_path("GUNPERF_GUNNAR").expect("GUNPERF_GUNNAR names the gunnar binary"),
            ui: env_path("GUNPERF_UI"),
            history: env_path("GUNPERF_HISTORY"),
            out: env_path("GUNPERF_OUT").unwrap_or_else(|| work.join("results.jsonl")),
            work,
            engines: std::env::var("GUNPERF_ENGINES")
                .unwrap_or_else(|_| "znippy,gix".into())
                .split(',')
                .map(str::to_string)
                .collect(),
            only: std::env::var("GUNPERF_ONLY")
                .ok()
                .filter(|s| !s.is_empty())
                .map(|s| s.split(',').map(str::to_string).collect()),
            reps: std::env::var("GUNPERF_REPS")
                .ok()
                .and_then(|s| s.parse().ok())
                .unwrap_or(1.0),
            hold: std::env::var("GUNPERF_HOLD").is_ok_and(|v| v == "1"),
            lfs_mib: std::env::var("GUNPERF_LFS_MIB")
                .ok()
                .and_then(|s| s.parse().ok())
                .unwrap_or(256),
            history_name: std::env::var("GUNPERF_HISTORY_NAME")
                .ok()
                .filter(|s| !s.is_empty())
                .unwrap_or_else(|| "git.git".into()),
            result: env_path("GUNPERF_RESULT"),
            machine: std::env::var("GUNPERF_MACHINE")
                .ok()
                .filter(|s| !s.is_empty())
                .unwrap_or_else(|| {
                    fs::read_to_string("/etc/hostname")
                        .unwrap_or_default()
                        .trim()
                        .to_string()
                }),
            source: std::env::var("GUNPERF_SOURCE").unwrap_or_default(),
            bench: std::env::var("GUNPERF_BENCH")
                .ok()
                .filter(|s| !s.is_empty())
                .unwrap_or_else(|| "gunnar-perfbench".into()),
        }
    }
    fn wants(&self, family: &str) -> bool {
        self.only
            .as_ref()
            .is_none_or(|o| o.iter().any(|f| family.starts_with(f.as_str())))
    }
    fn n(&self, base: usize) -> usize {
        ((base as f64 * self.reps).round() as usize).max(1)
    }
}

// ───────────────────────────── statistics ─────────────────────────────

#[derive(Default)]
struct Rec {
    arm: String,
    engine: String,
    transport: String,
    repo: String,
    times: Vec<Duration>,
    bytes: u64,
    cpu_ms: f64,
    client_cpu_ms: f64,
    hwm_mib: f64,
    anon_peak_mib: f64,
    rss_mib: f64,
    reqs_per_s: Option<f64>,
    extra: BTreeMap<String, String>,
}

fn pct(v: &[Duration], p: usize) -> f64 {
    if v.is_empty() {
        return f64::NAN;
    }
    let mut s: Vec<Duration> = v.to_vec();
    s.sort();
    let i = ((s.len() * p).div_ceil(100))
        .saturating_sub(1)
        .min(s.len() - 1);
    s[i].as_secs_f64() * 1e3
}

impl Rec {
    fn mean_ms(&self) -> f64 {
        if self.times.is_empty() {
            return f64::NAN;
        }
        self.times.iter().map(|d| d.as_secs_f64()).sum::<f64>() / self.times.len() as f64 * 1e3
    }
    fn mb_s(&self) -> Option<f64> {
        if self.bytes == 0 || self.times.is_empty() {
            return None;
        }
        // bytes is per operation; MB/s at the median.
        Some(self.bytes as f64 / 1e6 / (pct(&self.times, 50) / 1e3))
    }
    fn json(&self) -> String {
        let mut s = String::from("{");
        let f = |x: f64| {
            if x.is_finite() {
                format!("{x:.4}")
            } else {
                "null".into()
            }
        };
        let _ = write!(
            s,
            "\"arm\":\"{}\",\"engine\":\"{}\",\"transport\":\"{}\",\"repo\":\"{}\",\"n\":{},\"p50_ms\":{},\"p99_ms\":{},\"mean_ms\":{},\"bytes\":{},\"mb_s\":{},\"cpu_ms_per_op\":{},\"client_cpu_ms_per_op\":{},\"hwm_mib\":{},\"anon_peak_mib\":{},\"rss_mib\":{},\"req_s\":{}",
            self.arm,
            self.engine,
            self.transport,
            self.repo,
            self.times.len(),
            f(pct(&self.times, 50)),
            f(pct(&self.times, 99)),
            f(self.mean_ms()),
            self.bytes,
            self.mb_s().map(f).unwrap_or("null".into()),
            f(self.cpu_ms),
            f(self.client_cpu_ms),
            f(self.hwm_mib),
            f(self.anon_peak_mib),
            f(self.rss_mib),
            self.reqs_per_s.map(f).unwrap_or("null".into()),
        );
        for (k, v) in &self.extra {
            let _ = write!(
                s,
                ",\"{k}\":\"{}\"",
                v.replace('\\', "\\\\").replace('"', "\\\"")
            );
        }
        s.push('}');
        s
    }
    fn line(&self) -> String {
        format!(
            "{:<16} {:<7} {:<5} {:<12} n={:<4} p50 {:>10.2} ms  p99 {:>10.2} ms  {:>9} MB/s  {:>9} req/s  cpu {:>9.2} ms/op  client {:>9.2} ms/op  hwm {:>7.1} MiB  anon {:>7.1} MiB",
            self.arm,
            self.engine,
            self.transport,
            self.repo,
            self.times.len(),
            pct(&self.times, 50),
            pct(&self.times, 99),
            self.mb_s().map(|x| format!("{x:.1}")).unwrap_or("-".into()),
            self.reqs_per_s.map(|x| format!("{x:.0}")).unwrap_or("-".into()),
            self.cpu_ms,
            self.client_cpu_ms,
            self.hwm_mib,
            self.anon_peak_mib
        )
    }
}

// ───────────────────────────── the server process ─────────────────────────────

const CLK_TCK: f64 = 100.0;

/// Samples the server's anonymous RSS (`RssAnon`: the heap, not mapped files) every 10 ms
/// while an arm runs; `VmHWM` alone counts the mapped pack and archive pages too.
struct AnonSampler {
    stop: Arc<AtomicBool>,
    peak: Arc<AtomicU64>,
    th: Option<std::thread::JoinHandle<()>>,
}

impl AnonSampler {
    fn start(pid: u32) -> AnonSampler {
        let stop = Arc::new(AtomicBool::new(false));
        let peak = Arc::new(AtomicU64::new(0));
        let (s2, p2) = (stop.clone(), peak.clone());
        let th = std::thread::spawn(move || {
            let path = format!("/proc/{pid}/status");
            while !s2.load(Ordering::Relaxed) {
                if let Ok(st) = fs::read_to_string(&path) {
                    if let Some(kib) = st
                        .lines()
                        .find(|l| l.starts_with("RssAnon:"))
                        .and_then(|l| l.split_whitespace().nth(1))
                        .and_then(|v| v.parse::<u64>().ok())
                    {
                        p2.fetch_max(kib, Ordering::Relaxed);
                    }
                }
                std::thread::sleep(Duration::from_millis(10));
            }
        });
        AnonSampler {
            stop,
            peak,
            th: Some(th),
        }
    }
    fn stop_mib(mut self) -> f64 {
        self.stop.store(true, Ordering::Relaxed);
        if let Some(t) = self.th.take() {
            let _ = t.join();
        }
        self.peak.load(Ordering::Relaxed) as f64 / 1024.0
    }
}

/// CPU time of this process's waited-for children (the git clients): cutime + cstime.
fn client_cpu_ms() -> f64 {
    let s = fs::read_to_string("/proc/self/stat").unwrap_or_default();
    let rest = s.rsplit_once(')').map(|(_, r)| r).unwrap_or("");
    let f: Vec<&str> = rest.split_whitespace().collect();
    let cu: f64 = f.get(13).and_then(|v| v.parse().ok()).unwrap_or(0.0);
    let cs: f64 = f.get(14).and_then(|v| v.parse().ok()).unwrap_or(0.0);
    (cu + cs) / CLK_TCK * 1e3
}

struct Proc {
    child: Child,
    pid: u32,
    stopped: bool,
}

impl Proc {
    fn cpu_ms(&self) -> f64 {
        let s = fs::read_to_string(format!("/proc/{}/stat", self.pid)).unwrap_or_default();
        // The command name may hold spaces; the fields after the last ')' are fixed.
        let rest = s.rsplit_once(')').map(|(_, r)| r).unwrap_or("");
        let f: Vec<&str> = rest.split_whitespace().collect();
        // rest[0] is field 3 (state); utime is field 14, stime 15.
        let ut: f64 = f.get(11).and_then(|v| v.parse().ok()).unwrap_or(0.0);
        let st: f64 = f.get(12).and_then(|v| v.parse().ok()).unwrap_or(0.0);
        (ut + st) / CLK_TCK * 1e3
    }
    fn status_kib(&self, key: &str) -> f64 {
        let s = fs::read_to_string(format!("/proc/{}/status", self.pid)).unwrap_or_default();
        s.lines()
            .find(|l| l.starts_with(key))
            .and_then(|l| l.split_whitespace().nth(1))
            .and_then(|v| v.parse().ok())
            .unwrap_or(f64::NAN)
    }
    fn reset_hwm(&self) {
        let _ = fs::write(format!("/proc/{}/clear_refs", self.pid), "5");
    }
    fn alive(&mut self) -> bool {
        matches!(self.child.try_wait(), Ok(None))
    }
}

impl Drop for Proc {
    fn drop(&mut self) {
        self.stop();
    }
}

impl Proc {
    fn stop(&mut self) {
        if std::mem::replace(&mut self.stopped, true) {
            return;
        }
        // Our own child only, by its pid.
        let _ = Command::new("kill")
            .arg("-TERM")
            .arg(self.pid.to_string())
            .status();
        let deadline = Instant::now() + Duration::from_secs(10);
        while Instant::now() < deadline {
            if let Ok(Some(_)) = self.child.try_wait() {
                return;
            }
            std::thread::sleep(Duration::from_millis(50));
        }
        let _ = self.child.kill();
        let _ = self.child.wait();
    }
}

fn wait_port(port: u16, what: &str, p: &mut Proc) {
    let deadline = Instant::now() + Duration::from_secs(60);
    while Instant::now() < deadline {
        if TcpStream::connect(("127.0.0.1", port)).is_ok() {
            return;
        }
        assert!(p.alive(), "{what} exited before it listened on {port}");
        std::thread::sleep(Duration::from_millis(50));
    }
    panic!("{what} never listened on {port}");
}

// ───────────────────────────── commands ─────────────────────────────

struct Env {
    home: PathBuf,
    ssh_port: u16,
    http_port: u16,
    vars: Vec<(String, String)>,
}

impl Env {
    fn cmd(&self, prog: &str) -> Command {
        let mut c = Command::new(prog);
        c.env("HOME", &self.home)
            .env("GIT_CONFIG_NOSYSTEM", "1")
            .env("GIT_TERMINAL_PROMPT", "0")
            .env(
                "GIT_SSH_COMMAND",
                format!(
                    "ssh -p {} -i {}/.ssh/id_ed25519 -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ControlMaster=no",
                    self.ssh_port,
                    self.home.display()
                ),
            );
        for (k, v) in &self.vars {
            c.env(k, v);
        }
        c
    }
    fn ssh_url(&self, repo: &str) -> String {
        format!("ssh://git@127.0.0.1/bench/{repo}.git")
    }
    fn http_url(&self, repo: &str) -> String {
        format!("http://127.0.0.1:{}/bench/{repo}.git", self.http_port)
    }
    fn url(&self, transport: &str, repo: &str) -> String {
        if transport == "ssh" {
            self.ssh_url(repo)
        } else {
            self.http_url(repo)
        }
    }
}

fn run(mut c: Command) -> String {
    let what = format!("{c:?}");
    let o = c
        .stdin(Stdio::null())
        .output()
        .unwrap_or_else(|e| panic!("{what}: {e}"));
    if !o.status.success() {
        panic!(
            "{what} failed: {}\n{}",
            String::from_utf8_lossy(&o.stdout),
            String::from_utf8_lossy(&o.stderr)
        );
    }
    String::from_utf8_lossy(&o.stdout).into_owned()
}

fn git(env: &Env, dir: Option<&Path>, args: &[&str]) -> String {
    let mut c = env.cmd("git");
    if let Some(d) = dir {
        c.current_dir(d);
    }
    c.args(args);
    run(c)
}

fn dir_bytes(p: &Path) -> u64 {
    let mut n = 0;
    if let Ok(rd) = fs::read_dir(p) {
        for e in rd.flatten() {
            let m = match e.metadata() {
                Ok(m) => m,
                Err(_) => continue,
            };
            if m.is_dir() {
                n += dir_bytes(&e.path());
            } else {
                n += m.len();
            }
        }
    }
    n
}

// ───────────────────────────── seed repositories ─────────────────────────────

struct Rng(u64);
impl Rng {
    fn next(&mut self) -> u64 {
        self.0 ^= self.0 << 13;
        self.0 ^= self.0 >> 7;
        self.0 ^= self.0 << 17;
        self.0
    }
    fn fill(&mut self, b: &mut [u8]) {
        for c in b.chunks_mut(8) {
            let v = self.next().to_le_bytes();
            c.copy_from_slice(&v[..c.len()]);
        }
    }
    fn text(&mut self, n: usize) -> Vec<u8> {
        const W: &[&str] = &[
            "fn", "let", "mut", "self", "impl", "struct", "match", "return", "gunnar", "store",
            "pack", "ref", "=", "{", "}", "(", ")", ";", "\n", "    ",
        ];
        let mut v = Vec::with_capacity(n + 16);
        while v.len() < n {
            v.extend_from_slice(W[(self.next() % W.len() as u64) as usize].as_bytes());
            v.push(b' ');
        }
        v
    }
}

/// Feed `git fast-import` in `dir` (a fresh non-bare repository).
fn fast_import(env: &Env, dir: &Path, build: impl FnOnce(&mut dyn Write)) {
    fs::create_dir_all(dir).unwrap();
    git(env, Some(dir), &["init", "-q", "-b", "main"]);
    let mut c = env.cmd("git");
    c.current_dir(dir)
        .args(["fast-import", "--quiet"])
        .stdin(Stdio::piped());
    let mut ch = c.spawn().unwrap();
    {
        let mut w = io::BufWriter::with_capacity(1 << 20, ch.stdin.take().unwrap());
        build(&mut w);
        w.flush().unwrap();
    }
    assert!(
        ch.wait().unwrap().success(),
        "fast-import in {}",
        dir.display()
    );
}

fn fi_blob(w: &mut dyn Write, mark: usize, data: &[u8]) {
    write!(w, "blob\nmark :{mark}\ndata {}\n", data.len()).unwrap();
    w.write_all(data).unwrap();
    w.write_all(b"\n").unwrap();
}

fn fi_commit(
    w: &mut dyn Write,
    mark: usize,
    parent: Option<usize>,
    t: u64,
    msg: &str,
    files: &[(String, usize)],
) {
    write!(w, "commit refs/heads/main\nmark :{mark}\ncommitter bench <bench@mock.invalid> {t} +0000\ndata {}\n{msg}\n", msg.len()).unwrap();
    if let Some(p) = parent {
        writeln!(w, "from :{p}").unwrap();
    }
    for (path, m) in files {
        writeln!(w, "M 100644 :{m} {path}").unwrap();
    }
    w.write_all(b"\n").unwrap();
}

fn seed_small(env: &Env, dir: &Path) {
    let mut r = Rng(0x5eed_0001);
    fast_import(env, dir, |w| {
        let mut mark = 1;
        let mut parent = None;
        for c in 0..20 {
            let mut files = Vec::new();
            for f in 0..3 {
                fi_blob(w, mark, &r.text(2000));
                files.push((format!("src/f{}.rs", (c * 3 + f) % 10), mark));
                mark += 1;
            }
            if c == 0 {
                fi_blob(w, mark, b"# small\n\nA small repository.\n");
                files.push(("README.md".into(), mark));
                mark += 1;
            }
            fi_commit(
                w,
                mark,
                parent,
                1_700_000_000 + c as u64,
                &format!("commit {c}"),
                &files,
            );
            parent = Some(mark);
            mark += 1;
        }
    });
}

fn seed_wide(env: &Env, dir: &Path) {
    let mut r = Rng(0x5eed_0002);
    fast_import(env, dir, |w| {
        let mut mark = 1;
        let mut files = Vec::new();
        for i in 0..10_000 {
            let n = 800 + (r.next() % 2400) as usize;
            fi_blob(w, mark, &r.text(n));
            files.push((format!("d{:03}/f{:05}.rs", i / 100, i), mark));
            mark += 1;
        }
        fi_blob(w, mark, b"# wide\n\nTen thousand files.\n");
        files.push(("README.md".into(), mark));
        mark += 1;
        fi_commit(w, mark, None, 1_700_000_000, "ten thousand files", &files);
        let mut parent = mark;
        mark += 1;
        for c in 1..5 {
            let mut ch = Vec::new();
            for _ in 0..200 {
                let i = (r.next() % 10_000) as usize;
                fi_blob(w, mark, &r.text(1500));
                ch.push((format!("d{:03}/f{:05}.rs", i / 100, i), mark));
                mark += 1;
            }
            fi_commit(
                w,
                mark,
                Some(parent),
                1_700_000_000 + c,
                &format!("change {c}"),
                &ch,
            );
            parent = mark;
            mark += 1;
        }
    });
}

fn seed_big(env: &Env, dir: &Path) {
    let mut r = Rng(0x5eed_0003);
    fast_import(env, dir, |w| {
        let mut files = Vec::new();
        let mut buf = vec![0u8; 8 << 20];
        for i in 0..32 {
            r.fill(&mut buf);
            fi_blob(w, i + 1, &buf);
            files.push((format!("blob{i:02}.bin"), i + 1));
        }
        fi_commit(
            w,
            100,
            None,
            1_700_000_000,
            "256 MiB incompressible",
            &files,
        );
    });
}

fn seed_lfs(env: &Env, dir: &Path, mib: usize) {
    fs::create_dir_all(dir).unwrap();
    git(env, Some(dir), &["init", "-q", "-b", "main"]);
    git(env, Some(dir), &["lfs", "install", "--local"]);
    git(env, Some(dir), &["lfs", "track", "*.bin"]);
    let mut r = Rng(0x5eed_0004);
    let big = mib / 16;
    let mut buf = vec![0u8; big << 20];
    for i in 0..16 {
        r.fill(&mut buf);
        fs::write(dir.join(format!("big{i:02}.bin")), &buf).unwrap();
    }
    let mut small = vec![0u8; 64 << 10];
    for i in 0..64 {
        r.fill(&mut small);
        fs::write(dir.join(format!("small{i:02}.bin")), &small).unwrap();
    }
    git(env, Some(dir), &["add", "-A"]);
    git(env, Some(dir), &["commit", "-q", "-m", "lfs objects"]);
}

// ───────────────────────────── a tiny HTTP/1.1 client ─────────────────────────────

struct Http {
    port: u16,
    conn: Option<BufReader<TcpStream>>,
}

struct Resp {
    status: u16,
    body: Vec<u8>,
}

impl Http {
    fn new(port: u16) -> Http {
        Http { port, conn: None }
    }
    fn req(
        &mut self,
        method: &str,
        path: &str,
        headers: &[(&str, &str)],
        body: &[u8],
    ) -> io::Result<Resp> {
        for attempt in 0..2 {
            match self.try_req(method, path, headers, body) {
                Ok(r) => return Ok(r),
                Err(e) if attempt == 0 => {
                    self.conn = None;
                    let _ = e;
                }
                Err(e) => return Err(e),
            }
        }
        unreachable!()
    }
    fn try_req(
        &mut self,
        method: &str,
        path: &str,
        headers: &[(&str, &str)],
        body: &[u8],
    ) -> io::Result<Resp> {
        if self.conn.is_none() {
            let s = TcpStream::connect(("127.0.0.1", self.port))?;
            s.set_nodelay(true)?;
            s.set_read_timeout(Some(Duration::from_secs(60)))?;
            self.conn = Some(BufReader::with_capacity(64 << 10, s));
        }
        let conn = self.conn.as_mut().unwrap();
        let mut head = format!(
            "{method} {path} HTTP/1.1\r\nHost: 127.0.0.1:{}\r\nUser-Agent: gunnar-perfbench\r\n",
            self.port
        );
        for (k, v) in headers {
            let _ = write!(head, "{k}: {v}\r\n");
        }
        if !body.is_empty() || method == "POST" {
            let _ = write!(head, "Content-Length: {}\r\n", body.len());
        }
        head.push_str("\r\n");
        let mut msg = head.into_bytes();
        msg.extend_from_slice(body);
        conn.get_mut().write_all(&msg)?;
        let mut line = String::new();
        if conn.read_line(&mut line)? == 0 {
            return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "closed"));
        }
        let status: u16 = line
            .split_whitespace()
            .nth(1)
            .and_then(|s| s.parse().ok())
            .ok_or_else(|| io::Error::other(format!("bad status line {line:?}")))?;
        let mut len: Option<usize> = None;
        let mut chunked = false;
        let mut close = false;
        loop {
            line.clear();
            conn.read_line(&mut line)?;
            let l = line.trim_end();
            if l.is_empty() {
                break;
            }
            if let Some((k, v)) = l.split_once(':') {
                let k = k.trim().to_ascii_lowercase();
                let v = v.trim();
                match k.as_str() {
                    "content-length" => len = v.parse().ok(),
                    "transfer-encoding" if v.eq_ignore_ascii_case("chunked") => chunked = true,
                    "connection" if v.eq_ignore_ascii_case("close") => close = true,
                    _ => {}
                }
            }
        }
        let mut out = Vec::new();
        if method == "HEAD" || status == 204 || status == 304 {
        } else if chunked {
            loop {
                line.clear();
                conn.read_line(&mut line)?;
                let n = usize::from_str_radix(line.trim().split(';').next().unwrap_or("0"), 16)
                    .map_err(io::Error::other)?;
                if n == 0 {
                    // trailers
                    loop {
                        line.clear();
                        conn.read_line(&mut line)?;
                        if line.trim().is_empty() {
                            break;
                        }
                    }
                    break;
                }
                let at = out.len();
                out.resize(at + n, 0);
                conn.read_exact(&mut out[at..])?;
                line.clear();
                conn.read_line(&mut line)?;
            }
        } else if let Some(n) = len {
            out.resize(n, 0);
            conn.read_exact(&mut out)?;
        } else {
            conn.read_to_end(&mut out)?;
            close = true;
        }
        if close {
            self.conn = None;
        }
        Ok(Resp { status, body: out })
    }
}

// ───────────────────────────── the run ─────────────────────────────

struct Bench<'a> {
    cfg: &'a Cfg,
    engine: String,
    env: Env,
    srv: Proc,
    results: Vec<Rec>,
    root: PathBuf,
    grpc_port: u16,
    serve_log: PathBuf,
}

impl Bench<'_> {
    /// Stop the server and start a fresh process on the same data root: nothing of the last
    /// process's memory (handles, caches, reach bitmaps) is left.
    fn restart(&mut self) {
        self.srv.stop();
        self.srv = serve(
            self.cfg,
            &self.root,
            &self.engine,
            (self.env.ssh_port, self.env.http_port, self.grpc_port),
            &self.serve_log,
        );
    }

    /// What the server holds right now, as a row of its own: `VmRSS` (all) and `RssAnon`
    /// (heap). `rss_idle` right after the start, `rss_held` after every arm: the difference is
    /// what the server keeps between requests (the handle cache of open repositories with their
    /// redb caches, and the reach bitmaps), not what one request needs. `n` = the repositories
    /// the run created.
    fn memory(&mut self, arm: &str, repo: &str, repos: usize) {
        let r = Rec {
            arm: arm.into(),
            repo: repo.into(),
            hwm_mib: self.srv.status_kib("VmRSS:") / 1024.0,
            anon_peak_mib: self.srv.status_kib("RssAnon:") / 1024.0,
            cpu_ms: f64::NAN,
            client_cpu_ms: f64::NAN,
            extra: [("repos".to_string(), repos.to_string())].into(),
            ..Default::default()
        };
        self.rec(r);
    }

    fn rec(&mut self, mut r: Rec) {
        r.engine = self.engine.clone();
        r.rss_mib = self.srv.status_kib("VmRSS:") / 1024.0;
        let load = fs::read_to_string("/proc/loadavg").unwrap_or_default();
        r.extra.insert(
            "load1".into(),
            load.split_whitespace().next().unwrap_or("").to_string(),
        );
        println!("{}", r.line());
        let mut f = fs::OpenOptions::new()
            .create(true)
            .append(true)
            .open(&self.cfg.out)
            .unwrap();
        writeln!(f, "{}", r.json()).unwrap();
        self.results.push(r);
    }

    /// Time `n` runs of `op`, with the server's CPU and peak RSS around them.
    fn arm(
        &mut self,
        arm: &str,
        transport: &str,
        repo: &str,
        n: usize,
        mut op: impl FnMut(&mut Self, usize) -> u64,
    ) {
        self.srv.reset_hwm();
        let mut r = Rec {
            arm: arm.into(),
            transport: transport.into(),
            repo: repo.into(),
            ..Default::default()
        };
        let cpu0 = self.srv.cpu_ms();
        let ccpu0 = client_cpu_ms();
        let sampler = AnonSampler::start(self.srv.pid);
        let mut bytes = 0;
        for i in 0..n {
            let t = Instant::now();
            bytes = op(self, i);
            r.times.push(t.elapsed());
        }
        r.cpu_ms = (self.srv.cpu_ms() - cpu0) / n as f64;
        r.client_cpu_ms = (client_cpu_ms() - ccpu0) / n as f64;
        r.anon_peak_mib = sampler.stop_mib();
        r.hwm_mib = self.srv.status_kib("VmHWM:") / 1024.0;
        r.bytes = bytes;
        self.rec(r);
    }

    /// Requests on `c` keep-alive connections for `secs`: req/s, and latency at c=1 first.
    #[allow(clippy::too_many_arguments)]
    fn http_arm(
        &mut self,
        arm: &str,
        repo: &str,
        n1: usize,
        path: &str,
        method: &'static str,
        headers: &[(&'static str, String)],
        body: Vec<u8>,
        conc: usize,
    ) {
        self.http_arm_for(
            arm,
            repo,
            n1,
            path,
            method,
            headers,
            body,
            conc,
            Duration::from_secs(3),
        )
    }

    #[allow(clippy::too_many_arguments)]
    fn http_arm_for(
        &mut self,
        arm: &str,
        repo: &str,
        n1: usize,
        path: &str,
        method: &'static str,
        headers: &[(&'static str, String)],
        body: Vec<u8>,
        conc: usize,
        secs: Duration,
    ) {
        let port = self.env.http_port;
        let hdrs: Vec<(&str, &str)> = headers.iter().map(|(k, v)| (*k, v.as_str())).collect();
        // c=1 latency
        self.srv.reset_hwm();
        let mut h = Http::new(port);
        let warm = h.req(method, path, &hdrs, &body).expect("http request");
        let mut r = Rec {
            arm: arm.into(),
            transport: "http".into(),
            repo: repo.into(),
            ..Default::default()
        };
        // the client here is this process's own threads, not a measured git: no client CPU
        r.client_cpu_ms = f64::NAN;
        r.extra.insert("status".into(), warm.status.to_string());
        r.extra
            .insert("body_bytes".into(), warm.body.len().to_string());
        if warm.status >= 400 {
            r.extra.insert(
                "body_head".into(),
                String::from_utf8_lossy(&warm.body[..warm.body.len().min(160)]).into_owned(),
            );
        }
        let cpu0 = self.srv.cpu_ms();
        let sampler = AnonSampler::start(self.srv.pid);
        let mut statuses: BTreeMap<u16, usize> = BTreeMap::new();
        for _ in 0..n1 {
            let t = Instant::now();
            let resp = h.req(method, path, &hdrs, &body).expect("http request");
            r.times.push(t.elapsed());
            *statuses.entry(resp.status).or_default() += 1;
        }
        // Every status the c=1 run saw, so a refusal mid-run is a number, not a panic.
        let seen: Vec<String> = statuses.iter().map(|(k, v)| format!("{k}x{v}")).collect();
        r.extra.insert("statuses".into(), seen.join(" "));
        r.cpu_ms = (self.srv.cpu_ms() - cpu0) / n1 as f64;
        // c=conc throughput
        if conc > 1 {
            let stop = Arc::new(AtomicBool::new(false));
            let count = Arc::new(AtomicU64::new(0));
            let refused = Arc::new(AtomicU64::new(0));
            let mut th = Vec::new();
            for _ in 0..conc {
                let (stop, count, refused, path, body) = (
                    stop.clone(),
                    count.clone(),
                    refused.clone(),
                    path.to_string(),
                    body.clone(),
                );
                let headers: Vec<(String, String)> = headers
                    .iter()
                    .map(|(k, v)| (k.to_string(), v.clone()))
                    .collect();
                th.push(std::thread::spawn(move || {
                    let hd: Vec<(&str, &str)> = headers
                        .iter()
                        .map(|(k, v)| (k.as_str(), v.as_str()))
                        .collect();
                    let mut h = Http::new(port);
                    while !stop.load(Ordering::Relaxed) {
                        match h.req(method, &path, &hd, &body) {
                            Ok(resp) if resp.status < 400 => {
                                count.fetch_add(1, Ordering::Relaxed);
                            }
                            Ok(_) | Err(_) => {
                                refused.fetch_add(1, Ordering::Relaxed);
                            }
                        }
                    }
                }));
            }
            let t = Instant::now();
            std::thread::sleep(secs);
            stop.store(true, Ordering::Relaxed);
            for t in th {
                let _ = t.join();
            }
            r.reqs_per_s = Some(count.load(Ordering::Relaxed) as f64 / t.elapsed().as_secs_f64());
            r.extra.insert("conc".into(), conc.to_string());
            r.extra.insert(
                "conc_refused".into(),
                refused.load(Ordering::Relaxed).to_string(),
            );
        }
        r.anon_peak_mib = sampler.stop_mib();
        r.hwm_mib = self.srv.status_kib("VmHWM:") / 1024.0;
        r.bytes = warm.body.len() as u64;
        // MB/s is meaningless for small bodies; keep only req/s.
        if r.bytes < (1 << 20) {
            r.bytes = 0;
        }
        self.rec(r);
    }
}

/// One new commit (one new blob in a one-file tree) on top of `origin/main`, made in the
/// work clone `w` and pushed over SSH; `w`'s `origin/main` follows it.
fn push_new_commit(b: &Bench, w: &Path, label: &str) {
    let blob = format!("{label} {:?}\n", Instant::now());
    let mut c = b.env.cmd("git");
    c.current_dir(w)
        .args(["hash-object", "-w", "--stdin"])
        .stdin(Stdio::piped())
        .stdout(Stdio::piped());
    let mut ch = c.spawn().unwrap();
    ch.stdin.take().unwrap().write_all(blob.as_bytes()).unwrap();
    let oid = String::from_utf8(ch.wait_with_output().unwrap().stdout)
        .unwrap()
        .trim()
        .to_string();
    let tree = {
        let mut c = b.env.cmd("git");
        c.current_dir(w)
            .args(["mktree"])
            .stdin(Stdio::piped())
            .stdout(Stdio::piped());
        let mut ch = c.spawn().unwrap();
        ch.stdin
            .take()
            .unwrap()
            .write_all(format!("100644 blob {oid}\tnew-{label}.txt\n").as_bytes())
            .unwrap();
        String::from_utf8(ch.wait_with_output().unwrap().stdout)
            .unwrap()
            .trim()
            .to_string()
    };
    let head = git(&b.env, Some(w), &["rev-parse", "origin/main"])
        .trim()
        .to_string();
    let commit = git(
        &b.env,
        Some(w),
        &[
            "commit-tree",
            &tree,
            "-p",
            &head,
            "-m",
            &format!("inc {label}"),
        ],
    )
    .trim()
    .to_string();
    git(
        &b.env,
        Some(w),
        &["push", "-q", "origin", &format!("{commit}:refs/heads/main")],
    );
    git(&b.env, Some(w), &["fetch", "-q", "origin"]);
}

/// Start `gunnar serve` on `root` (a fresh process; its log is appended to `log`) and wait
/// until it listens on HTTP and SSH.
fn serve(cfg: &Cfg, root: &Path, engine: &str, ports: (u16, u16, u16), log: &Path) -> Proc {
    let (ssh_port, http_port, grpc_port) = ports;
    let log = fs::OpenOptions::new()
        .create(true)
        .append(true)
        .open(log)
        .unwrap();
    let child = Command::new(&cfg.gunnar)
        .env("GUNNAR_DATA_ROOT", root)
        .env("GUNNAR_ENGINE", engine)
        .env(
            "RUST_LOG",
            std::env::var("GUNPERF_SERVER_LOG").unwrap_or_else(|_| "warn".into()),
        )
        .args([
            "serve",
            "--ssh-bind",
            &format!("127.0.0.1:{ssh_port}"),
            "--http-bind",
            &format!("127.0.0.1:{http_port}"),
        ])
        .args([
            "--grpc-bind",
            &format!("127.0.0.1:{grpc_port}"),
            "--http-allow-cleartext-credentials",
        ])
        .stdout(log.try_clone().unwrap())
        .stderr(log)
        .spawn()
        .expect("gunnar serve");
    let pid = child.id();
    let mut srv = Proc {
        child,
        pid,
        stopped: false,
    };
    wait_port(http_port, "gunnar serve", &mut srv);
    wait_port(ssh_port, "gunnar serve", &mut srv);
    srv
}

fn admin(cfg: &Cfg, root: &Path, args: &[&str]) -> String {
    let mut c = Command::new(&cfg.gunnar);
    c.env("GUNNAR_DATA_ROOT", root)
        .env("RUST_LOG", "error")
        .args(args);
    run(c)
}

fn pkt(s: &str) -> String {
    format!("{:04x}{s}", s.len() + 4)
}

fn main() {
    let cfg = Cfg::from_env();
    fs::create_dir_all(&cfg.work).unwrap();
    if let Some(d) = cfg.out.parent() {
        fs::create_dir_all(d).unwrap();
    }
    let _ = fs::remove_file(&cfg.out);
    let started = utc_now();
    let load_before = load1();
    let mut all = Vec::new();
    for (ei, engine) in cfg.engines.clone().iter().enumerate() {
        all.extend(run_engine(&cfg, engine, ei as u16));
    }
    println!("\n{} records → {}", all.len(), cfg.out.display());
    if let Some(path) = &cfg.result {
        let doc = result_json(&cfg, &all, &started, load_before);
        if let Some(d) = path.parent() {
            fs::create_dir_all(d).unwrap();
        }
        fs::write(path, doc).unwrap();
        println!("result → {}", path.display());
    }
}

// ───────────────────────────── the result file ─────────────────────────────

fn load1() -> f64 {
    fs::read_to_string("/proc/loadavg")
        .unwrap_or_default()
        .split_whitespace()
        .next()
        .and_then(|v| v.parse().ok())
        .unwrap_or(f64::NAN)
}

/// Now, UTC, as `YYYY-MM-DDTHH:MM:SSZ`, from the clock alone.
fn utc_now() -> String {
    let secs = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs())
        .unwrap_or(0) as i64;
    let z = secs.div_euclid(86_400) + 719_468;
    let tod = secs.rem_euclid(86_400);
    let era = z.div_euclid(146_097);
    let doe = z - era * 146_097;
    let yoe = (doe - doe / 1460 + doe / 36_524 - doe / 146_096) / 365;
    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    let mp = (5 * doy + 2) / 153;
    let d = doy - (153 * mp + 2) / 5 + 1;
    let m = if mp < 10 { mp + 3 } else { mp - 9 };
    let y = yoe + era * 400 + i64::from(m <= 2);
    format!(
        "{y:04}-{m:02}-{d:02}T{:02}:{:02}:{:02}Z",
        tod / 3600,
        tod / 60 % 60,
        tod % 60
    )
}

fn jstr(s: &str) -> String {
    let mut o = String::from("\"");
    for c in s.chars() {
        match c {
            '"' => o.push_str("\\\""),
            '\\' => o.push_str("\\\\"),
            '\n' => o.push_str("\\n"),
            c if (c as u32) < 0x20 => {
                let _ = write!(o, "\\u{:04x}", c as u32);
            }
            c => o.push(c),
        }
    }
    o.push('"');
    o
}

fn jnum(x: f64) -> String {
    if x.is_finite() {
        let v = format!("{x:.4}");
        let v = v.trim_end_matches('0').trim_end_matches('.');
        if v.is_empty() || v == "-" {
            "0".into()
        } else {
            v.to_string()
        }
    } else {
        "null".into()
    }
}

/// The first line a client prints for its version (`git --version`, `ssh -V` on stderr).
fn version_of(prog: &str, args: &[&str]) -> String {
    Command::new(prog)
        .args(args)
        .stdin(Stdio::null())
        .output()
        .ok()
        .map(|o| {
            let mut t = String::from_utf8_lossy(&o.stdout).into_owned();
            t.push_str(&String::from_utf8_lossy(&o.stderr));
            t.lines().next().unwrap_or("").trim().to_string()
        })
        .unwrap_or_default()
}

/// A repository's size: (objects, refs, pack MB, commits), counted by git itself; `served`
/// names the refs the bench pushes (none: all of them).
fn repo_facts(dir: &Path, served: &[&str]) -> (f64, f64, f64, f64) {
    let g = |args: &[&str]| -> String {
        Command::new("git")
            .arg("-C")
            .arg(dir)
            .args(args)
            .env("GIT_CONFIG_NOSYSTEM", "1")
            .stdin(Stdio::null())
            .output()
            .ok()
            .filter(|o| o.status.success())
            .map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
            .unwrap_or_default()
    };
    let co = g(&["count-objects", "-v"]);
    let field = |k: &str| -> f64 {
        co.lines()
            .find_map(|l| l.strip_prefix(k))
            .and_then(|v| v.trim().parse::<f64>().ok())
            .unwrap_or(f64::NAN)
    };
    let objects = field("count:") + field("in-pack:");
    // size-pack and size are KiB; MB here is 10^6 bytes
    let pack_mb = (field("size-pack:") + field("size:")) * 1024.0 / 1e6;
    let mut fer = vec!["for-each-ref", "--format=x"];
    fer.extend_from_slice(served);
    let refs = g(&fer).lines().count() as f64;
    let mut rl = vec!["rev-list", "--count"];
    if served.is_empty() {
        rl.push("--all");
    } else {
        rl.push("refs/heads/master");
        rl.push("--tags");
    }
    let commits = g(&rl).trim().parse().unwrap_or(f64::NAN);
    (objects, refs, pack_mb, commits)
}

fn kind_of(arm: &str) -> &'static str {
    match arm {
        "push" | "clone_cold" | "fetch_cold" | "fetch_inc" | "index_build_first_fetch" => "cost",
        "rss_idle" | "rss_held" => "memory",
        _ => "query",
    }
}

/// The machine-readable result: schema `nordisk-bench-result/1` (xtask/src/bench_readme.rs).
fn result_json(cfg: &Cfg, all: &[Rec], started: &str, load_before: f64) -> String {
    let ended = utc_now();
    let threads = std::thread::available_parallelism()
        .map(|n| n.get())
        .unwrap_or(0);
    let ram_gib = fs::read_to_string("/proc/meminfo")
        .unwrap_or_default()
        .lines()
        .find_map(|l| l.strip_prefix("MemTotal:"))
        .and_then(|v| v.split_whitespace().next())
        .and_then(|v| v.parse::<f64>().ok())
        .map(|kib| kib / 1024.0 / 1024.0)
        .unwrap_or(f64::NAN);
    let loads: Vec<f64> = all
        .iter()
        .filter_map(|r| r.extra.get("load1").and_then(|v| v.parse::<f64>().ok()))
        .collect();
    let lmin = loads.iter().cloned().fold(f64::INFINITY, f64::min);
    let lmax = loads.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
    let name_of = |repo: &str| {
        if repo == "history" {
            cfg.history_name.clone()
        } else {
            repo.to_string()
        }
    };
    // the datasets: the real repository, then the seeds the rows name
    let mut datasets = Vec::new();
    let src = cfg.work.join("src");
    let mut seen: Vec<String> = Vec::new();
    for r in all {
        if !seen.contains(&r.repo) {
            seen.push(r.repo.clone());
        }
    }
    for repo in &seen {
        let (real, what, dir) = match repo.as_str() {
            "history" => (
                true,
                format!(
                    "{}, as served: without its commits carrying an AI trailer and the refs over them (Gunnar's ai-trailers gate refuses those)",
                    cfg.history_name
                ),
                cfg.history.clone(),
            ),
            "small" => (false, "20 commits of generated text".into(), Some(src.join("small"))),
            "wide" => (false, "10 000 files in one tree, 5 commits".into(), Some(src.join("wide"))),
            "big" => (false, "32 incompressible 8 MiB blobs, one commit".into(), Some(src.join("big"))),
            "lfs" => (false, format!("{} MiB of LFS objects", cfg.lfs_mib), Some(src.join("lfs"))),
            other => (false, other.to_string(), None),
        };
        let served: &[&str] = if repo == "history" {
            &["refs/heads/master", "refs/tags"]
        } else {
            &[]
        };
        let (o, refs, mb, c) = dir.as_deref().map(|d| repo_facts(d, served)).unwrap_or((
            f64::NAN,
            f64::NAN,
            f64::NAN,
            f64::NAN,
        ));
        datasets.push(format!(
            "    {{\"name\": {}, \"real\": {real}, \"what\": {}, \"objects\": {}, \"refs\": {}, \"pack_mb\": {}, \"commits\": {}}}",
            jstr(&name_of(repo)),
            jstr(&what),
            jnum(o),
            jnum(refs),
            jnum(mb),
            jnum(c)
        ));
    }
    let mut rows = Vec::new();
    for r in all {
        let n = r
            .extra
            .get("repos")
            .and_then(|v| v.parse::<usize>().ok())
            .unwrap_or(r.times.len());
        rows.push(format!(
            "    {{\"dataset\": {}, \"engine\": {}, \"op\": {}, \"transport\": {}, \"kind\": {}, \"n\": {n}, \"p50_ms\": {}, \"p99_ms\": {}, \"mean_ms\": {}, \"server_cpu_ms\": {}, \"client_cpu_ms\": {}, \"peak_rss_mib\": {}, \"peak_heap_mib\": {}, \"mb_s\": {}, \"req_s\": {}, \"load1\": {}}}",
            jstr(&name_of(&r.repo)),
            jstr(&r.engine),
            jstr(&r.arm),
            jstr(&r.transport),
            jstr(kind_of(&r.arm)),
            jnum(pct(&r.times, 50)),
            jnum(pct(&r.times, 99)),
            jnum(r.mean_ms()),
            jnum(r.cpu_ms),
            jnum(r.client_cpu_ms),
            jnum(r.hwm_mib),
            jnum(if r.anon_peak_mib > 0.0 { r.anon_peak_mib } else { f64::NAN }),
            jnum(r.mb_s().unwrap_or(f64::NAN)),
            jnum(r.reqs_per_s.unwrap_or(f64::NAN)),
            r.extra.get("load1").map(String::as_str).filter(|v| !v.is_empty()).unwrap_or("null"),
        ));
    }
    let clients = [
        version_of("git", &["--version"]).replace("git version ", "git "),
        version_of("ssh", &["-V"]),
        version_of("git", &["lfs", "version"]),
    ]
    .iter()
    .filter(|v| !v.is_empty())
    .cloned()
    .collect::<Vec<_>>()
    .join(", ");
    format!(
        "{{\n  \"schema\": \"nordisk-bench-result/1\",\n  \"product\": \"gunnar\",\n  \"bench\": {},\n  \"source\": {{\"repo\": \"gunnar\", \"commit\": {}}},\n  \"date\": {},\n  \"started_utc\": {},\n  \"ended_utc\": {},\n  \"machine\": {{\"name\": {}, \"threads\": {threads}, \"ram_gib\": {}}},\n  \"load\": {{\"before\": {}, \"min\": {}, \"max\": {}}},\n  \"notes\": {},\n  \"datasets\": [\n{}\n  ],\n  \"rows\": [\n{}\n  ]\n}}\n",
        jstr(&cfg.bench),
        jstr(&cfg.source),
        jstr(&started[..10]),
        jstr(started),
        jstr(&ended),
        jstr(&cfg.machine),
        jnum(ram_gib.round()),
        jnum(load_before),
        jnum(lmin),
        jnum(lmax),
        jstr(&format!(
            "Real clients: {clients}. One release build with both engines (`--features engine-gix`), the engine picked per run by `GUNNAR_ENGINE`"
        )),
        datasets.join(",\n"),
        rows.join(",\n"),
    )
}

fn run_engine(cfg: &Cfg, engine: &str, ei: u16) -> Vec<Rec> {
    let base = cfg.work.join(engine);
    let _ = fs::remove_dir_all(&base);
    let root = base.join("root");
    let home = base.join("home");
    fs::create_dir_all(home.join(".ssh")).unwrap();
    let port_base: u16 = std::env::var("GUNPERF_PORT_BASE")
        .ok()
        .and_then(|v| v.parse().ok())
        .unwrap_or(23000);
    let ssh_port = port_base + ei * 10;
    let http_port = port_base + 1 + ei * 10;
    let grpc_port = port_base + 2 + ei * 10;
    let ui_port = port_base + 3 + ei * 10;
    // Another bench on the same box holds the same ports: say so, never measure its server.
    for p in [ssh_port, http_port, grpc_port, ui_port] {
        assert!(
            std::net::TcpListener::bind(("127.0.0.1", p)).is_ok(),
            "127.0.0.1:{p} is taken (another bench on this box?): set GUNPERF_PORT_BASE"
        );
    }
    let env = Env {
        home: home.clone(),
        ssh_port,
        http_port,
        vars: vec![],
    };
    run({
        let mut c = Command::new("ssh-keygen");
        c.args(["-q", "-t", "ed25519", "-N", ""])
            .arg("-f")
            .arg(home.join(".ssh/id_ed25519"));
        c
    });
    git(&env, None, &["config", "--global", "user.name", "bench"]);
    git(
        &env,
        None,
        &["config", "--global", "user.email", "bench@mock.invalid"],
    );
    git(
        &env,
        None,
        &["config", "--global", "init.defaultBranch", "main"],
    );
    git(
        &env,
        None,
        &["config", "--global", "credential.helper", "store"],
    );
    git(
        &env,
        None,
        &["config", "--global", "lfs.locksverify", "false"],
    );
    git(&env, None, &["config", "--global", "protocol.version", "2"]);

    // The server's principal, key, token and repositories (offline, before it serves).
    admin(cfg, &root, &["namespace", "create", "bench"]);
    admin(cfg, &root, &["principal", "add", "alice", "--kind", "user"]);
    let pubkey = fs::read_to_string(home.join(".ssh/id_ed25519.pub")).unwrap();
    admin(cfg, &root, &["key", "add", "alice", pubkey.trim()]);
    admin(
        cfg,
        &root,
        &["grant", "--namespace", "bench", "alice", "admin"],
    );
    admin(cfg, &root, &["grant", "--server", "alice", "admin"]);
    let tok_out = admin(cfg, &root, &["token", "add", "alice", "--label", "bench"]);
    let token = tok_out
        .split_whitespace()
        .find(|w| w.starts_with("gunnar_pat_"))
        .expect("a token")
        .to_string();
    fs::write(
        home.join(".git-credentials"),
        format!("http://alice:{token}@127.0.0.1:{http_port}\n"),
    )
    .unwrap();
    // The console's guest: a principal with NO grants, its key where gunnar-ui looks for it.
    let ui_dir = base.join("ui");
    fs::create_dir_all(&ui_dir).unwrap();
    run({
        let mut c = Command::new("ssh-keygen");
        c.args(["-q", "-t", "ed25519", "-N", ""])
            .arg("-f")
            .arg(ui_dir.join("guest"));
        c
    });
    admin(
        cfg,
        &root,
        &["principal", "add", "console-guest", "--kind", "machine"],
    );
    let guest_pub = fs::read_to_string(ui_dir.join("guest.pub")).unwrap();
    admin(
        cfg,
        &root,
        &["key", "add", "console-guest", guest_pub.trim()],
    );
    let push_reps = cfg.n(3);
    let mut repos: Vec<String> = ["small", "wide", "history", "big", "lfs"]
        .iter()
        .map(|s| s.to_string())
        .collect();
    for kind in ["small", "big", "history"] {
        for t in ["ssh", "http"] {
            let reps = if kind == "history" { 1 } else { push_reps };
            for i in 0..reps {
                repos.push(format!("p-{kind}-{t}-{i}"));
            }
        }
    }
    for i in 0..push_reps {
        repos.push(format!("lfsup-{i}"));
    }
    for r in &repos {
        admin(
            cfg,
            &root,
            &[
                "repo",
                "create",
                &format!("bench/{r}"),
                "--hash",
                "sha1",
                "--public",
                "--default-branch",
                "main",
            ],
        );
    }

    let serve_log = base.join("serve.log");
    let srv = serve(
        cfg,
        &root,
        engine,
        (ssh_port, http_port, grpc_port),
        &serve_log,
    );
    let pid = srv.pid;
    println!("\n== engine {engine}: gunnar serve pid {pid}, ssh {ssh_port}, http {http_port}, grpc {grpc_port}; idle rss {:.1} MiB", srv.status_kib("VmRSS:") / 1024.0);

    // Seeds, once per run (shared by the engines).
    let src = cfg.work.join("src");
    let seed = |name: &str, f: &dyn Fn(&Path)| {
        let d = src.join(name);
        if !d.join(".git").exists() {
            let t = Instant::now();
            let _ = fs::remove_dir_all(&d);
            f(&d);
            println!("seeded {name} in {:.1} s", t.elapsed().as_secs_f64());
        }
        d
    };
    let small = seed("small", &|d| seed_small(&env, d));
    let wide = seed("wide", &|d| seed_wide(&env, d));
    let big = seed("big", &|d| seed_big(&env, d));
    let lfs = seed("lfs", &|d| seed_lfs(&env, d, cfg.lfs_mib));
    let history = cfg.history.clone();

    let mut b = Bench {
        cfg,
        engine: engine.to_string(),
        env,
        srv,
        results: Vec::new(),
        root: root.clone(),
        grpc_port,
        serve_log,
    };
    let held_repo = if history.is_some() {
        "history"
    } else {
        "small"
    };
    b.memory("rss_idle", held_repo, repos.len());
    if cfg.hold {
        println!("HOLD: server pid {pid}; press ENTER to start the arms");
        let mut s = String::new();
        let _ = io::stdin().read_line(&mut s);
    }

    // ── push: seed the served repositories, timed ──
    // `pack.window=0`: the big seed is 32 incompressible 8 MiB blobs, and git's delta search over
    // them costs the CLIENT ~49 s of CPU per push (measured); the arm is about the server.
    let push_one = |b: &Bench, from: &Path, url: &str| {
        git(
            &b.env,
            Some(from),
            &[
                "-c",
                "pack.window=0",
                "push",
                "-q",
                url,
                "+refs/heads/main:refs/heads/main",
            ],
        );
    };
    if cfg.wants("push") {
        for (kind, from) in [("small", &small), ("big", &big)] {
            let bytes = dir_bytes(&from.join(".git/objects"));
            for t in ["ssh", "http"] {
                b.arm("push", t, kind, push_reps, |b, i| {
                    let url = b.env.url(t, &format!("p-{kind}-{t}-{i}"));
                    push_one(b, from, &url);
                    bytes
                });
            }
        }
        if let Some(h) = &history {
            let bytes = dir_bytes(&h.join("objects"));
            for t in ["ssh", "http"] {
                b.arm("push", t, "history", 1, |b, _| {
                    let url = b.env.url(t, &format!("p-history-{t}-0"));
                    git(
                        &b.env,
                        Some(h),
                        &[
                            "push",
                            "-q",
                            &url,
                            "+refs/heads/master:refs/heads/main",
                            "+refs/tags/*:refs/tags/*",
                        ],
                    );
                    bytes
                });
            }
        }
    }
    // The served set (untimed when the push family is off; the push arm wrote its own copies).
    let t = Instant::now();
    push_one(&b, &small, &b.env.ssh_url("small"));
    push_one(&b, &wide, &b.env.ssh_url("wide"));
    if let Some(h) = &history {
        git(
            &b.env,
            Some(h),
            &[
                "push",
                "-q",
                &b.env.ssh_url("history"),
                "+refs/heads/master:refs/heads/main",
                "+refs/tags/*:refs/tags/*",
            ],
        );
    }
    println!("served set pushed in {:.1} s", t.elapsed().as_secs_f64());

    let clones = b.cfg.work.join(engine).join("clones");

    // ── the index build, before anything else asks the server ──
    // The client's copies come from the seed on disk, not from the server, so the timed fetch
    // is the first select the served repository ever sees: znippy-plugin-git builds its reach
    // index there (and persists it, from 0.2.3 on). A restart first: nothing in memory.
    if cfg.wants("index_build") {
        if let Some(h) = &history {
            let repo = "history";
            let d = clones.join("ib-fetch");
            let w = clones.join("w-ib");
            for p in [&d, &w] {
                let _ = fs::remove_dir_all(p);
            }
            let hs = h.to_str().unwrap();
            git(
                &b.env,
                None,
                &["clone", "-q", "--bare", hs, d.to_str().unwrap()],
            );
            git(
                &b.env,
                Some(&d),
                &["remote", "set-url", "origin", &b.env.http_url(repo)],
            );
            git(
                &b.env,
                None,
                &["clone", "-q", "--no-checkout", hs, w.to_str().unwrap()],
            );
            git(
                &b.env,
                Some(&w),
                &["remote", "set-url", "origin", &b.env.ssh_url(repo)],
            );
            // the served main is the seed's master
            git(
                &b.env,
                Some(&w),
                &[
                    "update-ref",
                    "refs/remotes/origin/main",
                    "refs/remotes/origin/master",
                ],
            );
            b.restart();
            push_new_commit(&b, &w, "index-build");
            b.arm("index_build_first_fetch", "http", repo, 1, |b, _| {
                git(
                    &b.env,
                    Some(&d),
                    &["fetch", "-q", "origin", "+refs/heads/*:refs/heads/*"],
                );
                0
            });
        }
    }

    let mut served: Vec<(&str, usize)> = vec![("small", cfg.n(20)), ("wide", cfg.n(5))];
    if history.is_some() {
        served.push(("history", cfg.n(3)));
    }

    // ── clone ──
    if cfg.wants("clone") {
        for (repo, n) in &served {
            for t in ["ssh", "http"] {
                b.arm("clone", t, repo, *n, |b, i| {
                    let d = clones.join(format!("c-{repo}-{t}-{i}"));
                    let _ = fs::remove_dir_all(&d);
                    git(
                        &b.env,
                        None,
                        &[
                            "clone",
                            "-q",
                            "--bare",
                            &b.env.url(t, repo),
                            d.to_str().unwrap(),
                        ],
                    );
                    let n = dir_bytes(&d.join("objects"));
                    let _ = fs::remove_dir_all(&d);
                    n
                });
            }
        }
    }

    // ── fetch: a no-op, then one new commit on top ──
    if cfg.wants("fetch") {
        for (repo, _) in &served {
            for t in ["ssh", "http"] {
                let d = clones.join(format!("f-{repo}-{t}"));
                let _ = fs::remove_dir_all(&d);
                git(
                    &b.env,
                    None,
                    &[
                        "clone",
                        "-q",
                        "--bare",
                        &b.env.url(t, repo),
                        d.to_str().unwrap(),
                    ],
                );
                b.arm("fetch_noop", t, repo, cfg.n(20), |b, _| {
                    git(
                        &b.env,
                        Some(&d),
                        &["fetch", "-q", "origin", "+refs/heads/*:refs/heads/*"],
                    );
                    0
                });
            }
        }
        // one new commit, pushed over SSH from a work clone, then fetched
        for (repo, _) in &served {
            let w = clones.join(format!("w-{repo}"));
            let _ = fs::remove_dir_all(&w);
            git(
                &b.env,
                None,
                &[
                    "clone",
                    "-q",
                    "--no-checkout",
                    &b.env.ssh_url(repo),
                    w.to_str().unwrap(),
                ],
            );
            for t in ["ssh", "http"] {
                let d = clones.join(format!("f-{repo}-{t}"));
                let n = cfg.n(10);
                let mut times = Vec::new();
                b.srv.reset_hwm();
                let mut cpu = 0.0;
                let ccpu = client_cpu_ms;
                let mut client = 0.0;
                let sampler = AnonSampler::start(b.srv.pid);
                for i in 0..n {
                    push_new_commit(&b, &w, &format!("{t}-{i}"));
                    let c0 = b.srv.cpu_ms();
                    let cc0 = ccpu();
                    let tt = Instant::now();
                    git(
                        &b.env,
                        Some(&d),
                        &["fetch", "-q", "origin", "+refs/heads/*:refs/heads/*"],
                    );
                    times.push(tt.elapsed());
                    cpu += b.srv.cpu_ms() - c0;
                    client += ccpu() - cc0;
                }
                let r = Rec {
                    arm: "fetch_inc".into(),
                    transport: t.into(),
                    repo: repo.to_string(),
                    times,
                    cpu_ms: cpu / n as f64,
                    client_cpu_ms: client / n as f64,
                    anon_peak_mib: sampler.stop_mib(),
                    hwm_mib: b.srv.status_kib("VmHWM:") / 1024.0,
                    ..Default::default()
                };
                b.rec(r);
            }
        }
    }

    // ── cold: what a query costs to get there, on the real repository ──
    // A fresh server process holds no handles, caches or reach bitmaps. Its first clone, and
    // its first push of one commit with the fetch of it (the select builds the reach index),
    // are the price every fast query after them already paid.
    if cfg.wants("cold") && history.is_some() {
        let repo = "history";
        let d = clones.join("cold-fetch");
        let w = clones.join("w-cold");
        for p in [&d, &w] {
            let _ = fs::remove_dir_all(p);
        }
        // a current client clone and a work clone, untimed, before the restarts
        git(
            &b.env,
            None,
            &[
                "clone",
                "-q",
                "--bare",
                &b.env.http_url(repo),
                d.to_str().unwrap(),
            ],
        );
        git(
            &b.env,
            None,
            &[
                "clone",
                "-q",
                "--no-checkout",
                &b.env.ssh_url(repo),
                w.to_str().unwrap(),
            ],
        );
        b.restart();
        b.arm("clone_cold", "http", repo, 1, |b, _| {
            let c = clones.join("c-cold");
            let _ = fs::remove_dir_all(&c);
            git(
                &b.env,
                None,
                &[
                    "clone",
                    "-q",
                    "--bare",
                    &b.env.http_url(repo),
                    c.to_str().unwrap(),
                ],
            );
            let n = dir_bytes(&c.join("objects"));
            let _ = fs::remove_dir_all(&c);
            n
        });
        b.restart();
        b.arm("fetch_cold", "http", repo, 1, |b, _| {
            push_new_commit(b, &w, "cold");
            git(
                &b.env,
                Some(&d),
                &["fetch", "-q", "origin", "+refs/heads/*:refs/heads/*"],
            );
            0
        });
    }

    // ── ls-remote ──
    if cfg.wants("ls_remote") {
        for (repo, _) in &served {
            for t in ["ssh", "http"] {
                b.arm("ls_remote", t, repo, cfg.n(50), |b, _| {
                    git(&b.env, None, &["ls-remote", &b.env.url(t, repo)]);
                    0
                });
            }
        }
    }

    // ── info/refs and ls-refs, raw ──
    if cfg.wants("info_refs") || cfg.wants("ls_refs") {
        for (repo, _) in &served {
            let path = format!("/bench/{repo}.git/info/refs?service=git-upload-pack");
            if cfg.wants("info_refs") {
                b.http_arm(
                    "info_refs_v0",
                    repo,
                    cfg.n(1000),
                    &path,
                    "GET",
                    &[],
                    vec![],
                    16,
                );
                b.http_arm(
                    "info_refs_v2",
                    repo,
                    cfg.n(1000),
                    &path,
                    "GET",
                    &[("Git-Protocol", "version=2".into())],
                    vec![],
                    16,
                );
            }
            if cfg.wants("ls_refs") {
                let body = format!(
                    "{}{}0001{}{}{}0000",
                    pkt("command=ls-refs\n"),
                    pkt("agent=git/2.47.3\n"),
                    pkt("peel\n"),
                    pkt("symrefs\n"),
                    pkt("ref-prefix HEAD\n")
                        + &pkt("ref-prefix refs/heads/")
                        + &pkt("ref-prefix refs/tags/")
                );
                b.http_arm(
                    "ls_refs",
                    repo,
                    cfg.n(1000),
                    &format!("/bench/{repo}.git/git-upload-pack"),
                    "POST",
                    &[
                        ("Git-Protocol", "version=2".into()),
                        (
                            "Content-Type",
                            "application/x-git-upload-pack-request".into(),
                        ),
                        ("Accept", "application/x-git-upload-pack-result".into()),
                    ],
                    body.into_bytes(),
                    16,
                );
            }
        }
    }

    // ── LFS over HTTP ──
    if cfg.wants("lfs") {
        let bytes = {
            let mut n = 0u64;
            for e in fs::read_dir(&lfs).unwrap().flatten() {
                if e.path().extension().is_some_and(|x| x == "bin") {
                    n += e.metadata().unwrap().len();
                }
            }
            n
        };
        b.arm("lfs_upload", "http", "lfs", push_reps, |b, i| {
            let url = b.env.http_url(&format!("lfsup-{i}"));
            git(&b.env, Some(&lfs), &["lfs", "push", "--all", &url]);
            bytes
        });
        // the served copy: objects and refs
        git(
            &b.env,
            Some(&lfs),
            &[
                "push",
                "-q",
                &b.env.http_url("lfs"),
                "+refs/heads/main:refs/heads/main",
            ],
        );
        let d = clones.join("lfs-down");
        let _ = fs::remove_dir_all(&d);
        let mut c = b.env.cmd("git");
        c.env("GIT_LFS_SKIP_SMUDGE", "1")
            .args(["clone", "-q", &b.env.http_url("lfs")])
            .arg(&d);
        run(c);
        b.arm("lfs_download", "http", "lfs", cfg.n(3), |b, _| {
            let _ = fs::remove_dir_all(d.join(".git/lfs/objects"));
            git(&b.env, Some(&d), &["lfs", "fetch", "--all", "origin"]);
            bytes
        });
    }

    // ── the console: the repository page's API calls as a guest, and the prerendered page ──
    if cfg.wants("console") || cfg.wants("prerender") {
        if let Some(ui) = &cfg.ui {
            console(&mut b, ui, &root, grpc_port, ui_port, &served);
        }
    }

    b.memory("rss_held", held_repo, repos.len());
    let hwm = b.srv.status_kib("VmHWM:") / 1024.0;
    println!("== engine {engine} done; server rss {:.1} MiB, last-arm hwm {hwm:.1} MiB, cpu total {:.1} s", b.srv.status_kib("VmRSS:") / 1024.0, b.srv.cpu_ms() / 1e3);
    std::mem::take(&mut b.results)
}

fn console(
    b: &mut Bench,
    ui: &Path,
    _root: &Path,
    grpc_port: u16,
    ui_port: u16,
    served: &[(&str, usize)],
) {
    let base = b.cfg.work.join(&b.engine).join("ui");
    let bundle = base.join("bundle");
    fs::create_dir_all(&bundle).unwrap();
    // A minimal console document: the prerender is spliced into the console's own index.html.
    fs::write(bundle.join("index.html"), "<!doctype html><html><head><meta charset=\"utf-8\"><title>Gunnar</title></head><body><div id=\"app\"></div></body></html>\n").unwrap();
    let ident = base.join("ui-id");
    run({
        let mut c = Command::new("ssh-keygen");
        c.args(["-q", "-t", "ed25519", "-N", ""])
            .arg("-f")
            .arg(&ident);
        c
    });
    let envs: Vec<(String, String)> = vec![
        ("GUNNAR_UI_CONTROL".into(), format!("127.0.0.1:{grpc_port}")),
        ("GUNNAR_UI_IDENTITY".into(), ident.display().to_string()),
        ("GUNNAR_UI_DATA".into(), base.display().to_string()),
        ("GUNNAR_UI_STATE".into(), base.join("state.redb").display().to_string()),
        ("GUNNAR_UI_BIND".into(), format!("127.0.0.1:{ui_port}")),
        ("GUNNAR_UI_DIR".into(), bundle.display().to_string()),
        ("GUNNAR_UI_WEBAUTHN_RP_ID".into(), "localhost".into()),
        ("GUNNAR_UI_WEBAUTHN_ORIGIN".into(), format!("http://localhost:{ui_port}")),
        ("GUNNAR_UI_GUEST_IDENTITY".into(), base.join("guest").display().to_string()),
        ("GUNNAR_UI_PUBLIC_SITE".into(), "on".into()),
        // The bench measures the console, not its throttle's refusals: every tier wide open.
        ("GUNNAR_UI_THROTTLE".into(), "guest=10000000/10000000/4096,credentialed=10000000/10000000/4096,global=10000000/10000000/8192,render=10000000/10000000,render_all=10000000/10000000".into()),
        ("RUST_LOG".into(), "warn".into()),
    ];
    let mut c = Command::new(ui);
    c.envs(envs.iter().cloned())
        .env("HOME", &b.env.home)
        .args(["enroll", "--admin-key"])
        .arg(b.env.home.join(".ssh/id_ed25519"))
        .args(["--name", "gunnar-ui"]);
    run(c);
    let extra: Vec<(String, String)> = std::env::var("GUNPERF_UI_ENV")
        .unwrap_or_default()
        .split_whitespace()
        .filter_map(|kv| {
            kv.split_once('=')
                .map(|(k, v)| (k.to_string(), v.to_string()))
        })
        .collect();
    let log = fs::File::create(base.join("ui.log")).unwrap();
    let child = Command::new(ui)
        .envs(envs.iter().cloned())
        .envs(extra)
        .env("HOME", &b.env.home)
        .stdout(log.try_clone().unwrap())
        .stderr(log)
        .spawn()
        .expect("gunnar-ui");
    let pid = child.id();
    let mut uip = Proc {
        child,
        pid,
        stopped: false,
    };
    wait_port(ui_port, "gunnar-ui", &mut uip);
    std::thread::sleep(Duration::from_millis(500));
    println!(
        "   console pid {pid} on {ui_port}, idle rss {:.1} MiB",
        uip.status_kib("VmRSS:") / 1024.0
    );
    let port = b.env.http_port;
    b.env.http_port = ui_port; // http_arm talks to the console
    for (repo, _) in served {
        let n = b.cfg.n(200);
        let paths = [
            ("console_repo", format!("/api/repos/bench/{repo}")),
            ("console_refs", format!("/api/repos/bench/{repo}/refs")),
            ("console_readme", format!("/api/repos/bench/{repo}/readme")),
            (
                "console_readme_html",
                format!("/api/repos/bench/{repo}/readme?as=html"),
            ),
            (
                "console_browse",
                format!("/api/repos/bench/{repo}/browse?rev=main&path="),
            ),
            (
                "console_log",
                format!("/api/repos/bench/{repo}/log?limit=50"),
            ),
            ("prerender", format!("/bench/{repo}")),
        ];
        for (arm, p) in paths {
            if !b.cfg.wants(arm) && !b.cfg.wants("console") {
                continue;
            }
            // The console's own CPU, not the server's, is what these arms are about: swap.
            let ui_cpu0 = uip.cpu_ms();
            let srv_cpu0 = b.srv.cpu_ms();
            // Latency at c=1 only: the console signs every gRPC call with a single-use
            // credential and gunnar remembers 65 536 of them for five minutes, so a burst here
            // would fill that guard and refuse every later arm. The burst is its own arm, last.
            b.http_arm_for(
                arm,
                repo,
                n,
                &p,
                "GET",
                &[("Accept", "application/json, text/html".into())],
                vec![],
                1,
                Duration::from_secs(1),
            );
            if let Some(r) = b.results.last_mut() {
                r.extra.insert(
                    "ui_cpu_ms_total".into(),
                    format!("{:.1}", uip.cpu_ms() - ui_cpu0),
                );
                r.extra.insert(
                    "srv_cpu_ms_total".into(),
                    format!("{:.1}", b.srv.cpu_ms() - srv_cpu0),
                );
                r.extra.insert(
                    "ui_rss_mib".into(),
                    format!("{:.1}", uip.status_kib("VmRSS:") / 1024.0),
                );
            }
        }
    }
    // The burst, last: c=8 on the repository record for ten seconds, then one c=1 probe of
    // what a visitor sees afterwards (the replay guard of gunnar's control plane).
    if b.cfg.wants("console_burst") || b.cfg.wants("console") {
        if let Some((repo, _)) = served.last() {
            let p = format!("/api/repos/bench/{repo}");
            let hdr = [("Accept", "application/json, text/html".into())];
            b.http_arm_for(
                "console_burst",
                repo,
                20,
                &p,
                "GET",
                &hdr,
                vec![],
                8,
                Duration::from_secs(10),
            );
            b.http_arm_for(
                "console_after",
                repo,
                20,
                &p,
                "GET",
                &hdr,
                vec![],
                1,
                Duration::from_secs(1),
            );
        }
    }
    b.env.http_port = port;
}