Compares the old (master branch) vs new (universal branch) WebSocket implementations across latency, throughput, GC allocations, frame time impact, and burst handling.
cd benchmarks/server
npm install
node index.jsServer listens on ws://localhost:3000. Set PORT env var to change.
cd benchmarks/ConsoleRunner
dotnet run -c ReleaseThis runs the old vs new comparison without opening Unity and is useful for quick iteration on the benchmark harness.
python3 benchmarks/compare_results.py \
/path/to/benchmark_old_sync.csv \
/path/to/benchmark_old_sync_2.csv \
/path/to/benchmark_new_sync.csv \
/path/to/benchmark_new_sync_2.csv \
--baseline old \
--candidate newHelpful filters:
--benchmarks Throughput_Recv FrameTime_Dispatch Burst--metrics msgs_per_sec avg_ms p95_ms p99_ms total_time_ms--payloads 32 256 1024 8192 65536
The benchmark assets now include BenchmarkBatchRunner.RunSyncContextBenchmarks, which creates an empty scene, runs receive throughput + frame time + burst tests, writes a CSV, and exits Unity in batchmode.
Useful command-line args:
-benchmarkImplementation old|new-benchmarkOutput benchmark_results.csv-benchmarkThroughputDuration 5-benchmarkPayloadSizes 32,256,1024,8192,65536
- Create a new Unity project (2019.4+)
- Copy
BenchmarkOld/Assets/contents into your project'sAssets/folder - Create an empty scene and add an empty GameObject with the
BenchmarkRunnercomponent - Configure parameters in the Inspector (defaults are reasonable)
- Ensure
implementationNameis set to"old" - Press Play
- Create a new Unity project (2019.4+)
- Copy
BenchmarkNew/Assets/contents into your project'sAssets/folder - Create an empty scene and add an empty GameObject with the
BenchmarkRunnercomponent - Ensure
implementationNameis set to"new" - Press Play
CSV files are written to Application.persistentDataPath/benchmark_results.csv. The path is logged to the Unity Console at startup.
CSV columns: timestamp, implementation, benchmark, metric, value, payload_size
| Benchmark | What | Key Metrics |
|---|---|---|
| Latency | Round-trip time (send → echo → receive) | min/avg/median/p95/p99/max/stddev (ms) |
| Throughput_Send | Messages sent per second | msgs/sec, MB/sec |
| Throughput_Recv | Messages received per second (server floods) | msgs/sec, MB/sec |
| Throughput_Bidir | Simultaneous send + receive | send & recv msgs/sec |
| GCAlloc_Send | Heap allocation per Send() call | bytes/op, GC collections |
| GCAlloc_Dispatch | Heap allocation per DispatchMessageQueue() | bytes/op, GC collections |
| FrameTime_Baseline | Dispatch overhead with no traffic | avg/p99/max/jitter (ms) |
| FrameTime_Dispatch | Dispatch overhead under 50k-message flood | avg/median/p95/p99/max/jitter (ms) |
| FrameTime_UnderLoad | Total frame time during flood | avg/p99/max/jitter (ms) |
| Burst | Process 100 messages sent in <10ms | total time, peak/frame, peak frame time |
Each benchmark runs across payload sizes: 32B, 256B, 1KB, 8KB, 64KB (configurable).
The Unity BenchmarkRunner can also selectively enable send / receive / bidirectional throughput runs through:
runThroughputSendrunThroughputReceiverunThroughputBidirectional
| Area | Old (master) | New (universal) | Expected Impact |
|---|---|---|---|
| Receive path | Always creates MemoryStream per message | Single-frame fast path (avoids MemoryStream) | Lower GC alloc per receive |
| Dispatch | Copies list (new List<>(m_MessageList)) |
Swaps two lists (zero-copy) | Lower dispatch alloc + faster |
| Send | Monitor.TryEnter + .Wait() (blocking) |
Fully async with ConfigureAwait(false) |
Less frame time jitter |
| Event dispatch | Manual DispatchMessageQueue() required |
Auto via SynchronizationContext |
Dispatch is a no-op in Unity |
| OnOpen threading | Fires directly from Connect (thread-unsafe) | Fires via SyncContext (thread-safe) | No functional impact on benchmark |
The benchmark server responds to:
- Binary messages: Echoed back immediately
flood:<count>:<size>: Sendcountbinary messages ofsizebytesflood_continuous:<size>: Continuously flood binary messages ofsizebytes until stoppedflood_stop: Stop a continuous flood and send aflood_stoppedtext signalsink:start:<sessionId>: Stop echoing binary traffic for that connection (used by send-throughput tests)burst:<count>:<size>: Sendcountmessages +"burst_done"text signal- Text messages: Echoed back
benchmarks/
├── server/ # Node.js WebSocket echo/flood server
│ ├── package.json
│ └── index.js
├── BenchmarkOld/ # Old implementation (master branch)
│ └── Assets/
│ ├── WebSocket/
│ │ └── WebSocket.cs # Monolithic 848-line file from master
│ └── Benchmark/
│ ├── BenchmarkRunner.cs # Orchestrator MonoBehaviour
│ ├── LatencyBench.cs
│ ├── ThroughputBench.cs
│ ├── GCAllocBench.cs
│ ├── FrameTimeBench.cs
│ ├── BurstBench.cs
│ ├── BenchmarkStats.cs # Percentile/stats calculator
│ └── BenchmarkLogger.cs # CSV + console output
├── BenchmarkNew/ # New implementation (universal branch)
│ └── Assets/
│ ├── NativeWebSocket/
│ │ ├── IWebSocket.cs
│ │ ├── WebSocket.cs
│ │ └── WebSocketTypes.cs
│ └── Benchmark/
│ └── ... (identical scripts)
└── README.md
The benchmark C# scripts are identical in both projects. Only the WebSocket implementation differs.