Skip to content

Commit 5e6b913

Browse files
enghitaloclaude
andcommitted
vanilla-epoll: serve static assets with sendfile(2) (zero-copy)
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>
1 parent 0f41222 commit 5e6b913

1 file changed

Lines changed: 30 additions & 10 deletions

File tree

  • frameworks/vanilla-epoll

frameworks/vanilla-epoll/main.v

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ module main
22

33
import vanilla.http_server
44
import vanilla.http_server.http1_1.request_parser
5+
import vanilla.http_server.core
56
import db.pg
67
import json
78
import os
@@ -47,11 +48,17 @@ struct Fortune {
4748
message string
4849
}
4950

50-
// A static asset preloaded into memory with its full HTTP response.
51+
// A static asset served with sendfile(2): the response head is precomputed, the
52+
// body is streamed zero-copy straight from the page-cached file fd (no per-request
53+
// copy into the write buffer, so write_buf never grows into a big scanned block).
5154
struct StaticFile {
52-
response []u8
55+
header []u8 // precomputed status line + headers (Content-Length = size)
56+
fd int // borrowed O_RDONLY fd, shared across conns (sendfile uses an explicit offset)
57+
size i64
5358
}
5459

60+
fn C.open(pathname &char, flags int) int
61+
5562
struct Shared {
5663
mut:
5764
pool pg.ConnectionPool
@@ -158,7 +165,9 @@ fn handle(req_buffer []u8, _fd int, mut out []u8, mut sh Shared) ! {
158165
write_resp(mut out, 'text/html; charset=utf-8', sh.fortunes())
159166
} else if route.starts_with('/static/') {
160167
if f := sh.assets[route[8..]] {
161-
out << f.response
168+
// Head into write_buf, body via sendfile(2) — zero userspace copy.
169+
out << f.header
170+
core.queue_file(f.fd, 0, f.size)
162171
} else {
163172
out << not_found
164173
}
@@ -714,9 +723,18 @@ fn main() {
714723
if name.ends_with('.gz') || name.ends_with('.br') {
715724
continue
716725
}
717-
bytes := os.read_bytes('${static_dir}/${name}') or { continue }
726+
path := '${static_dir}/${name}'
727+
fsize := i64(os.file_size(path))
728+
// Open once, keep the fd for the server's life (borrowed; sendfile reads
729+
// from the page cache with an explicit offset, so the fd is shared safely).
730+
fd := C.open(&char(path.str), 0) // O_RDONLY
731+
if fd < 0 {
732+
continue
733+
}
718734
assets[name] = StaticFile{
719-
response: static_response(content_type(name), bytes)
735+
header: static_header(content_type(name), fsize)
736+
fd: fd
737+
size: fsize
720738
}
721739
}
722740

@@ -744,14 +762,16 @@ fn main() {
744762
server.run()
745763
}
746764

747-
// static_response prebuilds the full HTTP response for a static file.
748-
fn static_response(ctype string, body []u8) []u8 {
749-
mut sb := strings.new_builder(body.len + 96)
765+
// static_header prebuilds the HTTP response head (status line + headers) for a
766+
// static file. The body is streamed separately, zero-copy, with sendfile(2) via
767+
// core.queue_file — so the body never gets copied into the per-connection write
768+
// buffer (which would grow into a large scanned GC block, the static "cliff").
769+
fn static_header(ctype string, size i64) []u8 {
770+
mut sb := strings.new_builder(96)
750771
sb.write_string('HTTP/1.1 200 OK\r\nServer: vanilla\r\nContent-Type: ')
751772
sb.write_string(ctype)
752773
sb.write_string('\r\nContent-Length: ')
753-
sb.write_decimal(i64(body.len))
774+
sb.write_decimal(size)
754775
sb.write_string('\r\nConnection: keep-alive\r\n\r\n')
755-
unsafe { sb.write_ptr(body.data, body.len) }
756776
return sb
757777
}

0 commit comments

Comments
 (0)