Skip to content

Commit 6c21236

Browse files
committed
feat: (gerbil-http-server) Add benchmark and fix fairness issues
- Add benchmark scripts (bench.ts, server.ts, server.py) - Fix Gerbil: remove unnecessary string-append, add Content-Length header - Fix Hono: unify response body to `hello\n` (6 bytes, same as others) - Fix bench.ts: randomize server execution order to reduce ordering bias - Add flake.nix devshell with Gerbil and Nix tooling
1 parent fba0e92 commit 6c21236

8 files changed

Lines changed: 552 additions & 3 deletions

File tree

gerbil-http-server/.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use flake
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
const DURATION_MS = 10_000;
2+
const CONCURRENCY = 50;
3+
const WARMUP_MS = 2_000;
4+
5+
async function loadTest(url: string, durationMs: number, concurrency: number): Promise<{
6+
rps: number;
7+
avgLatencyMs: number;
8+
p99LatencyMs: number;
9+
errors: number;
10+
total: number;
11+
}> {
12+
let completed = 0;
13+
let errors = 0;
14+
const latencies: number[] = [];
15+
const deadline = Date.now() + durationMs;
16+
17+
async function worker() {
18+
while (Date.now() < deadline) {
19+
const start = performance.now();
20+
try {
21+
const res = await fetch(url);
22+
await res.text();
23+
latencies.push(performance.now() - start);
24+
completed++;
25+
} catch {
26+
errors++;
27+
}
28+
}
29+
}
30+
31+
const workers = Array.from({ length: concurrency }, () => worker());
32+
await Promise.all(workers);
33+
34+
latencies.sort((a, b) => a - b);
35+
const avg = latencies.reduce((s, v) => s + v, 0) / latencies.length;
36+
const p99 = latencies[Math.floor(latencies.length * 0.99)] ?? 0;
37+
38+
return {
39+
rps: Math.round((completed / durationMs) * 1000),
40+
avgLatencyMs: Math.round(avg * 100) / 100,
41+
p99LatencyMs: Math.round(p99 * 100) / 100,
42+
errors,
43+
total: completed,
44+
};
45+
}
46+
47+
async function waitReady(url: string, timeoutMs = 5000) {
48+
const deadline = Date.now() + timeoutMs;
49+
while (Date.now() < deadline) {
50+
try {
51+
await fetch(url);
52+
return;
53+
} catch {
54+
await new Promise((r) => setTimeout(r, 100));
55+
}
56+
}
57+
throw new Error(`Server not ready: ${url}`);
58+
}
59+
60+
async function bench(name: string, url: string) {
61+
console.log(`\n[${name}] ${url}`);
62+
console.log(" Waiting for server...");
63+
await waitReady(url);
64+
65+
console.log(` Warmup ${WARMUP_MS / 1000}s...`);
66+
await loadTest(url, WARMUP_MS, CONCURRENCY);
67+
68+
console.log(` Benchmarking ${DURATION_MS / 1000}s, concurrency=${CONCURRENCY}...`);
69+
const result = await loadTest(url, DURATION_MS, CONCURRENCY);
70+
71+
console.log(` RPS: ${result.rps}`);
72+
console.log(` Avg latency: ${result.avgLatencyMs} ms`);
73+
console.log(` P99 latency: ${result.p99LatencyMs} ms`);
74+
console.log(` Total reqs: ${result.total}`);
75+
console.log(` Errors: ${result.errors}`);
76+
return result;
77+
}
78+
79+
const targets = [
80+
{ name: "Gerbil", url: "http://127.0.0.1:8080/" },
81+
{ name: "Hono (Deno)", url: "http://127.0.0.1:8000/" },
82+
{ name: "Python (stdlib)", url: "http://127.0.0.1:8001/" },
83+
];
84+
85+
const results: Record<string, { rps: number; avgLatencyMs: number; p99LatencyMs: number }> = {};
86+
87+
const shuffled = [...targets].sort(() => Math.random() - 0.5);
88+
console.log(`\nBench order: ${shuffled.map((t) => t.name).join(" → ")}`);
89+
90+
for (const t of shuffled) {
91+
const r = await bench(t.name, t.url);
92+
results[t.name] = r;
93+
}
94+
95+
console.log("\n=== Summary ===");
96+
const entries = Object.entries(results).sort((a, b) => b[1].rps - a[1].rps);
97+
const baseline = entries[entries.length - 1][1].rps;
98+
for (const [name, r] of entries) {
99+
const ratio = (r.rps / baseline).toFixed(2);
100+
console.log(`${name.padEnd(20)} RPS: ${String(r.rps).padStart(6)} avg: ${r.avgLatencyMs}ms p99: ${r.p99LatencyMs}ms (${ratio}x)`);
101+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# HTTP Server Benchmark Results
2+
3+
## Environment
4+
5+
- Date: 2026-05-31
6+
- Tool: [oha](https://github.com/hatoo/oha) 1.14.0
7+
- Requests: 100,000
8+
- Concurrency: 50
9+
- Endpoint: `GET /``hello\n`
10+
11+
## Servers
12+
13+
| Server | Runtime | Port |
14+
|---|---|---|
15+
| Gerbil | Gerbil (Gambit Scheme) | 8080 |
16+
| Hono | Deno 2.5.4 | 8000 |
17+
| Python | Python 3.15 stdlib (`ThreadingMixIn`) | 8001 |
18+
19+
## Results
20+
21+
| Metric | Hono (Deno) | Gerbil | Python (stdlib) |
22+
|---|---|---|---|
23+
| **RPS** | **80,307** | 47,482 | 3,631 |
24+
| **Avg latency** | **0.62 ms** | 1.10 ms | 13.76 ms |
25+
| **P50** | 0.55 ms | 0.50 ms | 12.61 ms |
26+
| **P99** | 2.12 ms | 3.10 ms | 30.97 ms |
27+
| **P99.99** | 7.63 ms | 1,225 ms | 64.29 ms |
28+
| **Slowest** | 9.62 ms | 1,435 ms | 68.26 ms |
29+
| **Total time** | 1.25 s | 2.11 s | 27.54 s |
30+
| **Errors** | 0 | 0 | 0 |
31+
| **vs Python** | 22.1x | 13.1x | 1.0x |
32+
33+
## Raw Output
34+
35+
### Gerbil
36+
37+
```
38+
Summary:
39+
Success rate: 100.00%
40+
Total: 2.1061 sec
41+
Slowest: 1.4350 sec
42+
Fastest: 0.0000 sec
43+
Average: 0.0011 sec
44+
Requests/sec: 47481.9067
45+
46+
Response time distribution:
47+
10.00% in 0.0003 sec
48+
25.00% in 0.0003 sec
49+
50.00% in 0.0005 sec
50+
75.00% in 0.0008 sec
51+
90.00% in 0.0014 sec
52+
95.00% in 0.0022 sec
53+
99.00% in 0.0031 sec
54+
99.90% in 0.0064 sec
55+
99.99% in 1.2257 sec
56+
```
57+
58+
### Hono (Deno)
59+
60+
```
61+
Summary:
62+
Success rate: 100.00%
63+
Total: 1245.2186 ms
64+
Slowest: 9.6206 ms
65+
Fastest: 0.1034 ms
66+
Average: 0.6194 ms
67+
Requests/sec: 80307.1842
68+
69+
Response time distribution:
70+
10.00% in 0.4214 ms
71+
25.00% in 0.4848 ms
72+
50.00% in 0.5510 ms
73+
75.00% in 0.6562 ms
74+
90.00% in 0.8235 ms
75+
95.00% in 1.0154 ms
76+
99.00% in 2.1199 ms
77+
99.90% in 3.4462 ms
78+
99.99% in 7.6295 ms
79+
```
80+
81+
### Python (stdlib)
82+
83+
```
84+
Summary:
85+
Success rate: 100.00%
86+
Total: 27541.5512 ms
87+
Slowest: 68.2616 ms
88+
Fastest: 4.8622 ms
89+
Average: 13.7616 ms
90+
Requests/sec: 3630.8776
91+
92+
Response time distribution:
93+
10.00% in 11.0040 ms
94+
25.00% in 11.6955 ms
95+
50.00% in 12.6071 ms
96+
75.00% in 14.1530 ms
97+
90.00% in 17.7083 ms
98+
95.00% in 21.1697 ms
99+
99.00% in 30.9676 ms
100+
99.90% in 47.8973 ms
101+
99.99% in 64.2919 ms
102+
```
103+
104+
## Notes
105+
106+
- GerbilはP99.99で1,225msのスパイクが発生(27件)。keep-alive接続の再確立コストが原因と推測
107+
- HonoはHTTP keep-aliveが効率的に機能しており安定したレイテンシ分布
108+
- PythonはI/Oブロッキングのスレッドモデルのため他と比較にならない差。`uvicorn`等の非同期サーバーでは改善が見込まれる
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import http.server
2+
import socketserver
3+
4+
class Handler(http.server.BaseHTTPRequestHandler):
5+
def do_GET(self):
6+
body = b"hello\n"
7+
self.send_response(200)
8+
self.send_header("Content-Type", "text/plain")
9+
self.send_header("Content-Length", str(len(body)))
10+
self.end_headers()
11+
self.wfile.write(body)
12+
13+
def log_message(self, format, *args):
14+
pass
15+
16+
class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
17+
daemon_threads = True
18+
19+
server = ThreadedHTTPServer(("127.0.0.1", 8001), Handler)
20+
server.serve_forever()

gerbil-http-server/benchmark/server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ import { Hono } from "jsr:@hono/hono";
22

33
const app = new Hono()
44

5-
app.get('/', (c) => c.text('hello'))
5+
app.get('/', (c) => c.text('hello\n'))
66

77
Deno.serve(app.fetch)

0 commit comments

Comments
 (0)