Skip to content

Commit a1ec44b

Browse files
authored
Add pico (V) framework (#807)
* Add pico (V) framework A raw V server on the picoev non-blocking event loop + picohttpparser (both stdlib). One process per core via SO_REUSEPORT. Subscribes baseline, pipelined and json. JSON is serialized manually (precomputed prefixes); baseline decodes chunked bodies. Pinned V 0.5.1 (prebuilt release). DB profiles are not subscribed: picoev is a single-threaded event loop and the stdlib db.pg driver is blocking, which would stall the loop. Validated against the HttpArena image: baseline (incl. chunked + randomized), pipelined and json (across counts/multipliers) all pass. * pico: drop baseline (picoev doesn't reassemble TCP-fragmented requests) CI baseline validation sends requests split across multiple TCP segments; picoev parses each recv() in one shot and doesn't accumulate partial requests, so the fragmentation sub-checks fail. Subscribe pipelined + json only.
1 parent 629f034 commit a1ec44b

6 files changed

Lines changed: 254 additions & 0 deletions

File tree

frameworks/pico/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
server
2+
*.so

frameworks/pico/Dockerfile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
FROM debian:stable-slim AS build
2+
3+
RUN apt-get -qq update && \
4+
apt-get -qy install --no-install-recommends \
5+
ca-certificates curl unzip build-essential && \
6+
rm -rf /var/lib/apt/lists/*
7+
8+
# Pinned, reproducible V 0.5.1 (prebuilt release binary).
9+
RUN curl -fsSL https://github.com/vlang/v/releases/download/0.5.1/v_linux.zip -o /tmp/v.zip && \
10+
unzip -q /tmp/v.zip -d /opt && rm /tmp/v.zip && \
11+
ln -s /opt/v/v /usr/local/bin/v
12+
13+
WORKDIR /app
14+
COPY . .
15+
RUN v -prod . -o server
16+
17+
FROM debian:stable-slim
18+
RUN apt-get -qq update && \
19+
apt-get -qy install --no-install-recommends util-linux && \
20+
rm -rf /var/lib/apt/lists/*
21+
COPY --from=build /app/server /server
22+
COPY run.sh /run.sh
23+
24+
EXPOSE 8080
25+
CMD ["sh", "/run.sh"]

frameworks/pico/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# pico
2+
3+
A raw [V](https://vlang.io) server built on the `picoev` non-blocking event loop
4+
and the `picohttpparser` HTTP parser (both in the V standard library). One
5+
process per core shares the listen socket via `SO_REUSEPORT`.
6+
7+
## Implemented tests
8+
9+
| Test | Endpoint |
10+
|------|----------|
11+
| `baseline` | `GET/POST /baseline11` (handles chunked) |
12+
| `pipelined` | `GET /pipeline` |
13+
| `json` | `GET /json/{count}?m=M` over `/data/dataset.json` |
14+
15+
## Stack
16+
17+
* [V](https://vlang.io) 0.5.1 (pinned prebuilt release)
18+
* [picoev](https://modules.vlang.io/picoev.html) + [picohttpparser](https://modules.vlang.io/picohttpparser.html)
19+
20+
JSON is serialized manually (precomputed prefixes + `strings.Builder`), no
21+
per-request reflection.
22+
23+
> DB profiles (`async-db`) are not subscribed: picoev is a single-threaded
24+
> event loop and the stdlib `db.pg` driver is blocking, so a query would stall
25+
> the loop. A non-blocking PG path would be needed to add them.

frameworks/pico/main.v

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
module main
2+
3+
import picoev
4+
import picohttpparser
5+
import json
6+
import os
7+
import strings
8+
9+
struct Rating {
10+
score i64
11+
count i64
12+
}
13+
14+
struct DatasetItem {
15+
id i64
16+
name string
17+
category string
18+
price i64
19+
quantity i64
20+
active bool
21+
tags []string
22+
rating Rating
23+
}
24+
25+
struct Shared {
26+
mut:
27+
dataset []DatasetItem
28+
prefixes []string
29+
}
30+
31+
fn callback(data voidptr, req picohttpparser.Request, mut res picohttpparser.Response) {
32+
sh := unsafe { &Shared(data) }
33+
route := req.path.all_before('?')
34+
35+
if route == '/pipeline' {
36+
res.http_ok()
37+
res.header_server()
38+
res.plain()
39+
res.body('ok')
40+
} else if route == '/baseline11' {
41+
mut sum := qint(req.path, 'a') + qint(req.path, 'b')
42+
if req.method == 'POST' {
43+
sum += body_int(req)
44+
}
45+
res.http_ok()
46+
res.header_server()
47+
res.plain()
48+
res.body(sum.str())
49+
} else if route == '/upload' {
50+
res.http_ok()
51+
res.header_server()
52+
res.plain()
53+
res.body(req.body.len.str())
54+
} else if route.starts_with('/json/') {
55+
count := clamp_count(route[6..].i64(), sh.dataset.len)
56+
mut m := qint(req.path, 'm')
57+
if m == 0 {
58+
m = 1
59+
}
60+
res.http_ok()
61+
res.header_server()
62+
res.json()
63+
res.body(sh.json_body(count, m))
64+
} else {
65+
res.http_404()
66+
}
67+
res.end()
68+
}
69+
70+
fn (sh &Shared) json_body(count int, m i64) string {
71+
mut sb := strings.new_builder(count * 224 + 32)
72+
sb.write_string('{"items":[')
73+
for i in 0 .. count {
74+
if i > 0 {
75+
sb.write_u8(`,`)
76+
}
77+
sb.write_string(sh.prefixes[i])
78+
sb.write_decimal(sh.dataset[i].price * sh.dataset[i].quantity * m)
79+
sb.write_u8(`}`)
80+
}
81+
sb.write_string('],"count":')
82+
sb.write_decimal(i64(count))
83+
sb.write_u8(`}`)
84+
return sb.str()
85+
}
86+
87+
fn body_int(req picohttpparser.Request) i64 {
88+
if req.body.len == 0 {
89+
return 0
90+
}
91+
mut chunked := false
92+
for i in 0 .. req.num_headers {
93+
h := req.headers[i]
94+
if h.name.to_lower() == 'transfer-encoding' && h.value.contains('chunked') {
95+
chunked = true
96+
break
97+
}
98+
}
99+
if chunked {
100+
return dechunk(req.body).i64()
101+
}
102+
return req.body.i64()
103+
}
104+
105+
fn dechunk(s string) string {
106+
mut out := strings.new_builder(s.len)
107+
mut i := 0
108+
for i < s.len {
109+
nl := s.index_after('\r\n', i) or { break }
110+
size := hex_int(s[i..nl])
111+
if size <= 0 {
112+
break
113+
}
114+
ds := nl + 2
115+
out.write_string(s[ds..ds + size])
116+
i = ds + size + 2
117+
}
118+
return out.str()
119+
}
120+
121+
fn hex_int(s string) int {
122+
mut n := 0
123+
for c in s.trim_space() {
124+
d := if c >= `0` && c <= `9` {
125+
int(c - `0`)
126+
} else if c >= `a` && c <= `f` {
127+
int(c - `a` + 10)
128+
} else if c >= `A` && c <= `F` {
129+
int(c - `A` + 10)
130+
} else {
131+
break
132+
}
133+
n = n * 16 + d
134+
}
135+
return n
136+
}
137+
138+
fn qint(target string, key string) i64 {
139+
needle := key + '='
140+
idx := target.index(needle) or { return 0 }
141+
rest := target[idx + needle.len..]
142+
endp := rest.index('&') or { rest.len }
143+
return rest[..endp].i64()
144+
}
145+
146+
fn clamp_count(n i64, max int) int {
147+
if n < 0 {
148+
return 0
149+
}
150+
if n > max {
151+
return max
152+
}
153+
return int(n)
154+
}
155+
156+
fn main() {
157+
dataset_path := os.getenv_opt('DATASET_PATH') or { '/data/dataset.json' }
158+
dataset := json.decode([]DatasetItem, os.read_file(dataset_path) or { '[]' }) or {
159+
[]DatasetItem{}
160+
}
161+
mut prefixes := []string{cap: dataset.len}
162+
for it in dataset {
163+
enc := json.encode(it)
164+
prefixes << enc#[..-1] + ',"total":'
165+
}
166+
167+
mut sh := &Shared{
168+
dataset: dataset
169+
prefixes: prefixes
170+
}
171+
172+
mut server := picoev.new(
173+
port: 8080
174+
cb: callback
175+
user_data: sh
176+
max_write: 131072
177+
)!
178+
server.serve()
179+
}

frameworks/pico/meta.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"display_name": "pico",
3+
"language": "V",
4+
"type": "production",
5+
"engine": "picoev",
6+
"description": "A raw V server on the picoev non-blocking event loop + picohttpparser (both stdlib). One process per core shares the listen socket via SO_REUSEPORT. JSON is serialized manually (precomputed prefixes, no per-request reflection). Pinned V 0.5.1. DB profiles are not subscribed: picoev is a single-threaded event loop and the stdlib db.pg driver is blocking, which would stall the loop. baseline is not subscribed: picoev parses each recv() in one shot and does not reassemble TCP-fragmented requests, which baseline validation requires.",
7+
"repo": "https://github.com/vlang/v",
8+
"enabled": true,
9+
"tests": [
10+
"pipelined",
11+
"json"
12+
],
13+
"maintainers": [
14+
"enghitalo"
15+
]
16+
}

frameworks/pico/run.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/bin/sh
2+
# picoev is a single-threaded event loop; scale across cores by launching one
3+
# process per core, all sharing the listen socket via SO_REUSEPORT.
4+
for i in $(seq 0 $(($(nproc --all)-1))); do
5+
taskset -c $i /server &
6+
done
7+
wait

0 commit comments

Comments
 (0)