Skip to content

vanilla: json-comp cache, zero-alloc routing, DB prepared statement + HTML escape#877

Open
enghitalo wants to merge 10 commits into
MDA2AV:mainfrom
enghitalo:perf/vanilla-json-comp-cache-route-slice
Open

vanilla: json-comp cache, zero-alloc routing, DB prepared statement + HTML escape#877
enghitalo wants to merge 10 commits into
MDA2AV:mainfrom
enghitalo:perf/vanilla-json-comp-cache-route-slice

Conversation

@enghitalo

@enghitalo enghitalo commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Allocation/CPU-reduction and large-I/O follow-ups to the zero-alloc handler (#866), in frameworks/vanilla-epoll/main.v (+ vanilla-io_uring where noted) — consolidated into one PR.

Allocation / GC-pressure (both backends)

  1. json-comp gzip cache — the response for a given (count, m) is deterministic and gzip CPU dominates, so the complete gzipped response is cached per (count, m) (bounded map + RwMutex) and appended on a hit. Compress once, reuse.
  2. Zero-alloc routing — route on the path before ? via a tos() view into the request buffer instead of all_before('?')'s per-request copy.
  3. Precomputed query keysqint/qstr take a precomputed const []u8 (qk_*) instead of key.bytes() per call; path ints (/json/<n>, /crud/items/<id>) parsed in place (parse_u_at) instead of route[n..] substrings.
  4. DB: prepared statement — async-db uses PQprepare/PQexecPrepared (lazily prepared per pooled connection) instead of exec_param_many's per-request server-side SQL re-parse.
  5. DB: single-pass HTML escapeescape_html (fortunes) does one pass with a no-alloc fast path instead of replace_each's five full-string passes.
  6. Hot-path byte-write armor — replace single-element out << b in the JSON paths with push_many/indexed writes (wi itoa into a stack scratch + one push_many; fused object }, separators). Single-element << is 4–7× slower on post-0.5.1 V (vlang/v#27468); this keeps the hot path fast across V versions. Output is byte-identical (verified across counts 0..4096 + edge ints).

Large I/O (epoll only; io_uring is a follow-up)

  1. /upload answered by Content-Length (streaming drain) — the engine now drains large request bodies into a fixed buffer (recv + discard) instead of buffering the whole 20 MB into a per-conn read buffer (which grew into a big scanned GC block → 64-core stop-the-world cliff). The handler answers from req.content_length() (lib change merged as epoll: stream (drain) large request bodies instead of buffering them — fix the upload cliff enghitalo/vanilla#31), falling back to body.len for small/buffered bodies (crud/json POSTs unaffected — only >1 MiB drains). Local (vs the upload leader zix): single-conn 45 req/s / 895 MB/s (zix 45 / 903); 32-parallel 303 req/s / 6.1 GB/s (zix 336 / 6.7) — within ~10%, the 86× gap is closed; server RSS 12 MB (was ~928 MB while buffering).
  2. Static assets via sendfile(2) — preload each asset's O_RDONLY fd + a precomputed response head; out << f.header then core.queue_file(f.fd, 0, f.size) streams the body zero-copy from the page cache, so the write buffer never grows into a large scanned block (the static "cliff"). Local vendor.js (307 KB, 64 conns): 25.7K → 59.3K req/s (2.3×), 7.36 → 16.97 GB/s; md5-identical, keep-alive OK.

Build

  1. Pin V 0.5.1, built from source (pinned-vc bootstrap vlang/vc@f461dfeb) — reproducible builds, and avoids the post-0.5.1 master << regression (#27468) that otherwise slows every path. Matches the-benchmarker pin (v: pin compiler to V 0.5.1 (from source); keep vanilla_io_uring but skip it in CI the-benchmarker/web-frameworks#9466).

Arena results (vanilla-epoll, 64-core, Δ vs #866's zero-alloc baseline)

Test Conn RPS Δ RPS Δ Mem
upload 32 11,109 +36,930% −89.5%
upload 256 12,660 +22,507% −85.4%
static 6800 1,168,588 +460.2% −90.1%
static 4096 1,142,779 +329.4% −89.9%
static 1024 1,075,725 +238.1% −87.1%
pipelined 512 35,233,232 +1,380.6% −9.4%
pipelined 4096 34,680,659 +1,406.7% −3.2%
json-comp 4096 844,718 +1,380.1% −0.5%
json-comp 16384 878,355 +1,142.3% −7.5%
json-comp 512 496,559 +914.7% −33.3%
json 4096 891,795 +84.0% +2.3%
baseline 512 650,591 +39.7% −4.2%
baseline 4096 621,002 +44.7% −2.6%
crud 4096 155,943 +30.4% +8.1%
fortunes 1024 3,894 +46.7% −57.6%
limited-conn 4096 370,133 +22.0% −2.2%
api-16 1024 21,254 +7.6% −1.1%
async-db 1024 11,094 +2.1% −4.0%
api-4 256 33,407 +1.7% −4.2%

Highlights: upload now leads the profile — the streaming drain (#7) took it from ~30 to 11,109 req/s, past the prior leader (~4.3K), at −89% memory. static +460% at −90% memory (sendfile(2), #8 — the buffer-growth cliff is gone). json-comp leads (+1,380%, gzip-response cache). pipelined +1,407% (zero-alloc routing + <<-armor). The DB-bound profiles (api-4/16, async-db) are flat by design — #877 doesn't touch the DB engine; closing those is the async DB driver's job (below).

Lib dependencies (all merged to enghitalo/vanilla main)

The DB-bound profiles stay flat because they're driver-bound (db.pg's synchronous text protocol; veb on the same stack is slower, so vanilla already maxes it). Closing that gap is the async, epoll-integrated DB driver: the async runtime (ac.watch(fd, …) + continuations) is now merged to enghitalo/vanilla main (#34#36#37, Linux epoll + macOS kqueue), and a measured PoC showed ~6× per-thread on the DB path. The remaining piece — a thin db_async consumer on that runtime — is tracked in enghitalo/vanilla#32. Correctness verified for all paths here.

Supersedes #878 (folded in here).

🤖 Generated with Claude Code

json-comp recompressed the gzip body on EVERY request even though the output
for a given (count, m) is fully deterministic — and gzip CPU, not allocation,
dominates that profile. Cache the COMPLETE gzipped response per (count, m) and
append the cached copy on a hit (bounded map, RwMutex). The benchmark hits only
a handful of (count, m) pairs, so the cache stays tiny.

Also route on the path WITHOUT allocating: a tos() view into the request buffer
instead of all_before('?')'s per-request string copy (one alloc per request on
the hot path), shaving GC churn off baseline/json too.

Local before/after (16-core loopback, gcannon, single listener):
  json-comp  58K -> 390K req/s  (+570%, 6.7x)

Correctness verified: gzip body decodes to the right items/count/total; the
cached response is byte-identical across requests; all other routes unchanged.
Applies to both the epoll and io_uring variants.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@enghitalo

Copy link
Copy Markdown
Contributor Author

/benchmark -f vanilla-epoll --save

@enghitalo

Copy link
Copy Markdown
Contributor Author

/benchmark -f vanilla-io_uring --save

@github-actions

Copy link
Copy Markdown
Contributor

👋 /benchmark request received. A collaborator will review and approve the run.

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

👋 /benchmark request received. A collaborator will review and approve the run.

enghitalo and others added 2 commits June 15, 2026 09:31
Remove the remaining small per-request allocations on the hot path:
  • qint/qstr took a `string` key and called `key.bytes()` every request (one
    []u8 alloc per parameter — baseline parses a+b, async-db min+max+limit…).
    Keys are now precomputed `const []u8` (qk_*), built once at init.
  • /json/<n> and /crud/items/<id> parsed the id via route[n..].i64(), a
    substring copy. parse_u_at() reads the digits straight from the path view.

Local before/after (16-core loopback) is within noise (baseline ~528K→530K,
json ~206K→212K) — these allocs are tiny next to the response builder MDA2AV#866
removed — but allocation scaled hard on the 64-core arena (json +322% there),
so this trims more GC churn for that environment at zero cost. Note: @[manualfree]
is a no-op under the GC build the arena uses (`v -prod` = Boehm GC; manualfree
only affects -autofree), so reducing allocations is the lever, not manualfree.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: vanilla-epoll | Test: all tests

Test Conn RPS CPU Mem Δ RPS Δ Mem
baseline 512 691,385 1430.3% 73MiB +48.5% +1.4%
baseline 4096 641,367 1422.2% 192MiB +49.4% -0.5%
pipelined 512 34,861,446 6533.4% 50MiB +1364.9% -5.7%
pipelined 4096 34,907,987 6679.4% 177MiB +1416.6% -5.3%
limited-conn 512 246,993 800.9% 61MiB +13.9% -1.6%
limited-conn 4096 351,525 1094.5% 170MiB +15.8% -5.0%
json 4096 829,499 3030.2% 184MiB +71.1% +4.0%
json-comp 512 626,441 1841.6% 69MiB +1180.1% -20.7%
json-comp 4096 817,306 2363.9% 196MiB +1332.1% +5.9%
json-comp 16384 857,291 2255.6% 640MiB +1112.5% -9.1%
upload 32 50 155.1% 851MiB +66.7% +0.8%
upload 256 64 157.8% 1.6GiB +14.3% +14.3%
api-4 256 31,193 343.4% 72MiB -5.1% +1.4%
api-16 1024 21,370 412.9% 96MiB +8.2% +3.2%
static 1024 324,639 6517.5% 735MiB +2.0% -1.2%
static 4096 269,324 6311.9% 2.8GiB +1.2% ~0%
static 6800 234,662 5927.6% 4.5GiB +12.5% ~0%
async-db 1024 10,385 350.4% 100MiB -4.4% ~0%
crud 4096 151,120 1095.6% 385MiB +26.4% +0.8%
fortunes 1024 2,538 462.8% 144MiB -4.4% ~0%
Full log
[run 1/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  200
  Templates: 20
  Expected:  200
  Duration:  15s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   28.87ms   15.90ms   68.50ms   209.50ms   349.90ms

  2123944 requests in 15.00s, 2123946 responses
  Throughput: 141.57K req/s
  Bandwidth:  44.34MB/s
  Status codes: 2xx=2123946, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 2123946 / 2123946 responses (100.0%)
  Reconnects: 8648
  Per-template: 102356,107229,106639,107585,108745,109607,108012,108333,110420,107164,108962,110297,112849,110516,108837,106943,102005,92179,93952,101316
  Per-template-ok: 102356,107229,106639,107585,108745,109607,108012,108333,110420,107164,108962,110297,112849,110516,108837,106943,102005,92179,93952,101316
[info] CPU 1074.1% | Mem 380MiB

[run 2/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  200
  Templates: 20
  Expected:  200
  Duration:  15s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   28.50ms   15.60ms   68.10ms   206.30ms   341.90ms

  2150552 requests in 15.00s, 2150552 responses
  Throughput: 143.35K req/s
  Bandwidth:  45.05MB/s
  Status codes: 2xx=2150552, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 2150547 / 2150552 responses (100.0%)
  Reconnects: 8792
  Per-template: 104423,106441,107804,108430,107574,108834,110222,110767,110824,109433,112401,112525,110592,108865,108727,109744,105989,96021,98867,102064
  Per-template-ok: 104423,106441,107804,108430,107574,108834,110222,110767,110824,109433,112401,112525,110592,108865,108727,109744,105989,96021,98867,102064
[info] CPU 1150.6% | Mem 384MiB

[run 3/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  200
  Templates: 20
  Expected:  200
  Duration:  15s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   27.02ms   12.80ms   68.50ms   216.00ms   359.00ms

  2267127 requests in 15.00s, 2266807 responses
  Throughput: 151.09K req/s
  Bandwidth:  47.56MB/s
  Status codes: 2xx=2266807, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 2266807 / 2266807 responses (100.0%)
  Reconnects: 9379
  Per-template: 109722,113876,113817,115498,118556,118661,119059,118259,117391,117556,118142,117400,115900,114695,114129,112793,111259,100633,97843,101618
  Per-template-ok: 109722,113876,113817,115498,118556,118661,119059,118259,117391,117556,118142,117400,115900,114695,114129,112793,111259,100633,97843,101618
[info] CPU 1095.6% | Mem 385MiB

=== Best: 151120 req/s (CPU: 1095.6%, Mem: 385MiB) ===
[info] input BW: 12.97MB/s (avg template: 90 bytes)
[info] saved results/crud/4096/vanilla-epoll.json
httparena-bench-vanilla-epoll
httparena-bench-vanilla-epoll

==============================================
=== vanilla-epoll / fortunes / 1024c (tool=gcannon) ===
==============================================
[info] resetting postgres for a clean per-profile baseline
[info] starting postgres sidecar
httparena-postgres
[info] postgres ready (seeded)
[info] waiting for server...
[info] server ready

[run 1/3]
gcannon v0.5.3
  Target:    localhost:8080/fortunes
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   391.62ms   326.00ms   802.60ms    1.26s    1.92s

  12391 requests in 5.00s, 12391 responses
  Throughput: 2.48K req/s
  Bandwidth:  58.74MB/s
  Status codes: 2xx=12391, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 12391 / 12391 responses (100.0%)
[info] CPU 425.4% | Mem 144MiB

[run 2/3]
gcannon v0.5.3
  Target:    localhost:8080/fortunes
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   382.01ms   316.90ms   769.60ms    1.31s    1.72s

  12694 requests in 5.00s, 12694 responses
  Throughput: 2.54K req/s
  Bandwidth:  60.17MB/s
  Status codes: 2xx=12694, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 12694 / 12694 responses (100.0%)
[info] CPU 462.8% | Mem 144MiB

[run 3/3]
gcannon v0.5.3
  Target:    localhost:8080/fortunes
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   382.68ms   327.30ms   768.80ms    1.22s    1.67s

  12674 requests in 5.00s, 12674 responses
  Throughput: 2.53K req/s
  Bandwidth:  60.08MB/s
  Status codes: 2xx=12674, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 12674 / 12674 responses (100.0%)
[info] CPU 467.2% | Mem 144MiB

=== Best: 2538 req/s (CPU: 462.8%, Mem: 144MiB) ===
[info] saved results/fortunes/1024/vanilla-epoll.json
httparena-bench-vanilla-epoll
httparena-bench-vanilla-epoll
[info] skip: vanilla-epoll does not subscribe to baseline-h2
[info] skip: vanilla-epoll does not subscribe to static-h2
[info] skip: vanilla-epoll does not subscribe to baseline-h2c
[info] skip: vanilla-epoll does not subscribe to json-h2c
[info] skip: vanilla-epoll does not subscribe to baseline-h3
[info] skip: vanilla-epoll does not subscribe to static-h3
[info] skip: vanilla-epoll does not subscribe to gateway-64
[info] skip: vanilla-epoll does not subscribe to gateway-h3
[info] skip: vanilla-epoll does not subscribe to production-stack
[info] skip: vanilla-epoll does not subscribe to unary-grpc
[info] skip: vanilla-epoll does not subscribe to unary-grpc-tls
[info] skip: vanilla-epoll does not subscribe to stream-grpc
[info] skip: vanilla-epoll does not subscribe to stream-grpc-tls
[info] skip: vanilla-epoll does not subscribe to echo-ws
[info] skip: vanilla-epoll does not subscribe to echo-ws-pipeline
[info] rebuilding site/data/*.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/frameworks.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/api-16-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/api-4-256.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/async-db-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/baseline-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/baseline-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/crud-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/fortunes-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-comp-16384.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-comp-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-comp-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/limited-conn-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/limited-conn-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/pipelined-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/pipelined-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/static-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/static-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/static-6800.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/upload-256.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/upload-32.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/current.json
[info] done
httparena-postgres
httparena-redis
[info] restoring loopback MTU to 65536

…scape

Folds the DB-path work into this PR so everything lands together:
  • async-db uses a PostgreSQL prepared statement (PQprepare/PQexecPrepared via
    db.pg, lazily prepared per pooled connection) instead of exec_param_many's
    per-request server-side SQL re-parse — local +9%.
  • escape_html (fortunes) does ONE pass with a no-alloc fast path instead of
    replace_each's five full-string passes — local +27% fortunes.

DB profiles remain bound by the stdlib db.pg driver (text protocol), so this
narrows the gap without closing it. Both backends.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@enghitalo enghitalo changed the title vanilla: cache json-comp gzip responses + zero-alloc routing vanilla: json-comp cache, zero-alloc routing, DB prepared statement + HTML escape Jun 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

👋 /benchmark request received. A collaborator will review and approve the run.

1 similar comment
@github-actions

Copy link
Copy Markdown
Contributor

👋 /benchmark request received. A collaborator will review and approve the run.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: vanilla-io_uring | Test: all tests

Test Conn RPS CPU Mem Δ RPS Δ Mem
baseline 512 647,072 1463.5% 1.1GiB NEW NEW
baseline 4096 703,870 1407.3% 1.1GiB NEW NEW
pipelined 512 36,003,929 6638.8% 975MiB NEW NEW
pipelined 4096 37,347,977 6334.6% 1.1GiB NEW NEW
limited-conn 512 347,829 1141.5% 1.2GiB NEW NEW
limited-conn 4096 420,182 1080.9% 1.3GiB NEW NEW
json 4096 955,599 2797.5% 1.3GiB NEW NEW
json-comp 512 880,396 2582.2% 1.2GiB NEW NEW
json-comp 4096 990,800 2357.9% 1.3GiB NEW NEW
json-comp 16384 919,244 1977.0% 1.6GiB NEW NEW
upload 32 42 125.0% 2.5GiB NEW NEW
upload 256 53 137.3% 3.0GiB NEW NEW
api-4 256 35,036 326.9% 1.2GiB NEW NEW
api-16 1024 22,284 558.7% 1.2GiB NEW NEW
static 1024 242,200 5663.9% 1.5GiB NEW NEW
static 4096 15,272 312.6% 2.3GiB NEW NEW
static 6800 11,767 221.4% 2.3GiB NEW NEW
async-db 1024 10,316 515.4% 1.3GiB NEW NEW
crud 4096 140,552 1053.9% 1.4GiB NEW NEW
fortunes 1024 3,005 546.2% 1.3GiB NEW NEW
Full log
[run 1/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  200
  Templates: 20
  Expected:  200
  Duration:  15s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   39.29ms   18.50ms   85.40ms   256.20ms    2.34s

  1558109 requests in 15.00s, 1556445 responses
  Throughput: 103.75K req/s
  Bandwidth:  32.51MB/s
  Status codes: 2xx=1556445, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 1556445 / 1556445 responses (100.0%)
  Reconnects: 5757
  Per-template: 75016,77481,79475,82505,78489,77556,79644,80415,82010,83121,80384,77613,78401,78382,78640,78521,74355,68226,71653,74558
  Per-template-ok: 75016,77481,79475,82505,78489,77556,79644,80415,82010,83121,80384,77613,78401,78382,78640,78521,74355,68226,71653,74558
[info] CPU 868.1% | Mem 1.3GiB

[run 2/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  200
  Templates: 20
  Expected:  200
  Duration:  15s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   29.71ms   13.60ms   78.10ms   242.90ms   378.50ms

  2061953 requests in 15.00s, 2061761 responses
  Throughput: 137.43K req/s
  Bandwidth:  43.07MB/s
  Status codes: 2xx=2061761, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 2061760 / 2061761 responses (100.0%)
  Reconnects: 8421
  Per-template: 100554,99694,104048,106050,107381,105455,104318,104801,102312,103708,103770,106445,108125,108873,106389,107476,100108,87328,93850,101075
  Per-template-ok: 100554,99694,104048,106050,107381,105455,104318,104801,102312,103708,103770,106445,108125,108873,106389,107476,100108,87328,93850,101075
[info] CPU 1094.4% | Mem 1.4GiB

[run 3/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  200
  Templates: 20
  Expected:  200
  Duration:  15s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   29.06ms   12.40ms   78.00ms   239.80ms   384.00ms

  2108291 requests in 15.00s, 2108291 responses
  Throughput: 140.53K req/s
  Bandwidth:  44.01MB/s
  Status codes: 2xx=2108291, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 2108290 / 2108291 responses (100.0%)
  Reconnects: 8578
  Per-template: 102864,104902,109917,112326,107508,107899,106518,108780,109341,106407,107645,105126,105749,106288,106395,108554,102387,92251,96817,100616
  Per-template-ok: 102864,104902,109917,112326,107508,107899,106518,108780,109341,106407,107645,105126,105749,106288,106395,108554,102387,92251,96817,100616
[info] CPU 1053.9% | Mem 1.4GiB

=== Best: 140552 req/s (CPU: 1053.9%, Mem: 1.4GiB) ===
[info] input BW: 12.06MB/s (avg template: 90 bytes)
[info] saved results/crud/4096/vanilla-io_uring.json
httparena-bench-vanilla-io_uring
httparena-bench-vanilla-io_uring

==============================================
=== vanilla-io_uring / fortunes / 1024c (tool=gcannon) ===
==============================================
[info] resetting postgres for a clean per-profile baseline
[info] starting postgres sidecar
httparena-postgres
[info] postgres ready (seeded)
[info] waiting for server...
[info] server ready

[run 1/3]
gcannon v0.5.3
  Target:    localhost:8080/fortunes
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   353.80ms   318.10ms   692.60ms    1.13s    1.37s

  13804 requests in 5.00s, 13804 responses
  Throughput: 2.76K req/s
  Bandwidth:  65.44MB/s
  Status codes: 2xx=13804, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 13804 / 13804 responses (100.0%)
[info] CPU 484.8% | Mem 1.2GiB

[run 2/3]
gcannon v0.5.3
  Target:    localhost:8080/fortunes
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   334.28ms   287.40ms   658.20ms    1.07s    1.42s

  14677 requests in 5.00s, 14677 responses
  Throughput: 2.93K req/s
  Bandwidth:  69.57MB/s
  Status codes: 2xx=14677, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 14677 / 14677 responses (100.0%)
[info] CPU 521.4% | Mem 1.3GiB

[run 3/3]
gcannon v0.5.3
  Target:    localhost:8080/fortunes
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   328.41ms   270.80ms   675.90ms    1.07s    1.27s

  15027 requests in 5.00s, 15027 responses
  Throughput: 3.00K req/s
  Bandwidth:  71.23MB/s
  Status codes: 2xx=15027, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 15027 / 15027 responses (100.0%)
[info] CPU 546.2% | Mem 1.3GiB

=== Best: 3005 req/s (CPU: 546.2%, Mem: 1.3GiB) ===
[info] saved results/fortunes/1024/vanilla-io_uring.json
httparena-bench-vanilla-io_uring
httparena-bench-vanilla-io_uring
[info] skip: vanilla-io_uring does not subscribe to baseline-h2
[info] skip: vanilla-io_uring does not subscribe to static-h2
[info] skip: vanilla-io_uring does not subscribe to baseline-h2c
[info] skip: vanilla-io_uring does not subscribe to json-h2c
[info] skip: vanilla-io_uring does not subscribe to baseline-h3
[info] skip: vanilla-io_uring does not subscribe to static-h3
[info] skip: vanilla-io_uring does not subscribe to gateway-64
[info] skip: vanilla-io_uring does not subscribe to gateway-h3
[info] skip: vanilla-io_uring does not subscribe to production-stack
[info] skip: vanilla-io_uring does not subscribe to unary-grpc
[info] skip: vanilla-io_uring does not subscribe to unary-grpc-tls
[info] skip: vanilla-io_uring does not subscribe to stream-grpc
[info] skip: vanilla-io_uring does not subscribe to stream-grpc-tls
[info] skip: vanilla-io_uring does not subscribe to echo-ws
[info] skip: vanilla-io_uring does not subscribe to echo-ws-pipeline
[info] rebuilding site/data/*.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/frameworks.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/api-16-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/api-4-256.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/async-db-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/baseline-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/baseline-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/crud-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/fortunes-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-comp-16384.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-comp-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-comp-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/limited-conn-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/limited-conn-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/pipelined-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/pipelined-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/static-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/static-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/static-6800.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/upload-256.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/upload-32.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/current.json
[info] done
httparena-postgres
httparena-redis
[info] restoring loopback MTU to 65536

@enghitalo

Copy link
Copy Markdown
Contributor Author

/benchmark -f vanilla-epoll

/benchmark -f vanilla-io_uring

@github-actions

Copy link
Copy Markdown
Contributor

👋 /benchmark request received. A collaborator will review and approve the run.

@MDA2AV

MDA2AV commented Jun 15, 2026

Copy link
Copy Markdown
Owner

/benchmark -f vanilla-epoll

@github-actions

Copy link
Copy Markdown
Contributor

👋 /benchmark request received. A collaborator will review and approve the run.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: vanilla-epoll | Test: all tests

Test Conn RPS CPU Mem Δ RPS Δ Mem
baseline 512 530,288 1255.3% 131MiB +13.9% +81.9%
baseline 4096 501,273 1195.8% 383MiB +16.8% +98.4%
pipelined 512 31,997,856 6546.6% 73MiB +1244.6% +37.7%
pipelined 4096 30,134,134 5101.2% 225MiB +1209.2% +20.3%
limited-conn 512 297,116 1096.1% 1018MiB +37.0% +1541.9%
limited-conn 4096 378,353 1190.9% 1.1GiB +24.7% +529.3%
json 4096 473,509 1693.8% 445MiB -2.3% +151.4%
json-comp 512 416,046 1610.9% 314MiB +750.2% +260.9%
json-comp 4096 431,079 1457.1% 337MiB +655.3% +82.2%
json-comp 16384 422,031 1394.8% 654MiB +496.9% -7.1%
upload 32 2,102 2586.8% 2.8GiB +6906.7% +239.7%
upload 256 2,307 5935.3% 8.8GiB +4019.6% +528.6%
api-4 256 27,404 390.0% 221MiB -16.6% +211.3%
api-16 1024 11,516 608.4% 430MiB -41.7% +362.4%
static 1024 322,967 6353.0% 732MiB +1.5% -1.6%
static 4096 268,919 6173.7% 2.7GiB +1.0% -3.6%
static 6800 251,503 5480.1% 4.5GiB +20.6% ~0%
async-db 1024 4,826 541.1% 419MiB -55.6% +319.0%
crud 4096 64,307 653.9% 361MiB -46.2% -5.5%
fortunes 1024 314 262.7% 282MiB -88.2% +95.8%
Full log
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  200
  Templates: 20
  Expected:  200
  Duration:  15s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   78.83ms   69.30ms   113.60ms   361.70ms   780.80ms

  777365 requests in 15.00s, 776981 responses
  Throughput: 51.79K req/s
  Bandwidth:  16.06MB/s
  Status codes: 2xx=776981, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 776981 / 776981 responses (100.0%)
  Reconnects: 1283
  Per-template: 37704,38194,39300,39000,39601,39907,39300,39665,39229,39873,39475,38877,39621,39894,39906,39859,35968,37149,37583,36876
  Per-template-ok: 37704,38194,39300,39000,39601,39907,39300,39665,39229,39873,39475,38877,39621,39894,39906,39859,35968,37149,37583,36876
[info] CPU 568.9% | Mem 344MiB

[run 2/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  200
  Templates: 20
  Expected:  200
  Duration:  15s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   66.08ms   63.20ms   102.30ms   188.20ms   300.30ms

  927306 requests in 15.00s, 925834 responses
  Throughput: 61.71K req/s
  Bandwidth:  19.31MB/s
  Status codes: 2xx=925834, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 925834 / 925834 responses (100.0%)
  Reconnects: 2535
  Per-template: 44709,44002,45686,47366,47146,46872,47200,47135,46967,47472,47350,47567,48705,47772,48881,48131,44425,41402,43611,43435
  Per-template-ok: 44709,44002,45686,47366,47146,46872,47200,47135,46967,47472,47350,47567,48705,47772,48881,48131,44425,41402,43611,43435
[info] CPU 650.5% | Mem 356MiB

[run 3/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  200
  Templates: 20
  Expected:  200
  Duration:  15s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   63.54ms   59.60ms   105.80ms   194.40ms   292.60ms

  964616 requests in 15.00s, 964616 responses
  Throughput: 64.30K req/s
  Bandwidth:  20.13MB/s
  Status codes: 2xx=964616, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 964616 / 964616 responses (100.0%)
  Reconnects: 2795
  Per-template: 44455,47154,49462,49447,49770,50402,50333,50011,49363,51307,49661,50349,50575,49533,50045,49939,45843,40484,42545,43938
  Per-template-ok: 44455,47154,49462,49447,49770,50402,50333,50011,49363,51307,49661,50349,50575,49533,50045,49939,45843,40484,42545,43938
[info] CPU 653.9% | Mem 361MiB

=== Best: 64307 req/s (CPU: 653.9%, Mem: 361MiB) ===
[info] input BW: 5.52MB/s (avg template: 90 bytes)
[info] saved results/crud/4096/vanilla-epoll.json
httparena-bench-vanilla-epoll
httparena-bench-vanilla-epoll

==============================================
=== vanilla-epoll / fortunes / 1024c (tool=gcannon) ===
==============================================
[info] resetting postgres for a clean per-profile baseline
[info] starting postgres sidecar
httparena-postgres
[info] postgres ready (seeded)
[info] waiting for server...
[info] server ready

[run 1/3]
gcannon v0.5.3
  Target:    localhost:8080/fortunes
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency    2.21s    2.34s    3.47s    4.36s    4.87s

  1574 requests in 5.00s, 1574 responses
  Throughput: 314 req/s
  Bandwidth:  7.46MB/s
  Status codes: 2xx=1574, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 1574 / 1574 responses (100.0%)
[info] CPU 262.7% | Mem 282MiB

[run 2/3]
gcannon v0.5.3
  Target:    localhost:8080/fortunes
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency    2.66s    2.68s    4.09s    4.90s    5.00s

  1242 requests in 5.00s, 1242 responses
  Throughput: 248 req/s
  Bandwidth:  5.89MB/s
  Status codes: 2xx=1242, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 1242 / 1242 responses (100.0%)
  Latency overflow (>5s): 2
[info] CPU 258.5% | Mem 322MiB

[run 3/3]
gcannon v0.5.3
  Target:    localhost:8080/fortunes
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency    2.41s    2.40s    3.63s    4.50s    5.00s

  1476 requests in 5.00s, 1476 responses
  Throughput: 295 req/s
  Bandwidth:  7.00MB/s
  Status codes: 2xx=1476, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 1476 / 1476 responses (100.0%)
  Latency overflow (>5s): 2
[info] CPU 288.6% | Mem 532MiB

=== Best: 314 req/s (CPU: 262.7%, Mem: 282MiB) ===
[info] saved results/fortunes/1024/vanilla-epoll.json
httparena-bench-vanilla-epoll
httparena-bench-vanilla-epoll
[info] skip: vanilla-epoll does not subscribe to baseline-h2
[info] skip: vanilla-epoll does not subscribe to static-h2
[info] skip: vanilla-epoll does not subscribe to baseline-h2c
[info] skip: vanilla-epoll does not subscribe to json-h2c
[info] skip: vanilla-epoll does not subscribe to baseline-h3
[info] skip: vanilla-epoll does not subscribe to static-h3
[info] skip: vanilla-epoll does not subscribe to gateway-64
[info] skip: vanilla-epoll does not subscribe to gateway-h3
[info] skip: vanilla-epoll does not subscribe to production-stack
[info] skip: vanilla-epoll does not subscribe to unary-grpc
[info] skip: vanilla-epoll does not subscribe to unary-grpc-tls
[info] skip: vanilla-epoll does not subscribe to stream-grpc
[info] skip: vanilla-epoll does not subscribe to stream-grpc-tls
[info] skip: vanilla-epoll does not subscribe to echo-ws
[info] skip: vanilla-epoll does not subscribe to echo-ws-pipeline
[info] rebuilding site/data/*.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/frameworks.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/api-16-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/api-4-256.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/async-db-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/baseline-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/baseline-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/crud-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/fortunes-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-comp-16384.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-comp-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-comp-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/limited-conn-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/limited-conn-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/pipelined-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/pipelined-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/static-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/static-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/static-6800.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/upload-256.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/upload-32.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/current.json
[info] done
httparena-postgres
httparena-redis
[info] restoring loopback MTU to 65536

@enghitalo enghitalo marked this pull request as draft June 15, 2026 18:17
Single-element array push (`arr << x`) is 4-7x slower on post-0.5.1 V
(vlang/v#27468) while bulk push_many, allocation and indexed writes are
unaffected. The two hot single-element `<<` sites are now bulk writes:

  - wi() built integer digits with `out << tmp[i]` per digit; it now itoa's
    back-to-front into the [20]u8 scratch and flushes with one push_many.
  - write_json_response() pushed the item separator `,` and closing `}`
    one byte at a time; the closing `}` is now fused with the separator
    into a single '},' / '}' push_many.

Output is byte-identical (verified across counts 0..4096 and edge-value
integers). This makes the JSON hot path fast on both the 0.5.1 release and
current master, independent of the upstream codegen regression. Both
epoll and io_uring backends.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@enghitalo enghitalo force-pushed the perf/vanilla-json-comp-cache-route-slice branch from 1b5fcc2 to 44fa661 Compare June 15, 2026 18:36
@enghitalo

Copy link
Copy Markdown
Contributor Author

/benchmark -f vanilla-epoll

@github-actions

Copy link
Copy Markdown
Contributor

👋 /benchmark request received. A collaborator will review and approve the run.

Build V from source at the 0.5.1 tag instead of the prebuilt release zip.
Plain `make` can't build an old tag: its latest_vc step `git pull`s the
newest vlang/vc bootstrap, which no longer matches 0.5.1's vlib (fails with
`unknown ident \`native\``). So pin vc to the commit cut for 0.5.1
(vlang/vc f461dfeb = "[v:master] 0c3183c - V 0.5.1") and run make's own
bootstrap recipe (cc -> v1 -> v2 -> v). Drop curl/unzip from the build deps.

Pinned by tag, not a master commit, because post-0.5.1 master carries a
codegen regression (single-element array push 4-7x slower, vlang/v#27468).
Both backends; verified the source-built compiler serves /json and /pipeline
correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: vanilla-epoll | Test: all tests

Test Conn RPS CPU Mem Δ RPS Δ Mem
baseline 512 659,864 1334.0% 69MiB +41.7% -4.2%
baseline 4096 624,890 1375.0% 194MiB +45.6% +0.5%
pipelined 512 35,168,531 6604.6% 51MiB +1377.8% -3.8%
pipelined 4096 34,964,400 6533.7% 178MiB +1419.0% -4.8%
limited-conn 512 250,691 821.5% 62MiB +15.6% ~0%
limited-conn 4096 347,316 1072.5% 172MiB +14.5% -3.9%
json 4096 881,803 3042.3% 185MiB +81.9% +4.5%
json-comp 512 626,271 1820.0% 79MiB +1179.8% -9.2%
json-comp 4096 824,402 2458.3% 196MiB +1344.5% +5.9%
json-comp 16384 894,877 2477.9% 647MiB +1165.6% -8.1%
upload 32 46 144.5% 847MiB +53.3% +0.4%
upload 256 71 155.2% 998MiB +26.8% -30.4%
api-4 256 31,195 343.6% 73MiB -5.0% +2.8%
api-16 1024 21,007 400.4% 97MiB +6.4% +4.3%
static 1024 322,818 6500.4% 737MiB +1.5% -0.9%
static 4096 268,029 6373.7% 2.8GiB +0.7% ~0%
static 6800 244,673 6050.3% 4.6GiB +17.3% +2.2%
async-db 1024 10,947 354.5% 101MiB +0.7% +1.0%
crud 4096 146,526 1156.2% 427MiB +22.5% +11.8%
fortunes 1024 3,077 490.6% 137MiB +15.9% -4.9%
Full log
[run 1/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  200
  Templates: 20
  Expected:  200
  Duration:  15s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   29.02ms   16.10ms   69.00ms   209.30ms   350.40ms

  2113916 requests in 15.00s, 2113892 responses
  Throughput: 140.90K req/s
  Bandwidth:  44.37MB/s
  Status codes: 2xx=2113892, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 2113892 / 2113892 responses (100.0%)
  Reconnects: 8567
  Per-template: 101405,106858,107829,106214,107816,109879,110692,111997,111260,111341,107354,105563,107516,108128,106042,106372,102942,93048,92750,98886
  Per-template-ok: 101405,106858,107829,106214,107816,109879,110692,111997,111260,111341,107354,105563,107516,108128,106042,106372,102942,93048,92750,98886
[info] CPU 1061.9% | Mem 421MiB

[run 2/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  200
  Templates: 20
  Expected:  200
  Duration:  15s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   27.86ms   15.20ms   66.80ms   200.30ms   335.80ms

  2198066 requests in 15.00s, 2197894 responses
  Throughput: 146.50K req/s
  Bandwidth:  45.95MB/s
  Status codes: 2xx=2197894, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 2197893 / 2197894 responses (100.0%)
  Reconnects: 9115
  Per-template: 106249,108287,113891,111366,111265,112215,113129,112007,111663,111816,114793,114691,112342,112572,115490,111680,106850,95790,99510,102287
  Per-template-ok: 106249,108287,113891,111366,111265,112215,113129,112007,111663,111816,114793,114691,112342,112572,115490,111680,106850,95790,99510,102287
[info] CPU 1156.2% | Mem 427MiB

[run 3/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  200
  Templates: 20
  Expected:  200
  Duration:  15s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   28.57ms   15.80ms   68.30ms   206.90ms   348.60ms

  2144855 requests in 15.00s, 2144664 responses
  Throughput: 142.96K req/s
  Bandwidth:  45.05MB/s
  Status codes: 2xx=2144664, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 2144663 / 2144664 responses (100.0%)
  Reconnects: 8840
  Per-template: 102391,105424,108681,109748,111774,111964,110179,110482,111350,110055,107495,107869,110809,106407,107743,110571,106809,95788,97946,101178
  Per-template-ok: 102391,105424,108681,109748,111774,111964,110179,110482,111350,110055,107495,107869,110809,106407,107743,110571,106809,95788,97946,101178
[info] CPU 1170.1% | Mem 433MiB

=== Best: 146526 req/s (CPU: 1156.2%, Mem: 427MiB) ===
[info] input BW: 12.58MB/s (avg template: 90 bytes)
[info] saved results/crud/4096/vanilla-epoll.json
httparena-bench-vanilla-epoll
httparena-bench-vanilla-epoll

==============================================
=== vanilla-epoll / fortunes / 1024c (tool=gcannon) ===
==============================================
[info] resetting postgres for a clean per-profile baseline
[info] starting postgres sidecar
httparena-postgres
[info] postgres ready (seeded)
[info] waiting for server...
[info] server ready

[run 1/3]
gcannon v0.5.3
  Target:    localhost:8080/fortunes
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   321.68ms   258.40ms   676.30ms    1.18s    1.69s

  15094 requests in 5.00s, 15094 responses
  Throughput: 3.02K req/s
  Bandwidth:  71.55MB/s
  Status codes: 2xx=15094, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 15094 / 15094 responses (100.0%)
[info] CPU 460.3% | Mem 135MiB

[run 2/3]
gcannon v0.5.3
  Target:    localhost:8080/fortunes
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   316.30ms   246.30ms   677.50ms    1.27s    1.77s

  15367 requests in 5.00s, 15367 responses
  Throughput: 3.07K req/s
  Bandwidth:  72.84MB/s
  Status codes: 2xx=15367, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 15367 / 15367 responses (100.0%)
[info] CPU 487.5% | Mem 137MiB

[run 3/3]
gcannon v0.5.3
  Target:    localhost:8080/fortunes
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   316.52ms   255.30ms   676.60ms    1.14s    1.51s

  15389 requests in 5.00s, 15389 responses
  Throughput: 3.08K req/s
  Bandwidth:  72.95MB/s
  Status codes: 2xx=15389, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 15389 / 15389 responses (100.0%)
[info] CPU 490.6% | Mem 137MiB

=== Best: 3077 req/s (CPU: 490.6%, Mem: 137MiB) ===
[info] saved results/fortunes/1024/vanilla-epoll.json
httparena-bench-vanilla-epoll
httparena-bench-vanilla-epoll
[info] skip: vanilla-epoll does not subscribe to baseline-h2
[info] skip: vanilla-epoll does not subscribe to static-h2
[info] skip: vanilla-epoll does not subscribe to baseline-h2c
[info] skip: vanilla-epoll does not subscribe to json-h2c
[info] skip: vanilla-epoll does not subscribe to baseline-h3
[info] skip: vanilla-epoll does not subscribe to static-h3
[info] skip: vanilla-epoll does not subscribe to gateway-64
[info] skip: vanilla-epoll does not subscribe to gateway-h3
[info] skip: vanilla-epoll does not subscribe to production-stack
[info] skip: vanilla-epoll does not subscribe to unary-grpc
[info] skip: vanilla-epoll does not subscribe to unary-grpc-tls
[info] skip: vanilla-epoll does not subscribe to stream-grpc
[info] skip: vanilla-epoll does not subscribe to stream-grpc-tls
[info] skip: vanilla-epoll does not subscribe to echo-ws
[info] skip: vanilla-epoll does not subscribe to echo-ws-pipeline
[info] rebuilding site/data/*.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/frameworks.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/api-16-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/api-4-256.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/async-db-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/baseline-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/baseline-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/crud-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/fortunes-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-comp-16384.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-comp-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-comp-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/limited-conn-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/limited-conn-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/pipelined-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/pipelined-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/static-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/static-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/static-6800.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/upload-256.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/upload-32.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/current.json
[info] done
httparena-postgres
httparena-redis
[info] restoring loopback MTU to 65536

@enghitalo enghitalo marked this pull request as ready for review June 15, 2026 20:20
The static handler copied each asset's full prebuilt response (up to ~300 KB)
into the per-connection write_buf every request — a userspace copy plus a large
*scanned* write_buf that grows the GC's stop-the-world cost at high conn counts
(why vanilla sat ~4x behind nginx/swerver on the static profile).

Preload each asset's fd once (O_RDONLY, page-cached, borrowed for the server's
life) and a precomputed response head; serve the head into write_buf and stream
the body zero-copy via core.queue_file (sendfile(2), already wired through the
epoll backend's deferred-send + EPOLLOUT path). write_buf no longer grows, the
body is never copied, and the kernel pushes file pages straight to the socket —
the same model nginx and swerver use.

Local (vendor.js 307 KB, 64c, wrk): 25.7K -> 59.3K req/s, 7.36 -> 16.97 GB/s
(2.3x). Output verified byte-identical (md5) incl. keep-alive. epoll only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@enghitalo enghitalo marked this pull request as draft June 16, 2026 01:33
…bodies)

The lib now streams (drains) request bodies larger than 1 MiB instead of
buffering them, so for a large upload req.body is empty — but the byte count the
upload profile wants is the declared Content-Length. Answer by
req.content_length() (falls back to the buffered body length when absent, which
also covers small bodies that still take the buffered path).

Depends on enghitalo/vanilla#31 (adds HttpRequest.content_length() + the engine
drain); the Dockerfile clones lib main, so that PR must merge before this builds.
Local (source-built V 0.5.1): upload single-conn 45 req/s / 907 MB/s, 32c 303
req/s / 6.1 GB/s — matching the top upload servers; RSS 14 MB (was ~1 GB
buffering). epoll only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@enghitalo enghitalo force-pushed the perf/vanilla-json-comp-cache-route-slice branch from 4647fd0 to 8c9f992 Compare June 16, 2026 05:59
…nt_length() (drain MDA2AV#31 merged); the prior run cloned vanilla before it landed
@enghitalo enghitalo marked this pull request as ready for review June 16, 2026 13:45
@enghitalo

Copy link
Copy Markdown
Contributor Author

/benchmark -f vanilla-epoll

@github-actions

Copy link
Copy Markdown
Contributor

👋 /benchmark request received. A collaborator will review and approve the run.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark Results

Framework: vanilla-epoll | Test: all tests

Test Conn RPS CPU Mem Δ RPS Δ Mem
baseline 512 650,591 1369.6% 69MiB +39.7% -4.2%
baseline 4096 621,002 1409.3% 188MiB +44.7% -2.6%
pipelined 512 35,233,232 6608.3% 48MiB +1380.6% -9.4%
pipelined 4096 34,680,659 6491.2% 181MiB +1406.7% -3.2%
limited-conn 512 204,299 668.1% 57MiB -5.8% -8.1%
limited-conn 4096 370,133 1126.4% 175MiB +22.0% -2.2%
json 4096 891,795 3053.9% 181MiB +84.0% +2.3%
json-comp 512 496,559 1397.4% 58MiB +914.7% -33.3%
json-comp 4096 844,718 2513.6% 184MiB +1380.1% -0.5%
json-comp 16384 878,355 2326.9% 651MiB +1142.3% -7.5%
upload 32 11,109 436.8% 89MiB +36930.0% -89.5%
upload 256 12,660 770.6% 210MiB +22507.1% -85.4%
api-4 256 33,407 346.1% 68MiB +1.7% -4.2%
api-16 1024 21,254 396.2% 92MiB +7.6% -1.1%
static 1024 1,075,725 5682.4% 96MiB +238.1% -87.1%
static 4096 1,142,779 5897.8% 290MiB +329.4% -89.9%
static 6800 1,168,588 5870.7% 456MiB +460.2% -90.1%
async-db 1024 11,094 355.8% 96MiB +2.1% -4.0%
crud 4096 155,943 1179.7% 413MiB +30.4% +8.1%
fortunes 1024 3,894 212.4% 61MiB +46.7% -57.6%
Full log
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  200
  Templates: 20
  Expected:  200
  Duration:  15s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   29.10ms   18.10ms   64.00ms   195.70ms   339.20ms

  2107829 requests in 15.00s, 2105909 responses
  Throughput: 140.37K req/s
  Bandwidth:  44.25MB/s
  Status codes: 2xx=2105909, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 2105909 / 2105909 responses (100.0%)
  Reconnects: 8535
  Per-template: 99782,105492,107162,107481,108502,109692,107950,108028,106327,106094,104934,107852,108680,106482,109334,110445,103379,94112,95875,98306
  Per-template-ok: 99782,105492,107162,107481,108502,109692,107950,108028,106327,106094,104934,107852,108680,106482,109334,110445,103379,94112,95875,98306
[info] CPU 1169.0% | Mem 386MiB

[run 2/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  200
  Templates: 20
  Expected:  200
  Duration:  15s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   27.03ms   15.40ms   62.00ms   194.70ms   331.50ms

  2265069 requests in 15.00s, 2265069 responses
  Throughput: 150.98K req/s
  Bandwidth:  47.09MB/s
  Status codes: 2xx=2265069, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 2265067 / 2265069 responses (100.0%)
  Reconnects: 9452
  Per-template: 108559,109792,114621,115292,115827,115937,115953,115360,116415,119653,117655,115657,115075,118048,114369,114251,108093,103410,104952,106148
  Per-template-ok: 108559,109792,114621,115292,115827,115937,115953,115360,116415,119653,117655,115657,115075,118048,114369,114251,108093,103410,104952,106148
[info] CPU 1265.9% | Mem 405MiB

[run 3/3]
gcannon v0.5.3
  Target:    localhost:8080/
  Threads:   64
  Conns:     4096 (64/thread)
  Pipeline:  1
  Req/conn:  200
  Templates: 20
  Expected:  200
  Duration:  15s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   26.18ms   12.60ms   64.30ms   214.10ms   364.60ms

  2339148 requests in 15.00s, 2339149 responses
  Throughput: 155.92K req/s
  Bandwidth:  48.85MB/s
  Status codes: 2xx=2339149, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 2339148 / 2339149 responses (100.0%)
  Reconnects: 9754
  Per-template: 113000,115298,118042,118429,120931,124076,121704,119053,115508,115734,119984,118902,118868,119639,121713,121463,113885,104935,108303,109681
  Per-template-ok: 113000,115298,118042,118429,120931,124076,121704,119053,115508,115734,119984,118902,118868,119639,121713,121463,113885,104935,108303,109681
[info] CPU 1179.7% | Mem 413MiB

=== Best: 155943 req/s (CPU: 1179.7%, Mem: 413MiB) ===
[info] input BW: 13.38MB/s (avg template: 90 bytes)
[info] saved results/crud/4096/vanilla-epoll.json
httparena-bench-vanilla-epoll
httparena-bench-vanilla-epoll

==============================================
=== vanilla-epoll / fortunes / 1024c (tool=gcannon) ===
==============================================
[info] resetting postgres for a clean per-profile baseline
[info] starting postgres sidecar
httparena-postgres
[info] postgres ready (seeded)
[info] waiting for server...
[info] server ready

[run 1/3]
gcannon v0.5.3
  Target:    localhost:8080/fortunes
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   5.71ms   5.96ms   8.36ms   13.10ms   32.10ms

  19473 requests in 5.00s, 19473 responses
  Throughput: 3.89K req/s
  Bandwidth:  92.31MB/s
  Status codes: 2xx=19473, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 19473 / 19473 responses (100.0%)
[info] CPU 212.4% | Mem 61MiB

[run 2/3]
gcannon v0.5.3
  Target:    localhost:8080/fortunes
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   6.21ms   6.86ms   8.42ms   10.30ms   17.10ms

  19406 requests in 5.00s, 19406 responses
  Throughput: 3.88K req/s
  Bandwidth:  91.99MB/s
  Status codes: 2xx=19406, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 19405 / 19406 responses (100.0%)
[info] CPU 210.7% | Mem 72MiB

[run 3/3]
gcannon v0.5.3
  Target:    localhost:8080/fortunes
  Threads:   64
  Conns:     1024 (16/thread)
  Pipeline:  1
  Req/conn:  unlimited (keep-alive)
  Expected:  200
  Duration:  5s


  Thread Stats   Avg      p50      p90      p99    p99.9
    Latency   252.26ms   8.71ms   218.90ms    4.49s    4.90s

  18755 requests in 5.00s, 18754 responses
  Throughput: 3.75K req/s
  Bandwidth:  88.90MB/s
  Status codes: 2xx=18754, 3xx=0, 4xx=0, 5xx=0
  Latency samples: 18754 / 18754 responses (100.0%)
  Latency overflow (>5s): 2
[info] CPU 326.9% | Mem 128MiB

=== Best: 3894 req/s (CPU: 212.4%, Mem: 61MiB) ===
[info] saved results/fortunes/1024/vanilla-epoll.json
httparena-bench-vanilla-epoll
httparena-bench-vanilla-epoll
[info] skip: vanilla-epoll does not subscribe to baseline-h2
[info] skip: vanilla-epoll does not subscribe to static-h2
[info] skip: vanilla-epoll does not subscribe to baseline-h2c
[info] skip: vanilla-epoll does not subscribe to json-h2c
[info] skip: vanilla-epoll does not subscribe to baseline-h3
[info] skip: vanilla-epoll does not subscribe to static-h3
[info] skip: vanilla-epoll does not subscribe to gateway-64
[info] skip: vanilla-epoll does not subscribe to gateway-h3
[info] skip: vanilla-epoll does not subscribe to production-stack
[info] skip: vanilla-epoll does not subscribe to unary-grpc
[info] skip: vanilla-epoll does not subscribe to unary-grpc-tls
[info] skip: vanilla-epoll does not subscribe to stream-grpc
[info] skip: vanilla-epoll does not subscribe to stream-grpc-tls
[info] skip: vanilla-epoll does not subscribe to echo-ws
[info] skip: vanilla-epoll does not subscribe to echo-ws-pipeline
[info] rebuilding site/data/*.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/frameworks.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/api-16-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/api-4-256.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/async-db-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/baseline-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/baseline-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/crud-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/fortunes-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-comp-16384.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-comp-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/json-comp-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/limited-conn-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/limited-conn-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/pipelined-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/pipelined-512.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/static-1024.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/static-4096.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/static-6800.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/upload-256.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/upload-32.json
[updated] /home/diogo/actions-runner/_work/HttpArena/HttpArena/site/data/current.json
[info] done
httparena-postgres
httparena-redis
[info] restoring loopback MTU to 65536

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants