Skip to content

Commit 4c30cd5

Browse files
authored
add benchmark suite (#867)
1 parent 73d1b8c commit 4c30cd5

File tree

11 files changed

+512
-0
lines changed

11 files changed

+512
-0
lines changed

.github/workflows/codspeed.yml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: CodSpeed Benchmarks
2+
3+
on:
4+
push:
5+
branches:
6+
- "main"
7+
pull_request:
8+
# `workflow_dispatch` allows CodSpeed to trigger backtest
9+
# performance analysis in order to generate initial data.
10+
workflow_dispatch:
11+
12+
env:
13+
BENCH_RESULT: /tmp/benchmark_results.json
14+
15+
permissions:
16+
contents: read
17+
id-token: write
18+
19+
jobs:
20+
benchmarks:
21+
name: Run benchmarks
22+
runs-on: ubuntu-latest
23+
24+
steps:
25+
- uses: actions/checkout@v4
26+
with:
27+
submodules: true
28+
29+
- uses: dtolnay/rust-toolchain@1.87.0
30+
with:
31+
targets: wasm32-unknown-unknown
32+
33+
- uses: actions/setup-node@v4
34+
with:
35+
node-version: '22'
36+
37+
- uses: Swatinem/rust-cache@v2
38+
with:
39+
key: cargo-bench-${{ hashFiles('**/Cargo.lock') }}
40+
41+
- name: Build wasm-bindgen and worker-build
42+
run: npm run build
43+
44+
- name: Install benchmark dependencies
45+
working-directory: benchmark
46+
run: npm install
47+
48+
- name: Build benchmark
49+
working-directory: benchmark
50+
run: node --run build
51+
52+
- name: Run benchmarks
53+
uses: CodSpeedHQ/action@v4
54+
with:
55+
mode: walltime
56+
run: cd benchmark && node run.js && node codspeed-convert.js $BENCH_RESULT

Cargo.lock

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

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ members = [
77
"worker-sys",
88
"examples/*",
99
"test/container-echo",
10+
"benchmark",
1011
]
1112
exclude = [
1213
"examples/coredump",
@@ -55,6 +56,7 @@ web-sys = { version = "0.3.90", features = [
5556
"FormData",
5657
"Headers",
5758
"MessageEvent",
59+
"Performance",
5860
"ProgressEvent",
5961
"ReadableStream",
6062
"ReadableStreamDefaultReader",

benchmark/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "workers-rs-benchmark"
3+
version = "0.1.0"
4+
edition = "2021"
5+
license = "MIT OR Apache-2.0"
6+
7+
[lib]
8+
crate-type = ["cdylib", "rlib"]
9+
10+
[dependencies]
11+
worker.workspace = true
12+
web-sys.workspace = true
13+
serde.workspace = true
14+
serde_json.workspace = true
15+
futures-util.workspace = true

benchmark/README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# workers-rs Benchmark Suite
2+
3+
Performance benchmark for workers-rs that measures streaming and parallel sub-request performance.
4+
5+
## How to run
6+
7+
First, make sure to clone workers-rs with all submodules.
8+
9+
Then from the root of workers-rs:
10+
11+
```bash
12+
npm run build
13+
```
14+
15+
to build the local `worker-build`.
16+
17+
Then run the benchmark:
18+
19+
```bash
20+
cd benchmark
21+
npm install
22+
npm run bench
23+
```
24+
25+
## What it does
26+
27+
- Streams 1MB of data from `/stream` endpoint in 8KB chunks
28+
- Makes 10 parallel sub-requests to `/stream` from `/benchmark` endpoint
29+
- All requests are internal (no network I/O) to isolate workers-rs performance
30+
- Runs 20 iterations with 3 warmup requests
31+
32+
## Output
33+
34+
The benchmark provides:
35+
36+
- Per-iteration timing for Node.js end-to-end and Worker internal execution
37+
- Summary statistics: average, min, and max times
38+
- Data transfer statistics (10MB per iteration = 10 parallel 1MB streams)
39+
- Average throughput in Mbps
40+
41+
## Configuration
42+
43+
Adjust parameters in `run.mjs`:
44+
- `iterations` - Number of benchmark runs (default: 20)
45+
- Warmup count (default: 3)
46+
47+
Adjust workload in `src/lib.rs`:
48+
- Number of parallel requests (default: 10)
49+
- Data size per request (default: 1MB)
50+
- Chunk size for streaming (default: 8KB)
51+
52+
## Rust Toolchain
53+
54+
`rust-toolchain.toml` in the root of workers-rs sets the Rust toolchain. Changing this can be used to
55+
benchmark against different toolchain versions.
56+
57+
## Compatibility Date
58+
59+
The current compaitibility date is set to `2025-11-01` in the `wrangler.toml`. Finalization registry was enabled as of `2025-05-05`, so is included.

benchmark/codspeed-convert.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Converts workers-rs benchmark results into CodSpeed walltime format.
5+
* Based on the codspeed-rust 4.1.0 result schema.
6+
*
7+
* Usage: node codspeed-convert.js <input.json>
8+
*
9+
* Writes results to $CODSPEED_PROFILE_FOLDER/results/<pid>.json
10+
* or ./codspeed-results/<pid>.json as fallback.
11+
*/
12+
13+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
14+
import { join } from 'node:path';
15+
16+
const IQR_OUTLIER_FACTOR = 1.5;
17+
const STDEV_OUTLIER_FACTOR = 3.0;
18+
19+
function quantile(sorted, q) {
20+
const pos = (sorted.length - 1) * q;
21+
const base = Math.floor(pos);
22+
const rest = pos - base;
23+
if (sorted[base + 1] !== undefined) {
24+
return sorted[base] + rest * (sorted[base + 1] - sorted[base]);
25+
}
26+
return sorted[base];
27+
}
28+
29+
function computeStats(name, uri, timesMs) {
30+
// Convert ms to ns
31+
const timesNs = timesMs.map(t => t * 1_000_000);
32+
const sorted = [...timesNs].sort((a, b) => a - b);
33+
const rounds = timesNs.length;
34+
35+
const mean = timesNs.reduce((a, b) => a + b, 0) / rounds;
36+
const stdev = rounds < 2 ? 0 : Math.sqrt(
37+
timesNs.reduce((sum, t) => sum + (t - mean) ** 2, 0) / (rounds - 1)
38+
);
39+
40+
const q1 = quantile(sorted, 0.25);
41+
const median = quantile(sorted, 0.5);
42+
const q3 = quantile(sorted, 0.75);
43+
const iqr = q3 - q1;
44+
45+
const iqrOutliers = timesNs.filter(
46+
t => t < q1 - IQR_OUTLIER_FACTOR * iqr || t > q3 + IQR_OUTLIER_FACTOR * iqr
47+
).length;
48+
const stdevOutliers = timesNs.filter(
49+
t => t < mean - STDEV_OUTLIER_FACTOR * stdev || t > mean + STDEV_OUTLIER_FACTOR * stdev
50+
).length;
51+
52+
return {
53+
name,
54+
uri,
55+
config: {},
56+
stats: {
57+
min_ns: sorted[0],
58+
max_ns: sorted[sorted.length - 1],
59+
mean_ns: mean,
60+
stdev_ns: stdev,
61+
q1_ns: q1,
62+
median_ns: median,
63+
q3_ns: q3,
64+
rounds,
65+
total_time: timesNs.reduce((a, b) => a + b, 0) / 1_000_000_000,
66+
iqr_outlier_rounds: iqrOutliers,
67+
stdev_outlier_rounds: stdevOutliers,
68+
iter_per_round: 1,
69+
warmup_iters: 3,
70+
},
71+
};
72+
}
73+
74+
const inputPath = process.argv[2];
75+
if (!inputPath) {
76+
console.error('Usage: node codspeed-convert.js <input.json>');
77+
process.exit(1);
78+
}
79+
80+
const results = JSON.parse(readFileSync(inputPath, 'utf-8'));
81+
82+
const benchmarks = [
83+
computeStats(
84+
'worker_internal_time',
85+
'benchmark/src/lib.rs::handle_benchmark::worker_internal_time',
86+
results.map(r => r.workerDuration)
87+
),
88+
computeStats(
89+
'e2e_time',
90+
'benchmark/run.js::runBenchmark::e2e_time',
91+
results.map(r => r.nodeJsDuration)
92+
),
93+
];
94+
95+
const output = {
96+
creator: {
97+
name: 'codspeed-node',
98+
version: '4.1.0',
99+
pid: process.pid,
100+
},
101+
instrument: { type: 'walltime' },
102+
benchmarks,
103+
};
104+
105+
const profileFolder = process.env.CODSPEED_PROFILE_FOLDER || 'codspeed-results';
106+
const resultsDir = join(profileFolder, 'results');
107+
mkdirSync(resultsDir, { recursive: true });
108+
109+
const outputPath = join(resultsDir, `${process.pid}.json`);
110+
writeFileSync(outputPath, JSON.stringify(output, null, 2));
111+
console.log(`CodSpeed results written to ${outputPath}`);

benchmark/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "workers-rs-benchmark",
3+
"version": "0.1.0",
4+
"type": "module",
5+
"description": "Performance benchmark suite for workers-rs",
6+
"private": true,
7+
"scripts": {
8+
"build": "WASM_BINDGEN_BIN=../wasm-bindgen/target/debug/wasm-bindgen ../target/debug/worker-build --release",
9+
"bench": "node --run build && node run.js"
10+
},
11+
"dependencies": {
12+
"miniflare": "^4.20260302.0"
13+
}
14+
}

0 commit comments

Comments
 (0)