You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Merge the receive-path optimization from PR #158 and add further low-allocation improvements for pooled receive buffers, text-send encoding, cached observables, and binary message logging.
Add a BenchmarkDotNet project with documented before/after results so receive buffering, text encoding, observable access, and response-message formatting can be measured repeatably.
* using Channels for high performance sending queue
22
+
* allocation-conscious receive and send paths using reusable buffers and pooled text encoding
23
+
24
+
### Performance
25
+
26
+
Websocket.Client is designed for long-running websocket consumers where per-message allocation and reconnect behavior matter:
27
+
28
+
* incoming messages are accumulated in a reusable pooled receive buffer
29
+
* text messages are decoded directly from the received bytes
30
+
* outgoing text messages are encoded into rented `ArrayPool<byte>` buffers
31
+
* the sending queue uses `System.Threading.Channels` with a single reader
32
+
* public observable wrappers are cached to avoid per-access wrapper allocations
33
+
34
+
Representative BenchmarkDotNet results show meaningful improvements on typical small and medium messages:
35
+
36
+
* 128 B text receive path: `355.76 ns / 560 B` to `54.36 ns / 280 B`
37
+
* 4 KB text receive path: `1.130 us / 8496 B` to `738.98 ns / 8216 B`
38
+
* 1024 char text send encoding: `134.04 ns / 1048 B` to `87.44 ns / 0 B`
39
+
* 8192 char text send encoding: `974.85 ns / 8216 B` to `501.86 ns / 0 B`
40
+
* stream-backed binary `ResponseMessage.ToString()` at 32 KB: `1.880 us / 32872 B` to `101.23 ns / 104 B`
41
+
42
+
For very large text messages, the resulting `string` allocation dominates the receive cost, so the library focuses on avoiding unnecessary intermediate allocations and avoiding retention of oversized receive buffers after traffic spikes.
43
+
44
+
See the [benchmarks](benchmarks/README.md) folder for the BenchmarkDotNet project, commands, and full result tables.
This folder contains BenchmarkDotNet benchmarks for the allocation-sensitive Websocket.Client hot paths.
4
+
5
+
The latest representative run below was captured on Windows 11, AMD Ryzen 9 3900X, .NET SDK 10.0.201, running the benchmarks on .NET 8.0.25 with BenchmarkDotNet `ShortRun`.
6
+
7
+
Run all benchmarks:
8
+
9
+
```powershell
10
+
dotnet run --configuration Release --project benchmarks\Websocket.Client.Benchmarks -- --filter "*"
11
+
```
12
+
13
+
Run the most relevant receive/send benchmarks:
14
+
15
+
```powershell
16
+
dotnet run --configuration Release --project benchmarks\Websocket.Client.Benchmarks -- --filter "*ReceiveBufferBenchmarks*"
17
+
dotnet run --configuration Release --project benchmarks\Websocket.Client.Benchmarks -- --filter "*TextSendEncodingBenchmarks*"
18
+
```
19
+
20
+
Results are written to `BenchmarkDotNet.Artifacts\results`.
21
+
22
+
## What The Benchmarks Cover
23
+
24
+
-`ReceiveBufferBenchmarks` compares the old per-message `RecyclableMemoryStream` receive path, PR #158's `ArrayBufferWriter<byte>` path, and the current pooled receive buffer.
25
+
-`TextSendEncodingBenchmarks` compares outgoing text encoding through `Encoding.GetBytes(string)` against encoding into an `ArrayPool<byte>` buffer.
26
+
-`ResponseMessageBenchmarks` measures the `ResponseMessage.ToString()` stream-copy fix.
27
+
-`ObservablePropertyBenchmarks` measures cached observable properties versus creating an `AsObservable()` wrapper on every access.
28
+
-`ClientReceiveBenchmarks` measures the current public `WebsocketClient` receive path using an in-memory scripted `WebSocket`.
29
+
30
+
Use the Ratio and Allocated columns for the quick answer. Values below 1.00 in Ratio are faster than the baseline, and lower Allocated values mean less GC pressure.
31
+
32
+
## Representative Results
33
+
34
+
### Receive Buffering
35
+
36
+
`ReceiveBufferBenchmarks` compares the original receive path, PR #158, and the current pooled buffer. These benchmarks include UTF-8 decoding to `string`, so large text messages are dominated by the required output string allocation.
37
+
38
+
| Message size | Before: RecyclableMemoryStream | PR #158: ArrayBufferWriter | Current: pooled buffer | Current impact |
39
+
| ---: | ---: | ---: | ---: | --- |
40
+
| 128 B | 355.76 ns, 560 B | 63.18 ns, 280 B | 54.36 ns, 280 B | 85% faster, 50% less allocation |
41
+
| 4 KB | 1.130 us, 8496 B | 847.05 ns, 8216 B | 738.98 ns, 8216 B | 35% faster, slightly less allocation |
42
+
| 32 KB | 4.966 us, 65840 B | 5.440 us, 65560 B | 5.166 us, 65560 B | roughly neutral |
43
+
| 128 KB | 98.302 us, 262476 B | 98.932 us, 262196 B | 105.350 us, 262224 B | roughly neutral; avoids retaining a large receive buffer |
44
+
45
+
### Text Send Encoding
46
+
47
+
`TextSendEncodingBenchmarks` measures the change from `Encoding.GetBytes(string)` to encoding into a rented `ArrayPool<byte>` buffer before calling `WebSocket.SendAsync`.
| 32 chars | 16.39 ns, 56 B | 40.13 ns, 0 B | removes allocation with a small CPU cost |
52
+
| 1024 chars | 134.04 ns, 1048 B | 87.44 ns, 0 B | 35% faster, allocation-free |
53
+
| 8192 chars | 974.85 ns, 8216 B | 501.86 ns, 0 B | 48% faster, allocation-free |
54
+
55
+
### Binary Message Logging
56
+
57
+
`ResponseMessageBenchmarks` measures `ResponseMessage.ToString()` for stream-backed binary messages. The previous implementation copied the stream with `ToArray()` just to print the length.
`ClientReceiveBenchmarks` measures the current public `WebsocketClient` receive flow using an in-memory scripted `WebSocket`. It is not a before/after benchmark; it is a package-level smoke benchmark for the real client path.
0 commit comments