/docs/benchmarks

Benchmarks

Recording runs in the path of every tool call an agent makes, so its cost has to be known rather than assumed. This page gives the measured numbers for nd7 record and nd7 verify: how long a hook takes, whether that time grows as the log grows, what happens when many hooks run at once, how fast verification is, and where verification spends its time. All of it is from BENCHMARKS.md, measured on 2026-09-19.

What was measured, and on what#

An Apple M1 Pro (10 cores, 16 GB) running macOS Darwin 25.6.0 arm64, release build, entries of 1507 bytes.

The harness, the script that runs the measurements, is bench/record_verify.py. It builds one session per size under a private XDG_STATE_HOME by looping nd7 record, one process per entry, with a roughly 1.5 KB PreToolUse payload for Bash. It then times each target with subprocess.run wrapped in time.perf_counter_ns(), and reports the median and the 99th percentile (p99 in the lists below) of the per-iteration samples.

Two details of the method matter for reading the numbers below. The record runs are interleaved round-robin, which means every round runs one sample of each target, including one /usr/bin/true. So if the machine speeds up or slows down during the run, it does so for every target equally, and the true median is a baseline for the cost of merely starting a process, measured under the same conditions. Running each target as one block instead showed the machine drifting between the first block and the last, which is nothing to do with the code. "Net of spawn" is a target's median minus that baseline.

All numbers are for a release build. A debug build installed as a hook during development is roughly 1 ms slower each time it runs, and that difference is not measured here.

record against log size#

Interleaved, 200 samples per target.

  • /usr/bin/true: median 3.404 ms, p99 9.130 ms, net of spawn —
  • record, 1,000 entries: median 5.005 ms, p99 11.777 ms, net of spawn 1.600 ms
  • record, 10,000 entries: median 5.009 ms, p99 8.254 ms, net of spawn 1.605 ms
  • record, 50,000 entries: median 5.026 ms, p99 10.842 ms, net of spawn 1.622 ms

A fresh session was measured as a block rather than interleaved, because it cannot be interleaved with itself, and the figures include creating the session directory: 4.960 ms median, 11.144 ms p99, 1.725 ms net of spawn.

Recording costs the same however large the log is, O(1) in log size. Over a fiftyfold growth the median moves by 22 microseconds, which is inside the noise. That is the head companion file and the backwards read of the log's last line doing their job: an append reads one entry and one small file, never the whole log. About 68% of a hook's wall-clock time is the cost of starting a process, which nd7 cannot influence; the part that is nd7's own is about 1.6 ms.

record with 50 processes at once#

50 nd7 record processes started against the 10,000-entry session, then given their standard input, then waited on.

  • total wall-clock time 98.3 ms, which is 1.97 ms per process on average
  • all 50 exited 0, every standard error empty
  • entries went 10,205 → 10,255, exactly 50 more, so no append was lost or written twice
  • nd7 verify on the session afterwards: clean

The advisory lock makes the appends happen one at a time, and none of the processes fails while waiting. The average cost per process is below the single-process figure because the processes start while others are still running.

record on the error paths#

A hook that cannot parse its input must still be cheap, must not block the agent, and must say one thing about it. 200 runs each.

  • malformed (nope): median 4.555 ms, p99 12.950 ms, net of spawn 1.319 ms; exit 0; stderr exactly 1 line
  • empty standard input: median 4.719 ms, p99 14.688 ms, net of spawn 1.484 ms; exit 0; stderr exactly 1 line

Both are slightly cheaper than a successful record, which is what it should be: the parse fails before nd7 writes anything.

verify against log size#

20 runs each, median of the per-run samples.

  • 1k: 1,205 entries, 1,814,825 bytes; median 8.201 ms, p99 10.300 ms; 221.3 MB/s; 6.81 µs per entry
  • 10k: 10,205 entries, 15,388,235 bytes; median 36.003 ms, p99 37.445 ms; 427.4 MB/s; 3.53 µs per entry
  • 50k: 50,205 entries, 75,748,235 bytes; median 161.018 ms, p99 169.102 ms; 470.4 MB/s; 3.21 µs per entry

The time grows in step with the number of bytes. The low throughput at 1k is the fixed cost of starting a process spread over a small file, not a different algorithm: net of spawn, the figures are about 363 MB/s at 1k against about 480 MB/s at 50k, and they converge.

Where verify's time goes#

Measured against the 50k log, 75.7 MB:

  • cat of the file: 22.4 ms — the I/O floor
  • BLAKE3 over the whole file as one buffer: 45 ms, 1,538 MB/s — the multi-chunk SIMD path
  • BLAKE3 over 50,000 separate 1,507-byte entries, the way verify actually hashes: 98 ms, 768 MB/s, 1.96 µs per entry

So the 161 ms splits roughly into 22 ms of reading the file, 98 ms of hashing, and 40 ms of splitting lines, finding fields and comparing them. Hashing dominates. Hashing entry by entry reaches only half the throughput of hashing one big buffer, because each entry is too small for the multi-chunk path, which is the faster route BLAKE3 takes on large inputs.

That last figure came from a throwaway crate that hashed 50,000 buffers of 1.5 KB with two hasher updates each, rather than from nd7 itself.

The one change left that would help is checking entries on several cores at once, and the per-entry check is already shaped for it: it depends on nothing but one line's bytes and the previous line's claimed hash. Nobody has made that change, because 161 ms per 50,000 entries is fast enough for a command a person runs by hand.

How to reproduce#

cargo build --release
python3 bench/record_verify.py                          # 1k, 10k and 50k, 200 iterations
python3 bench/record_verify.py --sizes 1000 --iters 50  # a quick pass

The script needs nothing but Python 3 and the release binary. It works in a private XDG_STATE_HOME under a temporary directory, which it removes at exit unless you pass --keep. Building the 50,000-entry session is the slow part — about five minutes, because every entry is one process.

What is not measured here#

The sandbox. These numbers cover nd7 record and nd7 verify only. Nothing published measures what nd7 run costs a session, what routing each Bash command through nd7-exec adds, or what nd7 ship costs. The measurements that do exist around the sandbox are in ADR-0003 and ADR-0007, and they are about process spawn costs and about what the kernel accepts, not about throughput.

Updated . This page as Markdown · Source on GitHub.