From 610cd560f43cc088e5e15bbcb84b5e6193273b64 Mon Sep 17 00:00:00 2001 From: Hitalo Souza Date: Mon, 29 Jun 2026 11:22:42 -0300 Subject: [PATCH 1/2] perf(vanilla-epoll): serve /static via static_assets (precompressed negotiation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /static handler hand-rolled a second static path: it preloaded ONLY the identity files (skipping the .br/.gz siblings) and never read Accept-Encoding, so it always shipped the uncompressed body. The static profile sends `Accept-Encoding: br;q=1, gzip;q=0.8`, so vanilla-epoll was sending ~4x the bytes of servers that serve the precompressed sibling (≈2x bandwidth, ≈half the rps). Replace it with the lib's audited static_assets module, mounted at /static/ via the new `url_prefix` (enghitalo/vanilla#80): one implementation that negotiates the .br/.gz sibling, sets ETag/Vary/Cache-Control, and streams large assets with sendfile(2) — emitted through the same core.queue_file handoff the worker already drains. Drops the duplicate StaticFile/static_header/content_type/C.open. Local A/B (server pinned to 2 cores, wrk + static-rotate.lua, br;q=1, both CPU- saturated): identity-only ~56.8K req/s @ 3.37 GB/s -> static_assets ~104K req/s @ 1.60 GB/s (+83% rps, -52% bandwidth) at equal CPU. curl-verified: /static/app.js + Accept-Encoding: br -> Content-Encoding: br, 47,275 B (vs 204,800 identity); .woff2 (no sibling) -> identity; traversal -> 404. Bumps the vanilla pin to main + #80 (url_prefix). kTLS/#79 and queue_buf/#75 ride along on main but are inert here (no -d vanilla_tls; queue_buf unused by epoll). Co-Authored-By: Claude Opus 4.8 (1M context) --- frameworks/vanilla-epoll/Dockerfile | 12 ++-- frameworks/vanilla-epoll/main.v | 85 +++++++---------------------- 2 files changed, 28 insertions(+), 69 deletions(-) diff --git a/frameworks/vanilla-epoll/Dockerfile b/frameworks/vanilla-epoll/Dockerfile index f5336dca7..4621a4db3 100644 --- a/frameworks/vanilla-epoll/Dockerfile +++ b/frameworks/vanilla-epoll/Dockerfile @@ -22,12 +22,14 @@ RUN git clone https://github.com/vlang/v /opt/v && \ # (or rate-limit) failed the whole build (MDA2AV/HttpArena#895). To pick up upstream # library fixes, bump this commit. # -# vanilla main @15bd57e picks up the allocation fixes from a lib-wide audit: -# enghitalo/vanilla#72 (pg_async: read wire ints at offset, drop per-read slices), -# #73 (static_assets: zero-copy conditional-GET/Range parsing) and #74 (TLS: pool the -# per-connection read/response buffers). +# vanilla bumped to main + enghitalo/vanilla#80 (static_assets `url_prefix`): this +# entry now serves /static/* through the audited static_assets module (precompressed +# .br/.gz negotiation per Accept-Encoding + ETag/Vary + sendfile) instead of a second, +# hand-rolled identity-only map that ignored Accept-Encoding. The bump also catches up +# to current vanilla main (#75 core.queue_buf, #79 kTLS record offload) — both inert +# in this image (built without -d vanilla_tls; queue_buf is unused by the epoll entry). RUN git clone https://github.com/enghitalo/vanilla /root/.vmodules/vanilla && \ - git -C /root/.vmodules/vanilla checkout 15bd57e5ae8cf1383bd386826e48e08a10f6d4b4 + git -C /root/.vmodules/vanilla checkout f59a534045e5731a6c03f16ea23f19cbfe2e7334 WORKDIR /app COPY . . diff --git a/frameworks/vanilla-epoll/main.v b/frameworks/vanilla-epoll/main.v index 445dcbd94..833e356f8 100644 --- a/frameworks/vanilla-epoll/main.v +++ b/frameworks/vanilla-epoll/main.v @@ -3,6 +3,7 @@ module main import vanilla.http_server import vanilla.http_server.http1_1.request_parser import vanilla.http_server.core +import vanilla.http_server.static_assets import vanilla.pg_async import json import os @@ -28,16 +29,6 @@ struct DatasetItem { rating Rating } -// A static asset served with sendfile(2): the response head is precomputed, the -// body is streamed zero-copy straight from the page-cached file fd. -struct StaticFile { - header []u8 - fd int - size i64 -} - -fn C.open(pathname &char, flags int) int - struct CrudCreate { id int name string @@ -57,7 +48,7 @@ struct Fortune { struct SharedRO { dataset []DatasetItem prefixes []string - assets map[string]StaticFile + asv static_assets.AssetServer // canonical static server (negotiation + sendfile), mounted at /static/ mut: // PROCESS-SHARED caches (mutex-guarded). They must be shared, not per-worker: // validate.sh does two GET /crud/items/42 and requires X-Cache MISS then HIT, @@ -354,17 +345,14 @@ fn handle(req_buffer []u8, mut out []u8, mut ac core.AsyncCtx) core.AsyncStep { } else if route == '/fortunes' { return w.start_fortunes(mut out, mut ac) } else if route.starts_with('/static/') { - // Zero-copy lookup key: a view into the request buffer. The map only hashes the - // key bytes and never retains it, so the view is safe. `route[8..]` would be a - // substr -> a fresh heap string every request, which `-gc none` never frees - // (+625 MiB over 20M requests, proven in isolation). Mirrors the vanilla lib's - // own static_assets module (http_server/static_assets/static_assets.v). - if f := w.ro.assets[unsafe { tos(route.str + 8, route.len - 8) }] { - wb(mut out, f.header) - core.queue_file(f.fd, 0, f.size) - } else { - wb(mut out, not_found) - } + // Canonical static serving via the lib's static_assets module: negotiates the + // precompressed .br/.gz sibling per Accept-Encoding (the arena sends + // `br;q=1, gzip;q=0.8`, so this ships the ~4x smaller .br body instead of the + // raw file), plus ETag/Vary/Cache-Control and sendfile(2) for large assets — + // ONE audited implementation instead of a second hand-rolled identity-only + // path that ignored Accept-Encoding. Mounted at /static/; emits via the same + // core.queue_file sendfile handoff the worker already drains. + w.ro.asv.respond_into(req_buffer, mut out) or { wb(mut out, not_found) } return .done } else if route == '/crud/items' { if method == 'POST' { @@ -1235,30 +1223,6 @@ fn parse_crud_body_fast(body []u8, need_id bool) ?CrudFastBody { } } -fn content_type(name string) string { - ext := name.all_after_last('.') - return match ext { - 'css' { 'text/css' } - 'js' { 'application/javascript' } - 'json' { 'application/json' } - 'html' { 'text/html' } - 'svg' { 'image/svg+xml' } - 'webp' { 'image/webp' } - 'woff2' { 'font/woff2' } - else { 'application/octet-stream' } - } -} - -fn static_header(ctype string, size i64) []u8 { - mut sb := strings.new_builder(96) - sb.write_string('HTTP/1.1 200 OK\r\nServer: vanilla\r\nContent-Type: ') - sb.write_string(ctype) - sb.write_string('\r\nContent-Length: ') - sb.write_decimal(size) - sb.write_string('\r\nConnection: keep-alive\r\n\r\n') - return sb -} - // parse_db_url turns postgres://user:pass@host:port/dbname into a pg_async.ConnConfig. fn parse_db_url(u string) pg_async.ConnConfig { mut s := u @@ -1314,29 +1278,22 @@ fn main() { prefixes << enc#[..-1] + ',"total":' } - mut assets := map[string]StaticFile{} static_dir := os.getenv_opt('STATIC_DIR') or { '/data/static' } - for name in os.ls(static_dir) or { []string{} } { - if name.ends_with('.gz') || name.ends_with('.br') { - continue - } - path := '${static_dir}/${name}' - fsize := i64(os.file_size(path)) - fd := C.open(&char(path.str), 0) - if fd < 0 { - continue - } - assets[name] = StaticFile{ - header: static_header(content_type(name), fsize) - fd: fd - size: fsize - } - } + // Canonical static server: loads every asset PLUS its .br/.gz siblings once, + // mounts them at /static/, and negotiates Accept-Encoding per request (serving + // the precompressed body when accepted). Replaces the former hand-rolled, + // identity-only map that ignored Accept-Encoding and always shipped the raw + // file. spa_fallback is off: the arena fixture set has no SPA entrypoint. + asv := static_assets.new(static_assets.Config{ + root: static_dir + url_prefix: '/static/' + spa_fallback: '' + }) or { panic('vanilla-epoll: static_assets init failed: ${err}') } ro := &SharedRO{ dataset: dataset prefixes: prefixes - assets: assets + asv: asv crud: []CrudSlot{len: crud_cache_slots} crud_mu: sync.new_rwmutex() gz: map[u64][]u8{} From ca4e32a39906072f4e8eb4bc7048dcd1741c5554 Mon Sep 17 00:00:00 2001 From: Hitalo Souza Date: Mon, 29 Jun 2026 11:41:45 -0300 Subject: [PATCH 2/2] chore(vanilla-epoll): pin vanilla main @b189036 (#80 + #81 merged) Both lib deps are now on vanilla main: enghitalo/vanilla#80 (static_assets url_prefix) and #81 (core.queue_buf borrowed send). Repin from the #80 branch commit to the merged main commit. No entry change; rebuilds clean. Co-Authored-By: Claude Opus 4.8 (1M context) --- frameworks/vanilla-epoll/Dockerfile | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frameworks/vanilla-epoll/Dockerfile b/frameworks/vanilla-epoll/Dockerfile index 4621a4db3..cfb4a57a9 100644 --- a/frameworks/vanilla-epoll/Dockerfile +++ b/frameworks/vanilla-epoll/Dockerfile @@ -22,14 +22,14 @@ RUN git clone https://github.com/vlang/v /opt/v && \ # (or rate-limit) failed the whole build (MDA2AV/HttpArena#895). To pick up upstream # library fixes, bump this commit. # -# vanilla bumped to main + enghitalo/vanilla#80 (static_assets `url_prefix`): this -# entry now serves /static/* through the audited static_assets module (precompressed -# .br/.gz negotiation per Accept-Encoding + ETag/Vary + sendfile) instead of a second, -# hand-rolled identity-only map that ignored Accept-Encoding. The bump also catches up -# to current vanilla main (#75 core.queue_buf, #79 kTLS record offload) — both inert -# in this image (built without -d vanilla_tls; queue_buf is unused by the epoll entry). +# vanilla main @b189036 (incl. enghitalo/vanilla#80 static_assets `url_prefix` and +# #81 core.queue_buf borrowed send): this entry serves /static/* through the audited +# static_assets module (precompressed .br/.gz negotiation per Accept-Encoding + +# ETag/Vary + sendfile) instead of a second, hand-rolled identity-only map that +# ignored Accept-Encoding. #81's borrowed send and #79 kTLS are inert here (built +# without -d vanilla_tls; epoll serves small bodies via the write buffer). RUN git clone https://github.com/enghitalo/vanilla /root/.vmodules/vanilla && \ - git -C /root/.vmodules/vanilla checkout f59a534045e5731a6c03f16ea23f19cbfe2e7334 + git -C /root/.vmodules/vanilla checkout b189036212e4283ef2cffe42b318b556f8a3d1bc WORKDIR /app COPY . .