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
--compare now also hosts gRPC for .NET (Grpc.AspNetCore 2.84.0, HTTP/2 +
protobuf) on the same Kestrel, answering the benchmark's five calls from
Protos/calculator.proto, and drives it with Grpc.Net.Client in the same shape
as the other rows: 16 channels with 256 calls in flight each. Two rows: unary
calls, and one bidirectional stream per channel with batched writes
(BufferHint on all but the last message of a refill; the server flushes when
its input runs dry, the once-per-read-group flush JsonRpcConnectionHandler
does). protobuf has no decimal, so Test2 uses the units/nanos DecimalValue
message; nullable values use proto3 optional. Every reply is checked.
Two 3 s runs: unary 142-183k RPC/s, stream 173-182k. The gRPC client shares the
8 cores with the server (there is no cheap raw client for HTTP/2 + protobuf),
which the README says; one channel alone reaches about 130k unary calls/s.
Also: the header line prints NuGet package versions (StreamJsonRpc 2.25.29,
not the 2.25.0.0 assembly version); the chart labels a sub-million range that
rounds to one value as that value; --compare help text names gRPC.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: README.md
+8-4Lines changed: 8 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -233,7 +233,7 @@ Registering a pre- or post-process handler switches the affected session onto a
233
233
```
234
234
dotnet run -c Release --project TestServer_Console -- --sync 3 # library only, 1..N threads (add a thread count, e.g. --sync 3 1, for one row)
235
235
dotnet run -c Release --project TestServer_Console -- --kestrel 3 # through the AspNetCore package, HTTP and TCP
236
-
dotnet run -c Release --project TestServer_Console -- --compare 3 # the same requests through StreamJsonRpc, side by side
236
+
dotnet run -c Release --project TestServer_Console -- --compare 3 # the same calls through StreamJsonRpc and gRPC for .NET, side by side
237
237
dotnet run -c Release --project TestServer_Console # menu: Enter = Task mode, s = sync, k = Kestrel, x = compare, q = quit
238
238
dotnet run --project samples/WasmHost # browser: "Run benchmark" on the page
239
239
```
@@ -289,11 +289,11 @@ Sync beats Task mode because Task mode measures the .NET thread pool and per-req
289
289
290
290
The TCP client keeps 256 requests in flight per connection and refills from a precomputed ring of request bytes with one `Send` per refill; the server side is the same `Process` call the HTTP endpoint makes, fed by `JsonFramer`.
291
291
292
-
### Versus StreamJsonRpc
292
+
### Versus StreamJsonRpc and gRPC
293
293
294
-
[StreamJsonRpc](https://www.nuget.org/packages/StreamJsonRpc) is Microsoft's JSON-RPC library, the one behind Visual Studio and the language-server stack. `--compare` hosts both libraries on the same Kestrel TCP listener and drives them with the same pipelining client (16 connections, 256 requests in flight each), so the only variable is the library answering. StreamJsonRpc requires the `jsonrpc` member, so every request in this mode carries `"jsonrpc":"2.0"`, which is why the JSON-RPC.Net rows are a little below the other tables. Each row was run twice for 3 s; both results are shown.
294
+
[StreamJsonRpc](https://www.nuget.org/packages/StreamJsonRpc) is Microsoft's JSON-RPC library, the one behind Visual Studio and the language-server stack. `--compare` hosts both libraries on the same Kestrel TCP listener and drives them with the same pipelining client (16 connections, 256 requests in flight each), so the only variable is the library answering. StreamJsonRpc requires the `jsonrpc` member, so every request in this mode carries `"jsonrpc":"2.0"`, which is why the JSON-RPC.Net rows are a little below the other tables. The same mode also hosts [gRPC for .NET](https://learn.microsoft.com/aspnet/core/grpc/) (HTTP/2, protobuf) on the same Kestrel, answering the same five calls from [calculator.proto](TestServer_Console/Protos/calculator.proto), driven by its own client with the same shape: 16 channels, 256 calls in flight each. Each row was run twice for 3 s; both results are shown.
295
295
296
-

296
+

297
297
298
298
| Library and path | RPC/s |
299
299
| --- | ---: |
@@ -304,9 +304,13 @@ The TCP client keeps 256 requests in flight per connection and refills from a pr
304
304
| StreamJsonRpc over Kestrel TCP, newline framing, System.Text.Json formatter | 1.05 M to 1.10 M |
305
305
| StreamJsonRpc over Kestrel TCP, `Content-Length` framing, System.Text.Json formatter | 1.14 M to 1.18 M |
306
306
| StreamJsonRpc over Kestrel TCP, `Content-Length` framing, Json.NET formatter (its default) | 515 k to 518 k |
307
+
| gRPC for .NET, unary calls over HTTP/2 (Grpc.Net.Client, 16 channels × 256 in flight) | 142 k to 183 k |
308
+
| gRPC for .NET, one bidirectional stream per channel, 256 in flight, batched writes | 173 k to 182 k |
307
309
308
310
StreamJsonRpc 2.25.29, defaults apart from the formatter and framing named in each row. It is a full bidirectional RPC framework (client proxies, cancellation, progress, marshaled objects, events), and its server side has no "document in, document out" call, so its in-process row is a pair of `System.IO.Pipelines` pipes, the closest it has to a direct call. The comparison is of the server side answering the same five requests; on that measure JSON-RPC.Net is about 10× faster on the same connections with the same JSON library underneath.
309
311
312
+
gRPC for .NET 2.84.0 with default settings apart from Kestrel's `MaxStreamsPerConnection` (raised to 256 so the pipeline depth is not capped at 100). protobuf has no `decimal`, so `Test2` carries the units/nanos `DecimalValue` message the gRPC docs recommend; nullable values use proto3 `optional`. The gRPC rows are a different kind of measurement from the rows above them: there is no cheap raw client for HTTP/2 + protobuf, so the client is Grpc.Net.Client on the same 8 cores as the server, and the figure is what a .NET caller and a .NET service get end to end. One channel alone reaches about 130 k unary calls per second; sixteen channels do not scale much further because client and server compete for the same cores. The streaming row batches its writes the way the TCP client does (BufferHint on every message but the last of a refill), and the server flushes only when its input runs dry, the same once-per-read-group flush `JsonRpcConnectionHandler` does.
313
+
310
314
### WebAssembly: in the browser
311
315
312
316
The [WasmHost sample](samples/WasmHost/README.md) compares JSON-RPC through JS interop with plain Blazor interop for the same `add(1, 2)` under the .NET 10 interpreter in Chrome. A plain `DotNet.invokeMethod` add costs about 64 µs (the JSON marshalling Blazor does); a JSON-RPC document written as UTF-8 straight into WebAssembly memory and run through a `[JSExport]` costs 53 µs, a batch of 100 that way reaches 27k RPC/s, and a typed `[JSExport]` add takes 0.35 µs. The interpreter is the bottleneck; `dotnet publish` AOT-compiles the sample when the `wasm-tools` workload is installed.
k.Listen(IPAddress.Loopback,sjrNewLineStjPort, l =>l.Run(c =>ServeStreamJsonRpc(c,Framing.NewLine,Formatter.SystemTextJson)));
75
77
k.Listen(IPAddress.Loopback,sjrHeaderStjPort, l =>l.Run(c =>ServeStreamJsonRpc(c,Framing.Header,Formatter.SystemTextJson)));
76
78
k.Listen(IPAddress.Loopback,sjrHeaderNewtonsoftPort, l =>l.Run(c =>ServeStreamJsonRpc(c,Framing.Header,Formatter.Newtonsoft)));
79
+
k.Listen(IPAddress.Loopback,grpcPort, l =>l.Protocols=HttpProtocols.Http2);
80
+
k.Limits.Http2.MaxStreamsPerConnection=pipeline;// Kestrel's default of 100 would cap the gRPC rows below the pipeline depth
77
81
});
78
82
builder.Services.AddJsonRpc();
83
+
builder.Services.AddGrpc();
79
84
varapp=builder.Build();
85
+
app.MapGrpcService<GrpcCompare.CalculatorGrpc>();
80
86
awaitapp.StartAsync();
81
87
82
88
try
83
89
{
84
-
print($"JSON-RPC.Net {typeof(JsonRpcProcessor).Assembly.GetName().Version} vs StreamJsonRpc {typeof(JsonRpc).Assembly.GetName().Version}; Kestrel TCP on loopback, {clients} clients, pipeline {pipeline}, {seconds:0.#} s per row\n");
90
+
print($"JSON-RPC.Net {typeof(JsonRpcProcessor).Assembly.GetName().Version} vs StreamJsonRpc {PackageVersion(typeof(JsonRpc))} vs gRPC for .NET {PackageVersion(typeof(Grpc.Net.Client.GrpcChannel))}; Kestrel on loopback, {clients} clients, pipeline {pipeline}, {seconds:0.#} s per row\n");
0 commit comments