Skip to content

Commit 85ecee4

Browse files
Add Prologue (Nim) web framework
Prologue is a powerful web framework for Nim that compiles to native C. Uses httpbeast/httpx under the hood with epoll and multi-threaded request handling. This is the first Nim framework in HttpArena, adding language diversity to the benchmark suite. Implements all standard endpoints: /pipeline, /baseline11, /baseline2, /json, /compression, /upload, /db, /static/{filename}
1 parent 511d334 commit 85ecee4

5 files changed

Lines changed: 322 additions & 0 deletions

File tree

frameworks/prologue/Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM nimlang/nim:2.0.14-alpine AS build
2+
WORKDIR /app
3+
COPY src/ ./
4+
RUN nimble install -y prologue zippy
5+
RUN nim c -d:release -d:danger --opt:speed --threads:on -o:server server.nim
6+
7+
FROM alpine:3.19
8+
RUN apk add --no-cache libgcc
9+
COPY --from=build /app/server /server
10+
EXPOSE 8080
11+
CMD ["/server"]

frameworks/prologue/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Prologue (Nim)
2+
3+
[Prologue](https://github.com/planety/prologue) is a powerful and flexible web framework written in Nim. Under the hood it uses [httpbeast](https://github.com/nicholasgasior/httpbeast) (via httpx) — a high-performance HTTP server that leverages epoll on Linux with multi-threaded request handling.
4+
5+
## Why Prologue?
6+
7+
- **Nim compiles to C** — native performance with a high-level, Python-like syntax
8+
- **httpbeast backend** — uses epoll and SO_REUSEPORT for multi-core scaling
9+
- **Zero-overhead routing** — trie-based router compiled at build time
10+
- **First Nim framework in HttpArena** — brings a new language to the benchmarks
11+
12+
## Build
13+
14+
```bash
15+
nim c -d:release -d:danger --opt:speed --threads:on -o:server server.nim
16+
```
17+
18+
## Endpoints
19+
20+
All standard HttpArena endpoints are implemented: `/pipeline`, `/baseline11`, `/baseline2`, `/json`, `/compression`, `/upload`, `/db`, `/static/{filename}`.

frameworks/prologue/meta.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"display_name": "prologue",
3+
"language": "Nim",
4+
"type": "framework",
5+
"engine": "asynchttpserver",
6+
"description": "Prologue web framework for Nim — compiles to native C with powerful routing and middleware support.",
7+
"repo": "https://github.com/planety/prologue",
8+
"enabled": true,
9+
"tests": [
10+
"baseline",
11+
"pipelined",
12+
"limited-conn",
13+
"json",
14+
"upload",
15+
"compression",
16+
"mixed",
17+
"noisy"
18+
]
19+
}

