Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frameworks/viltrum/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 8 additions & 6 deletions frameworks/viltrum/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
112 changes: 50 additions & 62 deletions frameworks/viltrum/main.v
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ module main

import json
import os
import runtime
import viltrum

struct Rating {
Expand All @@ -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)
}
8 changes: 4 additions & 4 deletions frameworks/viltrum/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
6 changes: 3 additions & 3 deletions site/data/frameworks.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading