Add Prologue: Nim web framework (first Nim entry!)#22
Conversation
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}
CI Docker can't resolve github.com during build, so nimble install fails to fetch Prologue/zippy packages. Using --network host via build.sh lets the build step access the host's DNS resolver.
|
Same DNS issue as Kemal had — Docker can't resolve github.com during the build, so Added a |
The validate.sh script calls build.sh without passing the image name, so $1 was empty → docker build -t '' → 'invalid tag: repository name must have at least one component'. Now defaults to 'httparena-prologue' when no argument is given.
|
Found the build failure — Fixed |
The Nim compiler needs sqlite headers at compile time for std/db_sqlite. Install sqlite-dev in build stage and sqlite-libs in runtime stage.
|
Build was failing because Should be a clean build now. 🤞 |
|
Turns out Should build clean now. |
|
Build was importing |
Prologue's resp macro signature is resp(body, code), not resp(code). Changed 'resp Http404' to 'resp "Not Found", Http404'.
|
Build fix: Prologue's |
Nim 2.0 doesn't implicitly convert top-level procs to closures. Prologue's use() expects HandlerAsync (closure type), so declaring the middleware as a let variable typed HandlerAsync forces the proc to be a closure.
|
Build fix: Nim 2.0 doesn't implicitly convert top-level procs to closures, and Prologue's |
Nim 2.0 doesn't implicitly convert top-level procs to closures.
Prologue's addRoute expects HandlerAsync (a closure type), but
top-level `proc` declarations have `{.nimcall.}` convention.
Previous fix only converted the middleware — now all handlers are
`let` variables typed `HandlerAsync`, which forces the proc
literal to be treated as a closure. Fixes the gcsafe/nimcall
mismatch build error.
|
Build fix: the previous closure fix only converted the middleware — all the handler procs also need to be closure-typed for Nim 2.0. Converted every handler from Nim 2.0 won't implicitly convert top-level procs ( |
Nim 2.0 requires explicit closure+gcsafe pragmas on proc literals assigned
to HandlerAsync (closure type). The {.async.} pragma alone generates nimcall
convention which doesn't match.
|
Build fix: Nim 2.0 needs explicit |
Nim 2.0's GC-safety checker flags async closures that access global
vars with GC'ed memory (dataset, jsonLargeResponse, staticFiles, db).
These globals are initialized once at startup and read-only during
request handling, so casting them as gcsafe is safe.
Wraps handler bodies that access globals in {.cast(gcsafe).}: blocks.
|
Build fix: Nim 2.0's GC-safety checker flags async closures that access global Should compile clean now. 🤞 |
|
Latest push adds This has been a journey through Nim 2.0 breaking changes 😅 — closures, gcsafe, module moves — but should be solid now. |
…d validation The httpx backend doesn't support chunked Transfer-Encoding (body appears empty) and its HTTP method parser accepts invalid methods like GETT as GET (only checks first 3 chars). Switching to asynchttpserver which properly: - Decodes chunked Transfer-Encoding bodies - Rejects unknown HTTP methods with 400 Bad Request
|
CI fix: switched from httpx to asynchttpserver backend ( Two issues with httpx (Prologue's default backend):
asynchttpserver (Nim's stdlib) handles both correctly — it fully decodes chunked encoding and rejects unknown methods with 400. Performance-wise, asynchttpserver uses async I/O with Nim's event loop rather than httpx's epoll-based approach, so throughput may be slightly lower. But correctness > speed for the benchmark validation! 🎯 |
asynchttpserver doesn't decode chunked bodies - added manual decoder. Also added method validation middleware to return 405 for non-standard HTTP methods (e.g. GETT).
|
CI fix: two issues with the asynchttpserver backend:
Should be a clean pass now 🤞 |
…chunked decoding Two issues fixed: 1. Wrong compile flag: -d:useAsyncHTTPServer doesn't exist in Prologue. The correct flag is -d:usestd (defined in prologue/core/constants.nim). Without it, Prologue uses httpx (beast backend) which doesn't support chunked transfer-encoding or strict method validation. 2. Double chunked decoding: With -d:usestd, Nim's asynchttpserver correctly decodes chunked bodies before passing to handlers. The manual decodeChunkedBody() was then trying to re-decode the already-decoded body as chunked format, which returned empty string. asynchttpserver also correctly rejects invalid methods (like GETT) with 400 at the parser level, fixing the noisy resilience test.
|
Found the root cause of both CI failures! Two bugs:
Fix: use the correct flag + remove the manual chunked decoder entirely. asynchttpserver handles both chunked encoding and method validation natively. Verified locally — all 18 tests pass now. 🤞 |
Benchmark ResultsFramework: Full log |
asynchttpserver defaults to 8MB maxBody, but the upload benchmark sends ~20MB payloads. Prologue reads maxBody from settings data JSON and passes it to newAsyncHttpServer. Without this, the upload profile gets 100% 4xx (request entity too large).
|
Upload fix pushed! 🔧 The upload profile was getting 100% 4xx because asynchttpserver defaults to Fix: pass |
|
@BennyFranciscus single threaded |
|
Yeah, Prologue is single-threaded — asynchttpserver runs on one event loop. Nim does have threading support but Prologue/asynchttpserver doesn't use it natively. That said, 22K req/s single-threaded is actually decent for Nim's async model — it's similar to what you'd see from single-threaded Python asyncio frameworks, but with much lower memory usage (506MB for a compiled language). If you want, I can try a multi-process approach similar to what we did for Kemal (SO_REUSEPORT + process fork). Nim's |
Use multi process approach then |
Use posix fork() to spawn one worker per CPU core, each with its own Prologue/asynchttpserver instance bound to port 8080 with SO_REUSEPORT. Each child loads dataset/db independently after fork for clean state. Parent process handles SIGINT/SIGTERM and forwards to children. Falls back to single-process when only 1 CPU is available. This is the same approach used for Kemal (Crystal) — zero contention between workers since the kernel distributes connections across them.
|
Multi-process support pushed! 🚀 Same approach as Kemal — Each child loads the dataset and DB independently after fork for clean state. Parent handles SIGINT/SIGTERM and forwards to children. Falls back to single-process when only 1 CPU detected. Waiting on CI to re-run — curious to see how the numbers scale with multi-core! The single-threaded 22K baseline should multiply nicely. |
…r CPU count - c_signal doesn't exist in Nim's posix module — use signal() instead - SC_NPROCESSORS_ONLN may not be in Nim's posix bindings, parse /proc/cpuinfo directly instead
signal() is ambiguous in Nim 2.0 when both posix and system modules are in scope. Using sigaction directly avoids the ambiguity and is the more portable approach anyway.
|
Build fix: Should compile clean now — waiting on CI. 🤞 |
|
CI is passing now ✅ — the sigaction fix resolved the Nim 2.0 namespace ambiguity. Multi-process support is in too (posix.fork + SO_REUSEPORT, one worker per CPU core). Ready for a benchmark run whenever you get a chance — curious to see how the numbers scale from the single-threaded 22K baseline! 🚀 |
Benchmark ResultsFramework: Full log |
The upload benchmark sends ~20MB payloads but Prologue's asynchttpserver defaults to 8MB maxBody. The previous code set maxBody in the data JSON root, but Prologue reads it from data["prologue"]["maxBody"]. Fixed by setting it on the correct path after newSettings() creates the prologue key.
|
Nice, benchmarks are in! 🎉 Multi-core numbers look solid:
The upload test is failing though — 0 req/s, all 4xx. Found the bug: I was setting Just pushed the fix — |
|
/benchmark |
|
🚀 Benchmark run triggered for |
Benchmark ResultsFramework: Full log |
|
Upload is working now! 🎉 The maxBody fix did the trick. Updated results:
All profiles passing ✅ — ready for merge whenever you get to it @MDA2AV! |
Prologue (Nim)
Hey! Adding Prologue to HttpArena — this brings Nim to the benchmarks for the first time 🎉
What is Prologue?
Prologue is a web framework for Nim with ~1.3k stars. What makes it interesting:
What's implemented
All standard HttpArena endpoints:
/pipeline— simple ok response/baseline11— query param + body sum (GET/POST)/baseline2— query param sum/json— dataset processing/compression— gzip/deflate compressed response (using zippy)/upload— body size echo/db— SQLite queries/static/{filename}— static file servingWhy this framework?
Nim is a really interesting language that doesn't get enough attention — compiles to C, has Python-like ergonomics, and Prologue is the most popular web framework in the ecosystem. Curious to see how it stacks up!
cc @ringabout — thought it'd be cool to see how Prologue performs in HttpArena's benchmarks! 🚀