diff --git a/frameworks/viltrum/Dockerfile b/frameworks/viltrum/Dockerfile index 00c70a3036..aee2b1eceb 100644 --- a/frameworks/viltrum/Dockerfile +++ b/frameworks/viltrum/Dockerfile @@ -9,7 +9,7 @@ RUN curl -fsSL -o /tmp/v.zip https://github.com/vlang/v/releases/download/0.5.2/ unzip -q /tmp/v.zip -d /opt && rm /tmp/v.zip ENV PATH=/opt/v:$PATH -ARG VILTRUM_REF=v0.13.0 +ARG VILTRUM_REF=v1.1.0 RUN git clone --depth 1 --branch ${VILTRUM_REF} https://github.com/Tuntii/viltrum /root/.vmodules/viltrum WORKDIR /app diff --git a/frameworks/viltrum/README.md b/frameworks/viltrum/README.md index 6289d8bf03..bad8668b2c 100644 --- a/frameworks/viltrum/README.md +++ b/frameworks/viltrum/README.md @@ -3,12 +3,14 @@ [viltrum](https://github.com/Tuntii/viltrum) is an HTTP framework for V with its own HTTP/1.1 engine (no stdlib `net.http` underneath). Docs: https://github.com/Tuntii/viltrum/tree/main/docs -This entry runs one process with one epoll loop per CPU (`epoll_cores`, `SO_REUSEPORT`), which is -why it is `tuned`: the framework default is thread-per-connection. +This entry runs viltrum with its default configuration (standard mode). | Profile | Endpoint | Notes | |---------|----------|-------| -| baseline, limited-conn | `GET/POST /baseline11` | query params via `req.query_param`, body via `req.body` (Content-Length or chunked) | -| pipelined | `GET /pipeline` | pipelined responses are batched into one write by the engine | -| json-comp | `GET /json/:count?m=` | `json.encode` per request, `viltrum.gzip` middleware | -| echo-ws | `/ws` | `app.ws`, viltrum's own WebSocket implementation | +| baseline, limited-conn | `GET/POST /baseline11` | `c.query_int`, `c.body_int` (Content-Length or chunked) | +| pipelined | `GET /pipeline` | the engine batches pipelined responses into one write | +| json-comp | `GET /json/:count?m=` | `c.json` per request, `viltrum.gzip` middleware | +| async | `GET /delay/:ms` | `c.after` answers from a per-worker timer; the worker is not held while the request waits | + +The entry is typed `experimental`: viltrum's first commit is from July 2026, so it is new work +that has not proved itself in production yet, which is what that tier is for. diff --git a/frameworks/viltrum/main.v b/frameworks/viltrum/main.v index 1d911d2bf4..2f4f83bd4c 100644 --- a/frameworks/viltrum/main.v +++ b/frameworks/viltrum/main.v @@ -2,7 +2,6 @@ module main import json import os -import runtime import viltrum struct Rating { @@ -19,85 +18,74 @@ struct Item { active bool tags []string rating Rating +mut: + total i64 } -struct PricedItem { - id int - name string - category string - price int - quantity int - active bool - tags []string - rating Rating - total i64 -} - -struct JsonPage { - items []PricedItem +struct Page { + items []Item count int } fn main() { - raw := os.read_file('/data/dataset.json') or { '[]' } - dataset := json.decode([]Item, raw) or { []Item{} } - + dataset := json.decode([]Item, os.read_file('/data/dataset.json') or { '[]' }) or { []Item{} } mut app := viltrum.new() - app.server_options(viltrum.ServerOptions{ - handle_signals: false - send_date: true - server_header: 'viltrum' - epoll_cores: runtime.nr_cpus() - }) - + // One copy of the dataset per worker: totals are written per request, and a + // worker only touches its own copy. + mut copies := [][]Item{} + for _ in 0 .. app.worker_count() { + copies << dataset.clone() + } app.get('/baseline11', baseline) app.post('/baseline11', baseline) - app.get('/pipeline', fn (_ viltrum.Request) viltrum.Response { - return viltrum.text(200, 'ok') + app.get('/pipeline', fn (mut c viltrum.Ctx) { + c.text(200, 'ok') }) - app.get('/json/:count', viltrum.chain([viltrum.gzip], fn [dataset] (req viltrum.Request) viltrum.Response { - mut n := (req.param('count') or { '0' }).int() + // Only scalars are local here: V -prod pins every local struct or array around each + // call statement (see docs/request-response.md), so the dataset stays captured. + app.get('/json/:count', viltrum.gzip, fn [mut copies] (mut c viltrum.Ctx) { + w := c.worker_id() + mut n := int(c.param_int('count') or { 0 }) if n < 0 { n = 0 - } else if n > dataset.len { - n = dataset.len + } else if n > copies[w].len { + n = copies[w].len } - m := i64((req.query_param('m') or { '1' }).int()) - mut items := []PricedItem{cap: n} - for it in dataset[..n] { - items << PricedItem{ - id: it.id - name: it.name - category: it.category - price: it.price - quantity: it.quantity - active: it.active - tags: it.tags - rating: it.rating - total: i64(it.price) * i64(it.quantity) * m - } + m := c.query_int('m') or { 1 } + for i in 0 .. n { + copies[w][i].total = i64(copies[w][i].price) * i64(copies[w][i].quantity) * m } - return viltrum.json(200, json.encode(JsonPage{ items: items, count: n })) - })) - app.ws('/ws', fn (mut s viltrum.WsSocket) { - for { - msg := s.read_message() or { break } - if msg.is_text() { - s.write_text(msg.text()) or { break } - } else if msg.is_binary() { - s.write_binary(msg.data) or { break } - } + c.json(200, Page{ + items: first(copies[w], n) + count: n + }) + }) + // The worker is not held while a request waits: after() answers it from a timer. + app.get('/delay/:ms', fn (mut c viltrum.Ctx) { + ms := c.param_int('ms') or { -1 } + if ms < 0 { + c.text(400, 'bad delay') + return } - s.close_quiet() + c.after(int(ms), delay_done, u64(ms)) }) app.listen('0.0.0.0:8080') or { panic(err) } } -fn baseline(req viltrum.Request) viltrum.Response { - mut sum := i64((req.query_param('a') or { '0' }).int()) + - (req.query_param('b') or { '0' }).int() - if req.body.len > 0 { - sum += req.text().trim_space().i64() +// first is a view of a's first n items; safe because n <= a.len and jsonenc only reads. +fn first(a []Item, n int) []Item { + mut v := unsafe { a } + unsafe { + v.len = n } - return viltrum.text(200, sum.str()) + return v +} + +fn delay_done(mut c viltrum.Ctx, ms u64) { + c.text_int(200, i64(ms)) +} + +fn baseline(mut c viltrum.Ctx) { + sum := (c.query_int('a') or { 0 }) + (c.query_int('b') or { 0 }) + (c.body_int() or { 0 }) + c.text_int(200, sum) } diff --git a/frameworks/viltrum/meta.json b/frameworks/viltrum/meta.json index 80fe423cf2..280c1e80d4 100644 --- a/frameworks/viltrum/meta.json +++ b/frameworks/viltrum/meta.json @@ -2,12 +2,12 @@ "display_name": "viltrum", "language": "V", "engine": "viltrum", - "type": "emerging", - "mode": "tuned", + "type": "experimental", + "mode": "standard", "completeness": { "routing": true, "middleware": true, "request": true, "response": true }, - "description": "HTTP framework for V with its own HTTP/1.1 engine: router with :params, middleware chain, first-party WebSocket, strict request framing (Content-Length and chunked, smuggling-hardened, fuzzed in CI). Tuned: one SO_REUSEPORT epoll loop per CPU (epoll_cores) instead of the default thread-per-connection I/O. json-comp uses viltrum's gzip middleware.", + "description": "HTTP framework for V with its own HTTP/1.1 engine: one SO_REUSEPORT epoll worker per CPU, pipelined responses batched into one write, and no allocation per request inside the framework (CI-enforced). Router with :params, middleware chain (c.next()), compile-time JSON encoder, gzip middleware on miniz. Request framing is smuggling-hardened (Content-Length and chunked) and fuzzed in CI. Default configuration.", "repo": "https://github.com/Tuntii/viltrum", "enabled": true, - "tests": ["baseline", "pipelined", "limited-conn", "json-comp", "echo-ws"], + "tests": ["baseline", "pipelined", "limited-conn", "json-comp", "latency-1m", "latency-10k", "latency-500k-8cpu", "async"], "maintainers": ["Tuntii"] } diff --git a/site/data/frameworks.json b/site/data/frameworks.json index e42b62f88e..8de6f02778 100644 --- a/site/data/frameworks.json +++ b/site/data/frameworks.json @@ -2420,11 +2420,11 @@ }, "viltrum": { "dir": "viltrum", - "description": "HTTP framework for V with its own HTTP/1.1 engine: router with :params, middleware chain, first-party WebSocket, strict request framing (Content-Length and chunked, smuggling-hardened, fuzzed in CI). Tuned: one SO_REUSEPORT epoll loop per CPU (epoll_cores) instead of the default thread-per-connection I/O. json-comp uses viltrum's gzip middleware.", + "description": "HTTP framework for V with its own HTTP/1.1 engine: one SO_REUSEPORT epoll worker per CPU, pipelined responses batched into one write, and no allocation per request inside the framework (CI-enforced). Router with :params, middleware chain (c.next()), compile-time JSON encoder, gzip middleware on miniz. Request framing is smuggling-hardened (Content-Length and chunked) and fuzzed in CI. Default configuration.", "repo": "https://github.com/Tuntii/viltrum", - "type": "emerging", + "type": "experimental", "engine": "viltrum", - "mode": "tuned", + "mode": "standard", "completeness": { "routing": true, "middleware": true, diff --git a/site/data/results/viltrum.json b/site/data/results/viltrum.json index e75c8ae88b..76a879b745 100644 --- a/site/data/results/viltrum.json +++ b/site/data/results/viltrum.json @@ -1,22 +1,42 @@ { "framework": "viltrum", "results": { + "async-32000": { + "framework": "viltrum", + "language": "V", + "rps": 2482163, + "avg_latency": "12.79ms", + "p99_latency": "15.40ms", + "cpu": "4478.6%", + "memory": "1.2GiB", + "connections": 32000, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "279.28MB/s", + "input_bw": "113.62MB/s", + "reconnects": 0, + "status_2xx": 24821637, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, "baseline-4096": { "framework": "viltrum", "language": "V", - "rps": 585594, - "avg_latency": "7.03ms", - "p99_latency": "17.70ms", - "cpu": "1871.9%", - "memory": "124MiB", + "rps": 3940680, + "avg_latency": "1.04ms", + "p99_latency": "2.19ms", + "cpu": "6270.3%", + "memory": "211MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "88.78MB/s", - "input_bw": "45.24MB/s", + "bandwidth": "443.36MB/s", + "input_bw": "304.41MB/s", "reconnects": 0, - "status_2xx": 2927973, + "status_2xx": 19703400, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -81,19 +101,19 @@ "json-comp-16384": { "framework": "viltrum", "language": "V", - "rps": 17366, - "avg_latency": "847.75ms", - "p99_latency": "1.16s", - "cpu": "2398.3%", - "memory": "481MiB", + "rps": 335612, + "avg_latency": "48.38ms", + "p99_latency": "114.60ms", + "cpu": "6406.9%", + "memory": "976MiB", "connections": 16384, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "29.61MB/s", - "input_bw": "1.29MB/s", - "reconnects": 0, - "status_2xx": 86834, + "bandwidth": "444.19MB/s", + "input_bw": "24.97MB/s", + "reconnects": 60589, + "status_2xx": 1678063, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -101,39 +121,111 @@ "json-comp-4096": { "framework": "viltrum", "language": "V", - "rps": 14418, - "avg_latency": "276.28ms", - "p99_latency": "454.20ms", - "cpu": "2249.2%", - "memory": "177MiB", + "rps": 341066, + "avg_latency": "11.99ms", + "p99_latency": "35.00ms", + "cpu": "6259.2%", + "memory": "347MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "24.59MB/s", - "input_bw": "1.07MB/s", - "reconnects": 345, - "status_2xx": 72091, + "bandwidth": "451.48MB/s", + "input_bw": "25.37MB/s", + "reconnects": 66721, + "status_2xx": 1705333, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 }, + "latency-10k-1024": { + "framework": "viltrum", + "language": "V", + "rps": 9980, + "avg_latency": "70.5us", + "p99_latency": "102.0us", + "cpu": "23.2%", + "memory": "75MiB", + "connections": 1024, + "threads": 64, + "duration": "20s", + "pipeline": 1, + "bandwidth": "1.17MB/s", + "reconnects": 0, + "status_2xx": 199627, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0, + "cpu_usec": 4844148, + "cpu_per_req_us": 24.266, + "target_rate": 10000, + "rate_ratio": 0.998, + "p99_9_latency": "141.0us" + }, + "latency-1m-1024": { + "framework": "viltrum", + "language": "V", + "rps": 998346, + "avg_latency": "93.8us", + "p99_latency": "166.0us", + "cpu": "2260.3%", + "memory": "76MiB", + "connections": 1024, + "threads": 64, + "duration": "20s", + "pipeline": 1, + "bandwidth": "116.81MB/s", + "reconnects": 0, + "status_2xx": 19970563, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0, + "cpu_usec": 412294705, + "cpu_per_req_us": 20.6451, + "target_rate": 1000000, + "rate_ratio": 0.9983, + "p99_9_latency": "3317.0us" + }, + "latency-500k-8cpu-1024": { + "framework": "viltrum", + "language": "V", + "rps": 496317, + "avg_latency": "55250.3us", + "p99_latency": "541952.0us", + "cpu": "792.5%", + "memory": "56MiB", + "connections": 1024, + "threads": 64, + "duration": "20s", + "pipeline": 1, + "bandwidth": "58.07MB/s", + "reconnects": 0, + "status_2xx": 9930067, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0, + "cpu_usec": 153203317, + "cpu_per_req_us": 15.4282, + "target_rate": 500000, + "rate_ratio": 0.9926, + "p99_9_latency": "611584.0us" + }, "limited-conn-4096": { "framework": "viltrum", "language": "V", - "rps": 538675, - "avg_latency": "7.58ms", - "p99_latency": "30.20ms", - "cpu": "2267.3%", - "memory": "131MiB", + "rps": 2555570, + "avg_latency": "1.46ms", + "p99_latency": "2.25ms", + "cpu": "5905.8%", + "memory": "499MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "81.66MB/s", - "input_bw": "41.61MB/s", - "reconnects": 267765, - "status_2xx": 2693376, + "bandwidth": "287.34MB/s", + "input_bw": "197.41MB/s", + "reconnects": 1277510, + "status_2xx": 12777852, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -141,18 +233,18 @@ "pipelined-4096": { "framework": "viltrum", "language": "V", - "rps": 878128, - "avg_latency": "74.40ms", - "p99_latency": "116.40ms", - "cpu": "1291.7%", - "memory": "150MiB", + "rps": 25879708, + "avg_latency": "2.53ms", + "p99_latency": "3.38ms", + "cpu": "6781.9%", + "memory": "204MiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "133.10MB/s", + "bandwidth": "2.84GB/s", "reconnects": 0, - "status_2xx": 4390640, + "status_2xx": 129398544, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/static/logs/async/32000/viltrum.log b/site/static/logs/async/32000/viltrum.log new file mode 100644 index 0000000000..4c36bd0d6d --- /dev/null +++ b/site/static/logs/async/32000/viltrum.log @@ -0,0 +1 @@ +[viltrum] listening on http://0.0.0.0:8080 (64 workers) diff --git a/site/static/logs/baseline/4096/viltrum.log b/site/static/logs/baseline/4096/viltrum.log index 9b2ffcb900..4c36bd0d6d 100644 --- a/site/static/logs/baseline/4096/viltrum.log +++ b/site/static/logs/baseline/4096/viltrum.log @@ -1,2 +1 @@ -[viltrum] epoll: upgrade/WS is handed off its own thread -[viltrum] listening on http://0.0.0.0:8080 (epoll_cores=128) +[viltrum] listening on http://0.0.0.0:8080 (64 workers) diff --git a/site/static/logs/json-comp/16384/viltrum.log b/site/static/logs/json-comp/16384/viltrum.log index 9b2ffcb900..4c36bd0d6d 100644 --- a/site/static/logs/json-comp/16384/viltrum.log +++ b/site/static/logs/json-comp/16384/viltrum.log @@ -1,2 +1 @@ -[viltrum] epoll: upgrade/WS is handed off its own thread -[viltrum] listening on http://0.0.0.0:8080 (epoll_cores=128) +[viltrum] listening on http://0.0.0.0:8080 (64 workers) diff --git a/site/static/logs/json-comp/4096/viltrum.log b/site/static/logs/json-comp/4096/viltrum.log index 9b2ffcb900..4c36bd0d6d 100644 --- a/site/static/logs/json-comp/4096/viltrum.log +++ b/site/static/logs/json-comp/4096/viltrum.log @@ -1,2 +1 @@ -[viltrum] epoll: upgrade/WS is handed off its own thread -[viltrum] listening on http://0.0.0.0:8080 (epoll_cores=128) +[viltrum] listening on http://0.0.0.0:8080 (64 workers) diff --git a/site/static/logs/latency-10k/1024/viltrum.log b/site/static/logs/latency-10k/1024/viltrum.log new file mode 100644 index 0000000000..4c36bd0d6d --- /dev/null +++ b/site/static/logs/latency-10k/1024/viltrum.log @@ -0,0 +1 @@ +[viltrum] listening on http://0.0.0.0:8080 (64 workers) diff --git a/site/static/logs/latency-1m/1024/viltrum.log b/site/static/logs/latency-1m/1024/viltrum.log new file mode 100644 index 0000000000..4c36bd0d6d --- /dev/null +++ b/site/static/logs/latency-1m/1024/viltrum.log @@ -0,0 +1 @@ +[viltrum] listening on http://0.0.0.0:8080 (64 workers) diff --git a/site/static/logs/latency-500k-8cpu/1024/viltrum.log b/site/static/logs/latency-500k-8cpu/1024/viltrum.log new file mode 100644 index 0000000000..71dfc8f547 --- /dev/null +++ b/site/static/logs/latency-500k-8cpu/1024/viltrum.log @@ -0,0 +1 @@ +[viltrum] listening on http://0.0.0.0:8080 (8 workers) diff --git a/site/static/logs/limited-conn/4096/viltrum.log b/site/static/logs/limited-conn/4096/viltrum.log index 9b2ffcb900..4c36bd0d6d 100644 --- a/site/static/logs/limited-conn/4096/viltrum.log +++ b/site/static/logs/limited-conn/4096/viltrum.log @@ -1,2 +1 @@ -[viltrum] epoll: upgrade/WS is handed off its own thread -[viltrum] listening on http://0.0.0.0:8080 (epoll_cores=128) +[viltrum] listening on http://0.0.0.0:8080 (64 workers) diff --git a/site/static/logs/pipelined/4096/viltrum.log b/site/static/logs/pipelined/4096/viltrum.log index 9b2ffcb900..4c36bd0d6d 100644 --- a/site/static/logs/pipelined/4096/viltrum.log +++ b/site/static/logs/pipelined/4096/viltrum.log @@ -1,2 +1 @@ -[viltrum] epoll: upgrade/WS is handed off its own thread -[viltrum] listening on http://0.0.0.0:8080 (epoll_cores=128) +[viltrum] listening on http://0.0.0.0:8080 (64 workers)