rpc, eth/catalyst: optimize json decode - #35518
Conversation
Bench:
|
| metric | base (cae76d5a3c3c7baad83bde2bd6c2d3ae8baca7d3) |
target (rpc-optimize-json-decode) |
Δ |
|---|---|---|---|
| throughput MGas/s | 343.5 ±0.3 | 376.5 ±0.8 | +9.6% ▲ |
| mean newPayload | 82.1 ±0.3 ms | 74.5 ±0.1 ms | -9.3% ▲ |
| p50 newPayload | 78.7 ±0.2 ms | 71.3 ±0.2 ms | -9.4% ▲ |
| p95 newPayload | 140.3 ±0.4 ms | 127.7 ±0.2 ms | -9.0% ▲ |
| p99 newPayload | 176.2 ±4.6 ms | 159.6 ±0.5 ms | -9.4% ▲ |
▲ improvement · ▼ regression · ≈ not established · ± half-range across runs
Per-run (3 runs/side)
| side | run | MGas/s | mean | p50 | p95 | p99 | min | max | total gas |
|---|---|---|---|---|---|---|---|---|---|
| base | 1 | 343.2 | 81.9 ms | 78.5 ms | 139.9 ms | 172.3 ms | 10.1 ms | 313.8 ms | 60.8 Ggas |
| base | 2 | 343.5 | 82.5 ms | 78.7 ms | 140.6 ms | 181.5 ms | 9.6 ms | 642.8 ms | 60.8 Ggas |
| base | 3 | 343.8 | 81.9 ms | 78.9 ms | 140.3 ms | 174.9 ms | 9.8 ms | 320.2 ms | 60.8 Ggas |
| target | 1 | 376.5 | 74.4 ms | 71.4 ms | 127.4 ms | 159.4 ms | 9.3 ms | 311.0 ms | 60.8 Ggas |
| target | 2 | 377.3 | 74.4 ms | 71.0 ms | 127.9 ms | 159.2 ms | 8.9 ms | 311.9 ms | 60.8 Ggas |
| target | 3 | 375.7 | 74.6 ms | 71.4 ms | 127.8 ms | 160.2 ms | 9.4 ms | 305.0 ms | 60.8 Ggas |
Execution breakdown (per block, 2000 blocks × 3 runs)
| bucket | base p50 | target p50 | base p95 | target p95 | mean Δ | median Δ | blocks faster |
|---|---|---|---|---|---|---|---|
| execution | 38.8 ±0.1 ms | 38.7 ±0.1 ms ≈ | 75.7 ±0.3 ms | 75.5 ±0.6 ms ≈ | -0.3% ±0.2% ▲ | -0.2% ±0.1% ▲ | 54% ±3% ▲ |
| state read | 3.37 ±0.02 ms | 3.44 ±0.03 ms ▼ | 15.5 ±0.3 ms | 16.0 ±0.1 ms ▼ | +1.9% ±1.2% ▼ | +1.4% ±0.6% ▼ | 47% ±1% ▼ |
| state hash | 5.84 ±0.02 ms | 5.89 ±0.04 ms ≈ | 14.8 ±0.4 ms | 15.5 ±0.5 ms ≈ | +2.0% ±0.7% ▼ | +0.8% ±0.5% ▼ | 47% ±2% ▼ |
| commit | 11.9 ±0.1 ms | 11.8 ±0.1 ms ≈ | 19.89 ±0.12 ms | 19.95 ±0.05 ms ≈ | -0.4% ±0.9% ≈ | -0.4% ±0.5% ≈ | 52% ±3% ≈ |
| block total | 61.9 ±0.3 ms | 62.0 ±0.3 ms ≈ | 113.8 ±0.7 ms | 113.6 ±0.5 ms ≈ | +0.1% ±0.3% ≈ | -0.0% ±0.2% ≈ | 50% ±3% ≈ |
| engine overhead | 15.92 ±0.04 ms | 8.64 ±0.02 ms ▲ | 28.5 ±0.1 ms | 15.2 ±0.2 ms ▲ | -46.4% ±0.8% ▲ | -46.0% ±0.1% ▲ | 99.8% ±0.1% ▲ |
| rate | base | target |
|---|---|---|
| account cache hits | 96.9% | 96.9% |
| storage cache hits | 90.9% | 90.8% |
| code cache hits | 99.9% | 99.9% |
| @@ -0,0 +1,167 @@ | |||
| // Copyright 2026 The go-ethereum Authors | |||
There was a problem hiding this comment.
@fjl is this something we would want to extract to another repo...similar to what you did with jsonw?
8bf45ff to
f1e252c
Compare
| // encoding/json matched field names case insensitively and | ||
| // unescaped them, so an unknown key may still name a field. | ||
| // Redo the message with it to keep that behavior. | ||
| redo = true |
There was a problem hiding this comment.
This is not necessary. The JSON-RPC standard tells the field names explicitly and we shouldn't accept uppercase ones.
There was a problem hiding this comment.
i added it to keep the behavior the same. i'll remove
a9b7514 to
e0eef6f
Compare
|
Some tests are failing |
e0eef6f to
dde81f1
Compare
oops, i forgot to fix my tests after addressing review feedback. should be fixed now |
newPayloadwas spending about 16 ms per block, roughly 20% of its total time, before block processing even started. Profiling that gap showed it was almost entirely JSON framing. The request bytes were being walked like 8 times and they cost more than the decode itself. The fix is two changes.Stop re-reading the same bytes. The layers above the decoder do not parse anything, they only need to know where a value starts and ends, but they were each re-parsing and copying the whole request to find out. They now find those bounds and hand out slices of the original bytes.
encoding/jsonstill performs every value conversion, and the scanning helpers injsonscan.goonly ever run on inputencoding/jsonhas already accepted.Read a message once instead of twice. Decoding a message into a
json.RawMessagemakes the decoder scan it to find where it ends, then theRawMessagescans it again before copying it out. Where the transport already knows where a message ends, an HTTP body or a WebSocket frame, the message is now read whole and checked once. Streams carry no framing of their own, so IPC, stdio and in-process connections keep the decoder.Benchmark
BenchmarkNewPayloadDecode, added here, so it needs no external data. It sends anengine_newPayloadV4request over HTTP with the chain stubbed out, and measures decoding the arguments plus assembling and hash checking the block.Small requests get faster too: a one line
eth_blockNumbergoes from 1005 ns to 552 ns.