Conversation
BennyFranciscus
left a comment
There was a problem hiding this comment.
Hey @p8, nice to see Roda joining HttpArena! 🎉 Roda's routing tree approach is really elegant and should be fast. A few things I spotted:
Copy-paste from Sinatra:
config.rumiddleware responses have'server' => 'sinatra'instead of'server' => 'roda'(bothMethodGuardandUploadHandler)- Comments mention "Rack/Sinatra" in the
UploadHandler
Duplicate route in app.rb:
r.is('/baseline11') { handle_baseline11 }
r.is('/baseline11') { handle_baseline11 } # duplicateRoda plugins needed:
content_type, headers, and halt aren't available in Roda by default — they need plugins:
plugin :halt
plugin :default_headers, 'Server' => 'roda'For content_type, you'd use response['Content-Type'] = 'application/json' or the :content_type plugin. And halt 500 would need the :halt plugin.
Without these plugins loaded, the app will likely crash on startup or when hitting those routes.
Otherwise the structure looks solid — jemalloc, YJIT, iodine, thread-local SQLite connections. Same patterns that work well for Sinatra/Rails. Looking forward to seeing the benchmark numbers!
a3c6fe4 to
9f7ab3d
Compare
BennyFranciscus
left a comment
There was a problem hiding this comment.
Hey @p8, nice update! The config.ru is clean now and the duplicate route is gone 👍
Two things that'll likely crash at runtime:
-
haltneedsplugin :halt— Roda doesn't havehaltby default. You're callinghalt 500, 'No dataset'in/jsonand/compressionbut the plugin isn't loaded. Without it you'll get aNoMethodError. -
paramsneeds a plugin or user.params— In the/dbroute you use bareparams['min']. Roda doesn't exposeparamsin the route block without a plugin. Either addplugin :all_verbsor just user.params['min']/request.params['min']which goes through Rack directly.
Quick fix would be adding these near the top:
plugin :haltAnd changing params to r.params in the db route (or add plugin :typecast_params if you want Roda's param handling).
Everything else looks solid — the routing tree, iodine setup, thread-local SQLite, gzip level 1. Should benchmark well once these are sorted!
BennyFranciscus
left a comment
There was a problem hiding this comment.
Hey @p8, nice — plugin :halt is in now 👍 and the config.ru cleanup looks great.
One thing that'll still crash:
params in /db needs a receiver — Roda doesn't have a bare params method. You're calling params['min'] and params['max'] but that'll raise NoMethodError. Quick fix: use request.params['min'] instead (same as you already do with request.GET in /baseline2).
Also no /upload route — that's fine if you want to skip the upload profile for now, just noting it in case it was an oversight.
Everything else looks solid!
BennyFranciscus
left a comment
There was a problem hiding this comment.
One more thing I missed earlier — the route is `/baseline11` but it should be `/baseline1` (same for the method name). This will 404 on the baseline1 test.
Also `params['min']' in `/db` still needs `request.params` (or `r.params`) — bare `params` isn't available in Roda without a plugin.
BennyFranciscus
left a comment
There was a problem hiding this comment.
Just noticed one more thing from the latest push — in the /db route, you have:
rows.map do |r|But r is already used as the Roda route variable from route do |r|. Shadowing it here means if anything inside that block tries to use the route r, it'll get a database row instead. Probably won't bite you in this block since you're just building JSON, but it's a subtle bug waiting to happen. Renaming the block param to row would be cleaner:
items = rows.map do |row|
{
'id' => row['id'], 'name' => row['name'], ...
}
endAlso still seeing /baseline11 — should be /baseline1 👀
aa665fd to
1e2c205
Compare
|
Nice cleanup @p8! Still seeing |
1335f77 to
f40ffce
Compare
|
Actually, scratch my last comment about Looks like the new commit is a force-push rewrite. The original |
BennyFranciscus
left a comment
There was a problem hiding this comment.
Hey @p8, this is looking much cleaner! Iodine as the server, jemalloc, YJIT — solid setup.
One bug I spotted:
Line 67 — r.is '/compression' has a leading slash
In Roda's routing tree, r.is matches against the remaining path with the leading slash already stripped. So r.is '/compression' won't match /compression — it needs to be r.is 'compression' (no leading slash), same as your other routes.
Everything else looks good to me 👍
084af32 to
7c2111f
Compare
BennyFranciscus
left a comment
There was a problem hiding this comment.
LGTM! All the issues are addressed — compression route without leading slash ✅, request.params for query access ✅, no more variable shadowing ✅. Clean Roda entry with Iodine + YJIT + jemalloc. Ready for benchmarking 🚀
|
/benchmark |
|
🚀 Benchmark run triggered for |
|
Benchmark run triggered 🚀 Results will show up once it completes — should take a bit. Curious to see how Roda stacks up! |
Benchmark ResultsFramework: Full log |
|
🎉 Benchmark results are in! Roda is looking really solid: Baseline (512c): 79.8K req/s — nearly 2x Rails (~40K) on the same Puma runtime. The minimal routing overhead really shows here. Pipelined (512c): 312K req/s — strong. ~1.45x ahead of Rails (215K). Roda's lightweight request handling shines with pipelining. JSON: ~60K req/s — decent throughput for per-request JSON computation. Compression: ~5K req/s — consistent with other Ruby/Puma frameworks. GZIPOutputStream is the bottleneck, not the framework. Memory: 660MB at baseline vs Rails' 2.3GB — Roda's minimalist design pays off big time on memory. Nearly 3.5x less. Mixed (4096c): ~14.8K req/s — solid. The "do less, go faster" philosophy is on full display here. Same Ruby runtime (Puma + MN threads), but Roda's thinner abstraction layer gives it a huge edge. Nice entry @p8! |
ff7c6fb to
6c28116
Compare
|
Hey @p8, noticed the latest push still has Everything else in the rewrite looks clean 👍 |
BennyFranciscus
left a comment
There was a problem hiding this comment.
Server header is fixed 👍 Roda entry looks ready to go!
BennyFranciscus
left a comment
There was a problem hiding this comment.
Heads up — the /compression endpoint has a hardcoded placeholder string instead of the actual large dataset:
payload = "aaaaaaaaaaaaaa" #opts[:large_json_payload]Looks like debug code that was left in. The large payload is correctly pre-built at startup but never used. Should be opts[:large_json_payload] there.
Everything else looks solid — Roda routing tree API used correctly, iodine server, standard Ruby libs, jemalloc + YJIT. No rule violations besides that bug.
9bcb81c to
9a0a1a3
Compare
|
/benchmark |
|
Fix looks good 👍 Server header is correct now. Ready for merge from my side. |
|
🚀 Benchmark run triggered for |
Benchmark ResultsFramework: Full log |
|
Heads up @p8 — the latest benchmark run failed across all profiles. Every test hit "Server did not start within 30s." Looking at the files, the Dockerfile has Once you add |
|
/benchmark |
|
🚀 Benchmark run triggered for |
Benchmark ResultsFramework: Full log |
|
Nice, server starts clean now! These numbers are interesting though — pretty different from the first run: Baseline (512c): 89K req/s (was 79.8K) — ~11% bump, but memory jumped from 660MB to 7.6GiB and CPU from 4083% → 6461%. Looks like Pipelined: 1.4M req/s at 4096c — this is the big one. Was 310K before. That's a 4.5x jump. With more Puma workers handling the pipelined load, the throughput scales almost linearly. 1.4M req/s from Ruby is genuinely impressive. JSON: 287K req/s (was 60K) — nearly 5x. Same story, more workers. Memory is the trade-off here. 7.6GiB at baseline vs 660MB before. The ~10.9% 4xx responses are from the 405 method guard endpoint — that's working as intended. Overall looking great. The first run must've had fewer workers for some reason (maybe CPU governor or container limits were different). These numbers with full worker scaling are the more representative ones. Ready to merge from my side 👍 |
No description provided.