frameworks/prologue/src/server.nim

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
import prologue
2+
import std/[json, strutils, math, os, tables]
3+
import std/db_sqlite
4+
import std/zippy
5+
6+
type
7+
Rating = object
8+
score: float
9+
count: int
10+
11+
DatasetItem = object
12+
id: int
13+
name: string
14+
category: string
15+
price: float
16+
quantity: int
17+
active: bool
18+
tags: seq[string]
19+
rating: Rating
20+
21+
var dataset: seq[DatasetItem]
22+
var jsonLargeResponse: string
23+
var staticFiles: Table[string, (string, string)] # filename -> (data, content_type)
24+
var db: DbConn
25+
var dbAvailable: bool
26+
27+
proc getMime(ext: string): string =
28+
case ext
29+
of ".css": "text/css"
30+
of ".js": "application/javascript"
31+
of ".html": "text/html"
32+
of ".woff2": "font/woff2"
33+
of ".svg": "image/svg+xml"
34+
of ".webp": "image/webp"
35+
of ".json": "application/json"
36+
else: "application/octet-stream"
37+
38+
proc loadDataset() =
39+
let path = getEnv("DATASET_PATH", "/data/dataset.json")
40+
if not fileExists(path):
41+
return
42+
let data = readFile(path)
43+
let j = parseJson(data)
44+
dataset = @[]
45+
for item in j:
46+
var tags: seq[string] = @[]
47+
for tag in item["tags"]:
48+
tags.add(tag.getStr())
49+
dataset.add(DatasetItem(
50+
id: item["id"].getInt(),
51+
name: item["name"].getStr(),
52+
category: item["category"].getStr(),
53+
price: item["price"].getFloat(),
54+
quantity: item["quantity"].getInt(),
55+
active: item["active"].getBool(),
56+
tags: tags,
57+
rating: Rating(
58+
score: item["rating"]["score"].getFloat(),
59+
count: item["rating"]["count"].getInt()
60+
)
61+
))
62+
63+
proc buildProcessedJson(items: seq[DatasetItem]): string =
64+
var processed = newJArray()
65+
for d in items:
66+
let total = round(d.price * float(d.quantity) * 100.0) / 100.0
67+
var tagsArr = newJArray()
68+
for t in d.tags:
69+
tagsArr.add(newJString(t))
70+
processed.add(%*{
71+
"id": d.id,
72+
"name": d.name,
73+
"category": d.category,
74+
"price": d.price,
75+
"quantity": d.quantity,
76+
"active": d.active,
77+
"tags": tagsArr,
78+
"rating": {"score": d.rating.score, "count": d.rating.count},
79+
"total": total
80+
})
81+
result = $(%*{"items": processed, "count": processed.len})
82+
83+
proc loadDatasetLarge() =
84+
let path = "/data/dataset-large.json"
85+
if not fileExists(path):
86+
return
87+
let data = readFile(path)
88+
let j = parseJson(data)
89+
var items: seq[DatasetItem] = @[]
90+
for item in j:
91+
var tags: seq[string] = @[]
92+
for tag in item["tags"]:
93+
tags.add(tag.getStr())
94+
items.add(DatasetItem(
95+
id: item["id"].getInt(),
96+
name: item["name"].getStr(),
97+
category: item["category"].getStr(),
98+
price: item["price"].getFloat(),
99+
quantity: item["quantity"].getInt(),
100+
active: item["active"].getBool(),
101+
tags: tags,
102+
rating: Rating(
103+
score: item["rating"]["score"].getFloat(),
104+
count: item["rating"]["count"].getInt()
105+
)
106+
))
107+
jsonLargeResponse = buildProcessedJson(items)
108+
109+
proc loadStaticFiles() =
110+
let dir = "/data/static"
111+
if not dirExists(dir):
112+
return
113+
for kind, path in walkDir(dir):
114+
if kind == pcFile:
115+
let filename = extractFilename(path)
116+
let data = readFile(path)
117+
let ext = if '.' in filename: filename[filename.rfind('.') .. ^1] else: ""
118+
let ct = getMime(ext)
119+
staticFiles[filename] = (data, ct)
120+
121+
proc loadDb() =
122+
try:
123+
db = open("/data/benchmark.db", "", "", "")
124+
dbAvailable = true
125+
except:
126+
dbAvailable = false
127+
128+
proc parseQuerySum(query: string): int =
129+
result = 0
130+
for pair in query.split('&'):
131+
let parts = pair.split('=', 1)
132+
if parts.len == 2:
133+
try:
134+
result += parseInt(parts[1])
135+
except ValueError:
136+
discard
137+
138+
proc pipelineHandler(ctx: Context) {.async.} =
139+
ctx.response.setHeader("Content-Type", "text/plain")
140+
resp "ok"
141+
142+
proc baseline11Handler(ctx: Context) {.async.} =
143+
var sum = 0
144+
let query = ctx.request.query
145+
if query.len > 0:
146+
sum = parseQuerySum(query)
147+
148+
let body = ctx.request.body
149+
if body.len > 0:
150+
try:
151+
sum += parseInt(body.strip())
152+
except ValueError:
153+
discard
154+
155+
ctx.response.setHeader("Content-Type", "text/plain")
156+
resp $sum
157+
158+
proc baseline2Handler(ctx: Context) {.async.} =
159+
var sum = 0
160+
let query = ctx.request.query
161+
if query.len > 0:
162+
sum = parseQuerySum(query)
163+
ctx.response.setHeader("Content-Type", "text/plain")
164+
resp $sum
165+
166+
proc jsonHandler(ctx: Context) {.async.} =
167+
let jsonStr = buildProcessedJson(dataset)
168+
ctx.response.setHeader("Content-Type", "application/json")
169+
resp jsonStr
170+
171+
proc compressionHandler(ctx: Context) {.async.} =
172+
let headers = ctx.request.headers
173+
let acceptEncoding = if headers.hasKey("Accept-Encoding"): $headers["Accept-Encoding"] else: ""
174+
ctx.response.setHeader("Content-Type", "application/json")
175+
if "gzip" in acceptEncoding:
176+
let compressed = compress(jsonLargeResponse, BestSpeed, dfGzip)
177+
ctx.response.setHeader("Content-Encoding", "gzip")
178+
resp compressed
179+
elif "deflate" in acceptEncoding:
180+
let compressed = compress(jsonLargeResponse, BestSpeed, dfDeflate)
181+
ctx.response.setHeader("Content-Encoding", "deflate")
182+
resp compressed
183+
else:
184+
resp jsonLargeResponse
185+
186+
proc uploadHandler(ctx: Context) {.async.} =
187+
let body = ctx.request.body
188+
ctx.response.setHeader("Content-Type", "text/plain")
189+
resp $body.len
190+
191+
proc dbHandler(ctx: Context) {.async.} =
192+
if not dbAvailable:
193+
ctx.response.setHeader("Content-Type", "application/json")
194+
resp "{\"items\":[],\"count\":0}"
195+
return
196+
197+
let minPrice = try: parseFloat(ctx.getQueryParams("min", "10")) except ValueError: 10.0
198+
let maxPrice = try: parseFloat(ctx.getQueryParams("max", "50")) except ValueError: 50.0
199+
200+
var items = newJArray()
201+
try:
202+
let rows = db.getAllRows(sql"SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count FROM items WHERE price BETWEEN ? AND ? LIMIT 50",
203+
$minPrice, $maxPrice)
204+
for row in rows:
205+
let tagsJson = try: parseJson(row[6]) except JsonParsingError: newJArray()
206+
items.add(%*{
207+
"id": parseInt(row[0]),
208+
"name": row[1],
209+
"category": row[2],
210+
"price": parseFloat(row[3]),
211+
"quantity": parseInt(row[4]),
212+
"active": parseInt(row[5]) == 1,
213+
"tags": tagsJson,
214+
"rating": {"score": parseFloat(row[7]), "count": parseInt(row[8])}
215+
})
216+
except:
217+
discard
218+
219+
ctx.response.setHeader("Content-Type", "application/json")
220+
resp $(%*{"items": items, "count": items.len})
221+
222+
proc staticHandler(ctx: Context) {.async.} =
223+
let filename = ctx.getPathParams("filename")
224+
if filename in staticFiles:
225+
let (data, ct) = staticFiles[filename]
226+
ctx.response.setHeader("Content-Type", ct)
227+
resp data
228+
else:
229+
resp Http404
230+
231+
# Set up and run
232+
loadDataset()
233+
loadDatasetLarge()
234+
loadStaticFiles()
235+
loadDb()
236+
237+
let settings = newSettings(
238+
port = Port(8080),
239+
debug = false,
240+
address = "0.0.0.0"
241+
)
242+
243+
var app = newApp(settings = settings)
244+
245+
proc addServerHeader(ctx: Context) {.async.} =
246+
ctx.response.setHeader("Server", "prologue")
247+
await switch(ctx)
248+
249+
app.use(addServerHeader)
250+
251+
app.addRoute("/pipeline", pipelineHandler, HttpGet)
252+
app.addRoute("/baseline11", baseline11Handler, HttpGet)
253+
app.addRoute("/baseline11", baseline11Handler, HttpPost)
254+
app.addRoute("/baseline2", baseline2Handler, HttpGet)
255+
app.addRoute("/json", jsonHandler, HttpGet)
256+
app.addRoute("/compression", compressionHandler, HttpGet)
257+
app.addRoute("/upload", uploadHandler, HttpPost)
258+
app.addRoute("/db", dbHandler, HttpGet)
259+
app.addRoute("/static/{filename}", staticHandler, HttpGet)
260+
261+
app.run()
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Package
2+
version = "0.1.0"
3+
author = "HttpArena"
4+
description = "HttpArena Prologue benchmark"
5+
license = "MIT"
6+
bin = @["server"]
7+
8+
# Dependencies
9+
requires "nim >= 2.0.0"
10+
requires "prologue >= 0.6.0"
11+
requires "zippy >= 0.10.0"

0 commit comments

Comments
 (0)