Benchmarks
2026-10-05 — Static file profiling and fix
Environment as in 2026-10-05-docker-desktop-smoke.md (Docker Desktop / WSL2, Ryzen 7
7800X3D, server on CPUs 0-3 with 4 Tokio workers, oha on 6-11, 64 connections, 10 s).
Document root on a Docker volume (ext4). Smoke-test grade; ratios matter, not absolutes.
Diagnosis (scripts/profile.sh --docker static-small, before)
- 17.2k req/s; 880k context switches in 10 s (~5 per request); 67 threads.
- ~38 % of samples in the kernel waking/switching threads (
futex_wake→try_to_wake_up,finish_task_switch), reached fromStaticHandler::serve(spawn_blockingfor resolve/open) andtokio::fs::File::poll_read(one blocking-pool hop per 64 KiB read plus one more for EOF). readlinkvisible:canonicalizecosts one syscall per path component.
Fix
- Linux lookup with one
openat2(RESOLVE_BENEATH | RESOLVE_NO_MAGICLINKS)from a root fd (also closes the symlink TOCTOU window; see ADR-0006). - Hot path on the async worker only when the kernel guarantees no blocking:
RESOLVE_CACHEDfor the lookup,preadv2(RWF_NOWAIT)for the data; otherwise the whole resolution runs once on the blocking pool. - Small files (≤ 128 KiB) read in the lookup step; large files streamed with 256 KiB positional reads (inline when cached), no EOF read, no thread held between chunks.
After (same run conditions, same volume)
scenario before rps after rps speed-up before p99 after p99 nginx rps
static-small 20763 69859 3.4x 6.97 ms 2.33 ms 98259
static-1m 2244 10393 4.6x 43.72 ms 12.28 ms 24515
tls-h2-static-small 18386 53186 2.9x 7.04 ms 2.85 ms 83439
respond (unchanged) 120050 124362 - 1.68 ms 1.76 ms 258238
proxy (unchanged) 52974 50638 - 2.68 ms 3.02 ms 92927Peak RSS 36 → 13 MiB. After the fix, static-small shows 70k context switches in 10 s (was 880k) and 7 threads (was 67).
Remaining gaps (recorded in ROADMAP M1)
- 1 MiB files (0.42× nginx): profile is now dominated by data copies
(
_copy_to_iterforpread,_copy_from_iterfor the socket write) and page zeroing; nginx usessendfile. Closing it needs a zero-copy body path that bypasses hyper for plain TCP — an architectural change, to be designed via ADR. - Per-request overhead (
respond, 0.48× nginx): no dominant user-space hotspot (largest: pipeline 1.3 %, malloc 1.3 %); scb uses ~2.9 of 4 CPUs and most kernel time is loopback wake-ups of the client. Points at scheduler/park behaviour of the work-stealing runtime vs nginx’s per-core event loops — handoff §14 says to investigate worker sharding / affinity only now that the portable design is correct. - 64 KiB chunks were measured 30 % slower than 256 KiB for 1 MiB files.