Skip to content

Commit 0bfaaf5

Browse files
committed
feat: enhance benchmarks with additional routes and middleware support
- Added new routes for plaintext, API responses, and settings in Gin, Hono, Mocket, Nitro, Node.js, PHP, and Spring Boot benchmarks. - Implemented middleware for handling requests in Gin and Mocket frameworks. - Introduced new endpoints for consuming requests and echoing JSON responses. - Updated the run.py script to include new routes and improved route handling logic. - Added performance tests for route matching and middleware execution in Mocket. - Enhanced existing routes to support dynamic parameters and query handling.
1 parent 74f203b commit 0bfaaf5

27 files changed

Lines changed: 941 additions & 39 deletions

File tree

benchmarks/README.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,45 @@ Routes:
88

99
- `GET /plaintext` returns `Hello, World!`
1010
- `GET /api/plaintext` returns `Hello, World!` for middleware-focused Mocket runs
11+
- `GET /middleware/plaintext` exercises a real middleware stack where the target supports it
12+
- `GET /api/v1/users/current/profile/settings` exercises a deep static route
13+
- `GET /static/999` exercises lookup in a router with 1000 registered static routes
1114
- `GET /json` returns `{ "message": "Hello, World!" }`
1215
- `GET /echo/:name` returns the path parameter
16+
- `GET /users/:user_id/posts/:post_id/comments/:comment_id` exercises multiple path parameters
17+
- `GET /wild/**` returns the wildcard suffix
18+
- `GET /query?name=moonbit&repeat=4` exercises query-string routing
19+
- `GET /headers` mutates response headers
20+
- `GET /missing` exercises the 404 path
1321
- `POST /echo` echoes the request body
22+
- `POST /json-echo` echoes a JSON request body
23+
- `POST /consume` reads a 16 KiB request body and returns `ok`
24+
25+
Route suites:
26+
27+
- `quick`: `plaintext`, `json`, `echo`
28+
- `routing`: static, deep static, many-route static, dynamic, wildcard, query, and 404 paths
29+
- `body`: small text, JSON, and 16 KiB request-body echo
30+
- `middleware`: dedicated middleware route plus `/api` middleware-index route
31+
- `comprehensive`: broad GET and POST coverage for framework comparison
32+
- `all`: every route case, including middleware-specific cases
1433

1534
Run a quick Mocket-only benchmark:
1635

1736
```sh
1837
python3 benchmarks/run.py --prepare mocket --duration 10 --connections 100
1938
```
2039

40+
Run the comprehensive Mocket suite:
41+
42+
```sh
43+
python3 benchmarks/run.py --prepare mocket --suite comprehensive --duration 10 --connections 100
44+
```
45+
2146
Run all configured targets:
2247

2348
```sh
24-
python3 benchmarks/run.py --prepare --duration 30 --connections 256
49+
python3 benchmarks/run.py --prepare --suite comprehensive --duration 30 --connections 256
2550
```
2651

