Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@
"test:browser:ui": "playwright test --ui",
"test:autobahn": "cd test/autobahn && ./run-wstest.js",
"bench": "vitest bench --run --config vitest.bench.config.mjs",
"bench:baseline": "node test/benchmark/track-performance.mjs save",
"bench:compare": "node test/benchmark/track-performance.mjs compare",
"bench:check": "node test/benchmark/track-performance.mjs check",
"bench:baseline": "vitest bench --run --config vitest.bench.config.mjs --outputJson test/benchmark/baseline.json",
"bench:compare": "vitest bench --run --config vitest.bench.config.mjs --compare test/benchmark/baseline.json",
"bench:check": "vitest bench --run --config vitest.bench.config.mjs --compare test/benchmark/baseline.json",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The benchmark scripts bench:baseline, bench:compare, and bench:check repeat the base command from the bench script. To improve maintainability and reduce duplication, you can reuse the bench script and pass additional arguments. This way, if the base vitest command needs to be changed in the future, you'll only need to update it in one place.

Suggested change
"bench:baseline": "vitest bench --run --config vitest.bench.config.mjs --outputJson test/benchmark/baseline.json",
"bench:compare": "vitest bench --run --config vitest.bench.config.mjs --compare test/benchmark/baseline.json",
"bench:check": "vitest bench --run --config vitest.bench.config.mjs --compare test/benchmark/baseline.json",
"bench:baseline": "pnpm run bench -- --outputJson test/benchmark/baseline.json",
"bench:compare": "pnpm run bench -- --compare test/benchmark/baseline.json",
"bench:check": "pnpm run bench -- --compare test/benchmark/baseline.json",

"lint": "eslint lib/**/*.js test/**/*.js",
"lint:fix": "eslint lib/**/*.js test/**/*.js --fix"
},
Expand Down
44 changes: 35 additions & 9 deletions test/benchmark/README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
# WebSocket-Node Performance Benchmarks

This directory contains performance benchmarks for critical WebSocket operations.
This directory contains performance benchmarks for critical WebSocket operations using Vitest's built-in benchmarking functionality.

## Running Benchmarks

```bash
# Run all benchmarks
pnpm run bench

# Compare with previous results
# Save current results as baseline
pnpm run bench:baseline

# Compare with baseline (shows ⇑/⇓ indicators)
pnpm run bench:compare

# Check for regressions (CI mode)
pnpm run bench:check

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The description for bench:check is a bit brief. To make it clearer for other contributors, especially in a CI context, it would be helpful to explicitly state that this command will fail (exit with a non-zero code) if a performance regression is detected.

Suggested change
# Check for regressions (CI mode)
pnpm run bench:check
# Check for regressions (exits with an error on performance drops, for CI)
pnpm run bench:check

```

## Benchmark Suites
Expand Down Expand Up @@ -46,15 +52,35 @@ Benchmarks output operations per second (hz) and timing statistics:

## Performance Baselines

These benchmarks establish baseline performance for regression detection:
1. Frame serialization should maintain 4M+ ops/sec for small frames
2. Connection operations should maintain 25K+ ops/sec
3. Large message handling (64KB) should not degrade significantly
Baseline results are stored in `baseline.json` using Vitest's JSON format. When running `bench:compare` or `bench:check`, Vitest automatically compares current results against the baseline and shows:
- `[1.05x] ⇑` for improvements (faster)
- `[0.95x] ⇓` for regressions (slower)
- Baseline values for reference

Expected performance ranges:
1. Frame serialization: 3-4.5M ops/sec
2. Message sending: 100K-900K ops/sec (varies by size)
3. Ping/Pong: 1.5-2M ops/sec
4. Connection creation: 30K ops/sec

## Benchmark Structure

Each operation is in its own `describe` block to prevent Vitest from treating them as alternative implementations for comparison. This structure ensures each operation is measured independently:

```javascript
describe('Send Ping Frame', () => {
bench('send ping frame', () => {
sharedConnection.ping();
});
});
```

## Adding New Benchmarks

