Skip to content

rpc, eth/catalyst: optimize json decode - #35518

Merged
fjl merged 8 commits into
ethereum:masterfrom
jrhea:rpc-optimize-json-decode
Aug 24, 2026
Merged

rpc, eth/catalyst: optimize json decode#35518
fjl merged 8 commits into
ethereum:masterfrom
jrhea:rpc-optimize-json-decode

Conversation

@jrhea

@jrhea jrhea commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

newPayload was 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.

  1. 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/json still performs every value conversion, and the scanning helpers in jsonscan.go only ever run on input encoding/json has already accepted.

  2. Read a message once instead of twice. Decoding a message into a json.RawMessage makes the decoder scan it to find where it ends, then the RawMessage scans 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.

Important Note: A body with two JSON values in it is now a parse error. The old code answered the first one and quietly ignored the rest.

Benchmark

BenchmarkNewPayloadDecode, added here, so it needs no external data. It sends an engine_newPayloadV4 request over HTTP with the chain stubbed out, and measures decoding the arguments plus assembling and hash checking the block.

payload master after change 1 after both allocated, master to both
95 KB, 64 txs 1.37 ms 0.74 ms 0.58 ms 0.91 MB to 0.43 MB
293 KB, 192 txs 4.15 ms 2.17 ms 1.61 ms 3.19 MB to 1.26 MB
584 KB, 384 txs 8.11 ms 4.29 ms 3.20 ms 6.33 MB to 2.47 MB

Small requests get faster too: a one line eth_blockNumber goes from 1005 ns to 552 ns.

@jrhea

jrhea commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Bench: cae76d5a3c3c7baad83bde2bd6c2d3ae8baca7d3rpc-optimize-json-decode

2k payload · blocks 25677501–25679500 · 3 runs/side · machine geth-benchmark-1
ran 2026-08-11 15:31 – 2026-08-11 16:08 UTC

  • base cae76d5a3c3c7baad83bde2bd6c2d3ae8baca7d3 @ cae76d5a3ccore/rawdb: verify canonical hash before reading ancient BAL (#35471) · go1.26.5
  • target rpc-optimize-json-decode @ 25a5dd32ecrpc, eth/catalyst: read HTTP and WebSocket requests once instead of twice · go1.26.5 · 2 commits ahead, 0 behind

Paired per-block newPayload (2000 blocks × 3 runs): median -9.5% ±0.2% · mean -9.3% ±0.5% · 92% of blocks faster

Results

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%

@jrhea
jrhea marked this pull request as ready for review August 12, 2026 20:30
Comment thread rpc/jsonscan.go
@@ -0,0 +1,167 @@
// Copyright 2026 The go-ethereum Authors

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fjl is this something we would want to extract to another repo...similar to what you did with jsonw?

@jrhea
jrhea force-pushed the rpc-optimize-json-decode branch from 8bf45ff to f1e252c Compare August 12, 2026 20:36
Comment thread rpc/json.go Outdated
Comment on lines +449 to +452
// 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not necessary. The JSON-RPC standard tells the field names explicitly and we shouldn't accept uppercase ones.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i added it to keep the behavior the same. i'll remove

@jrhea
jrhea requested a review from fjl August 19, 2026 18:45
@jrhea
jrhea force-pushed the rpc-optimize-json-decode branch from a9b7514 to e0eef6f Compare August 19, 2026 18:54
@MariusVanDerWijden

Copy link
Copy Markdown
Member

Some tests are failing

@jrhea
jrhea force-pushed the rpc-optimize-json-decode branch from e0eef6f to dde81f1 Compare August 20, 2026 17:04
@jrhea

jrhea commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

Some tests are failing

oops, i forgot to fix my tests after addressing review feedback. should be fixed now

@fjl
fjl merged commit 5515722 into ethereum:master Aug 24, 2026
9 checks passed
@fjl fjl added this to the 1.17.6 milestone Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants