10 — Benchmark baseline, BEFORE the stinky-lane changes
Why this file exists. Four lanes are changing holger right now (feature-edge cleanup, a Gate trait, plugin ABI v2, data-plane work) and a fifth is about to make holger-bench-scenarios a dev-dependency. A baseline taken after any of those lands is worthless. This is the before.
Every number here is stamped with its box. A benchmark number without its machine is not a measurement (workspace LAW; see also FACETT.md:191, where a rotate_bench hard-coded machine = "Loki" string ran for months on oden and made every recorded figure unattributable).
0. Provenance of this baseline
| field | value |
| commit | 542e296a9f97ce44a8d977167ea2b7a4ed35aa79 ("the appliance gets the one login an airgap can have: auth-passkey on the row", 2026-09-20 02:16:35 +0200) |
| branch measured from | main (worktree stinky/bench cut at that sha) |
| machine | loki — Ryzen 9 7900, nproc = 12, 61 GiB RAM, 291 GB free on / |
| OS build | host build (see caveat below) |
| rustc / cargo | rustc 1.98.1 (48a229cea 2026-09-01) / cargo 1.98.1 |
| profile | --release |
| container engine | rootless podman, socket /run/user/1000/podman/podman.sock, auto-exported as DOCKER_HOST by bench_scenarios::ensure_docker_host() |
| competitor images | docker.io/sonatype/nexus3:3.72.0, releases-docker.jfrog.io/jfrog/artifactory-oss:6.23.13 — both pulled fresh 2026-09-20 |
| other load on the box | PLACEHOLDER_LOAD |
Host-build caveat, stated plainly. The LAW says a build thing builds in a CONTAINER, never on the host, because otherwise the BOX decides the verdict. This baseline was compiled on loki's host toolchain, not through
DraupnirCargoRunner. That is acceptable for a benchmark only because a benchmark is a measurement OF THE BOX by definition — but it means these numbers are a loki-host row, not a portable verdict, and the after run must use the same road or the comparison is void.
1. The inventory — every benchmark in holger
bench-scenarios is the single source of the holger-vs-rival workload. The driver is xtask/examples/nornir-bench.rs, which registers 20 arms via nornir::register_bench! and prints ONE BenchRun JSON line on stdout.
1.1 How to run any of them (no warehouse server needed)
# ON LOKI, from a worktree — NOT from the owner's /home/rickard/git/holger
cd ~/git/_wt/bench-b3
cargo build --release -p xtask --example nornir-bench # ~15 min cold, 800+ crates
NORNIR_MACHINE=loki NORNIR_BENCH_VERSION=0.6.x HOLGER_BENCH_SCALE=quick \
NORNIR_BENCH_ONLY=holger. \
./target/release/examples/nornir-bench > run.json
NORNIR_BENCH_ONLY=holger.is mandatory, not cosmetic.inventorycollects at LINK time, so the binary carries everyregister_bench!in everything it links — including nornir's own 167 arms. nornir-bench 0.1.8's own comment recordsnornir bench run skadeexecuting 213nornir.mimir.*arms and reaching ZEROskade.*arms before it was stopped: "the skade bench was measuring nornir." Omit the filter and you measure nornir, not holger.HOLGER_BENCH_SCALE=quick(5 000 entries / 2 s loops, the default) |full(500 000 entries / 5 s) |huge(~100 GiB per type, needs 3×100 GiB).HOLGER_BENCH_BYTESoverrides the budget.HOLGER_BENCH_REAL=1seeds from real crates.io / Maven Central / PyPI downloads (default ON atfull/huge).- Fixtures live in
$HOME/holger-bench-data(overrideHOLGER_BENCH_DIR) and are keyed(FIXTURE_VERSION, real?, size)— never/tmp(tmpfs). nornir bench run holgeris the persisting form (writes the Icebergbench_runstable). The plain example run needs no server.
1.2 The 20 arms
| arm id | source | measures | needs | compiles | runs |
holger.nexus_load |
bench-scenarios/src/nexus.rs |
THE sales bench. Holger vs Sonatype Nexus 3 — HTTP fetch vs HTTP fetch, both over the wire. holger_served_mbs vs nexus_served_mbs, plus ops/s, p50/p99/p999/mean both sides, nexus_upload_mbs (publish phase), and holger_inprocess_read_ceiling_mbs (internal ceiling, never a rival column) |
container engine + image docker.io/sonatype/nexus3:3.72.0; else graceful SKIP via is_skip |
PLACEHOLDER | PLACEHOLDER |
holger.artifactory_load |
bench-scenarios/src/artifactory.rs |
THE other sales bench. Holger vs JFrog Artifactory OSS 6.23.13 (6.x, single Tomcat — 7.x OSS cannot bootstrap in a throwaway container). Same metric shape | container engine + image releases-docker.jfrog.io/jfrog/artifactory-oss:6.23.13 |
PLACEHOLDER | PLACEHOLDER |
holger.latency_tail |
bench-scenarios/src/nexus.rs::latency_tail |
tail latency + throughput against a fair-heap G1GC Nexus (heap = ~50 % of container memory, detected from this box; the old 512 MB handicap was removed in the 2026-06-28 fairness fix). p50_ms, p99_ms, throughput_mbs, heap_mb |
Nexus container | PLACEHOLDER | PLACEHOLDER |
holger.rust_crate_st / _mt32 |
bench-scenarios/src/lib.rs |
znippy-repo read throughput for Rust crates, single- and all-core. (mt32 is a historical name; threads = available_parallelism()) |
fixture archive only | PLACEHOLDER | PLACEHOLDER |
holger.java_jar_st / _mt32 |
same | ditto for Java jars | fixture archive | PLACEHOLDER | PLACEHOLDER |
holger.pip_wheel_st / _mt32 |
same | ditto for Python wheels | fixture archive | PLACEHOLDER | PLACEHOLDER |
holger.pip_index_serve |
bench-scenarios/src/served.rs |
the holger-ds Python journey: a wheel corpus behind holger's PEP 503 / PEP 691 simple index (the door uv/pip hit), bombarded over HTTP. holger_served_mbs, holger_index_ops_per_sec (the /simple/{name}/ resolve rate) |
no container — in-process holger gateway | PLACEHOLDER | PLACEHOLDER |
holger.promote_ledger |
bench-scenarios/src/promote.rs |
the REAL PromotionEngine + a durable jera::jobs redb ledger: blake3 → blob write → exclusive ref → running→done row. Distinct bytes per artifact defeat dedup. Single-writer |
jera (edda path dep) + tempdir |
PLACEHOLDER | PLACEHOLDER |
holger.audit_append |
bench-scenarios/src/audit.rs |
the durable audit write that fires on every fetch/put/delete/list: ArrowIpcAuditLog::record, one-row Arrow-IPC batch + stream write + flush. Re-reads the segment and asserts the row count |
tempdir | PLACEHOLDER | PLACEHOLDER |
holger.retention_planner |
bench-scenarios/src/retention.rs |
holger's grouped plan_retention head-to-head against a naive O(n²) full-scan — the ad-hoc "keep last N versions" script. Both must produce the identical keep/delete verdict (asserted). holger_* vs naive_* per suffix |
nothing | PLACEHOLDER | PLACEHOLDER |
holger.retention_executor |
same | carrying a retention plan out against a real file-backed repo (content-id-verified deletes) | tempdir | PLACEHOLDER | PLACEHOLDER |
holger.secure_update_prepare / _sign_finalize / _verify / _import |
bench-scenarios/src/secure_update.rs |
holger's unique capability, no rival column exists. The real holger.update-bundle/v1 lifecycle: prepare (znippy pack MB/s) → approve ×2 → finalize (ed25519 + x509) → offline verify → atomic import. Each arm carries a red-when-broken ANSWER floor (bundle verifies, digests match, anti-rollback rejects a replay) |
nothing — network-free, mints its PKI in-process with rcgen | PLACEHOLDER | PLACEHOLDER |
holger.blake3 (blake3_256MiB) |
nornir-bench.rs inline |
blake3 over 256 MiB — a box-calibration arm | nothing | PLACEHOLDER | PLACEHOLDER |
holger.memcopy_bandwidth |
nornir-bench.rs inline |
memcpy bandwidth — box calibration | nothing | PLACEHOLDER | PLACEHOLDER |
1.3 Side doors onto the same workloads
| thing | path | what it is |
secure_update_bench example |
bench-scenarios/examples/secure_update_bench.rs |
The light runner. Emits the same BenchRun envelope WITHOUT linking the nexus/artifactory/server-lib graph. cargo run --release -p holger-bench-scenarios --example secure_update_bench --features secure-update. Use on a disk-tight or contended host. |
retention_probe |
bench-scenarios/examples/retention_probe.rs |
cargo run --release -p holger-bench-scenarios --example retention_probe --features retention-planner — dev-box readout of the planner head-to-head. |
pip_probe |
bench-scenarios/examples/pip_probe.rs |
cargo run --release -p holger-bench-scenarios --example pip_probe --features served; env PIP_PROBE_MB (48), PIP_PROBE_SECS (4). |
art_probe |
bench-scenarios/examples/art_probe.rs |
cargo run -p holger-bench-scenarios --example art_probe --features artifactory — container-startup instrumentation probe, not a measurement. |
holger-ds CLI verb |
server/cli/src/main.rs (feature bench) |
the artifact-delivery benchmark shipped inside holger-server. See §4 — this is the one the dev-dependency ruling kills. |
| correctness twins | agent/cli/tests/integration_{nexus,artifactory,latency}_bench_twin.rs |
downscaled #[test] twins calling the SAME workload fns. Fixture-gated: no container ⇒ recorded SKIP, test still passes. cargo test -p <agent-cli> --test integration_nexus_bench_twin. |
1.4 DEAD — recommend deletion
| thing | path | verdict |
bench_nexus_container.sh |
server/repository/znippy-rust/tests/bench_nexus_container.sh, 237 lines |
DEAD and a LAW violation. Bash end to end: $CTR run, curl per REST call, grep/awk on /proc, sleep polling, trap cleanup EXIT — every one a subprocess the pure-Rust doctrine forbids. Referenced nowhere (grep -rn bench_nexus_container hits only its own usage string). Fully superseded by nexus.rs + artifactory.rs + served.rs, which apply the same fair-resources split — the script's own comment says so. It is the last shell-shaped organ of a harness that already regrew in Rust. |
1.5 Deleted harnesses found in history (the "damma av" targets)
| sha | date | deleted path | what it was |
135b8c80f5e8c659e53b0b532bc30bfa55a2605f |
2026-05-31 | server/repository/znippy-rust/tests/bench_znippy_throughput.rs |
--ignored integration bench over get_file(), handle_http2_request(), 32-thread concurrent access. Its descendant is bench-scenarios/src/served.rs — already dusted off, no action. |
21e491efa75f4df553d3e276e2ba8d8aa580c61c |
2026-05-31 | bench_history.json |
two JSONL rows, 2026-05-27/29: rust_crate_st 420 072 → 417 602 ops/s, java_jar_st 293 696 → 283 015, pip_wheel_st 312 268 → 313 469, rust_crate_mt32 9 845 583 → 10 084 953, java_jar_mt32 7 517 880 → 7 307 041, pip_wheel_mt32 7 766 948 → 7 740 871. No machine, no commit, no core count. Orphan numbers — they cannot be compared to anything. Deletion was correct (LANE-GG-CLOSEOUT-2026-09-05.md:132 rules such a per-box append log must not be tracked; comparisons are same-window only). |
2. WHAT THIS BASELINE WAS BUILT WITH — feature set AND dependency kind
Without this section the after comparison silently compares two different binaries. Recorded as of 542e296.
2.1 Every edge onto holger-bench-scenarios
| consumer | section | features requested | reaches a shipped binary? |
xtask (xtask/Cargo.toml:102) |
[dev-dependencies] |
nexus, artifactory, promote-ledger, retention-planner, audit-append, secure-update |
No. xtask is publish = false and the bench is an --example, which resolves dev-deps. This baseline was taken through this edge. |
agent/cli (agent/cli/Cargo.toml:70) |
[dev-dependencies] |
nexus, artifactory |
No — test-only twins. |
server/cli (server/cli/Cargo.toml:99) |
[dependencies], optional = true |
served |
YES. Pulled by feature bench, and bench is in default. This is the ONE non-dev edge, and the target of the owner's dev-dependency ruling. |
So: the measurement driver is already a dev-dependency. The ruling changes only the server/cli edge — which is not a bench driver at all, it is the holger-ds CLI verb shipped inside holger-server. See §4.
2.2 What holger-bench-scenarios pins holger-server-lib with
server-lib = { package = "holger-server-lib", version = "0.6.7", path = "../server/lib",
optional = true, default-features = false,
features = ["mini", "online", "auth-bearer", "auth-oidc",
"auth-oauth2", "auth-mtls", "auth-passkey"] }
default-features = false is already applied (commit 6f02ddf), and it already removed front — holger.rs's public web page — from every binary built with bench. That half of the irony is fixed.
What is NOT fixed, and what this baseline therefore ran with:
-
auth-passkeystill flows from the bench crate into the shipped server.server/cli'sdefaultset is["mini","online","bench","secure_update","front","auth-passkey"].auth-passkeywas added to that list on 2026-09-20, in542e296— the very commit this baseline is taken at — precisely because it had until then existed only through the bench edge. The manifest says so in as many words: MEASURED — dropbenchfrom that set andnornir-auth-webauthnleaves the tree, taking/-/auth/passkey/*with it. "A login door whose existence depends on a benchmark crate being linked is not a declaration." Status at baseline: named explicitly, so the binary no longer depends on the bench edge for it. The irony is closed, one commit old. -
auth-oidc+auth-oauth2+auth-mtlsare still ONLY declared by the bench crate. No other manifest in the graph names them for the default server. Cargo unifies features per GRAPH, so they reach the systemd binary. This is the live half of the irony. -
reqwestdefaults are never disabled (bench-scenarios/Cargo.toml):
reqwest = { version = "0.12", features = ["rustls-tls","json","blocking","http2"], optional = true }
No default-features = false, so reqwest's default-tls stays on → hyper-tls → native-tls → openssl-sys. The bench crate asks for rustls-tls and gets native OpenSSL anyway. CONFIRMED in Cargo.lock at this commit: hyper-tls 0.6.0 (line 8730) depends on native-tls → openssl + openssl-sys + openssl-probe; reqwest lists hyper-tls and native-tls among its deps (lines 13855, 13860). A C library reaches the shipped server because a benchmark crate did not spell one line.
2.3 Baseline stamp — record this verbatim for the after run
commit 542e296a9f97ce44a8d977167ea2b7a4ed35aa79
bench crate holger-bench-scenarios 0.1.1
dep KIND [dev-dependencies] of `xtask` (path = ../bench-scenarios)
bench features nexus, artifactory, promote-ledger, retention-planner,
audit-append, secure-update
server-lib default-features = false, features = mini, online, auth-bearer,
auth-oidc, auth-oauth2, auth-mtls, auth-passkey
TLS in graph rustls (asked for) AND native-tls/openssl-sys (inherited)
profile release
3. Which benches depend on things that are about to leave the graph
Answering the two live rulings.
3.1 The dev-dependency ruling — "den skall vara dev-beroende, dvs aldrig in i slutbinären!!"
Nothing that measures breaks. Both bench drivers (xtask example, agent/cli twins) are ALREADY dev-dependency edges. The change is a one-line removal on server/cli.
ONE thing breaks, and it is not a bench driver:
| casualty | what it is | verdict |
holger-server holger-ds … |
The artifact-delivery benchmark shipped as a verb of the production CLI (server/cli/src/main.rs:119, run_holger_ds, feature bench → bench-scenarios/served). Presets micro/small/large, autodetects free disk via statvfs, drives start_holger_gateway + bombard_http_get, and the pip journey over PEP 503 |
Let it go, and say so out loud. A benchmark verb inside the shipped server is exactly the defect the ruling names: it drags reqwest (and today, native OpenSSL) into the systemd binary for a dev/load-test tool. .nornir/mini-holger-for-henrik.md:126 already lists "run the holger-ds delivery benchmark — bench is off" as an accepted loss for the mini tier. Rehome it as bench-scenarios/examples/holger_ds.rs, reachable by cargo run --release -p holger-bench-scenarios --example holger_ds --features served. The served feature and every line of the driver survive unchanged — only the door moves. |
Watch this when it lands: dropping bench from server/cli's default set also drops the bench crate's server-lib pin, which is the only declaration in the graph of auth-oidc, auth-oauth2 and auth-mtls. auth-passkey was rescued by name in 542e296; the other three have not been. If the shipped server is meant to keep OIDC/OAuth2/mTLS, server/cli must name them the same way auth-passkey was named, in the same commit. Otherwise three auth doors vanish silently — the identical accident, three more times.
3.2 The feature-edge lane — does any bench NEED a feature being removed?
| feature | any bench need it? | action |
front |
No. Already removed from the bench crate's pin (6f02ddf). No arm touches the web page. |
none |
auth-oidc / auth-oauth2 / auth-mtls |
No arm exercises them. Grep of all 20 arms: no OIDC/OAuth2/mTLS code path. They are inherited noise in the bench crate's server-lib pin, not a requirement. |
Delete them from bench-scenarios' server-lib features. Then, separately, decide whether the server wants them and name them in server/cli. The bench must not be what decides. |
auth-passkey |
No arm exercises it. The bench crate names it only because served.rs fills GatewaySettings::passkeys unconditionally while that field is #[cfg(feature = "auth-passkey")]. |
Fix served.rs to #[cfg] the field fill, then drop the feature from the pin. The declaration already lives in server/cli as of 542e296, so nothing is lost. |
online |
Yes, genuinely. served.rs and the rival drivers need the HTTP client. |
keep |
mini |
yes — the tier the gateway is stood up as | keep |
auth-bearer |
yes — the served driver authenticates | keep |
reqwest default-tls |
No. Every bench path asks for rustls-tls. |
add default-features = false to the reqwest line. Pure win: removes openssl-sys from the graph and makes the crate's stated intent true. |
5. Ranked keep-list — what is an asset, what is a liability
Ranking rule: a bench nobody can run is a liability; a bench that can go RED is a gate; a bench that beats a rival is a sales asset.
Tier 1 — the standing regression gate (run on every release)
holger.secure_update_{prepare,sign_finalize,verify,import}— the only arms that carry a red-when-broken ANSWER floor (the bundle must verify, digests must match, anti-rollback must reject a replay). They need no container, no network, no dataset — they mint their own PKI withrcgen. They measure holger's unique capability, which Artifactory and Nexus have no equivalent for. Cheapest to run, hardest to fake, impossible to bit-rot silently. This is the gate. And it already has the cleanest historical record in the repo:bench-results/holger-secure-update-oden-2026-07-23.jsoncarries machine, cores, date, timestamp, bench_secs and the load average at both ends of the run — the best-attributed artifact in either repo.holger.audit_append— fires on the hot path of every single request, and it re-reads the segment and asserts the row count, so a broken writer fails the bench instead of reporting a fast wrong number. No deps. Keep.holger.retention_planner— asserts holger's planner and the naive O(n²) baseline produce the identical verdict before comparing speed. That assertion is what makes the 785× claim honest. No deps. Keep.
Tier 2 — the sales assets (run before any customer conversation, and on a quiet box)
holger.nexus_loadand 5.holger.artifactory_load— the reason this lane exists. They are not bit-rotted: they are fixture-gated, which is a setup job, not a dead bench. Both images pulled clean today. They are the only evidence for the headline numbers inREADME.mdand.nornir/README-full.md. Caveat that must travel with them:workspace_nordisk/.nornir/znippy-package-capabilities-2026-07-23.mdargues throughput is holger's weakest ground and that capability is the stronger pitch — and it explicitly refuses a claim (cross-file dedup is NOT IMPLEMENTED and is an Artifactory strength). Quote the numbers, keep that honesty attached.holger.latency_tail— the fair-heap G1GC comparison. Same container cost as #4; the p99 story is more persuasive than MB/s.
Tier 3 — keep, low ceremony
holger.pip_index_serve— the holger-ds Python journey; no container needed.holger.promote_ledger— real engine + real durable ledger.holger.retention_executor.rust_crate_*/java_jar_*/pip_wheel_*(6 arms) — the oldest series, and the reason the_mt32names must never be renamed (bench_runskeys regression history by metric name). Cheap. Keep.holger.blake3/holger.memcopy_bandwidth— not product measurements; they are box calibration, and that is exactly why they earn their place: they are how you tell "holger got slower" from "the box got slower".
Liabilities — delete or rehome
bench_nexus_container.sh— DELETE. Dead, unreferenced, and shells out in violation of the LAW. Fully superseded. (§1.4)holger-dsas a CLI verb ofholger-server— REHOME to abench-scenariosexample. (§3.1)art_probe— a container-startup instrumentation probe, not a measurement. Harmless, but do not mistake it for a bench.
The documentation debt this lane also uncovered
- No number anywhere in holger or workspace_nordisk names its commit. Machine and cores are usually stamped (
oden · 32 cores · 2026-07-23); the tree state never is. That is why this file leads with a sha. README.mdand.nornir/README-full.mddisagree: README shows one holger row (108.43 served_mbs) while README-full shows two runs (100.57 and 108.43). The README row is thenexus_loadrow promoted to stand for holger overall.workspace_nordisk/.nornir/suite-improvement-backlog.md:284quotes "108k ops/s vs Nexus/Artifactory". That number matches nothing measured: the real figures are 108.43 MB/s and 181 863 ops/s. It is the MB/s value re-typed as ops and scaled by 1000. A corrupted quotation in a planning document.workspace_nordisk/ORDERS-T14S-2026-09-03-gunnar-SUPPLEMENT.md:52claims the numbers live in.nornir/best-results-benchmarks.md. That file has never existed in holger's history..nornir/proxy-design.md:25-26and.nornir/design.md:255still citeserver/lib/src/upstream/{nexus,artifactory}.rsas live paths; both were deleted ina5775cd5(moved to Skidbladnir).
6. MEASURED: the suite was BIT-ROTTED at HEAD, and this lane fixed it
This is the single most important finding of the lane, and it is a measurement, not a reading.
At 542e296, xtask/examples/nornir-bench.rs — the ONLY driver for all 20 arms — did not compile.
cargo build --release -p xtask --example nornir-bench
error[E0063]: missing field `source` in initializer of `nornir::nornir_bench::BenchResult`
--> xtask/examples/nornir-bench.rs:34:5 (and 9 more sites)
error: could not compile `xtask` (example "nornir-bench") due to 10 previous errors
nornir-bench 0.1.8 added BenchResult.source: BenchSource (issue #13 — tags a row as a timed measurement vs a static-capabilities probe). The field is #[serde(default)] and skipped on the wire when Measured, so the JSON wire format did not change at all — but the struct literal did, and holger's driver used struct-literal syntax at 10 sites.
Scope of the rot: exactly those 10 lines. 1 361 crates compiled clean, including holger-bench-scenarios itself and every scenario module. The workloads never rotted; only the driver's glue did.
Fix applied on stinky/bench: all 10 sites moved to the canonical constructor BenchResult::measured(name, metrics) — which is exactly what nornir-bench documents as "the canonical constructor for a bencher's timed numbers". Behaviour-identical, and immune to the next field addition.
What this means, and it is uncomfortable:
- Nobody has built the holger bench suite since nornir-bench gained that field. The numbers in
README.mdand.nornir/README-full.mdare stampedoden · 32 cores · 2026-07-23— almost two months stale, and not reproducible from HEAD until this commit. - The rot is invisible to
cargo build,cargo testand CI, because an--exampleof apublish = falsextask is built by nothing in the normal flow.workspace_nordisk/.nornir/bench-coverage-audit-2026-07-23.mdcounted 363register_bench!arms across the constellation and marked them DESCRIPTIVE ONLY; this is the harder version of that finding — a bench that cannot even be compiled is not descriptive, it is absent. - Recommendation (cheap, high value): add
cargo build -p xtask --example nornir-benchto whatever gate already runs on holger. Compiling the driver costs one link and would have caught this the day it broke. A suite that only rots where nothing looks will rot again.
7. Exact re-run recipe — so "after" needs no archaeology
ssh loki
tmux new-session -d -s bench # the session named `holger` belongs to another job
cd ~/git/_wt/bench-b3 # your own worktree — NEVER ~/git/holger (owner's live checkout)
export PATH=$HOME/.cargo/bin:$PATH
# 1. build the driver (this is also the rot detector — see §6)
cargo build --release -p xtask --example nornir-bench
# 2. competitor images (already on loki as of 2026-09-20)
podman pull docker.io/sonatype/nexus3:3.72.0
podman pull releases-docker.jfrog.io/jfrog/artifactory-oss:6.23.13
systemctl --user start podman.socket # bench_scenarios::ensure_docker_host() finds it
# 3. stamp the box BEFORE and AFTER — a number without its box is not a measurement
(date; uptime; nproc) > run-env.txt
# 4. the run
NORNIR_MACHINE=loki \
NORNIR_BENCH_VERSION=0.6.8 \
HOLGER_BENCH_SCALE=quick \
NORNIR_BENCH_ONLY=holger. \
./target/release/examples/nornir-bench > run.json 2> run.err
(date; uptime) >> run-env.txt
Single arms: NORNIR_BENCH_ONLY=holger.nexus_load (prefix match, comma-separated list allowed). NORNIR_BENCH_SKIP drops matching ones. Both report on stderr what they filtered — a silent narrowing would read as "we benched everything".
The no-container subset, for a box with no podman: NORNIR_BENCH_ONLY=holger.secure_update,holger.audit_append,holger.retention,holger.promote_ledger,holger.blake3,holger.memcopy
8. THE BASELINE — MEASURED 2026-09-20 on loki
Run stamp. commit 542e296a9f97ce44a8d977167ea2b7a4ed35aa79 (+ the 10-line driver fix of §6, which changes no workload) · machine loki · nproc 12 · profile release · HOLGER_BENCH_SCALE=quick · bench crate reached through a [dev-dependencies] edge of xtask · features per §2.3 · 20 of 229 linked arms · started 09:14:03 CEST, ended 09:18:11 CEST, EXIT=0.
⚠ THE BOX WAS NOT QUIET, AND THAT IS RECORDED PER ARM. loki carried another lane's KVM estate bench (2 ×
qemu-system-x86_64) and a second cargo build throughout. Load average 16.03 at start, 12.81 at end, on 12 cores. The harness stampstelem_cores_busy_avgon every row and several arms ran at 12/12 host cores busy. These numbers are therefore a CONTENDED loki row. They are valid as a before/after pair only if the after run is taken under comparable contention — or, better, if BOTH are re-taken on a quiet box. Treat the ratios (holger ÷ rival, measured in the same window on the same box) as the durable result and the absolute MB/s as the soft one.
8.1 The head-to-heads — both rivals answered, neither cell is empty
| holger | rival | holger's lead | |
vs Sonatype Nexus 3.72.0 (nexus_load, 82.8 s, concurrency 12, 5 000 files) |
34.64 served MB/s · 58 088 ops/s · p50 0.148 ms · p99 1.150 · p999 2.988 · mean 0.204 | 1.587 served MB/s · 2 395 ops/s · p50 3.114 ms · p99 34.999 · p999 191.967 · mean 4.943 | 21.8× MB/s · 24.3× ops/s · 21.0× on p50 · 64.2× on p999 |
vs JFrog Artifactory OSS 6.23.13 (artifactory_load, 62.2 s, concurrency 12, repo example-repo-local) |
43.75 served MB/s · 73 230 ops/s · p50 0.127 ms · p99 0.848 · p999 1.868 · mean 0.160 | 4.729 served MB/s · 7 835 ops/s · p50 1.118 ms · p99 6.022 · p999 42.807 · mean 1.515 · 15 691 OK ops | 9.25× MB/s · 9.35× ops/s · 8.8× on p50 · 22.9× on p999 |
latency_tail — fair-heap G1GC Nexus, heap 23 788 MB (~50 % of container memory, auto-detected; the old 512 MB handicap is gone) |
— | p50 1.406 ms · p99 9.873 ms · 3.491 MB/s · 11 749 samples | the fairness arm: Nexus given every core and half the RAM as heap |
Publish-phase (separate, never the head-to-head): nexus_upload_mbs 0.784, artifactory_upload_mbs 0.367. Internal ceiling (never a rival column): holger_inprocess_read_ceiling_mbs 1 476.5 (nexus run) / 2 124.1 (artifactory run) — i.e. the HTTP door costs holger ~40–48× its in-process read, which is itself a finding.
The tail is the story. holger's p999 is 2.99 ms where Nexus's is 191.97 ms — that is the JVM GC pause showing up, and it is far more persuasive than MB/s.
8.2 Every other arm
| arm | ops/sec | MB/s | cores busy (avg/12) |
retention_planner |
6 109 157 records/s | 559.31 metadata MB/s | 10.56 |
rust_crate_mt32 |
2 383 289 | 1 420.73 | 12.00 |
rust_crate_st |
520 839 | 310.48 | 11.00 |
java_jar_mt32 |
2 370 207 | 1 678.43 | 12.00 |
java_jar_st |
350 797 | 248.41 | 11.00 |
pip_wheel_mt32 |
2 741 425 | 1 917.69 | 9.33 |
pip_wheel_st |
379 198 | 265.26 | 4.00 |
audit_append |
161 344 records/s | 374.22 | 4.00 |
retention_executor |
56 084 | — | 9.33 |
pip_index_serve |
35 226 | 24.64 served | 12.00 |
secure_update_sign_finalize |
2 051 bundles/s | 128.20 | 8.50 |
promote_ledger |
606 seals/s | 9.47 | 3.11 |
secure_update_verify |
84.99 | 679.93 | 2.33 |
secure_update_import |
67.11 | 536.87 | 4.00 |
secure_update_prepare |
1.35 | 10.84 | 10.50 |
blake3_256MiB |
— | 6 504.79 | 6.00 |
memcopy_512MiB |
— | 2 843.38 | 7.00 |
Retention head-to-head (retention_planner, 5 000 records, 2 000 iters): holger 6 109 157 rec/s · 1 222 plans/s · p50 0.817 ms · mean 0.818 · 559.31 MB/s vs naive O(n²) 76 368 rec/s · 15.27 plans/s · p50 65.504 ms · mean 65.473 · 6.99 MB/s → holger_speedup_x = 80.0×, and at 50 000 records holger still does 2 526 054 rec/s (50.5 plans/s, 19.79 ms). Both arms are asserted to produce the identical keep/delete verdict, which is what makes the 80× honest.
8.3 Three numbers that are BAD, and are therefore the most valuable rows here
secure_update_prepare= 10.84 MB/s, 1.35 bundles/s. This is the znippy pack path for a bundle, and it is two orders of magnitude below every other holger write path in this table. Comparesecure_update_verifyat 679.93 MB/s and_importat 536.87 — the same bundles, read back, run 50–63× faster than they are produced. On oden 2026-07-23 the same arm measured 8.40 MB/s, so this is not a loki artifact: prepare has always been the floor. It also ran at 10.5/12 cores busy while producing 10.84 MB/s, which is the signature of work being done serially inside a busy box rather than fanned out.promote_ledger= 606 seals/s at 9.47 MB/s, on 3.11/12 cores. A single promotion is blake3 + blob write + exclusive ref + one redb ledger row, all single-writer by design. 9.47 MB/s is the ceiling on how fast artifacts can be sealed, and 3 of 12 cores were busy — nine cores idle while the hot path queues. This is the "no job queue, everything inline on the RPC" shape the coordinator described, measured.pip_index_serve= 24.64 MB/s served, at 12/12 cores busy. The PEP 503 door saturates the box to deliver 24.64 MB/s, where the raw repo read (pip_wheel_mt32) does 1 917.69 MB/s on the same fixtures. A 78× gap between what the store can read and what the door can serve.
These three are the before-half of the stress story. None of them is a regression — they are the standing shape of the system, now written down with a commit and a box attached for the first time.
9. STRESS: the same arms at 100× the entries (full scale)
Second run, same commit, same box, same dev-dep edge, HOLGER_BENCH_SCALE=full (500 000 entries vs quick's 5 000) with HOLGER_BENCH_REAL=0 to keep the corpus synthetic and the run bounded. Started 09:19:51, ended 09:22:49 CEST, EXIT=0, 11 arms. Fixtures grew to ~730 MB under $HOME/holger-bench-data. Raw: bench-results/holger-bench-loki-2026-09-20-542e296-full.json.
| arm | quick (5k) | full (500k) | change | cores busy at full |
rust_crate_mt32 |
1 420.73 MB/s | 2 820.69 | +99 % | 7.67/12 |
java_jar_mt32 |
1 678.43 | 2 058.67 | +23 % | 7.24/12 |
pip_wheel_mt32 |
1 917.69 | 1 445.01 | −25 % | 12.00/12 |
rust_crate_st |
310.48 | 325.28 | +5 % | 1.00/12 |
java_jar_st |
248.41 | 233.64 | −6 % | 11.33/12 |
pip_wheel_st |
265.26 | 245.02 | −8 % | 6.33/12 |
audit_append |
161 344 ops/s | 177 484 | +10 % | 12.00/12 |
retention_planner |
6 109 157 rec/s | 6 432 641 | +5 % | 1.00/12 |
retention_executor |
56 084 ops/s | 64 225 | +15 % | 1.00/12 |
pip_index_serve (served) |
24.64 MB/s | 65.96 | +168 % | 10.15/12 |
promote_ledger |
606 seals/s · 9.47 MB/s | 534 seals/s · 8.35 MB/s | −12 % | 2.13/12 |
What the scale run proves
-
The READ paths scale. The WRITE path does not. Every multicore read arm holds or improves at 100× the corpus —
rust_crate_mt32nearly doubles. Butpromote_ledgergets slower with more work while using 2.13 of 12 cores. Ten cores idle while the seal path queues. That is not a tuning problem, it is the absence of a job queue: blake3 + blob write + exclusive ref- ledger row all run inline and single-writer.
-
pip_index_serveis the door, not the store. Served throughput rose from 24.64 to 65.96 MB/s — but the same wheels read out of the store at 1 445 MB/s in the same run. The PEP 503 door delivers 4.6 % of what the store can read, at 10.15/12 cores busy. The bottleneck is the serving path, and it is the single largest headroom in the product. -
pip_wheel_mt32is the one read arm that regresses (−25 %) and it is the one pinned at 12.00/12 cores. Worth a look: it is the only read arm whose core occupancy went UP while throughput went DOWN, which is the signature of contention rather than saturation. -
Note
retention_planner/retention_executor/rust_crate_stran athost_busy_max = 1— the other lane's jobs had finished by then, so those three rows are from a QUIET loki and the rest are not. Per-row honesty, recorded rather than averaged away.
10. COMPETITOR VERSIONS — the comparison is half-stale, and one claim is FALSE
This was the owner's suspicion and it is correct. Verdicts:
Sonatype Nexus — 3.72.0 (2024-09-04) → current 3.96.2 (2026-09-18) → YES-BUT
- 24 feature lines / 63 published tags / ~24.5 months behind. 3.72.0 is Sunsetted; standard support ended 2025-09-04.
- The good news: the pin is on the RIGHT side of the big cut. 3.71.0 dropped OrientDB and required H2/PostgreSQL + Java 17. 3.72.0 already runs H2 on JDK 17, so we are not secretly beating a legacy OrientDB Nexus. That was the worst-case trap and we did not fall into it.
- The bad news: 3.72.0 predates Cargo entirely. Native Rust/Cargo support landed in 3.73.0 (2024-10-10) — one release after our pin. A 3.72.0 container cannot serve cargo at all. So any claim of the form "holger beats Nexus on Rust crates" is not supported by this harness; the honest framing is "Nexus on maven/pypi, 2024 vintage".
- Subtler config drift, the trap within the trap: the pinned image hard-codes
-Xms2703m -Xmx2703m -XX:MaxDirectMemorySize=2703m. The current image sets no heap flags at all and lets JVM ergonomics take ~25 % of container RAM. Also UBI8/glibc → Alpine/musl, JDK 17 → JDK 21. And 3.96.0 added gzip'd npm JSON and abbreviated packuments (−89 % payload) plus more efficient Cargo sparse-index serving. A modern Nexus would serve materially less bytes for the same request. Our fair-heap arm detects the heap from the box, so on the current image it would be setting a heap the image no longer expects. - Licensing shift worth knowing before publishing numbers: since 3.77.0 (Feb 2025) it is Community Edition with a click-through EULA and a 40 000 components / 100 000 requests per day cap — a benchmark can hit that cap. Our 3.72.0 pin is EPL OSS with no EULA, which is the legally cleanest tag.
JFrog Artifactory — 6.23.13 (2021-02-21) → current 7.161.26 → NO. Not honest.
- Five years and one whole major line stale. 6.x is long EOL; the 6.x line itself ended at 6.23.42 (2022-03-28), so the pin is 29 patches behind inside its own dead line.
- The stated reason for the pin is FALSIFIED.
artifactory.rssays 7.x OSS "won't bootstrap" because of Access/router/keys microservices. In fact 7.x ships those microservices bundled inside the one image, and JFrog's own documented install is a singledocker run -p 8081:8081 -p 8082:8082. Independent proof: PlaytikaOSS/testcontainers-spring-boot ships a liveembedded-artifactorymodule pinned toreleases-docker.jfrog.io/jfrog/artifactory-oss:7.161.26. The only real cost is a ~1–3 min cold boot and 2–4 GB RAM, which means a longer wait strategy, not a different major version. - Format ceiling, in BOTH versions: Artifactory OSS serves only Maven/Gradle/Ivy/SBT + generic. No Cargo, no PyPI, no npm in any free JFrog edition. So Artifactory can only ever be a maven/generic data point against a Rust-first product, and the README should say so.
Action, ranked
- Bump Artifactory to
7.161.26and lengthen the wait strategy. This is the one unambiguous honesty fix, and the "can't containerize" blocker does not exist. MEASURED prerequisite: the image pulls clean (done on oden 2026-09-20). - Add a SECOND Nexus arm on
3.96.2, keep3.72.0as the historical series. Two rows, both labelled, is honest; silently swapping the tag would orphan thebench_runshistory keyed by metric name. The new arm is also the only way to get a cargo-vs-cargo comparison at all, and it needs the EULA POST to/service/rest/v1/system/eulaplus awareness of the daily request cap. - Add Gitea (
gitea/gitea:1.27.3). It is the only genuinely light container that covers cargo + maven + pypi, boots in seconds on SQLite withINSTALL_LOCK, and is MIT. Best value per line of harness code. - Optional fourth: Forgejo (near-identical API, independent release train). Skip Archiva (retired to the Attic), Harbor (no cargo/maven/pypi, 9-container compose) and Pulp (nginx+postgres+redis bundle).
Say this out loud in any customer-facing use of the numbers: today's headline compares holger against a 2024 Nexus that cannot serve Rust and a 2021 EOL Artifactory that cannot serve Rust or Python. The wins are real on the formats tested, and the p999 tail story is the strongest of them — but the version gap cuts both ways and must be closed before the numbers are used to sell.
11. The boxes, named — because the silicon is part of the measurement
| loki | oden | |
| CPU | AMD Ryzen 9 7900, 12 cores | AMD Ryzen Threadripper PRO 3975WX, 32 cores |
| Board | — | ASUS Pro WS WRX80E-SAGE SE WIFI |
| RAM | 61 GiB | 499 GB |
| Disk at run time | / 1.9 T, 276 GB free (86 %) |
/ 526 GB free (72 %); /home 168 GB free (98 %) |
/dev/kvm |
present but session-bound | present |
| rustc / cargo | 1.98.1 (48a229cea) / 1.98.1 | same |
| known hazard | drops off the network under full load | idle-suspends; must be woken by hand |
Thermal discipline on oden. Fans are held at 100 % through the measurements (owner ruling) and turned down to 70 % only afterwards. I do not change fan settings. Every oden run carries a thermal trace sampled every 5 s for the whole run — Tctl, Tccd1, mean/min/max achieved core clock across all 32 cores from /proc/cpuinfo, load1 and runnable procs — committed beside the result as *.thermal.csv.
Why the trace is not optional: a throttled CPU makes holger look slow and a benchmark cannot tell the difference. A 3975WX under sustained all-core load can fall well below boost for reasons that have nothing to do with this code. A run whose trace shows a clock cliff must be labelled or re-run, never quietly reported. Idle reference before the run: Tctl 58.9 °C, CPU_FAN 2800–2900 RPM, NVMe ~34 °C.
12. MEASURED while dusting off: Artifactory 7.x on oden
Trying to verify the falsified "7.x won't bootstrap" claim by actually booting releases-docker.jfrog.io/jfrog/artifactory-oss:7.161.26 in ONE container:
- The image pulls clean (as does
sonatype/nexus3:3.96.2). All four images — both pins and both currents — are now on oden. - The container exits 1 in under 10 s, and not for the reason the source comment gives. There is no microservices failure, no router, no Access complaint. The log says:
/opt/jfrog/artifactory/var is running with 2% free storage. Free up space or increase volume size and try again. Exiting - That is a percentage-based free-space guard, and it is reading oden's podman graphroot —
/home/rickard/.local/share/containers/storageon/home, which is 98 % full (168 GB free). 168 GB is plenty of bytes; the guard counts percent. Mounting the data volume elsewhere does not help, because the container's own root layer still lives on the 98 % filesystem.
Conclusion, and it matters for the harness: the blocker on 7.x is an oden housekeeping problem, not a containerization problem. To run the modern Artifactory arm, either free /home below ~95 % or move podman's graphroot to / (526 GB free, 72 %). Written down so the next person does not re-diagnose it as "7.x can't containerize" — which is exactly how the stale 6.23.13 pin got its justification in the first place.
13. Handover — what is done, what is next
Done and pushed (branch stinky/bench, remote origin):
- The bench driver compiles again (10 sites, canonical constructor).
- Baseline captured at
542e296on loki, quick scale, 20/20 arms, both rivals answering — committed with commit + machine + nproc + profile + feature set + dependency kind. - Scale/stress run at 100× the corpus, 11 arms.
- Competitor pins audited against current; one stated justification falsified.
- Raw
BenchRunenvelopes committed underbench-results/so no number in this document is unsourced.
Next, in value order:
- Bump Artifactory to 7.161.26 (free oden's
/homefirst) and add a Nexus 3.96.2 arm beside the 3.72.0 one. This is the honesty fix, and the 3.96.2 arm is the only route to a cargo-vs-cargo number. - Add Gitea — cheapest competitor per line of harness code, and the only light one covering cargo + maven + pypi.
- Gate on
cargo build -p xtask --example nornir-bench. One link, and §6 would never have happened. - Delete
bench_nexus_container.sh; rehomeholger-dsout of the shipped CLI into abench-scenariosexample. - Strip
auth-oidc/auth-oauth2/auth-mtlsfrom the bench crate'sserver-libpin anddefault-features = falseitsreqwest— but name whatever the server actually wants inserver/cliin the same commit, or three auth doors vanish the way the passkey door nearly did. - The stress gaps this lane did NOT close, and that no existing arm covers: concurrent uploads (all arms are read-heavy), a repo of many thousands of artifacts under sustained mixed read/write, and the edge behaviours — disk filling, a slow client, a client that disappears mid-upload. The
promote_ledgerandpip_index_serverows above say where the pain will be.
14. THE AUTHORITATIVE RUN — oden, 32 cores, quiet box, thermally traced
This is the row to quote. Same commit, same quick scale, same dev-dep edge, 20/20 arms, EXIT=0, 07:28:22 → 07:32:59 UTC (4 min 37 s).
Box: oden · AMD Ryzen Threadripper PRO 3975WX, 32 cores · ASUS Pro WS WRX80E-SAGE SE WIFI · 499 GB RAM · / 526 GB free (72 %) · rustc 1.98.1. Thermal trace: 56 samples @ 5 s = full run coverage. Tctl avg 56.0 °C, min 49.4, max 64.1 · all-core clock avg 2972 MHz, min 2247 (idle between arms), max 4167 under load. No throttling. Max Tctl 64.1 °C is ~30 °C below the 3975WX limit, and the all-core clock held 4167 MHz through the loaded arms. These numbers are about holger, not about the cooling. Fans were at 100 % throughout per the owner's ruling; I did not change them. Raw: bench-results/holger-bench-oden-2026-09-20-542e296.json + .thermal.csv + .env.txt.
14.1 The head-to-heads on a quiet 32-core box
| holger | rival | lead | |
| vs Sonatype Nexus 3.72.0 (concurrency 32) | 105.33 served MB/s · 176 687 ops/s · p50 0.159 ms · p99 0.434 · p999 0.668 · mean 0.177 | 9.999 MB/s · 14 919 ops/s · p50 1.495 · p99 8.080 · p999 72.022 · mean 2.116 | 10.5× MB/s · 11.8× ops/s · 9.4× p50 · 107.8× p999 |
| vs JFrog Artifactory OSS 6.23.13 (concurrency 32) | 97.55 served MB/s · 163 674 ops/s · p50 0.173 ms · p99 0.465 · p999 0.698 · mean 0.191 | 12.647 MB/s · 21 167 ops/s · p50 1.083 · p99 5.917 · p999 41.878 · mean 1.489 | 7.7× MB/s · 7.7× ops/s · 6.3× p50 · 60.0× p999 |
latency_tail fair-heap Nexus |
— | p50 1.728 ms · p99 7.433 · 8.287 MB/s · 27 754 samples · heap 191 721 MB | see note |
Publish phase: nexus_upload_mbs 1.178, artifactory_upload_mbs 1.266. Internal ceiling: 3 481 / 3 929 MB/s — the HTTP door costs ~33–40× the in-process read, the same shape loki showed.
Fairness is real and it is generous.
FairResources::detect()gave Nexus a 191 GB JVM heap on oden (~50 % of container memory) and every core. Nobody can say the JVM was starved. holger still wins p999 by 107.8×, and that gap is GC, not bandwidth.
These oden numbers independently reproduce the README. The static table in
README.md/.nornir/README-full.mdsaysoden · 32 cores · 2026-07-23: holger 108.43 served MB/s vs Nexus 10.60, and 100.57 vs Artifactory 12.24. Today, two months and many commits later, on the same box: 105.33 vs 9.999 and 97.55 vs 12.647. Within a few percent on every cell. The published claims are sound — they were merely unreproducible until §6 was fixed.
14.2 Every other arm on oden (quick scale)
| arm | ops/sec | MB/s | cores busy /32 |
pip_wheel_mt32 |
6 038 303 | 4 223.94 | 21.67 |
java_jar_mt32 |
5 609 126 | 3 972.02 | 21.67 |
retention_planner |
3 437 582 rec/s | — | 4.33 |
rust_crate_mt32 |
3 357 854 | 2 001.69 | 32.00 |
pip_index_serve |
171 544 | — | 28.00 |
audit_append |
123 104 | 285.53 | 2.00 |
retention_executor |
72 829 | — | 2.00 |
rust_crate_st |
458 072 | 273.07 | 32.00 |
pip_wheel_st |
308 330 | 215.68 | 1.67 |
java_jar_st |
295 471 | 209.23 | 1.67 |
secure_update_sign_finalize |
1 575 | 98.46 | 32.00 |
promote_ledger |
348.69 seals/s | 5.448 | 2.00 |
secure_update_verify |
56.26 | 450.06 | 20.25 |
secure_update_import |
37.59 | 300.72 | 32.00 |
secure_update_prepare |
0.961 | 7.692 | 32.00 |
blake3_256MiB |
— | 3 275.52 | 2.00 |
memcopy_512MiB |
— | 2 752.26 | 2.00 |
14.3 THE FINDING — the write path does not merely fail to scale, it goes BACKWARDS
Three boxes/scales, one arm:
promote_ledger |
seals/s | MB/s | cores used |
| loki 12c, quick | 606 | 9.47 | 1.01 / 12 |
| loki 12c, full (100× corpus) | 534 | 8.35 | 2.13 / 12 |
| oden 32c, quick | 349 | 5.45 | 1.01 / 32 |
Going from 12 cores to 32 made sealing 42 % SLOWER. It used one core on both boxes. The same run's read arms went the other way on the same hardware (pip_wheel_mt32 1 918 → 4 224 MB/s, +120 %; java_jar_mt32 1 678 → 3 972, +137 %). So the machine is fine and the reads scale beautifully — the seal path is strictly serial and is bounded by single-core speed and fsync latency, and loki's Ryzen 9 7900 simply has a faster single core than the 3975WX.
That is the clearest possible statement of the "no job queue, everything inline on the RPC" defect: adding 20 cores to the machine made the product slower at its write path. It is the single highest-value target in the system, and this table is the before-half.
Same shape, weaker: secure_update_prepare 10.84 (loki) → 7.69 MB/s (oden) while pinning 32/32 cores — busy cores, no throughput.
14.4 Cross-box summary for the honest reader
| arm | loki 12c | oden 32c | scales with cores? |
pip_wheel_mt32 |
1 917.69 | 4 223.94 | ✅ +120 % |
java_jar_mt32 |
1 678.43 | 3 972.02 | ✅ +137 % |
rust_crate_mt32 |
1 420.73 | 2 001.69 | ✅ +41 % |
| holger served vs Nexus | 34.64 | 105.33 | ✅ +204 % |
| holger served vs Artifactory | 43.75 | 97.55 | ✅ +123 % |
audit_append |
374.22 | 285.53 | ❌ −24 % (single-writer by design) |
secure_update_prepare |
10.84 | 7.69 | ❌ −29 % |
promote_ledger |
9.47 | 5.45 | ❌ −42 % |
Read paths scale with cores. Every write path is single-core bound and therefore tracks single-core clock, not core count. A customer who buys a bigger box gets faster reads and slower writes.
15. THE RIVAL REGISTER — pinned tag, its age, the current tag, and what the DEFAULT changed
Extends §10, which established that the comparison was half-stale and one justification was false. This section is the register the owner asked for: for every rival, the pinned tag, its release date, the current tag, and whether the default configuration changed between them. A pinned tag whose defaults have since changed is the subtle version of the same trap — the number is measured against software that no longer exists in that shape.
Release dates below are MEASURED from the image metadata on oden (podman inspect --format {{.Created}}), not read off a changelog.
| rival | pinned tag | pinned built | current tag | current built | gap | is the comparison honest? |
| Sonatype Nexus | 3.72.0 |
2024-09-04 | 3.96.2 |
2026-09-18 | ~24.5 months | NO on its own. The pin predates Cargo. Now paired with a 3.96.2 arm, and both run. |
| JFrog Artifactory OSS | 6.23.13 |
2021-02-21 | 7.161.26 |
2026-09-07 | ~4.5 years, one whole major line | NO on its own. EOL, 29 patches behind inside its own dead line. Now paired with a 7.161.26 arm, and both run. |
Both current-tag arms are running and measured — see §17. Getting there required two live boots to discover two defaults that no changelog reading would have caught, and each of them would otherwise have produced a false number.
15.1 Did the DEFAULT CONFIGURATION change? Yes, in both, and it is measurable
Nexus — the heap default is the trap. Read straight off the two images:
3.72.0 INSTALL4J_ADD_VM_PARAMS=-Xms2703m -Xmx2703m -XX:MaxDirectMemorySize=2703m …
3.96.2 INSTALL4J_ADD_VM_PARAMS=-Djava.util.prefs.userRoot=/nexus-data/javaprefs
The pinned image hard-codes a 2703 MB heap. The current image sets no heap flags at all and lets JVM ergonomics take its share of container RAM. Those are two different pieces of software under load, and a bench that pinned the old tag would have been measuring a fixed small heap while believing it measured "Nexus".
Our harness is not exposed to that particular error, and this is worth stating plainly because it is the one place the existing design was already right: FairResources::detect() overrides INSTALL4J_ADD_VM_PARAMS on both tags with a heap sized from the host box. On the authoritative oden run that was a 191 721 MB heap. So the heap default drift is neutralised by construction.
Nexus — anonymous read is the second, and it was found only by booting. A fresh 3.96.2 says so in its own log:
AnonymousManagerImpl - Using default configuration:
AnonymousConfigurationData{enabled=false, userId='anonymous', realmName='NexusAuthorizingRealm'}
3.72.0 allows anonymous reads. 3.96.2 Community Edition does not. The first run of the current-tag arm therefore served 0 of 91 757 GETs, and the only reason that is not now sitting in a slide as a ~0 MB/s "result" is the empty-rival-cell guard added in the same commit. The harness enables anonymous read over REST rather than authenticating the Nexus leg, so the transport stays identical to the pinned arm and the two rows differ by the software under test and by nothing else.
Artifactory — the database requirement is the whole story. 6.23.13 runs on the bundled Derby. 7.161.26 refuses to:
DbTypeNotAllowedException: DB Type derby is not allowed:
Cannot start the application with a database other than PostgreSQL.
So 7.x is a two-container arm: the harness now boots a postgres:16-alpine companion on a per-run network and points JF_SHARED_DATABASE_* at it. After that change the image logs Configured database type: POSTGRES, answers an authenticated ping in 47.5 s, and served 40 326 successful GETs.
This corrects two claims at once — the old source comment's ("7.x's microservices crash-loop, it cannot be containerised" — false) and this lane's own first assumption ("free the disk and it is a drop-in tag bump" — also false). See §15.5 for the full sequence, because the order in which those blockers appeared is the interesting part.
What is NOT neutralised, and travels with any number from the pinned tag:
3.72.0 |
3.96.2 |
|
| base image | UBI8 minimal (base-image-ref label: ubi8/ubi-minimal), glibc |
no RedHat labels — Alpine/musl line |
| JDK | 17 | 21 |
| licence | EPL OSS, no EULA | Community Edition, click-through EULA + 40 000 components / 100 000 requests per day cap |
| Cargo repositories | none — cannot serve cargo at all | yes |
| npm metadata | full packuments | gzipped + abbreviated (−89 % payload) |
| storage | H2 on JDK 17 (post-OrientDB — the pin is on the right side of the 3.71.0 cut) | H2/PostgreSQL |
Artifactory — the restricted-mode flag is new. 7.161.26 ships JF_SHARED_RESTRICTEDMODE_ENABLED=true in the image environment; 6.23.13 has no such variable and instead carries POSTGRESQL_VERSION=9.4.1212. Different product shape, not a patch difference.
15.2 THE FORMAT CEILING — say this wherever the numbers are written down
Two sentences that change what every Artifactory and Nexus number in this document means:
- Artifactory OSS serves Maven/Gradle/Ivy/SBT and generic repositories, and nothing else. No Cargo, no PyPI, no npm — in ANY free JFrog edition, 6.x or 7.x. Upgrading the tag does not fix this and never will. Every Artifactory row in this suite is a maven/generic data point measured against a Rust-first product.
- Nexus
3.72.0cannot serve Cargo at all. Native Rust/Cargo support landed in3.73.0(2024-10-10), one release after the pin. The3.96.2arm added in this lane is the only route in the harness to a cargo-versus-cargo comparison.
So the honest label for the pre-existing head-to-heads is: holger versus a 2024 Nexus that cannot serve Rust, and a 2021 EOL Artifactory that cannot serve Rust or Python, on raw/generic repositories. The wins are real on what was tested and the p999 tail story is the strongest of them. The version and format gaps cut both ways and must be stated before the numbers are used to sell.
15.3 Two rows, never one — why the old tags stay
bench_runs keys its regression history by metric name. Swapping the tag under nexus_load would have left the same series silently describing different software — the exact failure mode FACETT.md:191 records, where a hard-coded machine = "Loki" string ran on oden for months.
So the harness now carries four rival arms instead of two:
| arm | rival | why it exists |
holger.nexus_load |
Nexus 3.72.0 |
series continuity |
holger.nexus_load_current |
Nexus 3.96.2 |
honesty, and the only cargo-capable Nexus |
holger.artifactory_load |
Artifactory OSS 6.23.13 |
series continuity |
holger.artifactory_load_current |
Artifactory OSS 7.161.26 |
honesty |
Both halves of each pair run through identical code (nexus_arm / artifactory_arm in xtask/examples/nornir-bench.rs, and nexus_load_tag / artifactory_load_tag in the scenario modules). A comparison whose two halves went through different code is not a comparison. Every row now carries nexus_tag / artifactory_tag on the row itself.
15.4 What the code had to learn to run the modern tags
| change | file | why |
| tag is a parameter | bench-scenarios/src/nexus.rs, .../artifactory.rs |
so both tags go through one code path |
wait on "Started Sonatype Nexus", not "Started Sonatype Nexus OSS" |
nexus.rs |
3.96.2 is Community Edition and does not log the OSS banner. Waiting on the OSS string would have hung the modern arm to its boot timeout and recorded it as a skip — the modern rival would have silently never run. |
accept_eula() — GET the EULA document, POST it back with accepted: true |
nexus.rs |
CE (3.77.0+) refuses work until the click-through licence is accepted. Answers 404 on the pre-CE pinned tag, which is tolerated as the correct answer. |
| empty-rival-cell guard on the Nexus leg | nexus.rs |
the Artifactory leg has had this guard since its OSS repo-creation bug; Nexus did not. A rival that served 0 successful GETs would have been reported as ~0 MB/s — a spectacular fake win. |
| probe 8082 then 8081 | artifactory.rs |
6.x is one Tomcat on 8081; 7.x splits the platform (router on 8082, artifactory service on 8081). |
| boot window 300 s → 900 s for 7.x | artifactory.rs |
7.x is a multi-service platform in one image and takes minutes, not seconds, to reach an authenticated ping. Giving it 6.x's window is exactly how a healthy modern rival gets recorded as a skip. |
artifactory_boot_secs on the row |
artifactory.rs |
the boot cost is the ONLY real difference from 6.x, and the stale pin was justified by a guess about precisely this number. Now it is measured. |
| the false comment deleted | artifactory.rs |
the source said 7.x "won't bootstrap … Access/router/event microservices crash-loop on missing master.key/join.key". It was wrong and it froze the arm on a five-year-old image for years. Replaced with what was actually measured. |
15.5 The Artifactory 7.x blocker, in the order it actually appeared
Written out because the sequence is the lesson: each layer looked like the whole answer until it was removed.
| # | what was believed | what was measured | verdict |
| 0 | "7.x OSS won't bootstrap — Access/router/event microservices crash-loop on missing master.key/join.key." Written in artifactory.rs and used to justify a 2021 pin for years. |
Nothing of the sort appears in any log. No router failure, no key complaint. | FALSE. Deleted. |
| 1 | The container exits in under 10 s, so 7.x is broken. | …/var is running with 2% free storage. Free up space … Exiting — a percentage-based guard reading podman's graphroot on a /home that was 98 % full with 168 GB free. |
A bench-host housekeeping problem, recorded as a property of Artifactory. Fixed by freeing /home to 87 %. |
| 2 | With the disk freed, it will boot. | It stopped exiting and instead sat in Cluster join: Retry 740: Service registry ping failed for 15 minutes. Root cause in the log: DbTypeNotAllowedException: DB Type derby is not allowed. |
7.x requires PostgreSQL. A real, specific requirement — one more image, not a different major version. |
| 3 | — | With a postgres:16-alpine companion: Configured database type: POSTGRES, authenticated ping at 47.5 s, 40 326 successful GETs, full head-to-head measured. |
Running. §17.1. |
Three layers, and the top one was fiction. The general lesson is the one this whole lane keeps re-learning: a justification that was never re-measured is not a justification. Steps 1 and 2 were each invisible until the one above was removed, which is precisely why "we tried it once and it didn't work" decays into folklore.
16. THE STRESS ARMS NOBODY HAD — the write side, measured
§13 listed three gaps no arm covered: concurrent uploads (every arm was read-heavy), a repository of many thousands of artifacts under sustained mixed read/write, and the edges — the disk filling, a slow client, a client that disappears mid-upload. This section closes them.
New module: bench-scenarios/src/stress.rs, behind a new stress cargo feature (stress = ["served", "dep:tempfile"]), reached through the same [dev-dependencies] edge of xtask as every other measuring arm. Three new bench ids: holger.concurrent_upload, holger.mixed_read_write, holger.upload_edges.
16.1 What door these arms drive, and why that matters
All three go over the wire through a real Holger HTTP gateway (served::start_holger_gateway) standing over a writable znippy repository, using PUT /{repo}/api/v1/crates/new — the genuine cargo publish endpoint, with cargo's own body framing ([u32 json_len][json][u32 crate_len][crate]). Nothing is measured in-process. That path is:
PUT /{repo}/api/v1/crates/new
→ RustRepoZnippy::put
→ self.write_lock.lock() ← one process-wide mutex
→ znippy_common::meta_sink_append::append_files ← read-modify-write of the archive
→ invalidate_index_cache()
Two properties of that path are what these arms exist to quantify: the mutex serialises every publisher, and the append re-seals the whole archive, so the cost of one upload grows with how much is already stored. Neither had a number attached before today.
16.2 Why concurrent_upload has three phases and not two
A serial-then-concurrent measurement cannot distinguish a serialising lock from an append that got dearer as the archive grew: both make the second phase look bad. So the arm runs serial → concurrent → serial, and reports two separate ratios:
scaling_x= concurrent rate ÷ the mean of the two serial phases — did more clients buy anything?append_cost_growth_x= first serial rate ÷ last serial rate — did storing more make each upload dearer, on its own?
A shared atomic counter hands every upload in every phase a unique crate name. Without it the later phases would "measure" RustRepoZnippy::put's idempotent re-publish no-op, which is not an upload.
16.3 Honesty notes that must travel with these numbers
- There is no rival column, and there should not be one. Nexus and Artifactory are not driven by these arms. This is holger measured against holger, on holger's own write door.
- The disk-filling edge is an ANALOGUE, not a true ENOSPC. It makes the store un-writable via permissions rather than filling a real filesystem, because a genuine ENOSPC needs a bounded mount, which needs root, on a shared bench box. The row carries
enospc_is_analogue = 1so the limitation cannot be lost in a later quotation. What the analogue does answer is the part that matters: does a failed write get a clean refusal, and does the archive survive it. A true ENOSPC arm is an owner item — see §18. - A bad number in this section is a good result. It is the before-half of a before/after while another lane changes the write path.
17. MEASURED — four rivals and three stress arms, quiet oden, 2026-09-20
17.0 Provenance
| field | value |
| commit | e6238c154f5fcf5e108deecee4aa622b4ee87b61 (branch stinky/r4-rivals, cut from stinky/bench at a5ef6e6) |
| machine | oden — AMD Ryzen Threadripper PRO 3975WX, nproc 32, 499 GB RAM |
| built on | loki (rustc 1.98.1 / cargo 1.98.1, --release), binary rsynced to oden. Nothing was compiled on oden during a measurement. |
| profile | --release |
| scale | HOLGER_BENCH_SCALE=quick, concurrency 32 |
| bench crate reached via | [dev-dependencies] of xtask, features nexus, artifactory, promote-ledger, retention-planner, audit-append, secure-update, stress |
| server-lib pin | unchanged from §2.3 |
| disk | /home 87 % used, 776 GB free (the Artifactory free-space guard needs this) |
Box quietness, recorded rather than assumed. The owner had dwarves-server stopped for these runs, and it stays stopped. Immediately before the measured set: load average 2.16, top process valkey-server at 1.0 %, Tctl 42.6 °C, all-core clock 2809 MHz idle.
Thermal trace: bench-results/holger-r4-oden-2026-09-20.thermal.csv, sampled every 5 s straight from /sys/class/hwmon/hwmon5 (Tctl, Tccd1) plus the mean of all 32 cpu MHz lines and load1. Fans were at 100 % throughout, per the owner's standing instruction; I did not change them.
One set of numbers was taken while the box was NOT quiet and is kept separately, not averaged away. The first stress run happened while
dwarves-serverwas still using ~287 % CPU. Those rows are preserved underbench-results/contended/and are quoted in §17.3 beside the quiet ones, because the comparison between them is itself informative: the shape of every finding survived the contention, only the magnitudes moved.
17.1 The four rival arms — same box, same window, ~10 minutes apart
Both tags of each rival ran through identical code. Every row carries its tag.
| arm | rival tag | holger served | rival served | holger ops/s | rival ops/s | holger p999 | rival p999 |
nexus_load |
3.72.0 (2024-09-04) | 102.07 MB/s | 10.580 MB/s | 171 229 | 15 738 | 0.720 ms | 76.219 ms |
nexus_load_current |
3.96.2 (2026-09-18) | 103.68 MB/s | 4.220 MB/s | 173 860 | 6 338 | 0.689 ms | 68.763 ms |
artifactory_load |
6.23.13 (2021-02-21) | 98.38 MB/s | 12.568 MB/s | 165 060 | 21 120 | 0.683 ms | 54.737 ms |
artifactory_load_current |
7.161.26 (2026-09-07) | 98.26 MB/s | 12.008 MB/s | 164 823 | 20 130 | 0.686 ms | 30.525 ms |
holger's lead, per rival:
| rival | MB/s | ops/s | p50 | p999 |
| Nexus 3.72.0 | 9.65× | 10.9× | 8.8× | 105.9× |
| Nexus 3.96.2 | 24.6× | 27.4× | 21.4× | 99.8× |
| Artifactory 6.23.13 | 7.83× | 7.8× | 6.4× | 80.2× |
| Artifactory 7.161.26 | 8.18× | 8.2× | 7.8× | 44.5× |
Publish phase (separate, never the head-to-head): nexus_upload_mbs 1.199 (3.72.0) / 1.031 (3.96.2); artifactory_upload_mbs 1.381 (6.23.13) / 0.218 (7.161.26). Boot to an authenticated ping: Artifactory 17.1 s on 6.x, 47.5 s on 7.x.
These reproduce the published claims. README.md says oden · 32 cores · 2026-07-23: holger 108.43 vs Nexus 10.60, and 100.57 vs Artifactory 12.24. Today, against the same pinned tags: 102.07 vs 10.580 and 98.38 vs 12.568. Within a few percent on every cell, two months and many commits later.
17.2 What the modern rivals change, and it is not all in holger's favour
Two findings, and the second one costs us.
-
The modern Nexus serves less, not more. Nexus 3.96.2 delivered 4.220 MB/s where 3.72.0 delivered 10.580 — the current Community Edition is 2.5× slower on this workload than the 2024 tag, on the same box in the same window. holger's MB/s lead therefore grows from 9.65× to 24.6×. Quoting that larger number would be technically true and substantively misleading, so the honest headline against Nexus remains the p999, which barely moved (105.9× → 99.8×).
-
The modern Artifactory's tail is twice as good, and that halves our best number. Artifactory 7.161.26's p999 is 30.525 ms against 6.23.13's 54.737 ms, and its p99 improved from 6.144 to 4.288 ms at the same throughput. holger's p999 lead over Artifactory consequently falls from 80.2× to 44.5×. 44.5× is the number to quote. The five-year-old pin was flattering us by roughly a factor of two on the single metric the pitch leans on hardest.
44.5× is still a very large lead and it is now defensible. The old one was not.
17.3 The stress arms — the write path, measured for the first time
holger.concurrent_upload — more publishers make it slower.
| phase | publishers | uploads/s |
| serial, before | 1 | 377.99 |
| concurrent | 32 | 121.06 |
| serial, after | 1 | 140.28 |
scaling_x= 0.467. Thirty-two clients publishing at once achieved 47 % of what a single client achieves. Not 32×, not 1× — less than one.append_cost_growth_x= 2.694. A single publisher got 2.7× slower between the first and last phase, purely because the archive had grown to 3 888 files. That is the read-modify-write append cost, isolated by the third phase.- 1 944 uploads succeeded, 0 failed; p99 1 496 ms, p999 1 581 ms; 1.89 MB/s, at 1.4 of 32 cores busy. Thirty-two clients, one and a half cores, and a p99 of a second and a half.
The cause is not mysterious and it is in the source: RustRepoZnippy::put takes one process-wide write_lock and then calls append_files, which re-seals the whole archive. Publishers serialise on the mutex, and each one's work grows with what is already stored.
holger.mixed_read_write — four publishers cost the readers 94 % of their throughput. Corpus: 5 000 crates, served over the real cargo download path.
| reads alone | reads with 4 publishers | |
| read ops/s | 12 440.1 | 742.4 |
| read p99 | 5.42 ms | 5 054.18 ms |
| read p999 | 19.89 ms | 5 054.45 ms |
read_throughput_retained_pct= 5.97 %.read_p999_inflation_x= 254.1.read_failed_mixed= 3 650. Three and a half thousand reads did not return 2xx while publishing was in progress. That is not a latency number, it is reads failing, and it outranks everything else in this section.- The publishers themselves managed only 11.26 uploads/s (45 in the window, 0 failed) — against a 5 000-artifact archive, versus 377.99/s against an empty one.
This is the single most important measurement this lane produced. holger's entire pitch is the read tail — p999 0.68 ms against Artifactory's 30.5 ms. A single CI job publishing four crates takes that same p999 to 5 054 ms.
holger.upload_edges — all three edges are clean. This is the good news, and it is measured, not assumed.
| edge | measurement | verdict |
| 32 clients trickle-reading a response one byte per 100 ms | healthy clients kept 102.4 % of baseline throughput | no head-of-line damage |
| 64 uploads that declare 8 MiB, send 64 KiB, then vanish | /healthz still answers; throughput after = 104.3 % of baseline |
no leak, no wedge |
| a store that refuses writes (the ENOSPC analogue) | 16 of 16 publishes got a clean HTTP answer; reads still served afterwards | clean refusal, archive intact |
So holger handles the pathological single client well. What it does not handle is the ordinary concurrent one.
Quiet versus contended — the shape survived. The first stress set ran while dwarves-server held ~287 % CPU. Kept at bench-results/contended/:
| metric | contended | quiet |
scaling_x |
0.594 | 0.467 |
append_cost_growth_x |
2.549 | 2.694 |
read_throughput_retained_pct |
1.09 % | 5.97 % |
read_p999_inflation_x |
283.7 | 254.1 |
read_failed_mixed |
721 | 3 650 |
every upload_edges verdict |
pass | pass |
Every conclusion holds in both. The quiet set is the one to quote.
18. WHAT NEEDS THE OWNER
Five items. None of them can be settled by measuring harder.
-
A true ENOSPC arm needs a bounded filesystem, which needs root.
holger.upload_edgescurrently measures a write-refusal analogue (permissions), and says so on the row (enospc_is_analogue = 1). A real disk-filling arm needs a small loopback or tmpfs mount that the bench may fill and destroy. That is a mount on a shared bench box, so it is an owner decision, not something a benchmark should do on its own initiative. -
Publishing the rival numbers. Nexus
3.96.2is Community Edition under a click-through EULA which the harness now accepts programmatically, and CE carries a 40 000 components / 100 000 requests per day cap. The pinned3.72.0is EPL OSS with no EULA and is the legally cleanest tag to quote. Before any of these numbers goes in front of a customer, someone who is not a benchmark has to decide what may be published about a EULA'd product. -
Gitea as a fourth rival — go or no-go.
docker.io/gitea/gitea:1.27.3is pulled and on oden, ready. It is the only light container covering cargo + maven + pypi at once, boots in seconds on SQLite withINSTALL_LOCK, and is MIT — the best value per line of harness code of any candidate. It is not implemented here: it needs its own admin bootstrap, token mint and package-upload path, which is a scenario module of its own rather than a tag parameter. Recommended, but it is new work, not a fix. -
Which arms belong in the release gate now that there are seven more. The four rival arms plus three stress arms roughly double the runtime of a full pass, and the Artifactory 7.x boot alone costs minutes. §5's ranked keep-list predates all of them.
-
The write-path defect the numbers point at is one lane's work, not this one's.
promote_ledgergoing 606 → 349 seals/s from 12 to 32 cores, and the upload serialisation measured in §17, both come from the same shape: the write path runs inline and single-writer. This lane measured it and left it alone.
19. WHERE THESE NUMBERS LIVE — and a warehouse defect found on the way
The rule is that bench_runs is the source of truth and a run.json is a receipt. This lane put its rows in the table with nornir bench run holger on oden, and doing so surfaced two defects in that path. Both are recorded here because either one silently produces a number nobody can find again.
19.1 What is in the warehouse
| run id | arms | status |
2e2c2570-b958-418e-943d-e18f23db9229 |
concurrent_upload, mixed_read_write, upload_edges, nexus_load_current, artifactory_load_current |
5 results, 5 tests — VERIFIED by reading them back with nornir bench history-show holger |
75a24cd5-855c-441d-b285-a95d9d2daaf6 |
nexus_load, artifactory_load |
2 results, 2 tests — VERIFIED by reading them back |
ae74b8e5-f207-4f61-8b41-8ab0e249dc80 |
the same two | 2 results — written before the storage fix; unreadable, see §19.3 |
87d15865-3a96-49bc-b852-4d0975797113 |
the same five as above | 5 results — written to the wrong warehouse; see §19.3. Superseded by 2e2c2570. |
All seven arms — four rivals and three stress arms — now answer to nornir bench history-show holger. The verification matters and is the whole point of §19.3: 2e2c2570 and 75a24cd5 are the only runs confirmed by reading the rows back out of the table, rather than by trusting the CLI's success line.
Every arm is also committed as a compact BenchRun receipt under bench-results/holger-r4-<arm>-oden-2026-09-20-e6238c1.json, with the thermal trace and the box stamp beside them. The receipts are the quiet-box measurements and are the ones §17 quotes.
19.2 DEFECT 1 — nornir bench run cannot reach a lane worktree
On oden, with no nornir.toml present, nornir bench run holger resolves the repo to /home/rickard/git/holger — the pre-existing checkout — whatever directory it is invoked from. A lane working on a branch in a worktree therefore cannot persist its own arms: the driver it builds is the one from the other checkout, so brand-new arms are simply not linked and quietly do not appear. It is visible only as an arm count: running 2 of 229 linked arms where the lane's own binary reports 234.
Two things about the workaround are worth knowing:
[repo.holger] path = "holger"(relative to the workspace root) works.[repo.holger] path = "/absolute/path"is ignored — resolution falls back to<workspace_root>/holger, despitenornir-config's own doc comment saying an absolute path is honoured. Passing that config via--config <file>also re-derives a different workspace root. So the worktree must physically sit at<workspace_root>/<repo>, which in turn means its sibling path dependencies (edda,znippy,znippy-zoomies,modgunn,facett, …) must be reachable from there too, orcargo metadatafails before the bench starts.
19.3 DEFECT 2 — adding a nornir.toml silently moves the warehouse
This is the more dangerous one, and it is the same shape as the "16 of 46 receipts with no warehouse row" incident.
A nornir.toml containing only [repo.holger] and no [storage] section makes nornir bench run write to ~/.nornir/warehouse/warehouse/nornir/bench_runs, while nornir bench history-show in the same directory, with the same config, keeps reading <workspace_root>/.nornir/warehouse/nornir/bench_runs.
Two different warehouses, one command family, one invocation directory. And the run still printed its success line:
✓ 5 result(s), 5 test(s) persisted into bench_runs as 87d15865-…
That line is true — the rows exist, in an Iceberg table, that nothing reads. history-show answered holger — 2 run(s) and listed two blake3/memcopy rows belonging to a different lane. A success message is not proof of persistence; only reading the row back is. The repair is to name storage explicitly:
[storage]
kind = "local"
local_path = ".nornir"
19.4 Contention, stated
The quiet-box receipts in §17 were taken at load average 2.16 with dwarves-server stopped. The warehouse re-run happened after the machine policy changed to "oden builds", with other lanes compiling on the box (load 9–22). The warehouse rows therefore carry higher contention than the receipts, and they say so themselves: every row stamps telem_cores_busy_avg, telem_cores_busy_max and telem_cpu_pct_avg, which is precisely the mechanism that makes a contended row recognisable instead of silently wrong. Quote §17 for the numbers; use the warehouse rows for series continuity.
19.5 Scaffolding that was created and removed
To make the persist run possible, the following were created on oden and then reverted: a git worktree at ~/git/workspace_nordisk/holger (the pre-existing stray directory holding PLAN-2026-09-17-two-vm.md was moved aside and restored), symlinks to the sibling repos beside it, a temporary rename of the facett file there, and ~/git/workspace_nordisk/nornir.toml. None of it is left behind. It is listed so that the next person knows what a persist run from a worktree costs today — and §18 carries the ask to make it cost nothing.