Skip to content

feat: per-test engine payload sizes (SSZ + snappy + BAL)#236

Merged
skylenet merged 35 commits into
ethpandaops:masterfrom
CPerezz:feat/engine-payload-sizes
May 13, 2026
Merged

feat: per-test engine payload sizes (SSZ + snappy + BAL)#236
skylenet merged 35 commits into
ethpandaops:masterfrom
CPerezz:feat/engine-payload-sizes

Conversation

@CPerezz
Copy link
Copy Markdown
Contributor

@CPerezz CPerezz commented May 11, 2026

Summary

  • Compute and store per-test engine executionPayload byte sizes once per suite (suite-level invariant — identical across all clients): SSZ-encoded length, snappy-compressed length, and inline BlockAccessList (BAL) length. New SuiteTest fields land in summary.json with omitempty, so existing suites parse unchanged.
  • Surface the data as a new collapsible "Payload Sizes" section on SuiteDetailPage: ECharts horizontal bar chart (Non-BAL + BAL stacked → uncompressed total, plus a separate Snappy bar), single search input that filters chart + table together, sortable + paginated table beneath. Default sort: uncompressed desc.
  • Uses Prysm's enginev1 (github.com/OffchainLabs/prysm/v6, pinned to a commit on the bal branch) for SSZ marshaling — same encoder Prysm uses on mainnet — so the numbers reflect what a CL would actually gossip. Backfill happens automatically on the next run that hits an existing suite (MergePayloadSizes mirrors the mergeOpcodeData pattern at pkg/executor/suite.go); a one-shot CLI for legacy suites is intentionally out of scope here.

Test plan

  • Unit tests for EestToPrysmPayload across V1 Bellatrix → V6 Gloas (10 tests in pkg/eest/)
  • Error-path tests: unknown version, malformed hex, wrong-length hash
  • ExtractNewPayloadLines / ComputePayloadSizes / MergePayloadSizes unit tests (10 tests in pkg/executor/)
  • End-to-end CreateSuiteOutput test: new-suite path writes the three fields; merge path backfills them on a second run
  • Backward-compat unmarshal test (old summary.json without the new fields)
  • Go-side gofmt -l clean, go vet clean
  • TS tsc --noEmit clean; UI vite build succeeds
  • Manual UI verification: section renders, filter narrows chart + table, column sort applies to both, pagination cycles, dataZoom slider works on dense charts, dark-mode styling intact
  • Manual end-to-end against a real suite (deferred — requires gpgme system library on the build host, pre-existing setup; not specific to this PR)

Notes for reviewers

  • V4 (Electra) intentionally delegates to the Deneb SSZ struct because Prysm exposes no distinct ExecutionPayloadElectra payload type (Electra-specific data travels in the separate executionRequests engine API param, not inside the payload). Comment documents the rationale.
  • The SlotNumber field present on eest.ExecutionPayload is not forwarded to Prysm because Prysm's current ExecutionPayloadGloas struct does not define that field. Documented inline.
  • The docker / podman / podman-checkpoint-restore CI jobs are expected to fail on fork PRs: action.yaml:69 hard-codes git clone https://github.com/ethpandaops/benchmarkoor.git and then checks out the PR head SHA, which only exists on the fork. This is a pre-existing workflow limitation, not a regression from this PR.

@CPerezz
Copy link
Copy Markdown
Contributor Author

CPerezz commented May 11, 2026

The failure in CI is at action.yaml:69 — the action runs raw git clone https://github.com/ethpandaops/benchmarkoor.git && git checkout <head.sha>, but our head.sha only lives on the fork CPerezz/benchmarkoor.

So CI can never pass IIUC?
cc: @skylenet

CPerezz added 24 commits May 11, 2026 22:59
@CPerezz CPerezz force-pushed the feat/engine-payload-sizes branch 2 times, most recently from e231a70 to 0791af1 Compare May 11, 2026 21:01
@skylenet
Copy link
Copy Markdown
Member

