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

nordisk/holger

.nornir/stinky/10-benchmark-baseline.md

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

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:

  1. auth-passkey still flows from the bench crate into the shipped server. server/cli's default set is ["mini","online","bench","secure_update","front","auth-passkey"]. auth-passkey was added to that list on 2026-09-20, in 542e296 — 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 — drop bench from that set and nornir-auth-webauthn leaves 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.

  2. auth-oidc + auth-oauth2 + auth-mtls are 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.

  3. reqwest defaults 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)

  1. 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 with rcgen. 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.json carries machine, cores, date, timestamp, bench_secs and the load average at both ends of the run — the best-attributed artifact in either repo.
  2. 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.
  3. 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)

  1. holger.nexus_load and 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 in README.md and .nornir/README-full.md. Caveat that must travel with them: workspace_nordisk/.nornir/znippy-package-capabilities-2026-07-23.md argues 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.
  2. 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

  1. holger.pip_index_serve — the holger-ds Python journey; no container needed.
  2. holger.promote_ledger — real engine + real durable ledger.
  3. holger.retention_executor.
  4. rust_crate_* / java_jar_* / pip_wheel_* (6 arms) — the oldest series, and the reason the _mt32 names must never be renamed (bench_runs keys regression history by metric name). Cheap. Keep.
  5. 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

The documentation debt this lane also uncovered

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:

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 stamps telem_cores_busy_avg on 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

  1. 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. Compare secure_update_verify at 679.93 MB/s and _import at 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.
  2. 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.
  3. 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

  1. The READ paths scale. The WRITE path does not. Every multicore read arm holds or improves at 100× the corpus — rust_crate_mt32 nearly doubles. But promote_ledger gets 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.
  2. pip_index_serve is 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.

  3. pip_wheel_mt32 is 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.

  4. Note retention_planner/retention_executor/rust_crate_st ran at host_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

JFrog Artifactory — 6.23.13 (2021-02-21) → current 7.161.26 → NO. Not honest.

Action, ranked

  1. Bump Artifactory to 7.161.26 and 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).
  2. Add a SECOND Nexus arm on 3.96.2, keep 3.72.0 as the historical series. Two rows, both labelled, is honest; silently swapping the tag would orphan the bench_runs history 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/eula plus awareness of the daily request cap.
  3. Add Gitea (gitea/gitea:1.27.3). It is the only genuinely light container that covers cargo + maven + pypi, boots in seconds on SQLite with INSTALL_LOCK, and is MIT. Best value per line of harness code.
  4. 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:

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):

  1. The bench driver compiles again (10 sites, canonical constructor).
  2. Baseline captured at 542e296 on loki, quick scale, 20/20 arms, both rivals answering — committed with commit + machine + nproc + profile + feature set + dependency kind.
  3. Scale/stress run at 100× the corpus, 11 arms.
  4. Competitor pins audited against current; one stated justification falsified.
  5. Raw BenchRun envelopes committed under bench-results/ so no number in this document is unsourced.

Next, in value order:

  1. Bump Artifactory to 7.161.26 (free oden's /home first) 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.
  2. Add Gitea — cheapest competitor per line of harness code, and the only light one covering cargo + maven + pypi.
  3. Gate on cargo build -p xtask --example nornir-bench. One link, and §6 would never have happened.
  4. Delete bench_nexus_container.sh; rehome holger-ds out of the shipped CLI into a bench-scenarios example.
  5. Strip auth-oidc/auth-oauth2/auth-mtls from the bench crate's server-lib pin and default-features = false its reqwest — but name whatever the server actually wants in server/cli in the same commit, or three auth doors vanish the way the passkey door nearly did.
  6. 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_ledger and pip_index_serve rows 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.md says oden · 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:

  1. 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.
  2. Nexus 3.72.0 cannot serve Cargo at all. Native Rust/Cargo support landed in 3.73.0 (2024-10-10), one release after the pin. The 3.96.2 arm 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:

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

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-server was still using ~287 % CPU. Those rows are preserved under bench-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.

  1. 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×).

  2. 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

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

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.

  1. A true ENOSPC arm needs a bounded filesystem, which needs root. holger.upload_edges currently 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.

  2. Publishing the rival numbers. Nexus 3.96.2 is 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 pinned 3.72.0 is 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.

  3. Gitea as a fourth rival — go or no-go. docker.io/gitea/gitea:1.27.3 is pulled and on oden, ready. It is the only light container covering cargo + maven + pypi at once, boots in seconds on SQLite with INSTALL_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.

  4. 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.

  5. The write-path defect the numbers point at is one lane's work, not this one's. promote_ledger going 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:

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.