Skip to content

Commit 6cce0ec

Browse files
bench: Hono framework req/s variant + home-page chart
scripts/hono.js serves the same hello-world through Hono — a real third-party web framework pulled from node_modules — to show esrun runs unmodified npm ESM packages, not just runtime:http. Hono's Web-standard app.fetch plugs into runtime:http / Bun.serve / Deno.serve; Node uses @hono/node-server. rps.sh gains a SERVER selector (SERVER=scripts/hono.js). esrun stays at parity (~40k req/s, marginally highest). Adds RpsChart (higher-is-better) to the home-page benchmark card with the measured Hono numbers. node_modules is gitignored; manifest + lockfile tracked.
1 parent e18b641 commit 6cce0ec

8 files changed

Lines changed: 166 additions & 4 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
debug
44
target
55

6+
# Installed JS packages (bench framework deps; manifest + lockfile are tracked)
7+
node_modules
8+
69
# These are backup files generated by rustfmt
710
**/*.rs.bk
811

bench/README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,36 @@ earlier "2× slower" reading came from the in-process `http` workload, where esr
179179
pays for the client and the server on the same thread; measured server-to-client
180180
it isn't there.
181181

182+
### Through a framework (Hono)
183+
184+
The same shape served through [Hono] — a real, third-party web framework —
185+
instead of each runtime's bare server. This is the framework counterpart to the
186+
Bun framework charts: it shows esrun runs **unmodified npm ESM packages** off
187+
`node_modules`, not just its own server. Hono is Web-standard
188+
(`app.fetch(request) -> Response`), so it plugs straight into `runtime:http`,
189+
`Bun.serve`, and `Deno.serve`; Node uses Hono's `@hono/node-server` adapter.
190+
191+
```sh
192+
cd bench && bun install # hono + @hono/node-server
193+
SERVER=scripts/hono.js bench/rps.sh # -c 100 -p 1
194+
```
195+
196+
```
197+
runtime | req/sec
198+
--------+------------
199+
node | 33,358
200+
bun | 39,686
201+
deno | 40,150
202+
esrun | 40,220
203+
```
204+
205+
esrun is again at parity (marginally highest), and the framework layer costs all
206+
four about the same as the bare server — Express, by contrast, cannot run on
207+
esrun at all (it is CommonJS and needs `node:http`'s `(req, res)` API; esrun is
208+
ESM-only and rejects `node:` builtins).
209+
182210
[autocannon]: https://github.com/mcollina/autocannon
211+
[Hono]: https://hono.dev
183212

184213
## Caveats
185214

bench/bun.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bench/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"private": true,
3+
"description": "Dev deps for the cross-runtime benchmark — Hono powers scripts/hono.js (rps.sh SERVER=scripts/hono.js). Install with: bun install",
4+
"dependencies": {
5+
"@hono/node-server": "^2.0.4",
6+
"hono": "^4.12.25"
7+
}
8+
}

bench/rps.sh

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@
1111
# Needs `autocannon` (used via `bunx autocannon`, or a global install). If
1212
# neither is available the script explains and exits.
1313
#
14-
# Usage: bench/rps.sh (auto-detects installed runtimes)
15-
# CONN=250 PIPELINE=20 bench/rps.sh (higher load / HTTP pipelining)
14+
# Usage: bench/rps.sh (auto-detects installed runtimes)
15+
# CONN=250 PIPELINE=20 bench/rps.sh (higher load / HTTP pipelining)
1616
# DURATION=10 bench/rps.sh
17+
# SERVER=scripts/hono.js bench/rps.sh (serve through the Hono framework;
18+
# run `bun add hono @hono/node-server` first)
1719
set -uo pipefail
1820
cd "$(dirname "$0")"
1921

2022
ESRUN="${ESRUN:-../target/release/esrun}"
21-
PORT=3000 # scripts/helloserver.js binds this fixed port
23+
SERVER="${SERVER:-scripts/helloserver.js}" # the hello-world server to run
24+
PORT=3000 # the server scripts bind this fixed port
2225
CONN="${CONN:-100}"
2326
PIPELINE="${PIPELINE:-1}"
2427
DURATION="${DURATION:-10}"
@@ -56,7 +59,7 @@ trap cleanup EXIT
5659
# Pulls req/s + latency out of autocannon's JSON for one runtime.
5760
measure() {
5861
local cmd="$1" j
59-
$cmd scripts/helloserver.js >/dev/null 2>&1 &
62+
$cmd "$SERVER" >/dev/null 2>&1 &
6063
SERVER_PID=$!
6164
# Wait for the port to accept connections (up to ~5s).
6265
for _ in $(seq 50); do
@@ -73,6 +76,7 @@ print(f\"{d['requests']['average']:.0f} {d['latency']['average']} {d['latency'][
7376
}
7477

7578
echo "HTTP requests/sec — hello-world plaintext (\"Hello, World!\")"
79+
echo "server: $SERVER"
7680
echo "load: autocannon -c $CONN -p $PIPELINE -d ${DURATION}s on 127.0.0.1:$PORT"
7781
echo
7882
printf "%-7s | %12s | %11s | %11s\n" "runtime" "req/sec" "avg lat" "p99 lat"

bench/scripts/hono.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Same hello-world req/s shape as helloserver.js, but served through Hono — a
2+
// real, third-party web framework — to show esrun runs unmodified npm ESM
3+
// packages, not just its own server. This is the "framework" counterpart to the
4+
// Bun framework charts (e.g. their Express number). Hono is Web-standard: its
5+
// `app.fetch(request) -> Response` handler plugs straight into every runtime's
6+
// native server. Node has no Web-standard server, so it uses Hono's official
7+
// @hono/node-server adapter. Run by rps.sh via SERVER=scripts/hono.js.
8+
//
9+
// Install once (in bench/): bun add hono @hono/node-server
10+
import { Hono } from "hono";
11+
12+
const app = new Hono();
13+
app.get("/", (c) => c.text("Hello, World!"));
14+
15+
const PORT = 3000;
16+
17+
if (typeof Deno !== "undefined") {
18+
Deno.serve({ hostname: "127.0.0.1", port: PORT, onListen() {} }, app.fetch);
19+
} else if (typeof Bun !== "undefined") {
20+
Bun.serve({ hostname: "127.0.0.1", port: PORT, fetch: app.fetch });
21+
} else if (typeof process !== "undefined" && process.versions && process.versions.node) {
22+
const { serve } = await import("@hono/node-server");
23+
serve({ fetch: app.fetch, hostname: "127.0.0.1", port: PORT });
24+
} else {
25+
const { serve } = await import("runtime:http");
26+
serve({ hostname: "127.0.0.1", port: PORT }, app.fetch);
27+
}

site/app/page.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import BenchChart from "../components/BenchChart.jsx";
2+
import RpsChart from "../components/RpsChart.jsx";
23
import InstallBox from "../components/InstallBox.jsx";
34
import StatusIcon from "../components/StatusIcon.jsx";
45

@@ -89,6 +90,9 @@ export default function HomePage() {
8990
</span>
9091
</div>
9192
<BenchChart metrics={HERO_METRICS} />
93+
<div className="mt-5 border-t border-zinc-100 pt-5">
94+
<RpsChart />
95+
</div>
9296
<a
9397
href="/docs/benchmarks"
9498
className="mt-5 inline-block text-xs font-medium text-brand-600 hover:text-brand-700"

site/components/RpsChart.jsx

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Higher-is-better companion to BenchChart for the HTTP requests/sec result
2+
// (bench/rps.sh). Data is inline because it comes from an external load
3+
// generator (autocannon), not the bench/run.sh JSON the other charts read.
4+
//
5+
// NOTE: the @opentf/web compiler rewrites `.map()` into a reactive list helper,
6+
// so non-render computations must use plain loops, and dynamic styles must be
7+
// objects (a style string becomes Object.assign(..., str)).
8+
const LABELS = { esrun: "esrun", bun: "Bun", node: "Node.js", deno: "Deno" };
9+
10+
// req/s, Hono hello-world over autocannon -c 100, one Linux x86-64 box.
11+
const ROW = { esrun: 40220, deno: 40150, bun: 39686, node: 33358 };
12+
const ORDER = ["esrun", "deno", "bun", "node"];
13+
14+
function fmt(v) {
15+
return (v / 1000).toFixed(1) + "k";
16+
}
17+
18+
export default function RpsChart() {
19+
let max = 0;
20+
let winner = null;
21+
for (const rt of ORDER) {
22+
if (ROW[rt] > max) {
23+
max = ROW[rt];
24+
winner = rt;
25+
}
26+
}
27+
28+
return (
29+
<div>
30+
<div className="mb-1.5 flex items-baseline justify-between">
31+
<span className="text-xs font-semibold uppercase tracking-wider text-zinc-500">
32+
HTTP requests/sec · Hono hello-world
33+
</span>
34+
<span className="text-[10px] text-zinc-400">higher is better</span>
35+
</div>
36+
<div className="space-y-1.5">
37+
{ORDER.map((rt) => {
38+
const pct = Math.max((ROW[rt] / max) * 100, 2);
39+
const isWin = rt === winner;
40+
return (
41+
<div className="flex items-center gap-2.5">
42+
<span className="w-14 shrink-0 text-right text-[11px] font-medium text-zinc-600">
43+
{LABELS[rt]}
44+
</span>
45+
<div className="h-3 flex-1 overflow-hidden rounded-full bg-zinc-100">
46+
<div
47+
className={
48+
isWin
49+
? "h-full rounded-full bg-emerald-500"
50+
: "h-full rounded-full bg-zinc-300"
51+
}
52+
style={{ width: pct + "%" }}
53+
/>
54+
</div>
55+
<span
56+
className={
57+
isWin
58+
? "w-14 shrink-0 text-right text-[11px] font-semibold tabular-nums text-emerald-700"
59+
: "w-14 shrink-0 text-right text-[11px] tabular-nums text-zinc-500"
60+
}
61+
>
62+
{fmt(ROW[rt])}
63+
</span>
64+
</div>
65+
);
66+
})}
67+
</div>
68+
</div>
69+
);
70+
}

0 commit comments

Comments
 (0)