The failure in CI is at action.yaml:69 — the action runs raw git clone https://github.com/ethpandaops/benchmarkoor.git && git checkout <head.sha>, but our head.sha only lives on the fork CPerezz/benchmarkoor.

So CI can never pass IIUC? cc: @skylenet

Yes, fixing it here: #237 . I'll update this branch once it's on master.

skylenet added 2 commits May 12, 2026 11:36
Brings in ethpandaops#237 (fix CI for PRs from forks) so this PR's checks run
against the updated workflow.
Replaces the three flat scalars (payload_size_bytes,
payload_size_bytes_snappy, bal_size_bytes) on SuiteTest with a single
nested payload_sizes container keyed by step (setup/test/cleanup), each
holding aligned per-newPayload arrays. This lets multi-block tests be
inspected block-by-block instead of collapsing into one sum, and makes
room for non-test steps that happen to contain newPayloads.

Field names match what the bytes actually are: ssz_full (full SSZ-
encoded ExecutionPayload with BAL inline), ssz_bal (just the SSZ-encoded
BlockAccessList sub-field), ssz_full_snappy (snappy of ssz_full),
json_full (canonical JSON of the same ExecutionPayload), json_bal (BAL
hex string length as it appears in JSON, no quotes). The two JSON
fields are new — they let you compare SSZ vs JSON wire size for the
same content.

UI side: page-level PayloadSizesSection keeps showing one row per test
by summing the test-step ssz_* arrays into totals. The test-details
modal gets a new per-step table with one row per newPayload (#1, #2,
...) and a totals row when n>1, with BAL/Snappy/JSON-BAL annotated by
% of their container.

This is a breaking change to summary.json — old summaries parse
(payload_sizes is omitempty), but the previous scalar fields are gone.
skylenet added 9 commits May 12, 2026 17:45
Tests-tab table on the suite-details page gains a segmented view-mode
switcher (General / Payload sizes (SSZ) / Payload sizes (JSON)) so the
existing Steps column can swap out for payload-size data. The
standalone PayloadSizesSection table goes away — the chart stays as the
at-a-glance overview, and per-test totals now live inline in the main
table.

