Skip to content

perf: serialize JSON-RPC responses once - #1698

Open
Wodann wants to merge 8 commits into
mainfrom
perf/raw-value-response
Open

perf: serialize JSON-RPC responses once#1698
Wodann wants to merge 8 commits into
mainfrom
perf/raw-value-response

Conversation

@Wodann

@Wodann Wodann commented Aug 31, 2026

Copy link
Copy Markdown
Member

Follow-up to #1486, and stacked on it.

ResponseWithCallTraces.result was a serde_json::Value. Handlers built it with serde_json::to_value, then marshal_response_data walked the tree again with serde_json::to_string. Holding the serialized bytes instead makes the second pass a copy.

The performance gain is not significant but it is a prerequisite for further JSON-RPC round-trip optimisations.

What changes

  • result becomes a Box<serde_json::value::RawValue>, so each handler serializes once. Readers go through RawValue::get or the new deserialize_result.
  • marshal_response_data sizes its buffer from the result's length rather than growing from 128 bytes. That same length also decides the string limit up front, so an oversized response no longer serializes a 250 MB string only to discard it.
  • Batch responses are assembled from the already-serialized elements instead of being re-serialized through a Value::Array.
  • serde_json's raw_value feature is enabled workspace-wide, alongside the features already declared there.

Compatibility

The JSON reaching callers is byte-for-byte unchanged. A Box<RawValue> inside the untagged jsonrpc::ResponseData serializes exactly as the Value did, including through #[serde(flatten)], and arbitrary-precision numbers survive. marshal_response_data_matches_serde_for_success and its error counterpart pin both arms against serde's own output.

No #[napi] signature changes, so index.d.ts is untouched.

Benchmark Results

JS scenario runner benchmark (CI, vs latest main):

Scenario main #1486 #1698 #1486 / main #1698 / main #1698 / #1486
All Scenarios 210,629 153,020 148,833 0.73 0.71 0.97
synthetix_9a3a109f 156,751 111,860 108,024 0.71 0.69 0.97
neptune-mutual-blue-protocol_8db6480 21,999 17,240 16,885 0.78 0.77 0.98
rocketpool_6a9dbfd8 12,998 8,940 8,838 0.69 0.68 0.99
openzeppelin-contracts_0a5fba7a 9,110 7,250 7,337 0.80 0.81 1.01
seaport_585b2ef8 5,041 4,050 4,047 0.80 0.80 1.00
uniswap-v3-core_d8b1c63 4,068 3,190 3,205 0.78 0.79 1.00
safe-contracts_914d0f8 662 490 496 0.74 0.75 1.01

HH3 regression benchmark (CI, vs latest main):

Benchmark suite #1698 #1486 Ratio
ens-contracts / test vitest 3.9861710216199997 s (± 0.09548535425823759) 4.06399684068 s (± 0.012688302868408073) 0.98
ens-contracts / test vitest (peak RSS) 637 MB 638 MB 1.00
ens-contracts / test vitest (cpu) 81.23737028 s (± 0) 80.07114505999999 s (± 0) 1.01
lidofinance-core / test mocha 10.451533232740001 s (± 0.11958584155861957) 10.36830099496 s (± 0.020690816987286226) 1.01
lidofinance-core / test mocha (peak RSS) 795 MB 804 MB 0.99
lidofinance-core / test mocha (cpu) 379.8732149 s (± 0) 378.19546216 s (± 0) 1.00
openzeppelin-contracts / test mocha 57.92275289228 s (± 0.019179380485141086) 57.84666742306 s (± 0.010488213657848557) 1.00
openzeppelin-contracts / test mocha (peak RSS) 3531 MB 3498 MB 1.01
openzeppelin-contracts / test mocha (cpu) 62.59357925999999 s (± 0) 62.39426504 s (± 0) 1.00

Validation

  • New unit tests for the envelope and for batch assembly.
  • Scenario replay is unchanged on every fixture.

