NomicFoundation / NomicFoundation/edr
EIP-712 canonical-type collection re-parses whole build infos
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 108
- Forks
- 45
- Avg merge
- 2d 4h
- Merged PRs (30d)
- 94
Description
Hardhat's JS-side EIP-712 pipeline (builtin-plugins/solidity-test/eip712/) prepares the eip712CanonicalTypes config field for EDR. Its own logic costs milliseconds, but it parses entire build-info outputs — the AST-bearing JSON for all compiled sources — to extract a handful of struct definitions.
Measured in the Hardhat 3 + EDR profiling campaign on aave-v4's hardhat test solidity run, the only benchmark suite configuring EIP-712 types:
- JS side: 1.35 s of
parseJsonBytesunder the collector — 1.21 s ≈ 37% of active JS ≈ 6% of wall - EDR side: ~1.1 s in its native EIP-712 collector on the same run
- Combined: ~2.3 s ≈ 12% of that 19 s test run
Three structural inefficiencies: the parse is a superset (full ASTs, bytecode and metadata materialised for a few struct definitions); the result is recomputed on every test run even though build infos are immutable per build id; and the artifact loader may read the same build infos in the same run. Cost is zero when unconfigured — collection short-circuits on an empty include.
Direction
In order of likely payoff: cache the collected canonical types by build id plus EIP-712 config (build infos are content-addressed, so invalidation is trivial and warm runs pay ~0); or collect at compile time, where the output JSON is already in memory, storing a small artifact that test runs load instead of walking ASTs; or, if parsing must stay on the test path, share the parsed build info with the artifact loader rather than parsing twice.
Note: this collector is being moved to Rust (EDR feat/eip712-collector-crate-and-inline-config), including compile-time collection — so the remaining JS-side scope here may reduce to caching and shared parsing.
Implementation plan
# Stop re-parsing whole build infos in the EIP-712 canonical-type collector
## Problem
[Profiling Hardhat 3 e2e scenarios](https://app.notion.com/p/nomicfoundation/Runtime-Profiling-2026-08-05-3b3578cdeaf5808eafdff4e22ba425d0?source=copy_link) measures the JS-side EIP-712 canonical-type collection on aave-v4's `hardhat test solidity` run at 1.35 s of `parseJsonBytes` self-time under `collectEip712CanonicalTypes` — 1.21 s ≈ 37% of active JS ≈ **6% of wall**. EDR spends a further ~1.1 s in its native EIP-712 collector on the same run, so EIP-712 preparation totals ~2.3 s ≈ **12% of that 19 s test run**.
The collector's own logic costs milliseconds; the cost is the parsing. It is zero in scenarios that configure no EIP-712 types, since collection short-circuits on an empty `include`.
## Root cause
`packages/hardhat/src/internal/builtin-plugins/solidity-test/eip712/index.ts` — `collectEip712CanonicalTypes` parses **entire build-info output files** (`SolidityBuildInfoOutput`, the AST-bearing JSON for every compiled source) with `parseJsonBytes`, walks each source's AST (`ast-walker.ts`) and canonicalises struct definitions (`canonicalize.ts`). Three structural inefficiencies:
1. **Superset parsing** — the document carries full ASTs, bytecode and metadata for every source, while the collector needs only struct definitions and user-defined value types from selected sources. `JSON.parse` is all-or-nothing, so everything is materialised. A `bytesIncludesUtf8String` pre-filter exists; check what it actually filters out here.
2. **Recomputed every run** — build-info outputs are immutable per build id, yet canonical types are re-collected on every `hardhat test solidity` invocation.
3. **Duplicate parsing** — the artifact loading path may read the same build info in the same run; check `edr-artifacts.ts` (`BuildInfoAndOutput`) for overlap.
## Task
1. Reproduce the baseline (see Verification), and add a temporary timing log around `collectEip712CanonicalTypes` for aave-v4.
2. Evaluate, in order of likely payoff:
- **Cache by build id**: persist the collected canonical types keyed by build id plus EIP-712 config, next to the build info or in the cache directory. Build infos are content-addressed, so invalidation is trivial, and warm test runs pay ~0.
- **Collect at compile time**: compute the canonical types once at the end of compilation, where the output JSON is already in memory, and store them as a small artifact — test runs then load a tiny file instead of walking ASTs. Weigh the cost for users who never run EIP-712 Solidity tests; the config gate can decide.
- **Cheaper extraction**, if parsing must stay on the test path: share the parsed build-info object with the artifact loader when both need it in one run, or extract only the `ast` subtrees of selected sources — worthwhile only if measurements justify the complexity.
3. Behaviour must be identical: same canonical type strings, same `include`/`exclude` semantics, same cross-file dependency inlining (non-selected sources still feed the dependency graph — see the doc comment in `index.ts`).
4. Extend the existing eip712 unit tests with cache-hit and invalidation cases if caching lands. Run `pnpm lint`, `pnpm build`, `pnpm test` in `packages/hardhat`.
## Verification (before/after)
Profile before and after with `pnpm profiler` (a bare `pnpm profiler` prints its usage; see `scripts/README.md`):
```bash
pnpm build
pnpm profiler --scenario ./end-to-end/aave-v4 \
--prepare "cold compile" --command "test solidity" \
--mode js --init --use-local
```
Expect: `parseJsonBytes` gone from the top functions of the `.cpuprofile` — on the second, cache-warm run if caching lands, or on every run if compile-time collection does. Test results must be identical, and so must the canonical types handed to EDR (assert against a baseline dump).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/hardhat/src/internal/builtin-plugins/solidity-test/eip712/index.ts, then read ast-walker.ts, canonicalize.ts, and edr-artifacts.ts to understand parsing and artifact-loading overlap. Run the documented pnpm profiler scenario to establish the baseline before evaluating caching, compile-time collection, or shared parsing. Done means identical canonical types and test behavior, with the repeated parseJsonBytes cost removed or reduced as measured by profiling.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- performance
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100