Sort: every payload column (including #, sorting by original index)
acts as a sortable header. Direction cycles desc → asc → off. Sort
column + direction persist in the URL via psort / psortDir, alongside
testView, so a link reproduces the table state exactly.

JSON mode columns: JSON, JSON BAL, BAL %. SSZ mode keeps the existing
five columns. Sort state survives mode switches; a column not visible
in the current mode just doesn't show an active arrow but still applies
under the hood until the user re-sorts.

PayloadSortCol + isPayloadSortCol moved to a sibling payloadSort.ts so
TestFilesList.tsx exports only the component (keeps Fast Refresh
happy).
…oltip

- Always shown (drop the chevron collapse).
- Vertical bars with Test # on the X axis (categorical, formatted as
  #N) and bytes on the Y axis. Replaces the long-test-name horizontal
  axis that was unreadable for thousands of tests.
- Order toggle: Test # (default, monotonic X axis) or Size
  (descending). Local state — the URL doesn't need it.
- Tooltip matches the run-detail resource-usage charts: bold
  `Test #N`, decomposed name below in muted small text (respects the
  global useNameDisplayMode preference), one row per series with the
  series color dot + formatted bytes. `extraCssText` + `confine`
  prevent overflow.
- Dark-mode theming: textColor/axisLineColor/splitLineColor pattern
  copied from ResourceUsageCharts so legends, axis labels, and data-
  zoom controls aren't grey-on-grey. Local useDarkMode hook watches
  the <html> class so the chart re-themes live.
Clicking any column in the bar chart on the suite-details page now
opens the same modal that table-row clicks open. SuiteDetailPage wires
PayloadSizesSection.onTestClick to the existing handleDetailChange, so
the URL gets the same ?detail=<N> param and TestFilesList's modal
takes over from there.

The handler hooks the lower-level Zrender canvas (instead of ECharts'
default bar-only click) and converts the cursor pixel back to an
X-axis category via convertFromPixel, so clicks in the gaps above
short bars or between stacked segments resolve to the column the user
"meant" rather than getting eaten. containPixel('grid', ...) gates out
clicks on the legend, axis labels, and data-zoom slider.

categories / indexToName moved to useMemo above the new effect to keep
all hooks unconditionally called.
Adds a single search bar at the top of the Tests tab that drives every
downstream section (FacetPanel, TestHeatmap, OpcodeHeatmap,
PayloadSizesSection, the tests table). Sticky to the viewport on
scroll, using the same Tailwind pattern and FilterInput component as
the run-detail page. Lives above FacetPanel.

Now that one search box covers everything, the inline ones in
TestHeatmap, OpcodeHeatmap, and the tests-tab TestFilesList are gone:
- TestHeatmap: input + regex toggle removed outright (suite-only
  caller); chip-click on test names still toggles terms in the
  global query via onSearchChange.
- OpcodeHeatmap: passed the existing hideSearchInput prop.
- TestFilesList: new hideSearchInput prop; tests-tab passes it,
  pre-run-steps tab (no global bar) keeps its inline input.

PayloadSizesSection becomes fully controlled: its internal query
state and inline filter input are removed, replaced by a
searchQuery prop driven by the page's ?q= param. The "N of M
matching" count stays next to the order toggle when the search is
active.
Previously capped the initial dataZoom window to ~200 bars, which hid
most of the dataset on first paint and made the slider mandatory. Show
all bars by default; the slider is still there for narrowing in.
Adds cursor-pointer to the chart Order toggle (Test # / Size) and the
tests-table view-mode toggle (General / Payload sizes (SSZ) / Payload
sizes (JSON)) so it's visually clear they're clickable.
Moves the Non-BAL / BAL / Snappy legend below the data-zoom slider
instead of stacking it above the chart. Reworks the grid + slider
geometry so the bottom strip reads cleanly: X-axis labels → small gap
→ slider → small gap → legend. Chart height bumped to 400 to fit the
extra row.
Adds a psOrder search param ('index' default | 'size') so a link
reproduces the chart's bar order along with testView and psort/psortDir.
SuiteDetailPage threads psOrder through every URL-preserving handler
(pagination, detail-open, opcode-sort, test-view, payload-sort) so
those interactions don't drop it. PayloadSizesSection's order toggle
is now a controlled/uncontrolled prop pair, falling back to local
state when no parent callback is wired.
…-detail UX cleanup

PayloadSizesSection gets an Encoding toggle (SSZ default | JSON) next
to the Order toggle. SSZ keeps the existing Non-BAL + BAL stacked +
Snappy layout; JSON drops Snappy (no compressed JSON variant) and
shows Non-BAL + BAL stacked in the same palette so the BAL share
reads identically across encodings. The size sort, the
max-payload-size header annotation, the legend, and the chart series
all swap with the active encoding via a fullOf(r) helper.

Persisted in the URL as psEncoding (default 'ssz' stays clean).
Threaded through every URL-preserving handler in SuiteDetailPage
(pagination, detail-open, opcode-sort, test-view, payload-sort,
ps-order) so flipping any of those doesn't reset the encoding.

Also in this batch (suite-details Tests tab):
- Drop the collapsible OpcodeHeatmapSection wrapper. <OpcodeHeatmap />
  now renders inline like on the run-detail page; outer card and
  internal padding restored so it doesn't look unwrapped.
- File content in the test-details modal: min-w-0 on every flex
  ancestor of the <pre>, max-h-[60vh] + overflow-auto on the pre.
  Fixes a Chrome bug where long single-line JSON-RPC payloads
  blew up the flex column and made the content disappear off-screen.
@skylenet skylenet merged commit 7b20a6c into ethpandaops:master May 13, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants