knot-static startup bench (2026-11-08) #
base: b6c2803. rt: base + "answer cgi and ssh commands without a multi-thread runtime".
harness: bench/startup (spawn -> first stdout byte, spawn -> wait4, rusage, max threads by a 100us sampler of /proc/pid/task or proc_pidinfo).
fixture: scripts/startup-bench.sh (writes enabled, 40 accounts / ~80 keys in ssh-keys.json, meta repo via a partial init).
cases: cgi_405 = fcgiwrap-style GET to /xrpc/com.atproto.repo.createRecord (config load, 405 in writable());
inflate_405 = KNOT_STATIC_INFLATE GET (405 in inflate::repo); keys_list = keys list --command ... (all lines);
git_ssh_refuse = git-ssh --operator with SSH_ORIGINAL_COMMAND upload-pack of an unknown did (index rebuild, ERR pkt-line).
outputs of base and rt are byte-identical for every case.
linux, chernobog (x86_64, 12 cpus, load ~2.2), times in ms, 3 interleaved rounds x 100 per bin (n=300) after warmup #
/run/current-system/sw/bin/true floor: 0.821 ms exit median
| case | bin | first-byte med | p95 | mean | exit med | exit p95 | cpu (u+s) med | max threads | minflt |
|---|---|---|---|---|---|---|---|---|---|
| cgi_405 | base | 2.204 | 2.623 | 2.216 | 2.426 | 3.044 | 3.515 | 13 | 499 |
| cgi_405 | rt | 1.704 | 1.881 | 1.702 | 2.103 | 2.324 | 2.019 | 1 | 435 |
| inflate_405 | base | 2.147 | 2.418 | 2.145 | 2.361 | 2.673 | 3.462 | 13 | 496 |
| inflate_405 | rt | 1.716 | 2.282 | 1.790 | 2.111 | 2.760 | 2.034 | 1 | 433 |
| keys_list | base | 2.452 | 3.198 | 2.482 | 2.662 | 3.507 | 3.761 | 13 | 553 |
| keys_list | rt | 1.878 | 2.069 | 1.869 | 2.276 | 2.497 | 2.198 | 1 | 464 |
| git_ssh_refuse | base | 3.332 | 3.717 | 3.299 | 3.558 | 4.037 | 4.639 | 13 | 616 |
| git_ssh_refuse | rt | 2.811 | 3.211 | 2.817 | 3.318 | 3.794 | 3.223 | 1 | 550 |
clap reject (--version, exit 2) |
base | - | - | - | 2.164 | 2.427 | 2.862 | 13 | 491 |
clap reject (--version, exit 2) |
rt | - | - | - | 1.793 | 1.976 | 1.721 | 1 | 400 |
syscalls per run (strace -f -c, 3 runs):
| case | base syscalls | rt syscalls |
|---|---|---|
| cgi_405 | 541-550 (12 clone3) | 128 (0 clone3) |
| inflate_405 | 545-553 | 126 |
| keys_list | 551-557 | 119 (no futex, no epoll) |
| git_ssh_refuse | 802-814 | 386 |
tracing ablation (also skip init_tracing on cgi + keys list unless RUST_LOG), linux n=300 interleaved: cgi_405 1.700 -> 1.656 ms med (-45 us), keys_list 1.891 -> 1.873 (-20 us). not applied: cgi emits tracing::error!/warn! (writes.rs "stored changes weren't published", spindle, firehose) that print_error's eprintln doesn't cover.
mac (caladon, arm64, 10 cpus), quiet run, n=300 interleaved #
| case | bin | first-byte med | p95 | mean | exit med | exit p95 | cpu (u+s) med | max threads | minflt |
|---|---|---|---|---|---|---|---|---|---|
| cgi_405 | base | 3.212 | 3.465 | 3.232 | 3.554 | 3.854 | 3.290 | 11 | 712 |
| cgi_405 | rt | 3.111 | 3.342 | 3.126 | 3.381 | 3.636 | 2.830 | 1 | 675 |
| inflate_405 | base | 3.207 | 3.452 | 3.230 | 3.547 | 3.827 | 3.288 | 11 | 709 |
| inflate_405 | rt | 3.095 | 3.349 | 3.114 | 3.370 | 3.631 | 2.812 | 1 | 672 |
| keys_list | base | 3.505 | 3.906 | 3.552 | 3.885 | 4.302 | 3.588 | 11 | 797 |
| keys_list | rt | 3.285 | 3.500 | 3.294 | 3.563 | 3.796 | 2.998 | 1 | 725 |
| git_ssh_refuse | base | 4.098 | 4.317 | 4.105 | 4.467 | 4.726 | 4.185 | 11 | 912 |
| git_ssh_refuse | rt | 4.011 | 4.267 | 4.020 | 4.317 | 4.591 | 3.736 | 1 | 874 |
| (a later mac rerun under load 17-22 from another session's tests was noise-dominated, ~11 ms everything; discarded.) | |||||||||
| on the mac ~2.5 ms of the 3.1 ms is process/dyld startup (CoreFoundation/Security/SystemConfiguration frameworks), so the runtime change shows mostly as cpu and threads there. |
what's left on linux #
rt cgi ~1.7 ms vs true 0.82 ms. strace -ttt puts the biggest remaining chunk in ld.so relocating the 32 MB PIE (the 741 KB RELRO mprotect comes after the longest gap), not in our code. a next lever would be link-level, not main().