When adding benchmarks:
1. Pre-allocate buffers and data outside the benchmark loop
2. Use descriptive test names with size information
3. Focus on operations that directly impact production performance
4. Avoid testing implementation details
2. Create shared connections at module scope (not inside benchmark functions)
3. Use descriptive test names with size information
4. Put each unique operation in its own `describe` block
5. Focus on operations that directly impact production performance
6. Avoid testing implementation details
305 changes: 290 additions & 15 deletions test/benchmark/baseline.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,294 @@
{
"timestamp": "2025-10-06T17:27:59.641Z",
"results": {
"WebSocketConnection Performance 7359ms": {
"create connection instance": 28847.63,
"send small UTF-8 message": 914160.4,
"send medium UTF-8 message (1KB)": 108086.21,
"send binary message (1KB)": 222918.81,
"send ping frame": 2375454.67,
"send pong frame": 1935975.19
"files": [
{
"filepath": "/home/ubuntu/code/websocket-node/test/benchmark/connection-operations.bench.mjs",
"groups": [
{
"fullName": "test/benchmark/connection-operations.bench.mjs > Connection Creation",
"benchmarks": [
{
"id": "347648886_0_0",
"sampleCount": 14950,
"name": "create connection instance",
"rank": 1,
"rme": 5.721365271168026,
"totalTime": 500.017038999993,
"min": 0.02091599999994287,
"max": 5.860308999999916,
"hz": 29898.981102522404,
"period": 0.03344595578595271,
"mean": 0.03344595578595271,
"variance": 0.014250024909513178,
"sd": 0.1193734681975571,
"sem": 0.0009763088259937304,
"df": 14949,
"critical": 1.96,
"moe": 0.0019135652989477115,
"p75": 0.035286000000041895,
"p99": 0.07499600000005557,
"p995": 0.0863120000001345,
"p999": 0.40689399999996567
}
]
},
{
"fullName": "test/benchmark/connection-operations.bench.mjs > Send Small UTF-8 Message",
"benchmarks": [
{
"id": "347648886_1_0",
"sampleCount": 433879,
"name": "send small UTF-8 message",
"rank": 1,
"rme": 11.43139905071693,
"totalTime": 506.6114810000274,
"min": 0.00028599999996004044,
"max": 9.61037800000031,
"hz": 856433.4135174811,
"period": 0.0011676330981679856,
"mean": 0.0011676330981679856,
"variance": 0.0020121856762223642,
"sd": 0.04485739265965382,
"sem": 0.000068100407601955,
"df": 433878,
"critical": 1.96,
"moe": 0.00013347679889983178,
"p75": 0.0007550000000264845,
"p99": 0.0038449999997283157,
"p995": 0.01220100000000457,
"p999": 0.02408300000001873
}
]
},
{
"fullName": "test/benchmark/connection-operations.bench.mjs > Send Medium UTF-8 Message (1KB)",
"benchmarks": [
{
"id": "347648886_2_0",
"sampleCount": 48440,
"name": "send medium UTF-8 message (1KB)",
"rank": 1,
"rme": 4.570568475755207,
"totalTime": 500.00057599997626,
"min": 0.0008000000002539309,
"max": 5.454282000000148,
"hz": 96879.88839437316,
"period": 0.010322059785300914,
"mean": 0.010322059785300914,
"variance": 0.0028065008097464196,
"sd": 0.0529764174869009,
"sem": 0.0002407024543854945,
"df": 48439,
"critical": 1.96,
"moe": 0.0004717768105955692,
"p75": 0.015486000000237254,
"p99": 0.039346000000023196,
"p995": 0.04509699999971417,
"p999": 0.1321459999999206
}
]
},
{
"fullName": "test/benchmark/connection-operations.bench.mjs > Send Binary Message (1KB)",
"benchmarks": [
{
"id": "347648886_3_0",
"sampleCount": 108833,
"name": "send binary message (1KB)",
"rank": 1,
"rme": 6.408247345850968,
"totalTime": 500.0128869999694,
"min": 0.0002959999997074192,
"max": 8.217849999999999,
"hz": 217660.39002112093,
"period": 0.004594313186257563,
"mean": 0.004594313186257563,
"variance": 0.0024556597086718116,
"sd": 0.04955461339443394,
"sem": 0.00015021171062164863,
"df": 108832,
"critical": 1.96,
"moe": 0.0002944149528184313,
"p75": 0.004922000000078697,
"p99": 0.027522000000317348,
"p995": 0.03348600000026636,
"p999": 0.11101899999994203
}
]
},
{
"fullName": "test/benchmark/connection-operations.bench.mjs > Send Ping Frame",
"benchmarks": [
{
"id": "347648886_4_0",
"sampleCount": 1062702,
"name": "send ping frame",
"rank": 1,
"rme": 20.14485654032809,
"totalTime": 508.32634000000144,
"min": 0.00018199999976786785,
"max": 32.92630800000006,
"hz": 2090590.0725112867,
"period": 0.0004783338508819984,
"mean": 0.0004783338508819984,
"variance": 0.002568561363662073,
"sd": 0.05068097634874523,
"sem": 0.00004916309594081911,
"df": 1062701,
"critical": 1.96,
"moe": 0.00009635966804400545,
"p75": 0.00024899999971239595,
"p99": 0.0006320000002233428,
"p995": 0.0008950000001277658,
"p999": 0.011792999999670428
}
]
},
{
"fullName": "test/benchmark/connection-operations.bench.mjs > Send Pong Frame",
"benchmarks": [
{
"id": "347648886_5_0",
"sampleCount": 962038,
"name": "send pong frame",
"rank": 1,
"rme": 18.913749130520372,
"totalTime": 500.0001680007763,
"min": 0.00019799999972747173,
"max": 31.614644000000226,
"hz": 1924075.353507694,
"period": 0.0005197301645057433,
"mean": 0.0005197301645057433,
"variance": 0.0024198652313353448,
"sd": 0.049192125704581466,
"sem": 0.00005015329564809036,
"df": 962037,
"critical": 1.96,
"moe": 0.0000983004594702571,
"p75": 0.0003349999997226405,
"p99": 0.0009099999997488339,
"p995": 0.0018499999996492988,
"p999": 0.011822000000393018
}
]
}
]
},
"WebSocketFrame Performance 9745ms": {
"serialize small text frame (17 bytes, unmasked)": 4240401.67,
"serialize small text frame (17 bytes, masked)": 3070532.44,
"serialize medium binary frame (1KB)": 4418217.61,
"serialize large binary frame (64KB)": 3918678.38
{
"filepath": "/home/ubuntu/code/websocket-node/test/benchmark/frame-operations.bench.mjs",
"groups": [
{
"fullName": "test/benchmark/frame-operations.bench.mjs > Serialize Small Text Frame (Unmasked)",
"benchmarks": [
{
"id": "2141590085_0_0",
"sampleCount": 2221574,
"name": "serialize small text frame (17 bytes, unmasked)",
"rank": 1,
"rme": 0.9893597558817244,
"totalTime": 500.0001229996669,
"min": 0.0001449999999749707,
"max": 0.5502369999994698,
"hz": 4443146.90698882,
"period": 0.00022506570701658686,
"mean": 0.00022506570701658686,
"variance": 0.00000286731744387617,
"sd": 0.0016933155181111906,
"sem": 0.0000011360762905677454,
"df": 2221573,
"critical": 1.96,
"moe": 0.000002226709529512781,
"p75": 0.00019599999905040022,
"p99": 0.0005499999988387572,
"p995": 0.0006460000004153699,
"p999": 0.006724000000758679
}
]
},
{
"fullName": "test/benchmark/frame-operations.bench.mjs > Serialize Small Text Frame (Masked)",
"benchmarks": [
{
"id": "2141590085_1_0",
"sampleCount": 1534250,
"name": "serialize small text frame (17 bytes, masked)",
"rank": 1,
"rme": 3.3082056682221554,
"totalTime": 500.0079150010697,
"min": 0.0002190000013797544,
"max": 6.977795999999216,
"hz": 3068451.4264073553,
"period": 0.0003258972885781781,
"mean": 0.0003258972885781781,
"variance": 0.00004642270968057881,
"sd": 0.006813421290407544,
"sem": 0.00000550069008843143,
"df": 1534249,
"critical": 1.96,
"moe": 0.000010781352573325602,
"p75": 0.0002859999985957984,
"p99": 0.0007029999997030245,
"p995": 0.0009759999993548263,
"p999": 0.008757999999943422
}
]
},
{
"fullName": "test/benchmark/frame-operations.bench.mjs > Serialize Medium Binary Frame (1KB)",
"benchmarks": [
{
"id": "2141590085_2_0",
"sampleCount": 2164023,
"name": "serialize medium binary frame (1KB)",
"rank": 1,
"rme": 1.6649018583076653,
"totalTime": 500.0305290022534,
"min": 0.00015799999891896732,
"max": 2.1783230000000913,
"hz": 4327781.754282142,
"period": 0.00023106525623907575,
"mean": 0.00023106525623907575,
"variance": 0.000008336740867673836,
"sd": 0.0028873414878870557,
"sem": 0.0000019627600739937454,
"df": 2164022,
"critical": 1.96,
"moe": 0.000003847009745027741,
"p75": 0.00020399999993969686,
"p99": 0.0005010000004403992,
"p995": 0.0005930000006628688,
"p999": 0.006003999998938525
}
]
},
{
"fullName": "test/benchmark/frame-operations.bench.mjs > Serialize Large Binary Frame (64KB)",
"benchmarks": [
{
"id": "2141590085_3_0",
"sampleCount": 2251276,
"name": "serialize large binary frame (64KB)",
"rank": 1,
"rme": 1.1766836796382618,
"totalTime": 500.00007300055404,
"min": 0.00015700000039942097,
"max": 1.1734879999985424,
"hz": 4502551.342622515,
"period": 0.00022209630138665985,
"mean": 0.00022209630138665985,
"variance": 0.000004002383606964811,
"sd": 0.002000595812992922,
"sem": 0.0000013333525160699149,
"df": 2251275,
"critical": 1.96,
"moe": 0.000002613370931497033,
"p75": 0.0001940000001923181,
"p99": 0.0005029999992984813,
"p995": 0.0006020000000717118,
"p999": 0.005696000000170898
}
]
}
]
}
}
]
}
Loading