2752
Run a subset:
@@ -30,10 +55,17 @@ Run a subset:
3055
python3 benchmarks/run.py --prepare mocket nodejs bun deno gin axum
3156
```
3257

33-
Run the middleware-index benchmark:
58+
Run the middleware benchmark. For Mocket, the runner starts the internal
59+
middleware profile and reports the result under the `mocket` target:
60+
61+
```sh
62+
python3 benchmarks/run.py --prepare mocket --suite middleware --duration 10 --connections 100
63+
```
64+
65+
Run the MoonBit in-process microbenchmarks:
3466

3567
```sh
36-
python3 benchmarks/run.py --prepare mocket-middleware --routes plaintext api_plaintext --duration 10 --connections 100
68+
moon bench --release --target native -p oboard/mocket -f performance_wbtest.mbt
3769
```
3870

3971
The runner uses `oha` when it is installed. If `oha` is not available, it falls

benchmarks/axum/src/main.rs

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
use axum::{
22
body::Bytes,
3-
extract::Path,
3+
extract::{Path, Query, Request},
4+
http::HeaderMap,
5+
middleware::{self, Next},
6+
response::Response,
47
routing::{get, post},
58
Json, Router,
69
};
710
use serde::Serialize;
8-
use std::{env, net::SocketAddr};
11+
use std::{collections::HashMap, env, net::SocketAddr};
912

1013
#[derive(Serialize)]
1114
struct Payload {
@@ -22,23 +25,77 @@ async fn json() -> Json<Payload> {
2225
})
2326
}
2427

28+
async fn query(Query(params): Query<HashMap<String, String>>) -> String {
29+
params
30+
.get("name")
31+
.cloned()
32+
.unwrap_or_else(|| "moonbit".to_string())
33+
}
34+
2535
async fn echo_path(Path(name): Path<String>) -> String {
2636
name
2737
}
2838

39+
async fn comment_path(
40+
Path((_user_id, _post_id, comment_id)): Path<(String, String, String)>,
41+
) -> String {
42+
comment_id
43+
}
44+
45+
async fn wildcard(Path(path): Path<String>) -> String {
46+
path
47+
}
48+
49+
async fn headers() -> (HeaderMap, &'static str) {
50+
let mut headers = HeaderMap::new();
51+
headers.insert("x-benchmark", "axum".parse().unwrap());
52+
headers.insert("content-type", "text/plain; charset=utf-8".parse().unwrap());
53+
(headers, "ok")
54+
}
55+
2956
async fn echo_body(body: Bytes) -> Bytes {
3057
body
3158
}
3259

60+
async fn consume_body(body: Bytes) -> &'static str {
61+
let _ = body.len();
62+
"ok"
63+
}
64+
65+
async fn noop_middleware(request: Request, next: Next) -> Response {
66+
next.run(request).await
67+
}
68+
3369
#[tokio::main]
3470
async fn main() {
3571
let port = env::var("PORT").unwrap_or_else(|_| "3000".to_string());
3672
let addr: SocketAddr = format!("0.0.0.0:{port}").parse().unwrap();
37-
let app = Router::new()
73+
let mut app = Router::new()
3874
.route("/plaintext", get(plaintext))
75+
.route("/api/plaintext", get(plaintext))
76+
.route(
77+
"/middleware/plaintext",
78+
get(plaintext).route_layer(middleware::from_fn(noop_middleware)),
79+
)
80+
.route(
81+
"/api/v1/users/current/profile/settings",
82+
get(|| async { "settings" }),
83+
)
3984
.route("/json", get(json))
4085
.route("/echo/:name", get(echo_path))
41-
.route("/echo", post(echo_body));
86+
.route(
87+
"/users/:user_id/posts/:post_id/comments/:comment_id",
88+
get(comment_path),
89+
)
90+
.route("/wild/*path", get(wildcard))
91+
.route("/query", get(query))
92+
.route("/headers", get(headers))
93+
.route("/echo", post(echo_body))
94+
.route("/json-echo", post(echo_body))
95+
.route("/consume", post(consume_body));
96+
for i in 0..1000 {
97+
app = app.route(&format!("/static/{i}"), get(plaintext));
98+
}
4299

43100
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
44101
axum::serve(listener, app).await.unwrap();

benchmarks/bun/server.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const port = Number.parseInt(Bun.env.PORT ?? "3000", 10);
2+
const manyRoutes = new Set(Array.from({ length: 1000 }, (_, i) => `/static/${i}`));
23

34
Bun.serve({
45
hostname: "0.0.0.0",
@@ -12,6 +13,27 @@ Bun.serve({
1213
});
1314
}
1415

16+
if (request.method === "GET" && url.pathname === "/api/plaintext") {
17+
return new Response("Hello, World!", {
18+
headers: { "content-type": "text/plain; charset=utf-8" },
19+
});
20+
}
21+
22+
if (
23+
request.method === "GET" &&
24+
url.pathname === "/api/v1/users/current/profile/settings"
25+
) {
26+
return new Response("settings", {
27+
headers: { "content-type": "text/plain; charset=utf-8" },
28+
});
29+
}
30+
31+
if (request.method === "GET" && manyRoutes.has(url.pathname)) {
32+
return new Response("Hello, World!", {
33+
headers: { "content-type": "text/plain; charset=utf-8" },
34+
});
35+
}
36+
1537
if (request.method === "GET" && url.pathname === "/json") {
1638
return Response.json({ message: "Hello, World!" });
1739
}
@@ -23,12 +45,56 @@ Bun.serve({
2345
);
2446
}
2547

48+
const multiParamMatch = url.pathname.match(
49+
/^\/users\/([^/]+)\/posts\/([^/]+)\/comments\/([^/]+)$/,
50+
);
51+
if (request.method === "GET" && multiParamMatch) {
52+
return new Response(decodeURIComponent(multiParamMatch[3]), {
53+
headers: { "content-type": "text/plain; charset=utf-8" },
54+
});
55+
}
56+
57+
if (request.method === "GET" && url.pathname.startsWith("/wild/")) {
58+
return new Response(
59+
decodeURIComponent(url.pathname.slice("/wild/".length)),
60+
{ headers: { "content-type": "text/plain; charset=utf-8" } },
61+
);
62+
}
63+
64+
if (request.method === "GET" && url.pathname === "/query") {
65+
return new Response(url.searchParams.get("name") || "moonbit", {
66+
headers: { "content-type": "text/plain; charset=utf-8" },
67+
});
68+
}
69+
70+
if (request.method === "GET" && url.pathname === "/headers") {
71+
return new Response("ok", {
72+
headers: {
73+
"content-type": "text/plain; charset=utf-8",
74+
"x-benchmark": "bun",
75+
},
76+
});
77+
}
78+
2679
if (request.method === "POST" && url.pathname === "/echo") {
2780
return new Response(request.body, {
2881
headers: { "content-type": "application/octet-stream" },
2982
});
3083
}
3184

85+
if (request.method === "POST" && url.pathname === "/json-echo") {
86+
return new Response(request.body, {
87+
headers: { "content-type": "application/json; charset=utf-8" },
88+
});
89+
}
90+
91+
if (request.method === "POST" && url.pathname === "/consume") {
92+
await request.arrayBuffer();
93+
return new Response("ok", {
94+
headers: { "content-type": "text/plain; charset=utf-8" },
95+
});
96+
}
97+
3298
return new Response("Not Found", { status: 404 });
3399
},
34100
});

benchmarks/deno/server.ts

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const port = Number.parseInt(Deno.env.get("PORT") ?? "3000", 10);
2+
const manyRoutes = new Set(Array.from({ length: 1000 }, (_, i) => `/static/${i}`));
23

3-
Deno.serve({ hostname: "0.0.0.0", port }, (request) => {
4+
Deno.serve({ hostname: "0.0.0.0", port }, async (request) => {
45
const url = new URL(request.url);
56

67
if (request.method === "GET" && url.pathname === "/plaintext") {
@@ -9,6 +10,27 @@ Deno.serve({ hostname: "0.0.0.0", port }, (request) => {
910
});
1011
}
1112

13+
if (request.method === "GET" && url.pathname === "/api/plaintext") {
14+
return new Response("Hello, World!", {
15+
headers: { "content-type": "text/plain; charset=utf-8" },
16+
});
17+
}
18+
19+
if (
20+
request.method === "GET" &&
21+
url.pathname === "/api/v1/users/current/profile/settings"
22+
) {
23+
return new Response("settings", {
24+
headers: { "content-type": "text/plain; charset=utf-8" },
25+
});
26+
}
27+
28+
if (request.method === "GET" && manyRoutes.has(url.pathname)) {
29+
return new Response("Hello, World!", {
30+
headers: { "content-type": "text/plain; charset=utf-8" },
31+
});
32+
}
33+
1234
if (request.method === "GET" && url.pathname === "/json") {
1335
return Response.json({ message: "Hello, World!" });
1436
}
@@ -20,12 +42,58 @@ Deno.serve({ hostname: "0.0.0.0", port }, (request) => {
2042
);
2143
}
2244

45+
const multiParamMatch = url.pathname.match(
46+
/^\/users\/([^/]+)\/posts\/([^/]+)\/comments\/([^/]+)$/,
47+
);
48+
if (request.method === "GET" && multiParamMatch) {
49+
return new Response(decodeURIComponent(multiParamMatch[3]), {
50+
headers: { "content-type": "text/plain; charset=utf-8" },
51+
});
52+
}
53+
54+
if (request.method === "GET" && url.pathname.startsWith("/wild/")) {
55+
return new Response(
56+
decodeURIComponent(url.pathname.slice("/wild/".length)),
57+
{ headers: { "content-type": "text/plain; charset=utf-8" } },
58+
);
59+
}
60+
61+
if (request.method === "GET" && url.pathname === "/query") {
62+
return new Response(url.searchParams.get("name") || "moonbit", {
63+
headers: { "content-type": "text/plain; charset=utf-8" },
64+
});
65+
}
66+
67+
if (request.method === "GET" && url.pathname === "/headers") {
68+
return new Response("ok", {
69+
headers: {
70+
"content-type": "text/plain; charset=utf-8",
71+
"x-benchmark": "deno",
72+
},
73+
});
74+
}
75+
2376
if (request.method === "POST" && url.pathname === "/echo") {
24-
return new Response(request.body, {
77+
const body = await request.arrayBuffer();
78+
return new Response(body, {
2579
headers: { "content-type": "application/octet-stream" },
2680
});
2781
}
2882

83+
if (request.method === "POST" && url.pathname === "/json-echo") {
84+
const body = await request.arrayBuffer();
85+
return new Response(body, {
86+
headers: { "content-type": "application/json; charset=utf-8" },
87+
});
88+
}
89+
90+
if (request.method === "POST" && url.pathname === "/consume") {
91+
await request.arrayBuffer();
92+
return new Response("ok", {
93+
headers: { "content-type": "text/plain; charset=utf-8" },
94+
});
95+
}
96+
2997
return new Response("Not Found", { status: 404 });
3098
});
3199

0 commit comments

Comments
 (0)