Notes for review

  • raw_value is declared workspace-wide deliberately. Four crates name RawValue, and edr_rpc_client sits upstream of edr_provider, so it never inherits the feature.
  • The envelope is built through serde_json::to_writer into a pre-sized buffer rather than by hand. Concatenating {"result": measures the same but duplicates the wire format of a type in another crate.

@Wodann
Wodann requested a balanced review from Copilot August 31, 2026 08:54
@Wodann Wodann self-assigned this Aug 31, 2026
@Wodann
Wodann had a problem deploying to github-action-benchmark August 31, 2026 08:54 — with GitHub Actions Error
@changeset-bot

changeset-bot Bot commented Aug 31, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: b6f0be3

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@Wodann

Wodann commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

/bench scenarios="test mocha,test vitest"

Copilot AI left a comment

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.

Pull request overview

Optimizes provider JSON-RPC serialization by storing serialized results as RawValue and reusing those bytes through batching and N-API marshaling.

Changes:

  • Replaces intermediate serde_json::Value results with Box<RawValue>.
  • Pre-sizes response envelopes and directly assembles serialized batches.
  • Updates consumers, tests, workspace features, and release notes.

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
Cargo.toml Enables serde_json raw_value.
.changeset/tidy-jars-serialize.md Records the serialization optimization.
crates/edr_rpc_client/src/jsonrpc.rs Documents RawValue deserialization constraints.
crates/edr_provider/src/lib.rs Stores raw results and adds typed deserialization.
crates/edr_provider/src/requests/dispatch.rs Directly assembles serialized batch arrays.
crates/edr_provider/src/test_utils.rs Adapts provider test helpers.
crates/edr_provider/tests/common/provider.rs Adapts transaction helper deserialization.
crates/edr_provider/tests/integration/timestamp.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/rip7212.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/issues/issue_407.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/issues/issue_356.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/issues/issue_325.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/issues/issue_324.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/interval_mining.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/eth_max_priority_fee_per_gas.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/eth_get_proof.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/estimate_gas.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/eip7843.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/eip7778.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/eip7702.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/eip7691.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/eip4844.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/eip2537.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/coverage.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/calldata_floor.rs Uses typed result deserialization.
crates/edr_provider/tests/integration/block_timestamp_in_logs.rs Uses typed result deserialization.
crates/edr_op/tests/integration/provider.rs Adapts OP provider tests.
crates/edr_op/tests/integration/isthmus_operator_fee.rs Adapts OP fee tests.
crates/edr_napi/src/mock.rs Stores mock responses as raw JSON.
crates/edr_napi/src/context.rs Propagates mock serialization errors.
crates/edr_napi_core/src/spec.rs Optimizes envelope serialization and sizing.
Suppressed comments (1)

crates/edr_napi_core/src/spec.rs:197

  • This function's buffer can grow for error responses because their capacity is fixed at 128 bytes while the serialized error may be larger. Limit the claim to the success path so the documentation matches the implementation.
/// Serializes a JSON-RPC response into a buffer that never has to grow.
///
/// `serde_json::to_string` starts at 128 bytes and doubles, so it copies a
/// large result several times over.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/edr_rpc_client/src/jsonrpc.rs Outdated
Comment thread crates/edr_napi_core/src/spec.rs
@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.42308% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.01%. Comparing base (a65c6b1) to head (b6f0be3).

Files with missing lines Patch % Lines
crates/edr_napi_core/src/spec.rs 93.60% 6 Missing and 2 partials ⚠️
crates/edr_napi/src/mock.rs 0.00% 4 Missing ⚠️
crates/edr_provider/src/requests/dispatch.rs 92.98% 0 Missing and 4 partials ⚠️
crates/edr_provider/src/lib.rs 70.00% 0 Missing and 3 partials ⚠️
crates/edr_provider/src/test_utils.rs 81.81% 0 Missing and 2 partials ⚠️
crates/edr_napi/src/context.rs 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1698      +/-   ##
==========================================
+ Coverage   79.97%   80.01%   +0.04%     
==========================================
  Files         462      462              
  Lines       80509    80685     +176     
  Branches    80509    80685     +176     
==========================================
+ Hits        64388    64564     +176     
+ Misses      13893    13891       -2     
- Partials     2228     2230       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Starting regression benchmark for c76fd1d65ec2 against Hardhat main (projects matching test mocha,test vitest, benchmarks matching test solidity,test mocha,test vitest).

@Wodann
Wodann temporarily deployed to github-action-benchmark August 31, 2026 09:31 — with GitHub Actions Inactive
@Wodann
Wodann temporarily deployed to github-action-benchmark August 31, 2026 09:48 — with GitHub Actions Inactive
@Wodann
Wodann had a problem deploying to github-action-benchmark August 31, 2026 09:48 — with GitHub Actions Error
@Wodann
Wodann temporarily deployed to github-action-benchmark August 31, 2026 11:42 — with GitHub Actions Inactive
@Wodann
Wodann had a problem deploying to github-action-benchmark August 31, 2026 12:21 — with GitHub Actions Failure
@Wodann
Wodann temporarily deployed to github-action-benchmark August 31, 2026 12:21 — with GitHub Actions Inactive
@Wodann
Wodann requested a review from a team August 31, 2026 13:28
@github-actions

Copy link
Copy Markdown
Contributor

❌ Regression benchmark failed for c76fd1d65ec2 against Hardhat main. This is either a detected performance regression or an infrastructure failure — see the run for details.

View workflow run

@Wodann

Wodann commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

/bench benchmarks="test mocha,test vitest"

@github-actions

Copy link
Copy Markdown
Contributor

⏳ EDR CI for this commit hasn't passed yet, so the regression benchmark was not started. Comment /bench again once CI is green.

@Wodann
Wodann temporarily deployed to github-action-benchmark August 31, 2026 19:48 — with GitHub Actions Inactive
@Wodann
Wodann force-pushed the perf/raw-value-response branch from 0cc3ee1 to 6509d71 Compare August 31, 2026 20:43
@Wodann
Wodann had a problem deploying to github-action-benchmark August 31, 2026 20:43 — with GitHub Actions Error
@Wodann

Wodann commented Aug 31, 2026

Copy link
Copy Markdown
Member Author

/bench benchmarks="test mocha,test vitest"

@Wodann
Wodann force-pushed the perf/raw-value-response branch from 6509d71 to e074e5c Compare August 31, 2026 21:14
@github-actions

Copy link
Copy Markdown
Contributor

⏳ EDR CI for this commit hasn't passed yet, so the regression benchmark was not started. Comment /bench again once CI is green.

Base automatically changed from fix/interval-mining-validation to main August 31, 2026 21:34
@Wodann
Wodann force-pushed the perf/raw-value-response branch from e074e5c to be36bad Compare August 31, 2026 21:34
@Wodann
Wodann temporarily deployed to github-action-benchmark August 31, 2026 21:34 — with GitHub Actions Inactive
@Wodann
Wodann temporarily deployed to github-action-benchmark August 31, 2026 22:39 — with GitHub Actions Inactive
@Wodann
Wodann temporarily deployed to github-action-benchmark August 31, 2026 22:39 — with GitHub Actions Inactive
@Wodann

Wodann commented Sep 1, 2026

Copy link
Copy Markdown
Member Author

/bench benchmarks="test mocha,test vitest"

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🚀 Starting regression benchmark for be36bade231c against Hardhat main (benchmarks matching test mocha,test vitest).

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

✅ Regression benchmark passed for be36bade231c against Hardhat main.

View workflow run

@Wodann Wodann added the no changeset needed This PR doesn't require a changeset label Sep 1, 2026
@Wodann
Wodann force-pushed the perf/raw-value-response branch from be36bad to d77f283 Compare September 1, 2026 09:31
@Wodann
Wodann had a problem deploying to github-action-benchmark September 1, 2026 09:31 — with GitHub Actions Error
Wodann and others added 7 commits September 1, 2026 11:33
`ResponseWithCallTraces.result` was a `serde_json::Value`, built by
`to_json` and walked again by `marshal_response_data`. Holding the
serialized bytes instead makes the second pass a copy, and lets a
future response cache hand out clones for the price of one memcpy.

The wire format is unchanged: a `Box<RawValue>` inside the untagged
`jsonrpc::ResponseData` serializes byte-for-byte as the `Value` did,
through `#[serde(flatten)]` as well.

`result` and `marshal_response_data`'s parameter change type. Readers go
through `RawValue::get` or the new `deserialize_result`. Enabling
serde_json's `raw_value` feature is workspace-wide, as the crate's other
features already are.

`MockProvider` now converts its mocked response once, at construction,
rather than on every request.
`serde_json::to_string` starts at 128 bytes and doubles, so it copies a
large result several times over. A `RawValue` result makes the envelope's
length known before it is built, so the buffer can be sized once.

That same length also settles the string-limit question up front. An
oversized response no longer serializes a discarded 250 MB string before
falling back to a `serde_json::Value`.

Both arms are pinned against serde's output by
`marshal_response_data_matches_serde_for_success` and its error
counterpart.
`ResponseData<Box<RawValue>>` compiles and always fails at runtime, and
only on the `Success` variant, so a test covering the error path passes.
`client::parse_response_str` is generic over `SuccessT`, so anyone
forwarding a remote result without re-serializing it would hit this with
an error message that points nowhere near the cause.
The batch tests reached every branch, but only through a provider
fixture, and neither asserted the serialized text. Nothing covered a
value containing a separator, which is what hand-building the array
risks getting wrong.

Also fixes the reservation for an empty array, which asked for one byte
and needed two. `json_array_len` is now a named function so a test can
assert the reservation equals the output length.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
The limit becomes a parameter of a private helper so a test can reach
it without allocating 250 MB. Both `Either::B` sites are now exercised:
the oversized-success preflight and the post-serialization fallback.

`envelope_len_matches_the_serialized_success` pins the hard-coded
envelope overhead against serde, which the limit decision depends on.
The precision test records that the fallback re-parses the raw value
and that `arbitrary_precision` keeps the digits intact.

Mutating the preflight to `>=`, the fallback check to `<`, or the
overhead to 10 each fails at least one of these tests.
The name promised more than the test checks. `napi` panics marshalling
a `serde_json::Value` number wider than `u64`, so the digits this test
pins never reach JS through the `Value` arm.

Verified against napi 3.12.1: `as_u64().unwrap()` at serde.rs:180 has
no arm for an `arbitrary_precision` integer that fits neither `i64`,
`u64` nor `f64`.
@Wodann
Wodann force-pushed the perf/raw-value-response branch from d77f283 to c73070d Compare September 1, 2026 09:33
@Wodann
Wodann had a problem deploying to github-action-benchmark September 1, 2026 09:33 — with GitHub Actions Error

@popescuoctavian popescuoctavian left a comment

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.

LGTM, thanks! Just a small nit

Comment thread crates/edr_napi_core/src/spec.rs Outdated
Comment thread crates/edr_napi_core/src/spec.rs
The three `map_err` arms in `marshal_response_data_with_limit` built the
same `napi::Error` from a `serde_json::Error`.
@Wodann
Wodann temporarily deployed to github-action-benchmark September 1, 2026 10:06 — with GitHub Actions Inactive
@Wodann
Wodann temporarily deployed to github-action-benchmark September 1, 2026 10:16 — with GitHub Actions Inactive
@Wodann
Wodann temporarily deployed to github-action-benchmark September 1, 2026 10:16 — with GitHub Actions Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

no changeset needed This PR doesn't require a changeset

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants