-
Notifications
You must be signed in to change notification settings - Fork 86
Expand file tree
/
Copy pathexplorer.html
More file actions
358 lines (344 loc) · 43.3 KB
/
Copy pathexplorer.html
File metadata and controls
358 lines (344 loc) · 43.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>JSON-RPC.NET benchmark explorer</title>
<meta name="description" content="The benchmark data behind the JSON-RPC.NET README: toggle series, inspect exact values and ranges, switch axes, download the data.">
<meta name="data-revision" content="eacac12e8d55">
<style>
:root { --bg: #ffffff; --ink: #1f2328; --muted: #57606a; --grid: #e5e7eb; --edge: #d1d5db; --card: #f6f8fa; --focus: #0969da; }
@media (prefers-color-scheme: dark) {
:root { --bg: #0d1117; --ink: #f0f6fc; --muted: #9da7b3; --grid: #21262d; --edge: #30363d; --card: #161b22; --focus: #58a6ff; }
}
* { box-sizing: border-box; }
body { margin: 0; padding: 16px; background: var(--bg); color: var(--ink); font: 14px/1.45 -apple-system, "Segoe UI", Helvetica, Arial, sans-serif; }
main { max-width: 1100px; margin: 0 auto; }
h1 { font-size: 22px; margin: 0 0 4px; }
h2 { font-size: 17px; margin: 28px 0 8px; }
p, li { max-width: 80ch; }
.muted { color: var(--muted); }
.controls { display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: center; margin: 10px 0; }
.controls label { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; }
button, select { font: inherit; color: var(--ink); background: var(--card); border: 1px solid var(--edge); border-radius: 6px; padding: 4px 10px; cursor: pointer; }
button:focus-visible, select:focus-visible, input:focus-visible, [tabindex]:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
.legend { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 4px 16px; margin: 8px 0 12px; }
.legend label { display: flex; align-items: center; gap: 8px; }
.legend .only { margin-left: auto; font-size: 12px; padding: 1px 8px; }
.legend svg { flex: none; }
.chart-wrap { position: relative; overflow-x: auto; }
svg.chart { display: block; max-width: 100%; height: auto; font-family: inherit; }
.tip { position: absolute; pointer-events: none; background: var(--card); border: 1px solid var(--edge); border-radius: 6px; padding: 8px 10px; font-size: 12px; line-height: 1.4; box-shadow: 0 2px 8px rgba(0,0,0,.2); max-width: 320px; display: none; z-index: 2; }
.tip b { display: block; margin-bottom: 2px; }
table { border-collapse: collapse; margin: 8px 0; font-size: 13px; }
th, td { border: 1px solid var(--edge); padding: 4px 8px; text-align: right; vertical-align: top; }
th:first-child, td:first-child { text-align: left; }
thead th { background: var(--card); }
.sr { position: absolute; left: -9999px; }
details { margin: 10px 0; }
summary { cursor: pointer; }
code { background: var(--card); padding: 1px 4px; border-radius: 4px; }
</style>
</head>
<body>
<main>
<h1>JSON-RPC.NET benchmark explorer</h1>
<p class="muted">The data behind the README charts, data revision <code>eacac12e8d55</code>. Every range is the observed low and high over the stated runs, never a confidence interval. Hover, tap or tab to a marker for the exact values. The README stays complete without this page. <a href="https://github.com/Astn/JSON-RPC.NET#benchmarks">Back to the README</a>.</p>
<h2 id="sweep-title"></h2>
<p id="sweep-caption" class="muted"></p>
<div class="controls" role="group" aria-label="axes">
<label><input type="radio" name="scale" value="log" checked> log axis</label>
<label><input type="radio" name="scale" value="linear"> linear axis</label>
<button id="fit" type="button" title="Fit the axis to the selected series; the axis otherwise stays fixed while toggling">fit selected series</button>
<button id="reset-axis" type="button">full axis</button>
<button id="all" type="button">show all</button>
<button id="download" type="button">download data (JSON)</button>
</div>
<div id="legend" class="legend" role="group" aria-label="series"></div>
<div class="chart-wrap">
<svg id="sweep" class="chart" role="img" aria-labelledby="sweep-title sweep-caption"></svg>
<div id="tip" class="tip" role="status" aria-live="polite"></div>
</div>
<h2>All series at one connection count</h2>
<div class="controls">
<label>connections <select id="conn"></select></label>
</div>
<table id="at-conn"><caption class="sr">Every series at the chosen connection count</caption></table>
<details>
<summary>Full sweep table: low, median, high and every observation per cell</summary>
<table id="sweep-table"></table>
</details>
<h2>Snapshot sets from the README</h2>
<p class="muted">These are the tables the README publishes, with their measurement conditions. They are separate sessions from the sweep, so their absolute figures are not comparable with it.</p>
<div id="snapshots"></div>
</main>
<script id="data" type="application/json">{"families":{"grpc":"#d55e00","ours":"#009e73","scale":"#767676","theirs":"#0072b2"},"familyNames":{"grpc":"gRPC for .NET","ours":"JSON-RPC.Net","scale":"reference","theirs":"StreamJsonRpc"},"machine":"AMD Ryzen 7 7800X3D (8 cores / 16 threads, 4.2 GHz), 64 GB, Windows 11","revision":"eacac12e8d55","runtime":".NET 10, Release, Server GC, built-in serializer","sets":{"async1":{"conditions":"idle box; one 3 s run per row","date":"2026-09-25","groups":[{"heading":"methods that complete inline, RpcContextFlow.None","rule":false,"series":["async1-sync-none","async1-task-none","async1-valuetask-none"]},{"heading":"methods that complete inline, RpcContextFlow.Flow","rule":false,"series":["async1-task-flow","async1-valuetask-flow"]},{"heading":"one real suspension per request (yieldsOnce)","rule":true,"series":["async1-yield-none","async1-yield-flow"]}],"harness":"TestServer_Console --async 3 1","policy":"one run per row","series":[{"family":"ours","high":3470000,"id":"async1-sync-none","label":"synchronous methods, None","low":3470000,"published":"3.47 M","status":"single"},{"family":"ours","high":3470000,"id":"async1-task-none","label":"Task\u003cT>, None","low":3470000,"published":"3.47 M","status":"single"},{"family":"ours","high":3760000,"id":"async1-valuetask-none","label":"ValueTask\u003cT>, None","low":3760000,"published":"3.76 M","status":"single"},{"family":"ours","high":2940000,"id":"async1-task-flow","label":"Task\u003cT>, Flow","low":2940000,"published":"2.94 M","status":"single"},{"family":"ours","high":3110000,"id":"async1-valuetask-flow","label":"ValueTask\u003cT>, Flow","low":3110000,"published":"3.11 M","status":"single"},{"family":"ours","high":1140000,"id":"async1-yield-none","label":"yieldsOnce, None","low":1140000,"published":"1.14 M","status":"single"},{"family":"ours","high":915000,"id":"async1-yield-flow","label":"yieldsOnce, Flow","low":915000,"published":"915 k","status":"single"}],"source":"README.md, Async table, 1-worker column; branch async-scratch-cache","title":"ProcessAsync, one awaited worker, by registration","workload":"as async16, one worker"},"async16":{"conditions":"idle box; one 3 s run per row","date":"2026-09-25","groups":[{"heading":"methods that complete inline, RpcContextFlow.None","rule":false,"series":["async16-sync-none","async16-task-none","async16-valuetask-none"]},{"heading":"methods that complete inline, RpcContextFlow.Flow","rule":false,"series":["async16-task-flow","async16-valuetask-flow"]},{"heading":"one real suspension per request (yieldsOnce)","rule":true,"series":["async16-yield-none","async16-yield-flow"]}],"harness":"TestServer_Console --async 3 16","policy":"one run per row","series":[{"family":"ours","high":24600000,"id":"async16-sync-none","label":"synchronous methods, None","low":24600000,"note":"6 B per request, the StringMe result string","published":"24.6 M","status":"single"},{"family":"ours","high":30000000,"id":"async16-task-none","label":"Task\u003cT>, None","low":30000000,"note":"67 B per request, the service's own Task.FromResult","published":"30.0 M","status":"single"},{"family":"ours","high":32100000,"id":"async16-valuetask-none","label":"ValueTask\u003cT>, None","low":32100000,"note":"6 B per request","published":"32.1 M","status":"single"},{"family":"ours","high":22200000,"id":"async16-task-flow","label":"Task\u003cT>, Flow","low":22200000,"note":"251 B per request","published":"22.2 M","status":"single"},{"family":"ours","high":23400000,"id":"async16-valuetask-flow","label":"ValueTask\u003cT>, Flow","low":23400000,"note":"190 B per request","published":"23.4 M","status":"single"},{"family":"ours","high":8960000,"id":"async16-yield-none","label":"yieldsOnce, None","low":8960000,"note":"559 B per request","published":"8.96 M","status":"single"},{"family":"ours","high":6890000,"id":"async16-yield-flow","label":"yieldsOnce, Flow","low":6890000,"note":"741 B per request","published":"6.89 M","status":"single"}],"source":"README.md, Async table, 16-worker column; branch async-scratch-cache","title":"ProcessAsync, 16 awaited workers, by registration","workload":"the five benchmark requests (yieldsOnce: one request) through the byte-level JsonRpcProcessor.ProcessAsync from Task.Run workers that await each call"},"compare":{"conditions":"WSL VM shut down; two 3 s runs","date":"2026-09-23","groups":[{"heading":"JSON-RPC.Net","rule":false,"series":["ours-tcp-jsonrpc"]},{"heading":"StreamJsonRpc 2.25","rule":false,"series":["sjr-newline-stj","sjr-cl-stj","sjr-cl-newtonsoft"]},{"heading":"gRPC for .NET 2.84: its .NET client shares the server's cores","rule":false,"series":["grpc-unary","grpc-stream"]}],"harness":"TestServer_Console --compare 3","policy":"observed low and high over two runs; a single figure is one value that both runs rounded to","series":[{"family":"ours","high":14600000,"id":"ours-tcp-jsonrpc","label":"over Kestrel TCP","low":13700000,"note":"raw documents","published":"13.7 M to 14.6 M","status":"range"},{"family":"theirs","high":1440000,"id":"sjr-newline-stj","label":"over Kestrel TCP","low":1380000,"note":"newline framing, System.Text.Json formatter","published":"1.38 M to 1.44 M","status":"range"},{"family":"theirs","high":1450000,"id":"sjr-cl-stj","label":"over Kestrel TCP","low":1410000,"note":"Content-Length framing, System.Text.Json formatter","published":"1.41 M to 1.45 M","status":"range"},{"family":"theirs","high":625000,"id":"sjr-cl-newtonsoft","label":"over Kestrel TCP","low":625000,"note":"Content-Length framing, Json.NET formatter (default)","published":"625 k","status":"single"},{"family":"grpc","high":198000,"id":"grpc-unary","label":"unary calls","low":192000,"note":"HTTP/2 + protobuf, 16 channels \u00d7 256 in flight","published":"192 k to 198 k","status":"range"},{"family":"grpc","high":209000,"id":"grpc-stream","label":"bidirectional stream","low":200000,"note":"one stream per channel, 256 in flight, batched writes","published":"200 k to 209 k","status":"range"}],"source":"README.md, comparison table; commit 192a97f","title":"JSON-RPC.Net vs StreamJsonRpc vs gRPC for .NET, 16 connections","versions":"StreamJsonRpc 2.25.29, gRPC for .NET 2.84.0","workload":"the same five calls on the same Kestrel; 16 connections or channels with 256 requests in flight each; every request carries \"jsonrpc\":\"2.0\""},"headline":{"conditions":"idle box; one run per row","date":"2026-09-25","groups":[{"heading":"1.2.3, the last 1.x release on NuGet","rule":false,"series":["headline-123-string"]},{"heading":"2.0, the same string API and the same loop","rule":false,"series":["headline-20-string"]},{"heading":"2.0, the byte entry points","rule":false,"series":["headline-20-sync","headline-20-async"]}],"harness":"benchmarks/Baseline (1.2.3 from NuGet), then TestServer_Console menu entry t, --sync 3 16 and --async 3 16, one session","policy":"one run per row, all in one session; the day-to-day ranges are the sync, async, legacy and kestrel sets","series":[{"family":"scale","high":3080000,"id":"headline-123-string","label":"Task\u003cstring> Process(string), thread pool","low":3080000,"note":"best batch size, 1,200; Json.NET on every request","published":"3.08 M","status":"single"},{"family":"ours","high":13300000,"id":"headline-20-string","label":"Task\u003cstring> Process(string), thread pool","low":13300000,"note":"best batch size, 36,000; the 1.x overloads, kept for compatibility","published":"13.3 M","status":"single"},{"family":"ours","high":31700000,"id":"headline-20-sync","label":"Process(bytes), 16 dedicated threads","low":31700000,"note":"the library alone; 0 B per numeric request","published":"31.7 M","status":"single"},{"family":"ours","high":32100000,"id":"headline-20-async","label":"ProcessAsync(bytes), 16 awaited workers","low":32100000,"note":"ValueTask\u003cT> methods, RpcContextFlow.None; 6 B per request","published":"32.1 M","status":"single"}],"source":"README.md, Performance table; branch async-scratch-cache","title":"JSON-RPC.Net 1.2.3 and 2.0 on one machine","workload":"the five benchmark requests; the string rows are the best batch size of the thread-pool loop, the byte rows a loop on 16 dedicated threads or 16 awaited workers"},"inprocess":{"conditions":"WSL VM shut down; two 3 s runs","date":"2026-09-23","groups":[{"heading":"","rule":false,"series":["ours-direct-1","sjr-pipe","sjr-proxy"]}],"harness":"TestServer_Console --compare 3","policy":"observed low and high over two runs","series":[{"family":"ours","high":3600000,"id":"ours-direct-1","label":"JSON-RPC.Net, direct call, 1 thread","low":2600000,"note":"bytes in, bytes out, no transport","published":"2.6 M to 3.6 M","status":"range"},{"family":"theirs","high":142000,"id":"sjr-pipe","label":"StreamJsonRpc, Pipe pair, 1 client","low":117000,"note":"newline framing, System.Text.Json, 256 pipelined","published":"117 k to 142 k","status":"range"},{"family":"theirs","high":97000,"id":"sjr-proxy","label":"StreamJsonRpc, typed proxy","low":96000,"note":"sequential await per call, 10 \u00b5s per round trip","published":"96 k to 97 k","status":"range"}],"source":"README.md, comparison table; commit 192a97f","title":"In-process paths: different execution boundaries","workload":"the same five calls with no network: a direct call, a Pipe pair, a typed proxy"},"kestrel":{"conditions":"idle box; three 3 s runs of --kestrel 3 and two of --kestrel 3 async, one session","date":"2026-09-25","groups":[{"heading":"EnableAsyncMethods = false (the default)","rule":false,"series":["ours-http-1","ours-http-100","ours-tcp"]},{"heading":"EnableAsyncMethods = true: every document through ProcessAsync","rule":false,"series":["ours-tcp-async-inline","ours-tcp-async-yield"]},{"heading":"Reference: no transport","rule":true,"series":["ours-inproc-16"]}],"harness":"TestServer_Console --kestrel 3","policy":"low and high over the runs: three for the default rows, two for the EnableAsyncMethods = true rows","series":[{"family":"ours","high":182000,"id":"ours-http-1","label":"HTTP, 1 request per POST","low":123000,"note":"each client awaits one POST at a time; 88 to 130 \u00b5s per round trip","published":"123 k to 182 k","status":"range"},{"family":"ours","high":14500000,"id":"ours-http-100","label":"HTTP, batch of 100 per POST","low":12800000,"note":"one POST outstanding per client, 100 RPCs in it","published":"12.8 M to 14.5 M","status":"range"},{"family":"ours","high":16500000,"id":"ours-tcp","label":"TCP, 256 pipelined","low":14300000,"note":"JsonRpcConnectionHandler, 256 requests in flight per connection","published":"14.3 M to 16.5 M","status":"range"},{"family":"ours","high":15400000,"id":"ours-tcp-async-inline","label":"TCP, 256 pipelined, methods that complete inline","low":15000000,"note":"--kestrel 3 async","published":"15.0 M to 15.4 M","status":"range"},{"family":"ours","high":1290000,"id":"ours-tcp-async-yield","label":"TCP, 256 pipelined, methods that suspend once","low":1250000,"note":"five async Task\u003cT> methods awaiting Task.Yield(); documents are sequential per connection","published":"1.25 M to 1.29 M","status":"range"},{"family":"scale","high":37100000,"id":"ours-inproc-16","label":"in-process, 16 threads","low":33800000,"note":"the same requests through the byte entry point","published":"33.8 M to 37.1 M","status":"range"}],"source":"README.md, Kestrel table; branch async-scratch-cache","title":"JSON-RPC.Net by transport","workload":"AustinHarris.JsonRpc.AspNetCore on Kestrel, loopback, 16 clients on the server's 8 cores"},"legacy":{"conditions":"idle box; three runs of the t entry in one session","date":"2026-09-25","harness":"TestServer_Console, menu entry t","policy":"low and high over three runs","series":[{"family":"ours","id":"legacy-string","label":"JSON-RPC.Net, 1.x string overloads","points":[{"high":2030000,"low":1570000,"published":"1.6 M to 2.0 M","status":"range"},{"high":9190000,"low":7990000,"published":"8.0 M to 9.2 M","status":"range"},{"high":13260000,"low":12360000,"published":"12.4 M to 13.3 M","status":"range"},{"high":13290000,"low":11570000,"published":"11.6 M to 13.3 M","status":"range"},{"high":7800000,"low":7470000,"published":"7.5 M to 7.8 M","status":"range"},{"high":8030000,"low":7490000,"published":"7.5 M to 8.0 M","status":"range"}]}],"source":"README.md, Legacy table; branch async-scratch-cache","title":"Legacy string API through the thread pool, by batch size","workload":"the five benchmark requests through the 1.x Task\u003cstring> Process(string) overload, submitted in batches from every core through Parallel.For","x":{"name":"batch size","values":[50,300,6000,36000,252000,2016000]}},"sweep":{"conditions":"WSL VM running and other sessions active: a separate session from the tables, so its absolute figures are not comparable with them","date":"2026-09-23","machine":"AMD Ryzen 7 7800X3D, 8 cores / 16 threads, Windows 11, .NET 10, Release, Server GC","pipeline":256,"policy":"observed low, high and median over the runs; each run contributes one observation per cell","run_files":["sweep.json","sweep-2.json","sweep-3.json","sweep-4.json","sweep-5.json"],"runs":"sweep*.json","seconds_per_cell":2,"series":[{"concurrency":"256 requests in flight per connection","family":"ours","id":"sweep-ours-tcp","label":"TCP","name":"JSON-RPC.Net, TCP","points":[{"high":1819781,"low":1044121,"median":1272069,"n":5,"status":"range","values":[1069351,1044121,1753461,1272069,1819781]},{"high":3507388,"low":2864903,"median":3237018,"n":5,"status":"range","values":[3237018,3507388,2864903,2977904,3264403]},{"high":5606579,"low":5134810,"median":5407126,"n":5,"status":"range","values":[5284082,5407126,5134810,5461841,5606579]},{"high":9065076,"low":8320994,"median":8800925,"n":5,"status":"range","values":[8800925,8599018,8320994,8830555,9065076]},{"high":12964855,"low":10918454,"median":12342034,"n":5,"status":"range","values":[11580423,10918454,12342034,12635255,12964855]}],"style":0},{"concurrency":"one POST outstanding per connection, 100 RPCs in it","family":"ours","id":"sweep-ours-http-100","label":"HTTP, batch of 100 per POST","name":"JSON-RPC.Net, HTTP, batch of 100 per POST","points":[{"high":1033818,"low":674970,"median":887229,"n":5,"status":"range","values":[674970,845968,902499,887229,1033818]},{"high":2120158,"low":1655066,"median":1916457,"n":5,"status":"range","values":[1916457,1912938,1655066,1950239,2120158]},{"high":3736721,"low":3002007,"median":3251689,"n":5,"status":"range","values":[3291844,3002007,3251689,3155212,3736721]},{"high":6609939,"low":4500453,"median":5803041,"n":5,"status":"range","values":[5791331,4500453,5803041,6032561,6609939]},{"high":10154393,"low":8643939,"median":9773010,"n":5,"status":"range","values":[9206129,8643939,9773010,10046224,10154393]}],"style":1},{"concurrency":"one POST outstanding per connection","family":"ours","id":"sweep-ours-http-1","label":"HTTP, 1 request per POST","name":"JSON-RPC.Net, HTTP, 1 request per POST","points":[{"high":18617,"low":14644,"median":16608,"n":5,"status":"range","values":[16983,14693,16608,14644,18617]},{"high":42797,"low":34760,"median":41735,"n":5,"status":"range","values":[41752,41735,34760,39241,42797]},{"high":71053,"low":63810,"median":68160,"n":5,"status":"range","values":[69178,68160,68072,63810,71053]},{"high":143600,"low":136547,"median":137996,"n":5,"status":"range","values":[143600,141373,136547,137996,136975]},{"high":209586,"low":178050,"median":194976,"n":5,"status":"range","values":[178050,182739,194976,199511,209586]}],"style":2},{"concurrency":"256 requests in flight per connection","family":"theirs","id":"sweep-sjr-newline-stj","label":"TCP, newline + System.Text.Json","name":"StreamJsonRpc, TCP, newline + System.Text.Json","points":[{"high":273248,"low":110390,"median":239319,"n":5,"status":"range","values":[110390,156480,259099,239319,273248]},{"high":512457,"low":489920,"median":498498,"n":5,"status":"range","values":[497083,512457,489920,498769,498498]},{"high":775504,"low":616795,"median":716482,"n":5,"status":"range","values":[692733,716482,734867,616795,775504]},{"high":1143682,"low":927354,"median":1050993,"n":5,"status":"range","values":[1143682,1050993,960889,927354,1054633]},{"high":1237849,"low":1063106,"median":1167263,"n":5,"status":"range","values":[1119388,1237849,1063106,1191808,1167263]}],"style":0},{"concurrency":"256 requests in flight per connection","family":"theirs","id":"sweep-sjr-cl-stj","label":"TCP, Content-Length + System.Text.Json","name":"StreamJsonRpc, TCP, Content-Length + System.Text.Json","points":[{"high":286794,"low":199908,"median":268252,"n":5,"status":"range","values":[199908,268252,270328,265461,286794]},{"high":486043,"low":442018,"median":451627,"n":5,"status":"range","values":[451627,442018,476075,445920,486043]},{"high":707816,"low":550061,"median":657408,"n":5,"status":"range","values":[701605,550061,657408,636495,707816]},{"high":1091866,"low":870961,"median":1045688,"n":5,"status":"range","values":[870961,1075333,1045688,975373,1091866]},{"high":1368614,"low":1117102,"median":1301094,"n":5,"status":"range","values":[1301094,1278084,1117102,1368614,1342829]}],"style":1},{"concurrency":"256 requests in flight per connection","family":"theirs","id":"sweep-sjr-cl-newtonsoft","label":"TCP, Content-Length + Json.NET","name":"StreamJsonRpc, TCP, Content-Length + Json.NET","points":[{"high":195030,"low":110820,"median":179958,"n":5,"status":"range","values":[110820,179958,182308,155954,195030]},{"high":325667,"low":252428,"median":291457,"n":5,"status":"range","values":[325667,316537,255019,252428,291457]},{"high":427511,"low":339727,"median":392057,"n":5,"status":"range","values":[392057,339727,396942,387471,427511]},{"high":653316,"low":469099,"median":582792,"n":5,"status":"range","values":[469099,653316,582792,520293,645594]},{"high":676702,"low":544394,"median":637935,"n":5,"status":"range","values":[647221,637935,544394,612214,676702]}],"style":2},{"concurrency":"256 calls in flight per channel","family":"grpc","id":"sweep-grpc-unary","label":"unary","name":"gRPC for .NET, unary","points":[{"high":233405,"low":137744,"median":179749,"n":5,"status":"range","values":[137744,193335,179749,174616,233405]},{"high":366628,"low":319815,"median":347537,"n":5,"status":"range","values":[350661,347537,322979,319815,366628]},{"high":390318,"low":319253,"median":332854,"n":5,"status":"range","values":[390318,329847,332854,319253,387737]},{"high":417512,"low":295814,"median":361392,"n":5,"status":"range","values":[361392,295814,377916,334528,417512]},{"high":404186,"low":360097,"median":383022,"n":5,"status":"range","values":[404186,360097,361627,389365,383022]}],"style":0},{"concurrency":"one stream per channel, 256 in flight, batched writes","family":"grpc","id":"sweep-grpc-stream","label":"bidirectional stream","name":"gRPC for .NET, bidirectional stream","points":[{"high":74526,"low":58380,"median":62493,"n":5,"status":"range","values":[58380,63708,62493,61273,74526]},{"high":124083,"low":109486,"median":113543,"n":5,"status":"range","values":[124083,112901,113543,109486,119705]},{"high":228385,"low":187967,"median":204217,"n":5,"status":"range","values":[204217,187967,204608,202477,228385]},{"high":246839,"low":237856,"median":244421,"n":5,"status":"range","values":[244421,241483,246839,237856,246075]},{"high":291115,"low":232514,"median":265818,"n":5,"status":"range","values":[260798,232514,265818,291115,286672]}],"style":1}],"source":"TestServer_Console --sweep 2 \u003cfile>; one file per run, matched by the pattern below","title":"Every library and transport, by client connections","workload":"the same five calls on the same Kestrel, clients on the server's 8 cores; each cell warmed for 2 s, then timed","x":{"name":"client connections (gRPC: channels)","values":[1,2,4,8,16]}},"sync":{"conditions":"idle box; three 3 s runs of --sync 3 in one session","date":"2026-09-25","harness":"TestServer_Console --sync 3, and --sync 2 \u003cn> for single rows","policy":"low and high over three runs; ns per request is the harness's reported figure for one of them","series":[{"family":"ours","id":"ours-sync","label":"JSON-RPC.Net, byte entry point","points":[{"high":5000000,"low":4570000,"ns":214,"published":"4.6 M to 5.0 M","status":"range"},{"high":9100000,"low":8500000,"ns":235,"published":"8.5 M to 9.1 M","status":"range"},{"high":17100000,"low":16600000,"ns":234,"published":"16.6 M to 17.1 M","status":"range"},{"high":27700000,"low":22800000,"ns":292,"published":"22.8 M to 27.7 M","status":"range"},{"high":36400000,"low":31700000,"ns":439,"published":"31.7 M to 36.4 M","status":"range"}]}],"source":"README.md, Sync table; branch async-scratch-cache","title":"JSON-RPC.Net alone, by worker threads","workload":"the five benchmark requests through the byte-level JsonRpcProcessor.Process in a loop; no scheduler, no transport","x":{"name":"worker threads","values":[1,2,4,8,16]}},"wasm":{"conditions":"8-core desktop; interpreter from dotnet run, AOT from dotnet publish -c Release with the wasm-tools workload","date":"2026-09-23","groups":[{"heading":"plain Blazor interop, one method per operation","rule":false,"series":["wasm-plain-invoke","wasm-plain-invoke-async","wasm-plain-jsexport"]},{"heading":"JSON-RPC, one request document per call","rule":false,"series":["wasm-rpc-invoke","wasm-rpc-invoke-async","wasm-rpc-jsexport","wasm-rpc-bytes"]},{"heading":"JSON-RPC, a batch of 100 per call","rule":false,"series":["wasm-rpc-invoke-100","wasm-rpc-jsexport-100","wasm-rpc-bytes-100"]},{"heading":"Reference: the server in a .NET loop, no interop","rule":true,"series":["wasm-loop"]}],"harness":"samples/WasmHost, Run benchmark in Chrome 152","modes":[{"id":"interp","label":"interpreter","marker":"circle","policy":"one run"},{"id":"aot","label":"AOT","marker":"diamond","policy":"better of two runs"}],"policy":"interpreter: one run; AOT: the better of two runs. The interpreter-to-AOT distance is a deployment difference, not a range","series":[{"family":"scale","id":"wasm-plain-invoke","label":"invokeMethod Add","published":{"aot":"66,600","interp":"15,700"},"values":{"aot":66600,"interp":15700}},{"family":"scale","id":"wasm-plain-invoke-async","label":"invokeMethodAsync Add","published":{"aot":"59,300","interp":"14,500"},"values":{"aot":59300,"interp":14500}},{"family":"scale","id":"wasm-plain-jsexport","label":"[JSExport] AddExported, typed arguments","published":{"aot":"3,230,000","interp":"2,860,000"},"values":{"aot":3230000,"interp":2860000}},{"family":"ours","id":"wasm-rpc-invoke","label":"invokeMethod Process","published":{"aot":"33,500","interp":"6,000"},"values":{"aot":33500,"interp":6000}},{"family":"ours","id":"wasm-rpc-invoke-async","label":"invokeMethodAsync Process","published":{"aot":"36,000","interp":"5,700"},"values":{"aot":36000,"interp":5700}},{"family":"ours","id":"wasm-rpc-jsexport","label":"[JSExport] ProcessExported, strings","published":{"aot":"151,600","interp":"17,100"},"values":{"aot":151600,"interp":17100}},{"family":"ours","id":"wasm-rpc-bytes","label":"[JSExport] ProcessBytes, UTF-8 buffers","published":{"aot":"142,000","interp":"19,000"},"values":{"aot":142000,"interp":19000}},{"family":"ours","id":"wasm-rpc-invoke-100","label":"invokeMethod Process","published":{"aot":"136,000","interp":"13,000"},"values":{"aot":136000,"interp":13000}},{"family":"ours","id":"wasm-rpc-jsexport-100","label":"[JSExport] ProcessExported, strings","published":{"aot":"185,500","interp":"25,000"},"values":{"aot":185500,"interp":25000}},{"family":"ours","id":"wasm-rpc-bytes-100","label":"[JSExport] ProcessBytes, UTF-8 buffers","published":{"aot":"210,500","interp":"27,000"},"values":{"aot":210500,"interp":27000}},{"family":"scale","id":"wasm-loop","label":"ProcessMany, the string request N times","published":{"aot":"169,800","interp":"17,600"},"values":{"aot":169800,"interp":17600}}],"source":"samples/WasmHost/README.md table; commit 41371ec","title":"In the browser: interpreter vs AOT","workload":"add(1, 2) through each interop path, 20,000 RPCs per row"}},"styles":[["","circle"],["9 5","square"],["2 4","triangle"],["12 4 2 4","diamond"]]}</script>
<script>
(function () {
"use strict";
var DATA = JSON.parse(document.getElementById("data").textContent);
var sweep = DATA.sets.sweep;
var conns = sweep.x.values;
var families = DATA.families, familyNames = DATA.familyNames, styles = DATA.styles;
var css = getComputedStyle(document.documentElement);
var theme = function (name) { return css.getPropertyValue(name).trim(); };
var NS = "http://www.w3.org/2000/svg";
function fmt(v) {
if (v >= 1e6) return (+(v / 1e6).toPrecision(3)) + " M";
if (v >= 1e3) return (+(v / 1e3).toPrecision(3)) + " k";
return String(+v.toPrecision(3));
}
function fmtAxis(v) {
if (v >= 1e6) return (v / 1e6) + " M";
if (v >= 1e3) return (v / 1e3) + " k";
return String(v);
}
function exact(v) { return Math.round(v).toLocaleString("en-US"); }
function el(tag, attrs, parent) {
var e = document.createElementNS(NS, tag);
for (var k in attrs) e.setAttribute(k, attrs[k]);
if (parent) parent.appendChild(e);
return e;
}
function html(tag, attrs, parent, textContent) {
var e = document.createElement(tag);
for (var k in attrs) e.setAttribute(k, attrs[k]);
if (textContent != null) e.textContent = textContent;
if (parent) parent.appendChild(e);
return e;
}
function markerEl(shape, x, y, color, r, parent) {
r = r || 5;
if (shape === "circle") return el("circle", { cx: x, cy: y, r: r, fill: theme("--bg"), stroke: color, "stroke-width": 2 }, parent);
if (shape === "square") return el("rect", { x: x - r, y: y - r, width: 2 * r, height: 2 * r, fill: theme("--bg"), stroke: color, "stroke-width": 2 }, parent);
if (shape === "diamond") { var d = r * 1.3; return el("polygon", { points: [x, y - d, x + d, y, x, y + d, x - d, y].join(","), fill: color, stroke: color }, parent); }
var s = r * 1.25;
return el("polygon", { points: [x, y - s, x + s, y + s * 0.8, x - s, y + s * 0.8].join(","), fill: theme("--bg"), stroke: color, "stroke-width": 2, "stroke-linejoin": "round" }, parent);
}
// ---- state
var state = { scale: "log", visible: {}, domain: null };
sweep.series.forEach(function (s) { state.visible[s.id] = true; });
var fullDomain = (function () {
var lo = Infinity, hi = 0;
sweep.series.forEach(function (s) { s.points.forEach(function (p) { lo = Math.min(lo, p.low); hi = Math.max(hi, p.high); }); });
return [lo, hi];
})();
state.domain = fullDomain;
// ---- captions
document.getElementById("sweep-title").textContent = sweep.title;
document.getElementById("sweep-caption").textContent = sweep.workload + ". " + sweep.run_files.length + " runs of " + sweep.seconds_per_cell +
" s per point (" + sweep.run_files.join(", ") + "), " + sweep.date + ". " + sweep.conditions + ". Marker at the median, whisker low to high. " + sweep.machine + ".";
// ---- legend
var legend = document.getElementById("legend");
sweep.series.forEach(function (s) {
var color = families[s.family], st = styles[s.style % styles.length];
var label = html("label", {}, legend);
var cb = html("input", { type: "checkbox", "data-id": s.id }, label);
cb.checked = true;
cb.addEventListener("change", function () { state.visible[s.id] = cb.checked; draw(); });
var sw = el("svg", { width: 44, height: 14, "aria-hidden": "true" }, label);
el("line", { x1: 2, y1: 7, x2: 42, y2: 7, stroke: color, "stroke-width": 2, "stroke-dasharray": st[0] }, sw);
markerEl(st[1], 22, 7, color, 4, sw);
html("span", {}, label, familyNames[s.family] + ", " + s.label);
var only = html("button", { type: "button", "class": "only", title: "Show only this series" }, label, "only");
only.addEventListener("click", function (ev) {
ev.preventDefault();
sweep.series.forEach(function (o) { state.visible[o.id] = o.id === s.id; });
syncBoxes(); draw();
});
});
function syncBoxes() {
legend.querySelectorAll("input").forEach(function (cb) { cb.checked = !!state.visible[cb.getAttribute("data-id")]; });
}
document.getElementById("all").addEventListener("click", function () { sweep.series.forEach(function (s) { state.visible[s.id] = true; }); syncBoxes(); draw(); });
document.querySelectorAll("input[name=scale]").forEach(function (r) {
r.addEventListener("change", function () { state.scale = r.value; draw(); });
});
document.getElementById("fit").addEventListener("click", function () {
var lo = Infinity, hi = 0;
sweep.series.forEach(function (s) { if (!state.visible[s.id]) return; s.points.forEach(function (p) { lo = Math.min(lo, p.low); hi = Math.max(hi, p.high); }); });
if (hi > 0) { state.domain = [lo, hi]; draw(); }
});
document.getElementById("reset-axis").addEventListener("click", function () { state.domain = fullDomain; draw(); });
document.getElementById("download").addEventListener("click", function () {
var blob = new Blob([JSON.stringify(DATA, null, 2)], { type: "application/json" });
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "jsonrpc-net-benchmarks-" + DATA.revision + ".json";
document.body.appendChild(a); a.click(); a.remove();
setTimeout(function () { URL.revokeObjectURL(a.href); }, 1000);
});
// ---- scales
function logDomain(lo, hi) {
var d = Math.pow(10, Math.floor(Math.log10(lo)));
var l = [1, 2, 5].filter(function (m) { return d * m <= lo; }).map(function (m) { return d * m; }).pop();
var e = Math.pow(10, Math.floor(Math.log10(hi * 1.15)));
var h = [1, 2, 5, 10].map(function (m) { return e * m; }).filter(function (v) { return v >= hi * 1.15; })[0];
return [l, h];
}
function linearDomain(hi) {
var step = Math.pow(10, Math.floor(Math.log10(hi)));
if (hi / step < 2) step /= 4; else if (hi / step < 5) step /= 2;
return [0, Math.ceil(hi / step) * step];
}
function ticksFor(scale, dom) {
var out = [];
if (scale === "log") {
for (var d = Math.pow(10, Math.floor(Math.log10(dom[0]))); d <= dom[1]; d *= 10)
[1, 2, 5].forEach(function (m) { if (d * m >= dom[0] && d * m <= dom[1]) out.push({ v: d * m, major: m === 1 }); });
} else {
var span = dom[1] - dom[0], step = Math.pow(10, Math.floor(Math.log10(span)));
if (span / step < 3) step /= 2;
while (span / step > 6) step *= 2;
for (var v = 0; v <= dom[1] + 1e-9; v += step) out.push({ v: v, major: true });
}
return out;
}
// ---- draw
var svg = document.getElementById("sweep");
var tip = document.getElementById("tip");
function draw() {
while (svg.firstChild) svg.removeChild(svg.firstChild);
var W = 1060, left = 80, right = 24, top = 24, bottom = 54, H = 520;
svg.setAttribute("viewBox", "0 0 " + W + " " + H);
svg.setAttribute("width", W); svg.setAttribute("height", H);
var x0 = left, x1 = W - right, y0 = H - bottom, y1 = top;
var dom = state.scale === "log" ? logDomain(state.domain[0], state.domain[1]) : linearDomain(state.domain[1]);
var y = state.scale === "log"
? function (v) { return y0 + (y1 - y0) * (Math.log10(v) - Math.log10(dom[0])) / (Math.log10(dom[1]) - Math.log10(dom[0])); }
: function (v) { return y0 + (y1 - y0) * (v - dom[0]) / (dom[1] - dom[0]); };
var la = Math.log2(conns[0]), lb = Math.log2(conns[conns.length - 1]);
var x = function (c) { return conns.length === 1 ? (x0 + x1) / 2 : x0 + (x1 - x0) * (Math.log2(c) - la) / (lb - la); };
ticksFor(state.scale, dom).forEach(function (t) {
el("line", { x1: x0, y1: y(t.v), x2: x1, y2: y(t.v), stroke: theme("--grid"), "stroke-width": t.major ? 1 : 0.6 }, svg);
if (t.major) el("text", { x: x0 - 8, y: y(t.v) + 4, "text-anchor": "end", "font-size": 11, fill: theme("--muted") }, svg).textContent = fmtAxis(t.v);
});
conns.forEach(function (c) {
el("line", { x1: x(c), y1: y1, x2: x(c), y2: y0, stroke: theme("--grid"), "stroke-width": 0.6 }, svg);
el("text", { x: x(c), y: y0 + 18, "text-anchor": "middle", "font-size": 12, fill: theme("--ink") }, svg).textContent = c;
});
el("line", { x1: x0, y1: y0, x2: x1, y2: y0, stroke: theme("--muted") }, svg);
el("text", { x: (x0 + x1) / 2, y: H - 10, "text-anchor": "middle", "font-size": 12, fill: theme("--muted") }, svg).textContent = sweep.x.name + "; each step doubles";
el("text", { transform: "translate(18 " + (y0 + y1) / 2 + ") rotate(-90)", "text-anchor": "middle", "font-size": 12, fill: theme("--muted") }, svg).textContent =
"requests per second, " + (state.scale === "log" ? "log scale" : "linear scale");
var clip = el("clipPath", { id: "plot-clip" }, svg);
el("rect", { x: x0 - 8, y: y1 - 8, width: x1 - x0 + 16, height: y0 - y1 + 16 }, clip);
var g = el("g", { "clip-path": "url(#plot-clip)" }, svg);
sweep.series.forEach(function (s) {
if (!state.visible[s.id]) return;
var color = families[s.family], st = styles[s.style % styles.length];
el("polyline", { points: s.points.map(function (p, i) { return x(conns[i]) + "," + y(p.median); }).join(" "), fill: "none", stroke: color, "stroke-width": 2, "stroke-dasharray": st[0] }, g);
s.points.forEach(function (p, i) {
var cx = x(conns[i]);
if (p.low !== p.high) {
el("line", { x1: cx, y1: y(p.low), x2: cx, y2: y(p.high), stroke: color, "stroke-width": 3 }, g);
el("line", { x1: cx - 5, y1: y(p.low), x2: cx + 5, y2: y(p.low), stroke: color, "stroke-width": 3 }, g);
el("line", { x1: cx - 5, y1: y(p.high), x2: cx + 5, y2: y(p.high), stroke: color, "stroke-width": 3 }, g);
}
var m = markerEl(st[1], cx, y(p.median), color, 5.5, g);
m.setAttribute("tabindex", "0");
m.setAttribute("role", "img");
var text = familyNames[s.family] + ", " + s.label + " at " + conns[i] + " connections: median " + exact(p.median) +
", low " + exact(p.low) + ", high " + exact(p.high) + " RPC/s over " + p.n + " runs";
m.setAttribute("aria-label", text);
var show = function () {
tip.innerHTML = "";
html("b", {}, tip, familyNames[s.family] + ", " + s.label);
html("div", {}, tip, conns[i] + " connections; " + s.concurrency);
html("div", {}, tip, "median " + exact(p.median) + " RPC/s");
html("div", {}, tip, "low " + exact(p.low) + ", high " + exact(p.high) + " (" + p.status + ", n = " + p.n + ")");
html("div", { "class": "muted" }, tip, "runs: " + p.values.map(exact).join(", "));
html("div", { "class": "muted" }, tip, sweep.seconds_per_cell + " s per cell after a 2 s warm-up; revision " + DATA.revision);
tip.style.display = "block";
var r = svg.getBoundingClientRect(), k = r.width / W;
var px = cx * k, py = y(p.median) * k;
tip.style.left = Math.min(px + 14, r.width - 330) + "px";
tip.style.top = Math.max(0, py - 10) + "px";
};
var hide = function () { tip.style.display = "none"; };
m.addEventListener("mouseenter", show); m.addEventListener("mouseleave", hide);
m.addEventListener("focus", show); m.addEventListener("blur", hide);
m.addEventListener("click", show);
});
});
}
draw();
window.addEventListener("resize", function () { tip.style.display = "none"; });
// ---- one connection count, every series
var sel = document.getElementById("conn");
conns.forEach(function (c) { html("option", { value: c }, sel, c); });
sel.value = conns[conns.length - 1];
function atConn() {
var i = conns.indexOf(+sel.value), t = document.getElementById("at-conn");
t.innerHTML = "";
var h = html("tr", {}, html("thead", {}, t));
["series", "concurrency", "median RPC/s", "low", "high", "n"].forEach(function (c) { html("th", { scope: "col" }, h, c); });
var b = html("tbody", {}, t);
sweep.series.forEach(function (s) {
var p = s.points[i], r = html("tr", {}, b);
html("td", {}, r, familyNames[s.family] + ", " + s.label);
html("td", { style: "text-align:left" }, r, s.concurrency);
html("td", {}, r, exact(p.median)); html("td", {}, r, exact(p.low)); html("td", {}, r, exact(p.high)); html("td", {}, r, p.n);
});
}
sel.addEventListener("change", atConn);
atConn();
// ---- full table
(function () {
var t = document.getElementById("sweep-table");
var h = html("tr", {}, html("thead", {}, t));
["series", "connections", "low", "median", "high", "observations (one per run)"].forEach(function (c) { html("th", { scope: "col" }, h, c); });
var b = html("tbody", {}, t);
sweep.series.forEach(function (s) {
s.points.forEach(function (p, i) {
var r = html("tr", {}, b);
html("td", {}, r, familyNames[s.family] + ", " + s.label);
html("td", {}, r, conns[i]);
html("td", {}, r, exact(p.low)); html("td", {}, r, exact(p.median)); html("td", {}, r, exact(p.high));
html("td", {}, r, p.values.map(exact).join(", "));
});
});
})();
// ---- snapshot sets
var snaps = document.getElementById("snapshots");
["headline", "sync", "async1", "async16", "legacy", "kestrel", "compare", "inprocess", "wasm"].forEach(function (key) {
var st = DATA.sets[key];
if (!st) return;
var d = html("details", {}, snaps);
html("summary", {}, d, st.title);
html("p", { "class": "muted" }, d, [st.workload, st.conditions, st.policy, "source: " + st.source].join(". ") + ".");
var t = html("table", {}, d);
var h = html("tr", {}, html("thead", {}, t));
var b = html("tbody", {}, t);
if (st.x) {
[st.x.name, "published", "low", "high", "ns per request"].forEach(function (c) { html("th", { scope: "col" }, h, c); });
st.series[0].points.forEach(function (p, i) {
var r = html("tr", {}, b);
html("td", {}, r, st.x.values[i]); html("td", {}, r, p.published); html("td", {}, r, exact(p.low)); html("td", {}, r, exact(p.high)); html("td", {}, r, p.ns == null ? "" : p.ns);
});
} else if (key === "wasm") {
["path"].concat(st.modes.map(function (m) { return m.label + " (" + m.policy + ")"; })).forEach(function (c) { html("th", { scope: "col" }, h, c); });
st.groups.forEach(function (g) {
if (g.heading) html("td", { colspan: 3, style: "text-align:left;font-weight:600" }, html("tr", {}, b), g.heading);
g.series.forEach(function (id) {
var s = st.series.filter(function (x) { return x.id === id; })[0], r = html("tr", {}, b);
html("td", {}, r, s.label);
st.modes.forEach(function (m) { html("td", {}, r, s.published[m.id]); });
});
});
} else {
["row", "note", "published", "low", "high", "status"].forEach(function (c) { html("th", { scope: "col" }, h, c); });
st.groups.forEach(function (g) {
if (g.heading) html("td", { colspan: 6, style: "text-align:left;font-weight:600" }, html("tr", {}, b), g.heading);
g.series.forEach(function (id) {
var s = st.series.filter(function (x) { return x.id === id; })[0], r = html("tr", {}, b);
html("td", {}, r, s.label); html("td", { style: "text-align:left" }, r, s.note || "");
html("td", {}, r, s.published); html("td", {}, r, exact(s.low)); html("td", {}, r, exact(s.high)); html("td", {}, r, s.status);
});
});
}
});
})();
</script>
</body>
</html>