10 KiB
Milestone 12: performance measurement pass + aarch64 test execution
Goal: close the two gaps the post-Phase-10 completeness sweep found — PLAN §18 has no measurement infrastructure (deferred at specs/milestone-7.md:113 and never delivered), and aarch64 tests are cross-built but never executed (PLAN.md:145 promised qemu).
Rulings (binding)
-
Bench harness =
tools/bench.zig+zig build bench. Not in src/ — src/ is the shipped product; tests.zig aggregates everything shippable and the bench must be a separate compilation anyway (one-file-one-module rule; matcher.zig and dns_cache.zig already belong to the test compilation). Module wiring per the fuzz pattern (build.zig:68-92): modules rooted at src/filter/matcher.zig and src/cache/dns_cache.zig — their relative import closures come along; no sqlite/mbedTLS linking needed.if (b.args) |args| run.addArgs(args). -
What it measures (PLAN §18 targets):
filter:matcher.normalize+Snapshot.evaluateper op against a Snapshot built from a generated, sortedd{d:0>7}.example.comlist body (~1M lines, in-memory; DomainSet.max_count 4M and compiler.max_domains 2M leave headroom) plus a small wild body. Mixed case ratios: hit, miss, parent-walk. Target p95 < 1 ms.cache:buildKey+DnsCache.get+packet.setId(the handler's hit path; TTL aging happens inside get) on a ~10k-entry cache prefilled with the fuzz corpus response. Mixed hit/miss. Target p95 < 5 ms.compile:compiler.compileover a generated 1M-line hosts body — wall time, informational (no §18 target; no datapoint exists today).- Memory:
/proc/self/statusVmRSS (nothing in-repo wraps it; read it directly) plus the in-repo accounting (Snapshot.memoryBytes,DnsCache.memoryBytes). Target < 100 MB RSS with ~1M domains loaded. - Timing:
std.Io.Clock.awake.now(io)/durationTo— std.time.Timer does not exist at 0.16. Percentiles: collect per-op nanos in a preallocated []u64, sort (std.mem.sort), report p50/p95/p99/max. No percentile helper exists; write it in the tool.
-
Assertion policy. Default run is informational (prints a table).
--assertexits non-zero when a §18 target is exceeded — for the Pi 5 run, NOT for CI: required CI stays deterministic (AGENTS.md) and perf assertions on shared runners are flake generators. CI does not run the bench at all this milestone. -
Bench flags:
--domains=N(default 1_000_000),--iters=N(default 200_000),--seed=N(default fixed),--assert, subcommandsfilter|cache|compile|all(default all). Debug-mode runs print a one-line warning recommending-Doptimize=ReleaseFast. -
docs/performance.md: the §18 targets table; measured numbers from THIS x86_64 host (dated, hardware named honestly, marked as not-the-target-platform); the in-memory accounting numbers; the one-command Pi 5 recipe (
zig build bench -Doptimize=ReleaseFast -- --assert); a sentence on why CI does not gate on perf. The Pi 5 rows stay "to be measured on hardware". -
aarch64 execution = second test artifact,
zig build test-aarch64 -fqemu. Per stdlib evidence: Run steps try qemu only when enable_qemu (the-fqemuCLI flag; a *Build field, no per-step override) and execqemu-aarch64bare from PATH — so CI installsqemu-user(NOT qemu-user-static, which ships the-staticname Zig will not find; binfmt is unnecessary). Static musl means no sysroot/--libc-runtimes. build.zig: resolve aarch64-linux-musl, addTest with the same wiring as the native tests artifact (sqlite/mbedtls helpers are already target-parameterized), set.linkage = .staticon the artifact (TestOptions has no linkage field),run.skip_foreign_checks = true, leave failing_to_execute_foreign_is_an_error true so a missing qemu is loud. Fuzz artifacts excluded. -
qemu scope: plain suite only, blocking. No
-Dintegrationunder qemu: the integration tests are multithreaded loopback TLS with wall-clock budgets, and qemu-user's 5-20x slowdown makes them a flake source — required CI stays deterministic. The plain suite (the aggregator's tests without the 12 fuzz-artifact tests: 1159 pass + 113 integration-gated skips; all pure DNS/filter/cache logic included) is the portable-correctness signal aarch64 needs. Recorded here as the engineering call closing PLAN.md:145's promise. -
CI additions: one
test-aarch64job (setup-zig, apt qemu-user,zig build test-aarch64 -fqemu); plus the missing §18 size assert — the cross job gains a second build WITHOUT-Dweb-dist(placeholder dist) and asserts the stripped copies < 10 MiB per arch (the < 15 MiB with-assets assert already exists). Job graph otherwise untouched. -
No src/ changes. matcher/dns_cache/compiler public APIs are used as-is; if the bench needs something they do not expose, report — do not extend them.
Sessions
V1 then V2 (both edit build.zig — sequenced, not parallel).
Session V1: bench harness + performance doc
Owns tools/bench.zig, build.zig (the bench step only), docs/performance.md. Rulings 1-5. Runs the bench on this host (ReleaseFast) and writes the measured numbers into the doc.
V1 As built
Delivered tools/bench.zig, the build.zig bench step, and docs/performance.md. One
deviation from ruling 1's wiring: Zig 0.16 rejects separate modules rooted at
matcher.zig and dns_cache.zig ("file exists in multiple modules" — their closures
share model.zig/types.zig; verified with a minimal repro). Instead a b.addWriteFiles
stage copies src/ plus a generated 7-line aggregator root (bench_core.zig) into one
core module — the same staging trick the web assets use; a build.zig comment
records why. The fuzz-corpus response is a byte-for-byte copy with provenance
comment (importing corpus.zig recreates the module conflict; the corpus documents
copies as house style). /proc/self/status reads use readerStreaming (procfs stats
size 0, readFileAlloc returns empty). Measured on the build host (i7-14700K,
ReleaseFast, defaults: 1M domains, 200k iters): filter p95 0.18 µs (target < 1 ms),
cache p95 0.14 µs (target < 5 ms), VmRSS 31.8 MiB with the 1M-domain snapshot
(target < 100 MiB), Snapshot.memoryBytes 28.0 MiB, compile 96 ms ≈ 10.4M lines/s
(informational). --assert exit paths verified both ways (a 4M-domain run exceeds
the 1M-scoped RSS target and exits 1; that was an exit-path exercise, not a target
miss). No src/ changes; matcher/dns_cache/compiler APIs sufficed. Cache suite fixes
entries at 10k (the handler-default Config.size); --domains shapes filter and
compile only. Both suites 0 failed after the build.zig edit.
Session V2: aarch64 test execution + CI (after V1)
Owns build.zig (the test-aarch64 artifact/step only), .gitea/workflows/ci.yml
(the new job + the cross-job size-assert addition). Rulings 6-8. Runs
zig build test-aarch64 -fqemu locally if qemu-aarch64 is installable/present;
reports honestly if not.
V2 As built
build.zig gained the test-aarch64 block after the bench block: aarch64-linux-musl
addTest with wiring identical to the native tests artifact, .linkage = .static on
the artifact, run.skip_foreign_checks = true, failing-to-execute left loud. Stdlib
mechanics re-verified (Run.zig:71/78/226, Build.zig:73, system.zig:111 — bare
qemu-aarch64 from PATH). ci.yml gained the test-aarch64 job (apt qemu-user,
zig build test-aarch64 -fqemu) and the cross job builds a second no-dist pair to
--prefix zig-out/nodist (existing with-assets steps byte-identical) with a
< 10 MiB stripped assert; the new assert skips the static-linkage recheck (proven
on the same-config with-assets build). Local verification: real qemu execution —
1159 pass / 113 skip / 0 failed (native count minus the 12 excluded fuzz tests);
no-dist stripped sizes 5,880,336 (x86_64) and 5,212,640 (aarch64) bytes. No
-Dintegration under qemu per ruling 7.
Review (Codex, as built)
Two rounds on one thread; round 2 returned "No findings."
Round 1 (2 important, 2 minor): bench.zig's explicit body.deinit left the earlier errdefer armed on an undefined list (double-free on any later error) → clearAndFree keeps the list valid. Ubuntu's qemu-user recommends qemu-user-binfmt, which apt installs by default — binfmt registration would bypass the -fqemu path CI intends to exercise → --no-install-recommends. performance.md's headroom claim overstated the memory margin (3x, not orders of magnitude) → corrected. Ruling 7 quoted the native aggregate test count instead of the aarch64 artifact's own (1159 + 113 skips, fuzz excluded) → corrected.
Final gates: plain 1171/1284 passed, 113 skipped (integration-gated), 0 failed; integration 1280/1284, 4 skipped (live-network by design), 0 failed; qemu aarch64 run 1159 passed, 113 skipped, 0 failed; cross ReleaseSafe with dist 18/18; no-dist stripped sizes 5.9/5.2 MB under the 10 MiB assert.
Module layout (new)
tools/bench.zig, docs/performance.md.
File ownership
V1 tools/bench.zig + docs/performance.md + build.zig(bench); V2 build.zig(aarch64) + ci.yml. Orchestrator: spec sync, review, commit.
Acceptance (milestone complete)
zig build bench -Doptimize=ReleaseFastruns all three suites and prints p50/p95/p99/max + memory;--assertenforces the §18 targets.- docs/performance.md holds dated x86_64 numbers + the Pi 5 recipe.
zig build test-aarch64 -fqemupasses locally under qemu (or its unavailability is recorded with the exact CI-equivalent command).- CI: test-aarch64 job green-by-construction (same command CI runs); cross job asserts < 10 MiB stripped without assets, < 15 MiB with (existing).
- Both existing suites 0 failed; no src/ changes.
Anti-requirements
- No perf assertions in CI; no bench in the test step.
- No qemu integration suite; no binfmt setup; no qemu-user-static.
- No new pub API on matcher/dns_cache/compiler; no synthetic-load DNS server benchmark (the ≥100 qps target is end-to-end on the Pi — the operator recipe covers it via the real binary, not a harness).