Skip to content

Commit ccce23c

Browse files
Astnclaude
andcommitted
README: SVG charts for the StreamJsonRpc comparison, transports, thread sweep
benchmarks/charts/render.py emits the three horizontal bar charts from the README numbers; the SVGs are committed so GitHub renders them inline. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 75d09fe commit ccce23c

5 files changed

Lines changed: 223 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,8 @@ All numbers below are from an AMD Ryzen 7 7800X3D (8 cores / 16 threads, 4.2 GHz
244244

245245
`--sync` calls the byte-level `JsonRpcProcessor.Process` in a loop from 1, 2, 4, ... threads up to the core count, so it measures parsing, dispatch, binding and response writing with no scheduler in the way.
246246

247+
![JSON-RPC.Net alone, by thread count](benchmarks/charts/sync-threads.svg)
248+
247249
| Threads | RPC/s | ns per request per thread | Allocations per request |
248250
| ---: | ---: | ---: | --- |
249251
| 1 | 4.4 M | 227 | 0 bytes for numeric shapes, one string for `StringMe` |
@@ -273,6 +275,8 @@ Sync beats Task mode because Task mode measures the .NET thread pool and per-req
273275

274276
`--kestrel` starts a real Kestrel on loopback with `MapJsonRpc` and `JsonRpcConnectionHandler`, then drives it with 16 clients on the same machine, so every figure is an upper bound for one box talking to itself. The in-process rows are the same requests through the byte entry point with no transport at all, for scale.
275277

278+
![JSON-RPC.Net by transport](benchmarks/charts/kestrel-transports.svg)
279+
276280
| Transport | RPC/s | Note |
277281
| --- | ---: | --- |
278282
| in-process, 1 thread | 4.2 M | |
@@ -287,6 +291,8 @@ The TCP client keeps 256 requests in flight per connection and refills from a pr
287291

288292
[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.
289293

294+
![JSON-RPC.Net vs StreamJsonRpc](benchmarks/charts/compare-streamjsonrpc.svg)
295+
290296
| Library and path | RPC/s |
291297
| --- | ---: |
292298
| JSON-RPC.Net in-process, 1 thread (direct call, bytes in, bytes out) | 3.1 M to 3.3 M |
@@ -307,6 +313,8 @@ The [WasmHost sample](samples/WasmHost/README.md) compares JSON-RPC through JS i
307313

308314
simdjson was evaluated as a fourth parser and not adopted: through the only maintained .NET binding its parse alone costs more than the whole built-in envelope read, and walking the result is 5 to 6 times slower with 350 bytes or more of garbage per request. The harness and numbers are in [benchmarks/SimdJsonEval/RESULTS.md](benchmarks/SimdJsonEval/RESULTS.md).
309315

316+
The charts above are rendered by `python benchmarks/charts/render.py` from the numbers in this file.
317+
310318
For comparison, under the previous harness (one pass per batch, workstation GC) the two-million batch ran at about 525,000 RPC/s on 1.3 and 1,584,906 RPC/s on 2.0 on the same machine. (The 1.x figure published earlier in this README was measured while the benchmark service was not bound, so every request took the "method not found" path; the benchmark now prints the responses so that cannot go unnoticed.)
311319

312320
## Upgrading from 1.x
Lines changed: 38 additions & 0 deletions
Loading
Lines changed: 32 additions & 0 deletions
Loading

‎benchmarks/charts/render.py‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Renders the README benchmark charts as SVG.
2+
3+
python benchmarks/charts/render.py
4+
5+
Numbers are the ones in README.md (8-core Ryzen 7 7800X3D, .NET 10, Release, Server GC). Edit the tables
6+
below when the README numbers change and re-run; the SVGs are committed so GitHub can show them.
7+
"""
8+
import os
9+
10+
HERE = os.path.dirname(os.path.abspath(__file__))
11+
12+
FONT = "-apple-system, 'Segoe UI', Helvetica, Arial, sans-serif"
13+
COLORS = {
14+
"ours": "#16a34a", # JSON-RPC.Net
15+
"theirs": "#3b82f6", # StreamJsonRpc
16+
"scale": "#9ca3af", # in-process reference rows
17+
}
18+
19+
20+
def fmt(v):
21+
if v >= 1e6:
22+
return f"{v / 1e6:.1f} M"
23+
if v >= 1e3:
24+
return f"{v / 1e3:.0f} k"
25+
return f"{v:.0f}"
26+
27+
28+
def chart(title, subtitle, rows, filename, width=920):
29+
"""rows: (label, low, high, kind, note). Bar length is the mean of low and high; the label shows the range."""
30+
left, right, top, row_h, bar_h = 330, 110, 78, 34, 20
31+
height = top + row_h * len(rows) + 46
32+
plot_w = width - left - right
33+
vmax = max((lo + hi) / 2 for _, lo, hi, _, _ in rows)
34+
35+
out = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}" '
36+
f'font-family="{FONT}" font-size="13">',
37+
f'<rect width="{width}" height="{height}" rx="10" fill="#ffffff" stroke="#e5e7eb"/>',
38+
f'<text x="24" y="32" font-size="18" font-weight="600" fill="#111827">{title}</text>',
39+
f'<text x="24" y="54" font-size="12" fill="#6b7280">{subtitle}</text>']
40+
41+
# gridlines at round values
42+
step = 10 ** (len(str(int(vmax))) - 1)
43+
if vmax / step < 3:
44+
step /= 2
45+
g = step
46+
while g <= vmax:
47+
x = left + plot_w * g / vmax
48+
out.append(f'<line x1="{x:.1f}" y1="{top - 8}" x2="{x:.1f}" y2="{height - 40}" stroke="#f3f4f6"/>')
49+
out.append(f'<text x="{x:.1f}" y="{height - 24}" text-anchor="middle" font-size="11" fill="#9ca3af">{fmt(g)}</text>')
50+
g += step
51+
out.append(f'<text x="{left + plot_w:.1f}" y="{height - 10}" text-anchor="end" font-size="11" fill="#9ca3af">requests per second</text>')
52+
53+
for i, (label, lo, hi, kind, note) in enumerate(rows):
54+
y = top + i * row_h
55+
v = (lo + hi) / 2
56+
w = max(2.0, plot_w * v / vmax)
57+
out.append(f'<text x="{left - 12}" y="{y + bar_h / 2 + 4}" text-anchor="end" fill="#111827">{label}</text>')
58+
if note:
59+
out.append(f'<text x="{left - 12}" y="{y + bar_h / 2 + 17}" text-anchor="end" font-size="10.5" fill="#6b7280">{note}</text>')
60+
out.append(f'<rect x="{left}" y="{y}" width="{w:.1f}" height="{bar_h}" rx="4" fill="{COLORS[kind]}"/>')
61+
if lo == hi:
62+
value = fmt(v)
63+
else:
64+
a, b = fmt(lo), fmt(hi)
65+
if a == b: # the range rounds to the same text at one decimal: show two
66+
a, b = f"{lo / 1e6:.2f} M", f"{hi / 1e6:.2f} M"
67+
value = f"{a} to {b}"
68+
out.append(f'<text x="{left + w + 8:.1f}" y="{y + bar_h / 2 + 4}" font-weight="600" fill="#111827">{value}</text>')
69+
70+
out.append('</svg>')
71+
path = os.path.join(HERE, filename)
72+
with open(path, "w", encoding="utf-8", newline="\n") as f:
73+
f.write("\n".join(out) + "\n")
74+
print("wrote", os.path.relpath(path))
75+
76+
77+
chart(
78+
"JSON-RPC.Net vs StreamJsonRpc",
79+
"Same five requests, same Kestrel TCP listener, same pipelining client (16 connections, 256 in flight). Two 3 s runs each.",
80+
[
81+
("JSON-RPC.Net over Kestrel TCP", 11.4e6, 12.1e6, "ours", "raw documents"),
82+
("StreamJsonRpc over Kestrel TCP", 1.14e6, 1.18e6, "theirs", "Content-Length framing, System.Text.Json formatter"),
83+
("StreamJsonRpc over Kestrel TCP", 1.05e6, 1.10e6, "theirs", "newline framing, System.Text.Json formatter"),
84+
("StreamJsonRpc over Kestrel TCP", 515e3, 518e3, "theirs", "Content-Length framing, Json.NET formatter (default)"),
85+
("JSON-RPC.Net in-process, 1 thread", 3.1e6, 3.3e6, "ours", "direct call, bytes in, bytes out"),
86+
("StreamJsonRpc in-process, 1 client", 116e3, 166e3, "theirs", "Pipe pair, 256 pipelined"),
87+
("StreamJsonRpc typed proxy", 90e3, 90e3, "theirs", "sequential await, 11 µs per round trip"),
88+
],
89+
"compare-streamjsonrpc.svg",
90+
)
91+
92+
chart(
93+
"JSON-RPC.Net by transport",
94+
"AustinHarris.JsonRpc.AspNetCore on Kestrel, loopback, 16 clients on the same 8-core machine.",
95+
[
96+
("TCP, 256 pipelined", 14.3e6, 14.8e6, "ours", "JsonRpcConnectionHandler"),
97+
("HTTP, batch of 100 per POST", 9.5e6, 10.6e6, "ours", "MapJsonRpc"),
98+
("HTTP, 1 request per POST", 50e3, 130e3, "ours", "130 to 300 µs per round trip per client"),
99+
("in-process, 16 threads", 32.3e6, 32.3e6, "scale", "no transport, for scale"),
100+
("in-process, 1 thread", 4.2e6, 4.2e6, "scale", "no transport, for scale"),
101+
],
102+
"kestrel-transports.svg",
103+
)
104+
105+
chart(
106+
"JSON-RPC.Net alone, by thread count",
107+
"Byte entry point in a loop, no scheduler, no transport. 8 cores / 16 threads.",
108+
[
109+
("1 thread", 4.4e6, 4.4e6, "ours", "227 ns per request, 0 bytes allocated"),
110+
("2 threads", 8.8e6, 8.8e6, "ours", ""),
111+
("4 threads", 15.1e6, 15.1e6, "ours", ""),
112+
("8 threads", 24.0e6, 24.0e6, "ours", ""),
113+
("16 threads", 32.8e6, 32.8e6, "ours", "SMT: two threads per core"),
114+
],
115+
"sync-threads.svg",
116+
)

‎benchmarks/charts/sync-threads.svg‎

Lines changed: 29 additions & 0 deletions
Loading

0 commit comments

Comments
 (0)