Skip to content

Benchmark the solx DWARF decode path - #1718

Draft
nebasuke wants to merge 3 commits into
mainfrom
bas/dwarf-decode-bench
Draft

Benchmark the solx DWARF decode path#1718
nebasuke wants to merge 3 commits into
mainfrom
bas/dwarf-decode-bench

Conversation

@nebasuke

@nebasuke nebasuke commented Sep 1, 2026

Copy link
Copy Markdown
Member

Claude summary

Why

Reviewing #1626 (addr2line 0.25→0.27 + gimli 0.32→0.34, which switches DIE attribute parsing from lazy to eager) surfaced that nothing benchmarks EDR's DWARF consumption: providerBenchmark/soltestsBenchmark are solc-compiled, the crate's contracts_identifier bench covers the solc source-map path, and the solx compile benchmark measures solx emitting DWARF, not EDR consuming it. The measurement for #1626 had to be done with an ad-hoc, uncommitted driver.

Two properties make a standing benchmark worthwhile here:

  • dwarf::decode_instructions re-parses hex → ELF → DWARF on every call, once per contract bytecode section at build-info load — so it is a load-time cost that scales with project size.
  • Decode time grows ~bytes^1.7 in blob size (per-blob throughput collapses from ~4.8 MiB/s at 13 KB blobs to ~0.5 MiB/s at 84 KB blobs), so a null result measured on small blobs does not transfer to large projects.

What

  • benches/dwarf_decode.rs — criterion bench, runs unconditionally on the committed scenarios fixture (no silent env-var skip). Per corpus: a full_pass over all decodable blobs and a largest_blob bench, both reporting byte throughput. Because the committed fixture tops out at ~13 KB blobs (the cheap regime), the bench also accepts EDR_DWARF_BENCH_DIR pointing at a directory of <name>.input.json/<name>.output.json solx standard-JSON pairs, each benchmarked as its own corpus.
  • examples/dwarf_ab.rs — the A/B driver used to validate fix(deps): update rust crate addr2line to 0.27 #1626, committed so the next dependency bump or decode change doesn't need to rebuild it. It reports what criterion can't: per-stage RSS checkpoints, per-blob timings for fitting the scaling exponent, and an order-independent output digest for cross-revision equivalence checks. Its doc header carries the recipe for building a large corpus pair with solx.

Numbers (this branch)

bench time throughput
full_pass/scenarios (96 blobs, 490 KB DWARF) ~86 ms ~5.4 MiB/s
full_pass/aave-v4 (118 blobs, 1.14 MB DWARF) ~509 ms ~2.1 MiB/s
full_pass/corpusB (sourcify contract, 1.36 MB DWARF) ~905 ms ~1.4 MiB/s
largest_blob/scenarios/13052B ~2.6 ms ~4.8 MiB/s
largest_blob/aave-v4/145428B ~134 ms ~1.0 MiB/s
largest_blob/corpusB/84160B ~156 ms ~0.5 MiB/s

