# write latency

`scripts/write-latency-bench.sh <knot-static binary> (<knot config.toml> | --fixture <repos>)`

an owner `issue comment` from spawn to exit, through `writes::perform` (the cgi path minus auth),
with a firehose socket configured as on a deployed knot. budget: p95 <= 2000 ms (`BUDGET_MS`) on
the single-writer scenarios; the script exits 1 when one is over. concurrent scenarios are reported,
not gated. `RUNS` writes per scenario (default 5), `KNOT_PROFILE=1` with a marks build prints the
average breakdown.

scenarios:

- quiet: nothing else running.
- mid-render: a site hook stub that sleeps and takes 3 s to die on TERM; each write supersedes it.
- during-drain: `post-activate` (the drain timer's work) started alongside each write.
- first-after-deploy: a fresh copy of the binary, its `prepare` (the deploy unit), then the write.
- 2 / 5 concurrent: writes started together to different repos (up to 5 that take an owner issue)
  or all to one.
- huge repo (a kernel-sized knot): not run. prod hosts nothing near that size.

`BENCH_REPOS="r1 r2 ..."` picks the repos (the first takes the single-writer scenarios). before each
run here, `gc --force` cleared what earlier runs retired: every write retires a generation, and
until gc can tell nothing still reads them they wait out the drain window and pile up.

## 2026-11-10, mac (Apple M5, 10 cores, 24 GB, apfs), fc741ae release, no mbx

3-repo fixture (`--fixture 3`):

| scenario | med ms | p95 ms |
|---|---|---|
| quiet | 291 | 307 |
| mid-render | 313 | 323 |
| during-drain | 343 | 419 |
| first-after-deploy | 360 | 363 |
| 2 concurrent, different | 621 | 875 |
| 2 concurrent, same | 621 | 678 |
| 5 concurrent, same | 991 | 1238 |

92-repo clone of site-work:

| scenario | med ms | p95 ms | max ms |
|---|---|---|---|
| quiet | 1311 | 1459 | 1459 |
| mid-render | 1731 | 1797 | 1797 |
| during-drain | 1739 | 1807 | 1807 |
| first-after-deploy | 1834 | 1930 | 1930 |
| 2 concurrent, different | 4364 | 5762 | 5762 |
| 2 concurrent, same | 3772 | 3949 | 3949 |
| 5 concurrent, different | 11359 | 21062 | 21063 |
| 5 concurrent, same | 5604 | 28598 | 43230 |

concurrent writes queue on the output lock, behind each other's builds and each write's detached
gc + hook diff.

## 2026-11-10, chernobog (Ryzen 5 5600, 12 threads, btrfs), fc741ae nix build, 6G fence, nice 19

92-repo clone of site-work:

| scenario | med ms | p95 ms |
|---|---|---|
| quiet | 651 | 670 |
| mid-render | 868 | 873 |
| during-drain | 737 | 909 |
| first-after-deploy | 747 | 781 |
| 2 concurrent, different | 1715 | 2232 |
| 2 concurrent, same | 1541 | 1624 |
| 5 concurrent, different | 4515 | 4626 |
| 5 concurrent, same | 4134 | 4323 |

## a repo that took many writes, chernobog

a 5-repo fixture whose r1 took N owner comments one write at a time (`seed.sh`, not committed),
nix builds of 5f72bd9 (before) and c79f9ca (after: an export merges the packs past its framed
replies geometrically). each write used to add a pack to r1's export for good.

| | before | after |
|---|---|---|
| r1 export packs at 500 writes | 807 | 7 |
| r1 export packs at 2000 writes | 2036 | 7 |
| one comment on r1 at 500 writes | 0.77-0.83 s | 0.48-0.64 s |

the scenario bench at 2000 writes, r1 first:

| scenario | before med / p95 ms | after med / p95 ms |
|---|---|---|
| quiet | 2591 / 2760 | 1180 / 1189 |
| mid-render | 3049 / 3155 | 1190 / 1219 |
| during-drain | 3258 / 3349 | 1223 / 1462 |
| first-after-deploy | 3524 / 3665 | 1266 / 1297 |
| 2 concurrent, different | 2560 / 5599 | 1606 / 2501 |
| 2 concurrent, same | 4187 / 4299 | 2393 / 2468 |
| 5 concurrent, different | 5954 / 7647 | 4069 / 5143 |
| 5 concurrent, same | 5555 / 5684 | 3672 / 3802 |

a comment on r1 still costs more than on a fresh repo (1.2 s against 0.23 s): what else grows with a
repo's write count is the next thing to profile.

concurrent writes share builds already: of five at once, some build and the rest find their change
built by a later one. they wait out about two builds, so they speed up with the build itself.
