diff --git a/frameworks/vanilla/.gitignore b/frameworks/vanilla-epoll/.gitignore similarity index 100% rename from frameworks/vanilla/.gitignore rename to frameworks/vanilla-epoll/.gitignore diff --git a/frameworks/vanilla/Dockerfile b/frameworks/vanilla-epoll/Dockerfile similarity index 100% rename from frameworks/vanilla/Dockerfile rename to frameworks/vanilla-epoll/Dockerfile diff --git a/frameworks/vanilla/README.md b/frameworks/vanilla-epoll/README.md similarity index 89% rename from frameworks/vanilla/README.md rename to frameworks/vanilla-epoll/README.md index f29fe29a8..ab51e1788 100644 --- a/frameworks/vanilla/README.md +++ b/frameworks/vanilla-epoll/README.md @@ -1,8 +1,10 @@ -# vanilla +# vanilla-epoll [vanilla](https://github.com/enghitalo/vanilla) is a minimalist, high-performance -HTTP server written in [V](https://vlang.io) — multi-threaded, non-blocking -epoll I/O, lock-free, copy-free, with `SO_REUSEPORT`. +HTTP server written in [V](https://vlang.io) — multi-threaded, non-blocking, +lock-free, copy-free, with `SO_REUSEPORT`. This entry runs the **epoll** I/O +backend (`io_multiplexing: .epoll`); see `vanilla-io_uring` for the io_uring +backend. ## Implemented profiles diff --git a/frameworks/vanilla/main.v b/frameworks/vanilla-epoll/main.v similarity index 83% rename from frameworks/vanilla/main.v rename to frameworks/vanilla-epoll/main.v index 826ae5f71..1f8e8e0ed 100644 --- a/frameworks/vanilla/main.v +++ b/frameworks/vanilla-epoll/main.v @@ -70,22 +70,65 @@ struct CrudCreate { quantity int } -fn handle(req_buffer []u8, _fd int, mut sh Shared) ![]u8 { +// ws appends a string's bytes to `out` with no allocation (push_many copies +// straight from the string's backing storage into the connection write buffer). +@[inline] +fn ws(mut out []u8, s string) { + unsafe { out.push_many(s.str, s.len) } +} + +// wi appends the decimal digits of a non-negative integer to `out`, no +// allocation (itoa into a stack scratch, emitted most-significant-first). +@[direct_array_access] +fn wi(mut out []u8, n i64) { + if n == 0 { + out << u8(`0`) + return + } + mut x := n + mut tmp := [20]u8{} + mut i := 0 + for x > 0 { + tmp[i] = u8(`0`) + u8(x % 10) + x /= 10 + i++ + } + for i > 0 { + i-- + out << tmp[i] + } +} + +// write_resp appends a complete HTTP/1.1 response (status line + headers + body) +// straight into the connection's persistent write buffer — no intermediate +// strings.Builder, no body→response copy, no per-request heap allocation. This +// is the zero-alloc twin of `ok()`; the latter survives only for the DB paths +// that are allocation-bound anyway. +fn write_resp(mut out []u8, ctype string, body string) { + ws(mut out, 'HTTP/1.1 200 OK\r\nServer: vanilla\r\nContent-Type: ') + ws(mut out, ctype) + ws(mut out, '\r\nContent-Length: ') + wi(mut out, i64(body.len)) + ws(mut out, '\r\nConnection: keep-alive\r\n\r\n') + ws(mut out, body) +} + +fn handle(req_buffer []u8, _fd int, mut out []u8, mut sh Shared) ! { req := request_parser.decode_http_request(req_buffer)! method := unsafe { tos(&req.buffer[req.method.start], req.method.len) } target := unsafe { tos(&req.buffer[req.path.start], req.path.len) } route := target.all_before('?') if route == '/pipeline' { - return ok('text/plain', 'ok') + write_resp(mut out, 'text/plain', 'ok') } else if route == '/baseline11' { mut sum := qint(req, 'a') + qint(req, 'b') if method == 'POST' { sum += body_int(req) } - return ok('text/plain', sum.str()) + write_resp(mut out, 'text/plain', sum.str()) } else if route == '/upload' { - return ok('text/plain', req.body.len.str()) + write_resp(mut out, 'text/plain', req.body.len.str()) } else if route.starts_with('/json/') { count := clamp_count(route[6..].i64(), sh.dataset.len) mut m := qint(req, 'm') @@ -94,32 +137,37 @@ fn handle(req_buffer []u8, _fd int, mut sh Shared) ![]u8 { } if accepts_gzip(req) { // json-comp profile: gzip the body and set Content-Encoding. - return ok_gzip('application/json', sh.json_body(count, m)) + sh.write_json_gzip(mut out, count, m) + } else { + sh.write_json_response(mut out, count, m) } - return sh.json_response(count, m) } else if route == '/async-db' { - return ok('application/json', sh.async_db(qint(req, 'min'), qint(req, 'max'), + write_resp(mut out, 'application/json', sh.async_db(qint(req, 'min'), qint(req, 'max'), qint(req, 'limit'))) } else if route == '/fortunes' { - return ok('text/html; charset=utf-8', sh.fortunes()) + write_resp(mut out, 'text/html; charset=utf-8', sh.fortunes()) } else if route.starts_with('/static/') { if f := sh.assets[route[8..]] { - return f.response + out << f.response + } else { + out << not_found } - return not_found } else if route == '/crud/items' { if method == 'POST' { - return sh.crud_create(req) + out << sh.crud_create(req) + } else { + out << sh.crud_list(qstr(req, 'category'), qint(req, 'page'), qint(req, 'limit')) } - return sh.crud_list(qstr(req, 'category'), qint(req, 'page'), qint(req, 'limit')) } else if route.starts_with('/crud/items/') { id := route[12..].int() if method == 'PUT' { - return sh.crud_update(id, req) + out << sh.crud_update(id, req) + } else { + out << sh.crud_get(id) } - return sh.crud_get(id) + } else { + out << not_found } - return not_found } // crud_list returns a paginated, category-filtered page of items. @@ -245,7 +293,7 @@ const bad_request = 'HTTP/1.1 400 Bad Request\r\nServer: vanilla\r\nContent-Leng // single allocation — no per-request reflection and no body→response copy. // Only `total` (price*quantity*m) varies per request; the rest is a precomputed // prefix. Content-Length is computed up front so everything lands in one buffer. -fn (sh &Shared) json_response(count int, m i64) []u8 { +fn (sh &Shared) write_json_response(mut out []u8, count int, m i64) { mut clen := 21 + digits(i64(count)) // len('{"items":[') + len('],"count":') + '}' + count digits if count > 0 { clen += count - 1 // item separators @@ -254,22 +302,36 @@ fn (sh &Shared) json_response(count int, m i64) []u8 { t := sh.dataset[i].price * sh.dataset[i].quantity * m clen += sh.prefixes[i].len + digits(t) + 1 // prefix + total + '}' } - mut sb := strings.new_builder(clen + 96) - sb.write_string('HTTP/1.1 200 OK\r\nServer: vanilla\r\nContent-Type: application/json\r\nContent-Length: ') - sb.write_decimal(i64(clen)) - sb.write_string('\r\nConnection: keep-alive\r\n\r\n{"items":[') + ws(mut out, 'HTTP/1.1 200 OK\r\nServer: vanilla\r\nContent-Type: application/json\r\nContent-Length: ') + wi(mut out, i64(clen)) + ws(mut out, '\r\nConnection: keep-alive\r\n\r\n{"items":[') for i in 0 .. count { if i > 0 { - sb.write_u8(`,`) + out << `,` } - sb.write_string(sh.prefixes[i]) - sb.write_decimal(sh.dataset[i].price * sh.dataset[i].quantity * m) - sb.write_u8(`}`) + ws(mut out, sh.prefixes[i]) + wi(mut out, sh.dataset[i].price * sh.dataset[i].quantity * m) + out << `}` } - sb.write_string('],"count":') - sb.write_decimal(i64(count)) - sb.write_u8(`}`) - return sb + ws(mut out, '],"count":') + wi(mut out, i64(count)) + out << `}` +} + +// write_json_gzip is the json-comp path: build the body, gzip it, and append +// headers + compressed bytes into `out`. gzip needs a contiguous input, so this +// path still allocates the body — but json-comp is compression-bound, not +// allocation-bound, and the response no longer round-trips through a Builder. +fn (sh &Shared) write_json_gzip(mut out []u8, count int, m i64) { + body := sh.json_body(count, m) + gz := gzip.compress(body.bytes()) or { + write_resp(mut out, 'application/json', body) + return + } + ws(mut out, 'HTTP/1.1 200 OK\r\nServer: vanilla\r\nContent-Encoding: gzip\r\nContent-Type: application/json\r\nContent-Length: ') + wi(mut out, i64(gz.len)) + ws(mut out, '\r\nConnection: keep-alive\r\n\r\n') + unsafe { out.push_many(gz.data, gz.len) } } // json_body builds just the /json body string (used for the gzip path). @@ -486,19 +548,6 @@ fn ok_xcache(ctype string, body string, cache string) []u8 { return sb } -// ok_gzip gzip-compresses the body and sets Content-Encoding: gzip. -fn ok_gzip(ctype string, body string) []u8 { - gz := gzip.compress(body.bytes()) or { return ok(ctype, body) } - mut sb := strings.new_builder(gz.len + 128) - sb.write_string('HTTP/1.1 200 OK\r\nServer: vanilla\r\nContent-Encoding: gzip\r\nContent-Type: ') - sb.write_string(ctype) - sb.write_string('\r\nContent-Length: ') - sb.write_decimal(i64(gz.len)) - sb.write_string('\r\nConnection: keep-alive\r\n\r\n') - unsafe { sb.write_ptr(gz.data, gz.len) } - return sb -} - // accepts_gzip reports whether the request advertises gzip in Accept-Encoding. fn accepts_gzip(req request_parser.HttpRequest) bool { ae := req.get_header_value_slice('Accept-Encoding') or { return false } @@ -595,8 +644,8 @@ fn main() { limits: http_server.Limits{ max_request_bytes: 32 * 1024 * 1024 // accept the 20 MiB upload bodies } - request_handler: fn [mut sh] (req_buffer []u8, fd int) ![]u8 { - return handle(req_buffer, fd, mut sh) + request_handler: fn [mut sh] (req_buffer []u8, fd int, mut out []u8) ! { + handle(req_buffer, fd, mut out, mut sh)! } })! server.run() diff --git a/frameworks/vanilla/meta.json b/frameworks/vanilla-epoll/meta.json similarity index 96% rename from frameworks/vanilla/meta.json rename to frameworks/vanilla-epoll/meta.json index cd2bc93a3..a090c3dc3 100644 --- a/frameworks/vanilla/meta.json +++ b/frameworks/vanilla-epoll/meta.json @@ -1,5 +1,5 @@ { - "display_name": "vanilla", + "display_name": "vanilla-epoll", "language": "V", "type": "engine", "engine": "epoll", diff --git a/frameworks/vanilla-io_uring/.gitignore b/frameworks/vanilla-io_uring/.gitignore new file mode 100644 index 000000000..58ba2924e --- /dev/null +++ b/frameworks/vanilla-io_uring/.gitignore @@ -0,0 +1,2 @@ +server +*.so diff --git a/frameworks/vanilla-io_uring/Dockerfile b/frameworks/vanilla-io_uring/Dockerfile new file mode 100644 index 000000000..913e3aaeb --- /dev/null +++ b/frameworks/vanilla-io_uring/Dockerfile @@ -0,0 +1,28 @@ +FROM debian:stable-slim AS build + +RUN apt-get -qq update && \ + apt-get -qy install --no-install-recommends \ + ca-certificates curl unzip build-essential git libpq-dev liburing-dev && \ + rm -rf /var/lib/apt/lists/* + +# Pinned, reproducible V 0.5.1 (prebuilt release binary — no source build, and it +# avoids the `git checkout && make` vc-mismatch problem). +RUN curl -fsSL https://github.com/vlang/v/releases/download/0.5.1/v_linux.zip -o /tmp/v.zip && \ + unzip -q /tmp/v.zip -d /opt && rm /tmp/v.zip && \ + ln -s /opt/v/v /usr/local/bin/v + +# Install the vanilla HTTP server as the `vanilla` module (import vanilla.http_server). +RUN git clone --depth 1 https://github.com/enghitalo/vanilla /root/.vmodules/vanilla + +WORKDIR /app +COPY . . +RUN v -prod . -o server + +FROM debian:stable-slim +RUN apt-get -qq update && \ + apt-get -qy install --no-install-recommends libpq5 liburing2 && \ + rm -rf /var/lib/apt/lists/* +COPY --from=build /app/server /server + +EXPOSE 8080 +CMD ["/server"] diff --git a/frameworks/vanilla-io_uring/README.md b/frameworks/vanilla-io_uring/README.md new file mode 100644 index 000000000..c400f4ce7 --- /dev/null +++ b/frameworks/vanilla-io_uring/README.md @@ -0,0 +1,38 @@ +# vanilla-io_uring + +[vanilla](https://github.com/enghitalo/vanilla) is a minimalist, high-performance +HTTP server written in [V](https://vlang.io) — multi-threaded, non-blocking, +lock-free, copy-free, with `SO_REUSEPORT`. This entry runs the **io_uring** I/O +backend (`io_multiplexing: .io_uring`); see `vanilla-epoll` for the epoll +backend. The harness runs it with `--security-opt seccomp=unconfined` and +`--ulimit memlock=-1:-1` (keyed off `engine: io_uring` in `meta.json`). + +## Implemented profiles + +| Profile | Endpoint | Notes | +|---|---|---| +| `baseline` | `GET/POST /baseline11` | `a + b` (+ body on POST); handles chunked + TCP-fragmented requests | +| `pipelined` | `GET /pipeline` | returns `ok` | +| `upload` | `POST /upload` | returns body byte count (up to 20+ MiB via `max_request_bytes`) | +| `limited-conn` | `GET /baseline11` | short-lived connections | +| `json` | `GET /json/{count}?m=M` | single-allocation response, precomputed item prefixes | +| `json-comp` | `GET /json/...` + `Accept-Encoding` | gzip-compressed response | +| `static` | `GET /static/` | assets preloaded into memory, MIME by extension, 404 on miss | +| `async-db` | `GET /async-db?min&max&limit` | `db.pg` ConnectionPool | +| `crud` | `GET/POST/PUT /crud/items[/id]` | list + read + create + update; in-memory cache-aside (`X-Cache` MISS/HIT, invalidated on update — no Redis) | +| `fortunes` | `GET /fortunes` | DB rows + runtime row, HTML-escaped | +| `api-4`, `api-16` | mixed baseline + json + async-db | | + +## Stack + +* [V](https://vlang.io) 0.5.1 (pinned prebuilt release) +* [vanilla](https://github.com/enghitalo/vanilla) — raw io_uring HTTP server +* `db.pg`, `json`, `compress.gzip` (stdlib) + +## Environment + +* `DATABASE_URL`, `DATABASE_MAX_CONN` — Postgres connection + pool size +* `DATASET_PATH` (default `/data/dataset.json`), `STATIC_DIR` (default `/data/static`) + +> HTTP/2, HTTP/3 and gRPC profiles need protocol support vanilla doesn't have +> yet — tracked in [enghitalo/vanilla#18](https://github.com/enghitalo/vanilla/issues/18). diff --git a/frameworks/vanilla-io_uring/main.v b/frameworks/vanilla-io_uring/main.v new file mode 100644 index 000000000..c61da0d9a --- /dev/null +++ b/frameworks/vanilla-io_uring/main.v @@ -0,0 +1,664 @@ +module main + +import vanilla.http_server +import vanilla.http_server.http1_1.request_parser +import db.pg +import json +import os +import strings +import sync +import compress.gzip + +struct Rating { + score i64 + count i64 +} + +// Dataset item as stored in /data/dataset.json. +struct DatasetItem { + id i64 + name string + category string + price i64 + quantity i64 + active bool + tags []string + rating Rating +} + +struct DbItem { + id int + name string + category string + price int + quantity int + active bool + tags []string + rating Rating +} + +struct DbResp { + items []DbItem + count int +} + +struct Fortune { + id int + message string +} + +// A static asset preloaded into memory with its full HTTP response. +struct StaticFile { + response []u8 +} + +struct Shared { +mut: + pool pg.ConnectionPool + dataset []DatasetItem + prefixes []string // per item: `{…,"total":` (everything but the request-dependent total) + assets map[string]StaticFile // /static/ -> prebuilt response + cache map[int]string // crud cache-aside: id -> item JSON + cache_mu &sync.RwMutex = unsafe { nil } +} + +struct CrudCreate { + id int + name string + category string + price int + quantity int +} + +// ws appends a string's bytes to `out` with no allocation (push_many copies +// straight from the string's backing storage into the connection write buffer). +@[inline] +fn ws(mut out []u8, s string) { + unsafe { out.push_many(s.str, s.len) } +} + +// wi appends the decimal digits of a non-negative integer to `out`, no +// allocation (itoa into a stack scratch, emitted most-significant-first). +@[direct_array_access] +fn wi(mut out []u8, n i64) { + if n == 0 { + out << u8(`0`) + return + } + mut x := n + mut tmp := [20]u8{} + mut i := 0 + for x > 0 { + tmp[i] = u8(`0`) + u8(x % 10) + x /= 10 + i++ + } + for i > 0 { + i-- + out << tmp[i] + } +} + +// write_resp appends a complete HTTP/1.1 response (status line + headers + body) +// straight into the connection's persistent write buffer — no intermediate +// strings.Builder, no body→response copy, no per-request heap allocation. This +// is the zero-alloc twin of `ok()`; the latter survives only for the DB paths +// that are allocation-bound anyway. +fn write_resp(mut out []u8, ctype string, body string) { + ws(mut out, 'HTTP/1.1 200 OK\r\nServer: vanilla\r\nContent-Type: ') + ws(mut out, ctype) + ws(mut out, '\r\nContent-Length: ') + wi(mut out, i64(body.len)) + ws(mut out, '\r\nConnection: keep-alive\r\n\r\n') + ws(mut out, body) +} + +fn handle(req_buffer []u8, _fd int, mut out []u8, mut sh Shared) ! { + req := request_parser.decode_http_request(req_buffer)! + method := unsafe { tos(&req.buffer[req.method.start], req.method.len) } + target := unsafe { tos(&req.buffer[req.path.start], req.path.len) } + route := target.all_before('?') + + if route == '/pipeline' { + write_resp(mut out, 'text/plain', 'ok') + } else if route == '/baseline11' { + mut sum := qint(req, 'a') + qint(req, 'b') + if method == 'POST' { + sum += body_int(req) + } + write_resp(mut out, 'text/plain', sum.str()) + } else if route == '/upload' { + write_resp(mut out, 'text/plain', req.body.len.str()) + } else if route.starts_with('/json/') { + count := clamp_count(route[6..].i64(), sh.dataset.len) + mut m := qint(req, 'm') + if m == 0 { + m = 1 + } + if accepts_gzip(req) { + // json-comp profile: gzip the body and set Content-Encoding. + sh.write_json_gzip(mut out, count, m) + } else { + sh.write_json_response(mut out, count, m) + } + } else if route == '/async-db' { + write_resp(mut out, 'application/json', sh.async_db(qint(req, 'min'), qint(req, 'max'), + qint(req, 'limit'))) + } else if route == '/fortunes' { + write_resp(mut out, 'text/html; charset=utf-8', sh.fortunes()) + } else if route.starts_with('/static/') { + if f := sh.assets[route[8..]] { + out << f.response + } else { + out << not_found + } + } else if route == '/crud/items' { + if method == 'POST' { + out << sh.crud_create(req) + } else { + out << sh.crud_list(qstr(req, 'category'), qint(req, 'page'), qint(req, 'limit')) + } + } else if route.starts_with('/crud/items/') { + id := route[12..].int() + if method == 'PUT' { + out << sh.crud_update(id, req) + } else { + out << sh.crud_get(id) + } + } else { + out << not_found + } +} + +// crud_list returns a paginated, category-filtered page of items. +fn (mut sh Shared) crud_list(category string, page i64, limit i64) []u8 { + mut p := page + if p < 1 { + p = 1 + } + mut lim := limit + if lim < 1 { + lim = 10 + } + if lim > 100 { + lim = 100 + } + offset := (p - 1) * lim + mut conn := sh.pool.acquire() or { return ok('application/json', '{"items":[],"total":0,"page":1}') } + rows := conn.exec_param_many('SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE category = \$1 ORDER BY id LIMIT \$2 OFFSET \$3', + [category, lim.str(), offset.str()]) or { + sh.pool.release(conn) + return ok('application/json', '{"items":[],"total":0,"page":1}') + } + trows := conn.exec_param_many('SELECT count(*) FROM items WHERE category = \$1', [category]) or { + [] + } + sh.pool.release(conn) + total := if trows.len > 0 { nn(trows[0].vals[0]).int() } else { 0 } + mut items := []DbItem{cap: rows.len} + for row in rows { + items << row_to_item(row) + } + mut sb := strings.new_builder(items.len * 200 + 64) + sb.write_string('{"items":') + sb.write_string(json.encode(items)) + sb.write_string(',"total":') + sb.write_decimal(i64(total)) + sb.write_string(',"page":') + sb.write_decimal(p) + sb.write_u8(`}`) + return ok('application/json', sb.str()) +} + +// crud_get returns a single item, using a cache-aside in-memory cache and +// reporting the result via the X-Cache header (MISS on first read, HIT after). +fn (mut sh Shared) crud_get(id int) []u8 { + sh.cache_mu.@rlock() + cached := sh.cache[id] or { '' } + sh.cache_mu.runlock() + if cached.len > 0 { + return ok_xcache('application/json', cached, 'HIT') + } + mut conn := sh.pool.acquire() or { return not_found } + rows := conn.exec_param_many('SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE id = \$1', + [id.str()]) or { + sh.pool.release(conn) + return not_found + } + sh.pool.release(conn) + if rows.len == 0 { + return not_found + } + body := json.encode(row_to_item(rows[0])) + sh.cache_mu.@lock() + sh.cache[id] = body + sh.cache_mu.unlock() + return ok_xcache('application/json', body, 'MISS') +} + +// crud_create inserts a new item from the JSON body and returns 201. +fn (mut sh Shared) crud_create(req request_parser.HttpRequest) []u8 { + raw := unsafe { tos(&req.buffer[req.body.start], req.body.len) } + c := json.decode(CrudCreate, raw) or { return bad_request } + mut conn := sh.pool.acquire() or { return bad_request } + conn.exec_param_many("INSERT INTO items (id, name, category, price, quantity, active, tags, rating_score, rating_count) VALUES (\$1, \$2, \$3, \$4, \$5, true, '[]', 0, 0) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, category = EXCLUDED.category, price = EXCLUDED.price, quantity = EXCLUDED.quantity", + [c.id.str(), c.name, c.category, c.price.str(), c.quantity.str()]) or { + sh.pool.release(conn) + return bad_request + } + sh.pool.release(conn) + return created +} + +// crud_update updates an item and invalidates its cache entry. +fn (mut sh Shared) crud_update(id int, req request_parser.HttpRequest) []u8 { + raw := unsafe { tos(&req.buffer[req.body.start], req.body.len) } + c := json.decode(CrudCreate, raw) or { return bad_request } + mut conn := sh.pool.acquire() or { return bad_request } + conn.exec_param_many('UPDATE items SET name = \$2, category = \$3, price = \$4, quantity = \$5 WHERE id = \$1', + [id.str(), c.name, c.category, c.price.str(), c.quantity.str()]) or { + sh.pool.release(conn) + return bad_request + } + sh.pool.release(conn) + sh.cache_mu.@lock() + sh.cache.delete(id) + sh.cache_mu.unlock() + return ok('application/json', '{"status":"ok"}') +} + +fn row_to_item(row pg.Row) DbItem { + return DbItem{ + id: nn(row.vals[0]).int() + name: nn(row.vals[1]) + category: nn(row.vals[2]) + price: nn(row.vals[3]).int() + quantity: nn(row.vals[4]).int() + active: nn(row.vals[5]) == 't' + tags: json.decode([]string, nn3(row.vals[6], '[]')) or { [] } + rating: Rating{ + score: nn(row.vals[7]).i64() + count: nn(row.vals[8]).i64() + } + } +} + +const not_found = 'HTTP/1.1 404 Not Found\r\nServer: vanilla\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes() + +const created = 'HTTP/1.1 201 Created\r\nServer: vanilla\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes() + +const bad_request = 'HTTP/1.1 400 Bad Request\r\nServer: vanilla\r\nContent-Length: 0\r\nConnection: keep-alive\r\n\r\n'.bytes() + +// json_response builds the FULL HTTP response (headers + body) for /json in a +// single allocation — no per-request reflection and no body→response copy. +// Only `total` (price*quantity*m) varies per request; the rest is a precomputed +// prefix. Content-Length is computed up front so everything lands in one buffer. +fn (sh &Shared) write_json_response(mut out []u8, count int, m i64) { + mut clen := 21 + digits(i64(count)) // len('{"items":[') + len('],"count":') + '}' + count digits + if count > 0 { + clen += count - 1 // item separators + } + for i in 0 .. count { + t := sh.dataset[i].price * sh.dataset[i].quantity * m + clen += sh.prefixes[i].len + digits(t) + 1 // prefix + total + '}' + } + ws(mut out, 'HTTP/1.1 200 OK\r\nServer: vanilla\r\nContent-Type: application/json\r\nContent-Length: ') + wi(mut out, i64(clen)) + ws(mut out, '\r\nConnection: keep-alive\r\n\r\n{"items":[') + for i in 0 .. count { + if i > 0 { + out << `,` + } + ws(mut out, sh.prefixes[i]) + wi(mut out, sh.dataset[i].price * sh.dataset[i].quantity * m) + out << `}` + } + ws(mut out, '],"count":') + wi(mut out, i64(count)) + out << `}` +} + +// write_json_gzip is the json-comp path: build the body, gzip it, and append +// headers + compressed bytes into `out`. gzip needs a contiguous input, so this +// path still allocates the body — but json-comp is compression-bound, not +// allocation-bound, and the response no longer round-trips through a Builder. +fn (sh &Shared) write_json_gzip(mut out []u8, count int, m i64) { + body := sh.json_body(count, m) + gz := gzip.compress(body.bytes()) or { + write_resp(mut out, 'application/json', body) + return + } + ws(mut out, 'HTTP/1.1 200 OK\r\nServer: vanilla\r\nContent-Encoding: gzip\r\nContent-Type: application/json\r\nContent-Length: ') + wi(mut out, i64(gz.len)) + ws(mut out, '\r\nConnection: keep-alive\r\n\r\n') + unsafe { out.push_many(gz.data, gz.len) } +} + +// json_body builds just the /json body string (used for the gzip path). +fn (sh &Shared) json_body(count int, m i64) string { + mut sb := strings.new_builder(count * 224 + 32) + sb.write_string('{"items":[') + for i in 0 .. count { + if i > 0 { + sb.write_u8(`,`) + } + sb.write_string(sh.prefixes[i]) + sb.write_decimal(sh.dataset[i].price * sh.dataset[i].quantity * m) + sb.write_u8(`}`) + } + sb.write_string('],"count":') + sb.write_decimal(i64(count)) + sb.write_u8(`}`) + return sb.str() +} + +// fortunes queries the fortune table, appends the runtime row, sorts by message +// and renders the HTML table (escaped). 199 seeded + 1 runtime + header = 201 . +fn (mut sh Shared) fortunes() string { + mut fortunes := []Fortune{} + mut conn := sh.pool.acquire() or { + return '
' + } + rows := conn.exec_param_many('SELECT id, message FROM fortune', []) or { [] } + sh.pool.release(conn) + for row in rows { + fortunes << Fortune{ + id: nn(row.vals[0]).int() + message: nn(row.vals[1]) + } + } + fortunes << Fortune{ + id: 0 + message: 'Additional fortune added at request time.' + } + fortunes.sort(a.message < b.message) + mut sb := strings.new_builder(32768) + sb.write_string('Fortunes') + for f in fortunes { + sb.write_string('') + } + sb.write_string('
idmessage
') + sb.write_decimal(i64(f.id)) + sb.write_string('') + sb.write_string(escape_html(f.message)) + sb.write_string('
') + return sb.str() +} + +fn escape_html(s string) string { + return s.replace_each(['&', '&', '<', '<', '>', '>', '"', '"', "'", ''']) +} + +// digits returns the number of decimal digits in a non-negative integer. +fn digits(n i64) int { + if n < 10 { + return 1 + } + mut x := n + mut d := 0 + for x > 0 { + d++ + x /= 10 + } + return d +} + +fn (mut sh Shared) async_db(min i64, max i64, limit i64) string { + mut lim := limit + if lim < 1 { + lim = 1 + } + if lim > 50 { + lim = 50 + } + mut conn := sh.pool.acquire() or { return '{"items":[],"count":0}' } + rows := conn.exec_param_many('SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN \$1 AND \$2 LIMIT \$3', + [min.str(), max.str(), lim.str()]) or { + sh.pool.release(conn) + return '{"items":[],"count":0}' + } + sh.pool.release(conn) + mut items := []DbItem{cap: rows.len} + for row in rows { + items << DbItem{ + id: nn(row.vals[0]).int() + name: nn(row.vals[1]) + category: nn(row.vals[2]) + price: nn(row.vals[3]).int() + quantity: nn(row.vals[4]).int() + active: nn(row.vals[5]) == 't' + tags: json.decode([]string, nn3(row.vals[6], '[]')) or { [] } + rating: Rating{ + score: nn(row.vals[7]).i64() + count: nn(row.vals[8]).i64() + } + } + } + return json.encode(DbResp{ items: items, count: items.len }) +} + +// nn unwraps a nullable column value to a plain string ('' for NULL). +@[inline] +fn nn(v ?string) string { + return v or { '' } +} + +// nn3 unwraps a nullable column value with a custom default. +@[inline] +fn nn3(v ?string, d string) string { + return v or { d } +} + +// qint reads a query parameter as an integer (0 if absent / non-numeric). +fn qint(req request_parser.HttpRequest, key string) i64 { + s := req.get_query_slice(key.bytes()) or { return 0 } + return unsafe { tos(&req.buffer[s.start], s.len) }.i64() +} + +// qstr reads a query parameter as a string ('' if absent). Clones so the value +// outlives the request buffer (it is passed to the DB driver). +fn qstr(req request_parser.HttpRequest, key string) string { + s := req.get_query_slice(key.bytes()) or { return '' } + return unsafe { tos(&req.buffer[s.start], s.len) }.clone() +} + +fn clamp_count(n i64, max int) int { + if n < 0 { + return 0 + } + if n > max { + return max + } + return int(n) +} + +// body_int parses the request body as an integer, decoding chunked transfer +// encoding when present. +fn body_int(req request_parser.HttpRequest) i64 { + if req.body.len == 0 { + return 0 + } + raw := unsafe { tos(&req.buffer[req.body.start], req.body.len) } + if te := req.get_header_value_slice('Transfer-Encoding') { + val := unsafe { tos(&req.buffer[te.start], te.len) } + if val.contains('chunked') { + return dechunk(raw).i64() + } + } + return raw.i64() +} + +// dechunk decodes an HTTP/1.1 chunked body into its payload. +fn dechunk(s string) string { + mut out := strings.new_builder(s.len) + mut i := 0 + for i < s.len { + nl := s.index_after('\r\n', i) or { break } + size := strconv_hex(s[i..nl]) + if size <= 0 { + break + } + data_start := nl + 2 + out.write_string(s[data_start..data_start + size]) + i = data_start + size + 2 // skip data + trailing CRLF + } + return out.str() +} + +fn strconv_hex(s string) int { + mut n := 0 + for c in s.trim_space() { + d := if c >= `0` && c <= `9` { + int(c - `0`) + } else if c >= `a` && c <= `f` { + int(c - `a` + 10) + } else if c >= `A` && c <= `F` { + int(c - `A` + 10) + } else { + break + } + n = n * 16 + d + } + return n +} + +// ok builds a complete HTTP/1.1 response with the given content type. +fn ok(ctype string, body string) []u8 { + mut sb := strings.new_builder(body.len + 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(i64(body.len)) + sb.write_string('\r\nConnection: keep-alive\r\n\r\n') + sb.write_string(body) + return sb +} + +// ok_xcache builds a JSON response carrying an X-Cache: HIT|MISS header. +fn ok_xcache(ctype string, body string, cache string) []u8 { + mut sb := strings.new_builder(body.len + 96) + sb.write_string('HTTP/1.1 200 OK\r\nServer: vanilla\r\nX-Cache: ') + sb.write_string(cache) + sb.write_string('\r\nContent-Type: ') + sb.write_string(ctype) + sb.write_string('\r\nContent-Length: ') + sb.write_decimal(i64(body.len)) + sb.write_string('\r\nConnection: keep-alive\r\n\r\n') + sb.write_string(body) + return sb +} + +// accepts_gzip reports whether the request advertises gzip in Accept-Encoding. +fn accepts_gzip(req request_parser.HttpRequest) bool { + ae := req.get_header_value_slice('Accept-Encoding') or { return false } + return unsafe { tos(&req.buffer[ae.start], ae.len) }.contains('gzip') +} + +// content_type maps a file extension to a MIME type for the static handler. +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' } + } +} + +// parse_db_url turns postgres://user:pass@host:port/dbname into a pg.Config. +fn parse_db_url(u string) pg.Config { + mut s := u + if s.contains('://') { + s = s.all_after('://') + } + creds := s.all_before('@') + rest := s.all_after('@') + host_port := rest.all_before('/') + mut port := 5432 + if host_port.contains(':') { + port = host_port.all_after(':').int() + } + return pg.Config{ + host: host_port.all_before(':') + port: port + user: creds.all_before(':') + password: creds.all_after(':') + dbname: rest.all_after('/') + } +} + +fn main() { + url := os.getenv_opt('DATABASE_URL') or { 'postgres://bench:bench@localhost:5432/benchmark' } + mut size := (os.getenv_opt('DATABASE_MAX_CONN') or { '64' }).int() + if size < 1 { + size = 64 + } + if size > 200 { + size = 200 // leave headroom under Postgres max_connections + } + mut pool := pg.new_connection_pool(parse_db_url(url), size)! + + dataset_path := os.getenv_opt('DATASET_PATH') or { '/data/dataset.json' } + dataset_raw := os.read_file(dataset_path) or { '[]' } + dataset := json.decode([]DatasetItem, dataset_raw) or { []DatasetItem{} } + + // Precompute each item's JSON prefix once: `{…,"rating":{…},"total":` + // (drop the closing brace, append the total key). Only the total value is + // request-dependent, so the hot path never serializes a struct. + mut prefixes := []string{cap: dataset.len} + for it in dataset { + enc := json.encode(it) + prefixes << enc#[..-1] + ',"total":' + } + + // Preload static assets into memory as ready-to-send responses (originals + // only; skip the precompressed .gz/.br siblings — we serve identity). + 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 + } + bytes := os.read_bytes('${static_dir}/${name}') or { continue } + assets[name] = StaticFile{ + response: static_response(content_type(name), bytes) + } + } + + mut sh := Shared{ + pool: pool + dataset: dataset + prefixes: prefixes + assets: assets + cache: map[int]string{} + cache_mu: sync.new_rwmutex() + } + + mut server := http_server.new_server(http_server.ServerConfig{ + port: 8080 + io_multiplexing: .io_uring + limits: http_server.Limits{ + max_request_bytes: 32 * 1024 * 1024 // accept the 20 MiB upload bodies + } + request_handler: fn [mut sh] (req_buffer []u8, fd int, mut out []u8) ! { + handle(req_buffer, fd, mut out, mut sh)! + } + })! + server.run() +} + +// static_response prebuilds the full HTTP response for a static file. +fn static_response(ctype string, body []u8) []u8 { + mut sb := strings.new_builder(body.len + 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(i64(body.len)) + sb.write_string('\r\nConnection: keep-alive\r\n\r\n') + unsafe { sb.write_ptr(body.data, body.len) } + return sb +} diff --git a/frameworks/vanilla-io_uring/meta.json b/frameworks/vanilla-io_uring/meta.json new file mode 100644 index 000000000..1334155ef --- /dev/null +++ b/frameworks/vanilla-io_uring/meta.json @@ -0,0 +1,26 @@ +{ + "display_name": "vanilla-io_uring", + "language": "V", + "type": "engine", + "engine": "io_uring", + "description": "vanilla is a minimalist, high-performance HTTP server written in V: multi-threaded, non-blocking io_uring I/O, lock-free, copy-free, SO_REUSEPORT. Handlers are pure (request)->[]u8 returning raw response bytes. JSON is built in a single allocation with precomputed prefixes (no per-request reflection); json-comp gzips on Accept-Encoding; static assets are preloaded into memory; fortunes renders the DB rows + a runtime row with HTML escaping; async-db uses the stdlib db.pg ConnectionPool. Pinned V 0.5.1 (prebuilt release). crud uses an in-memory cache-aside (X-Cache MISS/HIT, invalidated on update) \u2014 no Redis required.", + "repo": "https://github.com/enghitalo/vanilla", + "enabled": true, + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json", + "json-comp", + "upload", + "static", + "async-db", + "crud", + "fortunes", + "api-4", + "api-16" + ], + "maintainers": [ + "enghitalo" + ] +} diff --git a/site/data/api-16-1024.json b/site/data/api-16-1024.json index 8597b3647..d0d6bb5b4 100644 --- a/site/data/api-16-1024.json +++ b/site/data/api-16-1024.json @@ -1294,30 +1294,30 @@ "tpl_async_db": 234853 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 15157, - "avg_latency": "65.25ms", - "p99_latency": "1.29s", - "cpu": "300.4%", - "memory": "73MiB", + "rps": 19749, + "avg_latency": "50.33ms", + "p99_latency": "967.70ms", + "cpu": "388.9%", + "memory": "93MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "76.29MB/s", - "input_bw": "873.30KB/s", - "reconnects": 45328, - "status_2xx": 227357, + "bandwidth": "99.35MB/s", + "input_bw": "1.11MB/s", + "reconnects": 59032, + "status_2xx": 296243, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_baseline": 85169, - "tpl_json": 85305, + "tpl_baseline": 111087, + "tpl_json": 111184, "tpl_db": 0, "tpl_upload": 0, "tpl_static": 0, - "tpl_async_db": 56883 + "tpl_async_db": 73972 }, { "framework": "workerman", diff --git a/site/data/api-4-256.json b/site/data/api-4-256.json index 2f1de0e85..fa93135f9 100644 --- a/site/data/api-4-256.json +++ b/site/data/api-4-256.json @@ -1294,30 +1294,30 @@ "tpl_async_db": 93027 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 23632, - "avg_latency": "9.80ms", - "p99_latency": "216.60ms", - "cpu": "308.7%", - "memory": "66MiB", + "rps": 32853, + "avg_latency": "6.60ms", + "p99_latency": "89.90ms", + "cpu": "338.5%", + "memory": "71MiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "118.89MB/s", - "input_bw": "1.33MB/s", - "reconnects": 70831, - "status_2xx": 354485, + "bandwidth": "165.26MB/s", + "input_bw": "1.85MB/s", + "reconnects": 98562, + "status_2xx": 492803, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "tpl_baseline": 132902, - "tpl_json": 132868, + "tpl_baseline": 184799, + "tpl_json": 184696, "tpl_db": 0, "tpl_upload": 0, "tpl_static": 0, - "tpl_async_db": 88715 + "tpl_async_db": 123308 }, { "framework": "workerman", diff --git a/site/data/async-db-1024.json b/site/data/async-db-1024.json index e3705586e..8552534bb 100644 --- a/site/data/async-db-1024.json +++ b/site/data/async-db-1024.json @@ -1074,21 +1074,21 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 8626, - "avg_latency": "99.73ms", - "p99_latency": "2.68s", - "cpu": "284.1%", - "memory": "72MiB", + "rps": 10868, + "avg_latency": "85.93ms", + "p99_latency": "2.06s", + "cpu": "361.8%", + "memory": "100MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "33.02MB/s", - "input_bw": "589.67KB/s", - "reconnects": 3170, - "status_2xx": 86260, + "bandwidth": "41.64MB/s", + "input_bw": "742.93KB/s", + "reconnects": 4065, + "status_2xx": 108682, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/baseline-4096.json b/site/data/baseline-4096.json index a5443f750..5079f162d 100644 --- a/site/data/baseline-4096.json +++ b/site/data/baseline-4096.json @@ -1566,21 +1566,21 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 329262, - "avg_latency": "2.64ms", - "p99_latency": "2.93ms", - "cpu": "515.0%", - "memory": "67MiB", + "rps": 429242, + "avg_latency": "9.48ms", + "p99_latency": "156.30ms", + "cpu": "1087.8%", + "memory": "193MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "33.59MB/s", - "input_bw": "25.43MB/s", + "bandwidth": "43.79MB/s", + "input_bw": "33.16MB/s", "reconnects": 0, - "status_2xx": 1646312, + "status_2xx": 2146213, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/baseline-512.json b/site/data/baseline-512.json index 2c4e05569..6beb2635b 100644 --- a/site/data/baseline-512.json +++ b/site/data/baseline-512.json @@ -1566,21 +1566,21 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 344666, - "avg_latency": "286us", - "p99_latency": "276us", - "cpu": "524.4%", - "memory": "49MiB", + "rps": 465632, + "avg_latency": "1.09ms", + "p99_latency": "17.50ms", + "cpu": "1088.0%", + "memory": "72MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "35.16MB/s", - "input_bw": "26.62MB/s", + "bandwidth": "47.50MB/s", + "input_bw": "35.97MB/s", "reconnects": 0, - "status_2xx": 1723331, + "status_2xx": 2328160, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/crud-4096.json b/site/data/crud-4096.json index 6c2360045..edb368905 100644 --- a/site/data/crud-4096.json +++ b/site/data/crud-4096.json @@ -378,21 +378,21 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 122937, - "avg_latency": "31.78ms", - "p99_latency": "215.10ms", - "cpu": "934.2%", - "memory": "220MiB", + "rps": 119593, + "avg_latency": "34.19ms", + "p99_latency": "221.70ms", + "cpu": "898.0%", + "memory": "382MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "37.95MB/s", - "input_bw": "10.55MB/s", - "reconnects": 7321, - "status_2xx": 1844057, + "bandwidth": "37.18MB/s", + "input_bw": "10.26MB/s", + "reconnects": 7094, + "status_2xx": 1793908, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/fortunes-1024.json b/site/data/fortunes-1024.json index 57fdbba8f..5e38d905a 100644 --- a/site/data/fortunes-1024.json +++ b/site/data/fortunes-1024.json @@ -133,20 +133,20 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 3110, - "avg_latency": "6.12ms", - "p99_latency": "21.90ms", - "cpu": "179.3%", - "memory": "63MiB", + "rps": 2655, + "avg_latency": "361.81ms", + "p99_latency": "1.94s", + "cpu": "391.5%", + "memory": "144MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "73.73MB/s", + "bandwidth": "62.95MB/s", "reconnects": 0, - "status_2xx": 15552, + "status_2xx": 13279, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/frameworks.json b/site/data/frameworks.json index 4b91a8c69..9d51199b0 100644 --- a/site/data/frameworks.json +++ b/site/data/frameworks.json @@ -400,6 +400,13 @@ "engine": "swoole", "mode": "standard" }, + "ioxide": { + "dir": "ioxide", + "description": "ioxide \u2014 a shared-nothing io_uring runtime for .NET, consumed as its published NuGet packages (ioxide, ioxide.pg, ioxide.file). One ring per reactor thread: multishot accept/recv into provided buffer rings, inline IValueTaskSource continuations, raw-syscall io_uring (no liburing). The HTTP/1.1 handler (request line, headers, Content-Length + chunked bodies, keep-alive, pipelining, fragmented reads) is hand-written on the raw recv/send API; async-db rides ioxide.pg's ring-native pooled Postgres driver (SCRAM-SHA-256), static rides ioxide.file's baked asset snapshots.", + "repo": "https://github.com/MDA2AV/ioxide", + "type": "engine", + "engine": "io_uring" + }, "ktor": { "dir": "ktor", "description": "JetBrains Ktor 3.x on Netty with Kotlin coroutines, kotlinx.serialization, JDK 21.", @@ -823,13 +830,20 @@ "type": "engine", "engine": "uvloop" }, - "vanilla": { - "dir": "vanilla", + "vanilla-epoll": { + "dir": "vanilla-epoll", "description": "vanilla is a minimalist, high-performance HTTP server written in V: multi-threaded, non-blocking epoll I/O, lock-free, copy-free, SO_REUSEPORT. Handlers are pure (request)->[]u8 returning raw response bytes. JSON is built in a single allocation with precomputed prefixes (no per-request reflection); json-comp gzips on Accept-Encoding; static assets are preloaded into memory; fortunes renders the DB rows + a runtime row with HTML escaping; async-db uses the stdlib db.pg ConnectionPool. Pinned V 0.5.1 (prebuilt release). crud uses an in-memory cache-aside (X-Cache MISS/HIT, invalidated on update) \u2014 no Redis required.", "repo": "https://github.com/enghitalo/vanilla", "type": "engine", "engine": "epoll" }, + "vanilla-io_uring": { + "dir": "vanilla-io_uring", + "description": "vanilla is a minimalist, high-performance HTTP server written in V: multi-threaded, non-blocking io_uring I/O, lock-free, copy-free, SO_REUSEPORT. Handlers are pure (request)->[]u8 returning raw response bytes. JSON is built in a single allocation with precomputed prefixes (no per-request reflection); json-comp gzips on Accept-Encoding; static assets are preloaded into memory; fortunes renders the DB rows + a runtime row with HTML escaping; async-db uses the stdlib db.pg ConnectionPool. Pinned V 0.5.1 (prebuilt release). crud uses an in-memory cache-aside (X-Cache MISS/HIT, invalidated on update) \u2014 no Redis required.", + "repo": "https://github.com/enghitalo/vanilla", + "type": "engine", + "engine": "io_uring" + }, "veb": { "dir": "veb", "description": "veb is the web framework in V's standard library (multi-threaded, built-in HTTP/1.1 server). JSON responses are built without per-request reflection (precomputed item prefixes + manual serialization); async-db uses the stdlib db.pg channel-based ConnectionPool sized from DATABASE_MAX_CONN. Built with -prod on a pinned V 0.5.1. Note: the baseline profile is not subscribed because veb does not decode chunked request bodies.", diff --git a/site/data/json-4096.json b/site/data/json-4096.json index e402e00bf..8d83913f0 100644 --- a/site/data/json-4096.json +++ b/site/data/json-4096.json @@ -1294,21 +1294,21 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 71719, - "avg_latency": "39.42ms", - "p99_latency": "1.10s", - "cpu": "282.2%", - "memory": "74MiB", + "rps": 484687, + "avg_latency": "8.07ms", + "p99_latency": "187.70ms", + "cpu": "1666.0%", + "memory": "177MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "248.89MB/s", - "input_bw": "3.42MB/s", - "reconnects": 13820, - "status_2xx": 358597, + "bandwidth": "1.64GB/s", + "input_bw": "23.11MB/s", + "reconnects": 95713, + "status_2xx": 2423438, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/json-comp-16384.json b/site/data/json-comp-16384.json index b6310c10d..4a604c3fb 100644 --- a/site/data/json-comp-16384.json +++ b/site/data/json-comp-16384.json @@ -1040,21 +1040,21 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 31322, - "avg_latency": "111.43ms", - "p99_latency": "2.63s", - "cpu": "461.3%", - "memory": "175MiB", + "rps": 70705, + "avg_latency": "218.40ms", + "p99_latency": "1.06s", + "cpu": "1063.9%", + "memory": "704MiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "40.82MB/s", - "input_bw": "2.33MB/s", - "reconnects": 5202, - "status_2xx": 156611, + "bandwidth": "92.27MB/s", + "input_bw": "5.26MB/s", + "reconnects": 4920, + "status_2xx": 353526, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/json-comp-4096.json b/site/data/json-comp-4096.json index 1e78ccfba..8eae5ffd8 100644 --- a/site/data/json-comp-4096.json +++ b/site/data/json-comp-4096.json @@ -1040,21 +1040,21 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 32042, - "avg_latency": "68.02ms", - "p99_latency": "1.80s", - "cpu": "500.5%", - "memory": "93MiB", + "rps": 57072, + "avg_latency": "54.22ms", + "p99_latency": "1.43s", + "cpu": "767.1%", + "memory": "185MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "41.76MB/s", - "input_bw": "2.38MB/s", - "reconnects": 5737, - "status_2xx": 160212, + "bandwidth": "74.49MB/s", + "input_bw": "4.25MB/s", + "reconnects": 10658, + "status_2xx": 285361, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/json-comp-512.json b/site/data/json-comp-512.json index d67d4c77b..7687979a0 100644 --- a/site/data/json-comp-512.json +++ b/site/data/json-comp-512.json @@ -1040,21 +1040,21 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 32834, - "avg_latency": "14.23ms", - "p99_latency": "409.50ms", - "cpu": "517.1%", - "memory": "66MiB", + "rps": 48935, + "avg_latency": "9.81ms", + "p99_latency": "294.90ms", + "cpu": "749.3%", + "memory": "87MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "42.85MB/s", - "input_bw": "2.44MB/s", - "reconnects": 6470, - "status_2xx": 164172, + "bandwidth": "63.89MB/s", + "input_bw": "3.64MB/s", + "reconnects": 9669, + "status_2xx": 244675, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/limited-conn-4096.json b/site/data/limited-conn-4096.json index 7cc5b1c86..a0ecb96fa 100644 --- a/site/data/limited-conn-4096.json +++ b/site/data/limited-conn-4096.json @@ -1566,21 +1566,21 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 257501, - "avg_latency": "13.12ms", - "p99_latency": "234.70ms", - "cpu": "843.7%", - "memory": "69MiB", + "rps": 303451, + "avg_latency": "13.13ms", + "p99_latency": "265.60ms", + "cpu": "957.5%", + "memory": "179MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "26.27MB/s", - "input_bw": "19.89MB/s", - "reconnects": 127324, - "status_2xx": 1287506, + "bandwidth": "30.96MB/s", + "input_bw": "23.44MB/s", + "reconnects": 151192, + "status_2xx": 1517255, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/limited-conn-512.json b/site/data/limited-conn-512.json index a6078ad59..3a956ef7d 100644 --- a/site/data/limited-conn-512.json +++ b/site/data/limited-conn-512.json @@ -1566,21 +1566,21 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 224975, - "avg_latency": "2.25ms", - "p99_latency": "69.50ms", - "cpu": "771.1%", - "memory": "43MiB", + "rps": 216908, + "avg_latency": "2.34ms", + "p99_latency": "57.70ms", + "cpu": "726.7%", + "memory": "62MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "22.95MB/s", - "input_bw": "17.38MB/s", - "reconnects": 112409, - "status_2xx": 1124877, + "bandwidth": "22.13MB/s", + "input_bw": "16.76MB/s", + "reconnects": 108440, + "status_2xx": 1084542, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/pipelined-4096.json b/site/data/pipelined-4096.json index ade62e826..489ff1785 100644 --- a/site/data/pipelined-4096.json +++ b/site/data/pipelined-4096.json @@ -1520,22 +1520,22 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 119343, - "avg_latency": "10.34ms", - "p99_latency": "13.40ms", - "cpu": "862.7%", - "memory": "41MiB", + "rps": 2301772, + "avg_latency": "28.23ms", + "p99_latency": "245.40ms", + "cpu": "887.1%", + "memory": "187MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "19.68MB/s", - "reconnects": 596745, - "status_2xx": 596718, + "bandwidth": "234.80MB/s", + "reconnects": 0, + "status_2xx": 11508864, "status_3xx": 0, - "status_4xx": 596729, + "status_4xx": 0, "status_5xx": 0 }, { diff --git a/site/data/pipelined-512.json b/site/data/pipelined-512.json index 92064c962..7026dafe7 100644 --- a/site/data/pipelined-512.json +++ b/site/data/pipelined-512.json @@ -1520,22 +1520,22 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 119662, - "avg_latency": "4.19ms", - "p99_latency": "9.33ms", - "cpu": "874.6%", - "memory": "36MiB", + "rps": 2379737, + "avg_latency": "3.43ms", + "p99_latency": "86.40ms", + "cpu": "851.4%", + "memory": "53MiB", "connections": 512, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "19.74MB/s", - "reconnects": 598280, - "status_2xx": 598314, + "bandwidth": "242.75MB/s", + "reconnects": 0, + "status_2xx": 11898688, "status_3xx": 0, - "status_4xx": 598278, + "status_4xx": 0, "status_5xx": 0 }, { diff --git a/site/data/static-1024.json b/site/data/static-1024.json index f9588c159..14cbd3774 100644 --- a/site/data/static-1024.json +++ b/site/data/static-1024.json @@ -1215,20 +1215,20 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 375872, - "avg_latency": "50.19ms", - "p99_latency": "50.19ms", - "cpu": "2706.7%", - "memory": "46MiB", + "rps": 318175, + "avg_latency": "4.48ms", + "p99_latency": "233.62ms", + "cpu": "6403.3%", + "memory": "744MiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "22.30GB", + "bandwidth": "18.87GB", "reconnects": 0, - "status_2xx": 1916611, + "status_2xx": 1622616, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/static-4096.json b/site/data/static-4096.json index bba23ec84..0fda23e13 100644 --- a/site/data/static-4096.json +++ b/site/data/static-4096.json @@ -1215,20 +1215,20 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 354771, - "avg_latency": "55.29ms", - "p99_latency": "55.29ms", - "cpu": "2490.1%", - "memory": "70MiB", + "rps": 266163, + "avg_latency": "16.56ms", + "p99_latency": "283.83ms", + "cpu": "6266.1%", + "memory": "2.8GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "21.05GB", + "bandwidth": "15.79GB", "reconnects": 0, - "status_2xx": 1809329, + "status_2xx": 1357644, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/static-6800.json b/site/data/static-6800.json index 940586732..03250b330 100644 --- a/site/data/static-6800.json +++ b/site/data/static-6800.json @@ -1215,20 +1215,20 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 347455, - "avg_latency": "67.36ms", - "p99_latency": "67.36ms", - "cpu": "2512.6%", - "memory": "91MiB", + "rps": 208613, + "avg_latency": "55.22ms", + "p99_latency": "1.20s", + "cpu": "5321.7%", + "memory": "4.5GiB", "connections": 6800, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "20.61GB", + "bandwidth": "12.38GB", "reconnects": 0, - "status_2xx": 1772290, + "status_2xx": 1064145, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/upload-256.json b/site/data/upload-256.json index 311533ad9..562e0fc01 100644 --- a/site/data/upload-256.json +++ b/site/data/upload-256.json @@ -1159,21 +1159,21 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 54, - "avg_latency": "579.42ms", - "p99_latency": "4.15s", - "cpu": "216.7%", - "memory": "2.2GiB", + "rps": 56, + "avg_latency": "368.94ms", + "p99_latency": "3.88s", + "cpu": "131.5%", + "memory": "1.4GiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "5.95KB/s", - "input_bw": "438.60MB/s", - "reconnects": 23, - "status_2xx": 273, + "bandwidth": "6.15KB/s", + "input_bw": "454.84MB/s", + "reconnects": 48, + "status_2xx": 282, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/data/upload-32.json b/site/data/upload-32.json index cc445d85c..ed64185a9 100644 --- a/site/data/upload-32.json +++ b/site/data/upload-32.json @@ -1159,21 +1159,21 @@ "status_5xx": 0 }, { - "framework": "vanilla", + "framework": "vanilla-epoll", "language": "V", - "rps": 31, - "avg_latency": "585.06ms", - "p99_latency": "4.00s", - "cpu": "168.0%", - "memory": "1.1GiB", + "rps": 30, + "avg_latency": "330.02ms", + "p99_latency": "3.67s", + "cpu": "144.2%", + "memory": "844MiB", "connections": 32, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "3.40KB/s", - "input_bw": "251.79MB/s", - "reconnects": 25, - "status_2xx": 155, + "bandwidth": "3.29KB/s", + "input_bw": "243.66MB/s", + "reconnects": 27, + "status_2xx": 150, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/static/logs/api-16/1024/vanilla-epoll.log b/site/static/logs/api-16/1024/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/api-16/1024/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/api-4/256/vanilla-epoll.log b/site/static/logs/api-4/256/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/api-4/256/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/async-db/1024/vanilla-epoll.log b/site/static/logs/async-db/1024/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/async-db/1024/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/baseline/4096/vanilla-epoll.log b/site/static/logs/baseline/4096/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/baseline/4096/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/baseline/512/vanilla-epoll.log b/site/static/logs/baseline/512/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/baseline/512/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/crud/4096/vanilla-epoll.log b/site/static/logs/crud/4096/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/crud/4096/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/fortunes/1024/vanilla-epoll.log b/site/static/logs/fortunes/1024/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/fortunes/1024/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/json-comp/16384/vanilla-epoll.log b/site/static/logs/json-comp/16384/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/json-comp/16384/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/json-comp/4096/vanilla-epoll.log b/site/static/logs/json-comp/4096/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/json-comp/4096/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/json-comp/512/vanilla-epoll.log b/site/static/logs/json-comp/512/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/json-comp/512/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/json/4096/vanilla-epoll.log b/site/static/logs/json/4096/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/json/4096/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/limited-conn/4096/vanilla-epoll.log b/site/static/logs/limited-conn/4096/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/limited-conn/4096/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/limited-conn/512/vanilla-epoll.log b/site/static/logs/limited-conn/512/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/limited-conn/512/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/pipelined/4096/vanilla-epoll.log b/site/static/logs/pipelined/4096/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/pipelined/4096/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/pipelined/512/vanilla-epoll.log b/site/static/logs/pipelined/512/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/pipelined/512/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/static/1024/vanilla-epoll.log b/site/static/logs/static/1024/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/static/1024/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/static/4096/vanilla-epoll.log b/site/static/logs/static/4096/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/static/4096/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/static/6800/vanilla-epoll.log b/site/static/logs/static/6800/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/static/6800/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/upload/256/vanilla-epoll.log b/site/static/logs/upload/256/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/upload/256/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing diff --git a/site/static/logs/upload/32/vanilla-epoll.log b/site/static/logs/upload/32/vanilla-epoll.log new file mode 100644 index 000000000..4b4818f51 --- /dev/null +++ b/site/static/logs/upload/32/vanilla-epoll.log @@ -0,0 +1 @@ +[socket] SO_REUSEPORT enabled for load balancing