(corpusB = sourcify 11155111_0xa270236e935b27af69535f377eeea36ee48742f3; solx 0.1.7 and 0.1.8 emit byte-identical DWARF for it. aave-v4 = the HH3 migration fork's real hardhat-slang-solx build-info input recompiled with released solx 0.1.8; its largest blob is SpokeInstance's deployed bytecode. aave's 145 KB blob decoding faster than corpusB's 84 KB blob shows per-blob cost tracks the model's per-file tables, not blob bytes alone — corpusB is a single 2.2 MB source file, aave spreads 123 sources.)

Building the aave corpus also surfaced a real parse bug: OptimizerSettings.runs is Option<u32>, but aave ships optimizer.runs: 444444444444 (valid for solc, which reads 64-bit) — so EDR's build-info parsing hard-fails on that project's real build-info, solc and solx paths alike. Tracked separately; the bench corpus clamps the field as a workaround.

CI: same-machine A/B on PRs

.github/workflows/dwarf-decode-benchmark.yml benches the PR's merge commit against its first parent (the base tip) on the same runner in the same job, so machine variance cancels, and lets criterion do the statistics. The gate fails only when the 95% confidence interval puts the mean regression above 10% — measured same-machine rerun noise reaches ~+6% mean (CI lower bound +2.7%), so anything gated tighter than the CI bound would false-alarm. This is deliberately not the single-sample-vs-last-stored-value pattern of the wall-time benchmarks, whose 110% threshold sits inside the soltests series' ~14% noise band.

Path-filtered to crates/edr_solidity/**, Cargo.lock, and the workflow itself — dependency bumps are the proven trigger (#1626). workflow_dispatch gives an informational single run.

Scope limits, stated openly: CI only exercises the committed scenarios fixture (small blobs — the large real-world corpora embed UNLICENSED third-party sources and cannot be committed; they stay local via EDR_DWARF_BENCH_DIR), and the 10%-confident gate won't catch subtle regressions. Both are addressed by the planned follow-up: deterministic counters (iai-callgrind → Bencher, the slang pattern) plus a synthesized, owned large-DWARF fixture.

Out of scope

  • The deterministic-counter setup above (Bencher for edr) — follow-up.
  • The decode scaling fix itself (RangeSweep/DecodeCaches, validated at 5.5× on corpus B) — separate PR; this benchmark is how its effect stays visible.

Nothing benchmarked EDR's DWARF consumption: the JS scenario benchmarks
are solc-compiled and the crate's contracts_identifier bench covers the
solc source-map path. Meanwhile decode_instructions re-parses
hex -> ELF -> DWARF on every call and its time grows ~bytes^1.7, so
dependency changes in gimli/addr2line (e.g. #1626's lazy-to-eager DIE
attribute parsing) and decode changes need a standing measurement.

Two pieces:

- benches/dwarf_decode: criterion bench that always runs on the
  committed scenarios fixture (full pass + largest blob, with byte
  throughput), and accepts EDR_DWARF_BENCH_DIR with standard-JSON
  input/output pairs for large-blob corpora, since the committed
  fixture's 13 KB max blob is the cheap regime of the scaling curve.
- examples/dwarf_ab: the A/B driver used to validate #1626, for what
  criterion cannot report — per-stage RSS checkpoints, per-blob timings
  for fitting the scaling exponent, and an order-independent output
  digest for cross-revision equivalence checks.
Unlike the wall-time benchmarks that compare a single sample against the
last stored value on main (a design whose alert threshold sits inside its
own noise band), this job benches the PR's merge commit against its first
parent on the same runner in the same job, and lets criterion do the
statistics. The gate fails only when the 95% confidence interval puts the
mean regression above 10%, so a few percent of runner drift per side
cannot fail an innocent PR; regressions subtler than that are deferred to
a deterministic-counter setup (iai-callgrind + Bencher, the slang
pattern).

Path-filtered to the decode path's modules and fixtures, Cargo.lock, and
the workflow itself — dependency bumps are the proven risk vector (a
gimli major bump is what prompted the benchmark). CI only exercises the
committed scenarios fixture: the large real-world corpora contain
UNLICENSED third-party sources and stay local via EDR_DWARF_BENCH_DIR.
@nebasuke
nebasuke had a problem deploying to github-action-benchmark September 1, 2026 09:56 — with GitHub Actions Error
@changeset-bot

changeset-bot Bot commented Sep 1, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ec2e25d

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

A PR that introduces or moves the bench target (this one included) has no
base side to A/B against; benching HEAD^1 would fail on the missing bench
and the gate would then fail loudly on the absent comparison. Detect the
bench at the base commit and fall back to a single informational run.
@nebasuke nebasuke added the no changeset needed This PR doesn't require a changeset label Sep 1, 2026
@codecov

codecov Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.97%. Comparing base (9ad7d09) to head (ec2e25d).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1718      +/-   ##
==========================================
+ Coverage   79.90%   79.97%   +0.06%     
==========================================
  Files         461      462       +1     
  Lines       80120    80509     +389     
  Branches    80120    80509     +389     
==========================================
+ Hits        64023    64387     +364     
- Misses      13889    13893       +4     
- Partials     2208     2229      +21     

☔ 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.

@nebasuke
nebasuke temporarily deployed to github-action-benchmark September 1, 2026 10:10 — with GitHub Actions Inactive
@nebasuke
nebasuke temporarily deployed to github-action-benchmark September 1, 2026 10:35 — with GitHub Actions Inactive
@nebasuke
nebasuke temporarily deployed to github-action-benchmark September 1, 2026 10:35 — with GitHub Actions Inactive
nebasuke added a commit that referenced this pull request Sep 1, 2026
Real projects ship vanity runs values -- aave-v4's config used
optimizer.runs: 444444444444, which solc accepts (it reads the field as
64-bit, as does foundry-compilers) but overflowed our Option<u32>. The
serde error then propagated out of BuildInfoBuffers::parse, failing the
entire build-info parse and with it stack traces, over a field the
decoder never reads. Found while building a DWARF decode benchmark
corpus from aave-v4's real build-info (#1718).

The regression test round-trips the value through serialization so a
silent clamp would also fail it; it was proven red on the pre-fix code.
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.

1 participant