Skip to content

feat: Benchmarkoor - Nethermind CI (podman + CRIU memory checkpoints, per-test summary, dotTrace profiling) - #159

Merged
kamilchodola merged 16 commits into
mainfrom
feat/benchmarkoor-nethermind
Jul 15, 2026
Merged

feat: Benchmarkoor - Nethermind CI (podman + CRIU memory checkpoints, per-test summary, dotTrace profiling)#159
kamilchodola merged 16 commits into
mainfrom
feat/benchmarkoor-nethermind

Conversation

@kamilchodola

Copy link
Copy Markdown
Contributor

What

Integrates ethpandaops/benchmarkoor into gas-benchmarks CI, exclusively for Nethermind, as the Benchmarkoor - Nethermind workflow running on the stateful-generator self-hosted runner — with CRIU memory-checkpoint rollbacks instead of the restart-per-test flow used for the stateful category today.

Highlights

  • Memory snapshots via podman + CRIU (container-checkpoint-restore, with restore_in_place): pre-run steps (gas-bump/funding) are baked into a checkpoint once; every test then gets an instant ZFS datadir rollback + ~4 s in-place process restore. Full 550-test jochemnet stateful suite: 1 h 22 m, 550/550 passed (vs ~3 h+ with per-test restores through archive import, and far longer with container recreation).
  • Self-bootstrapping ZFS: the runner's plain-fs snapshot (/mnt/sda/<network>/nethermind) is seeded once into an auto-sized file-backed zpool (grown in place when undersized, resumable seeding tracked via a gb:seeded dataset property). The source snapshot is never written to.
  • Per-test MGas/s job summary: every run appends a median/min/max headline + collapsible per-test table to the GitHub job summary (.github/scripts/benchmarkoor-pertest-summary.jq over the run's result.json).
  • dotTrace profiling (diagnostics_mode: dottrace + a diag image, e.g. nethermindeth/nethermind:bal-devnet-7-diag): sampling snapshot collected via a bind-mounted diag dir, uploaded as an artifact, and auto-converted to XML by a Windows Reporter.exe job (same approach and package as nethermind's run-expb-reproducible-benchmarks). Note: the diag images' own DIAG_WITH entrypoint profiles with --profiling-type=timeline, which Reporter.exe cannot convert — the workflow overrides the entrypoint with a flat-argv sampling invocation instead.
  • trace_blocks (NethermindEth/nethermind#12444 contract): injects NETHERMIND_PROFILE_BLOCKS + --use-api for per-block dotTrace windows scoped to testing blocks only (excluding gas-bump and setup); auto maps to the jochemnet testing blocks. Requires an image with the BlockProfiler plugin to produce output.
  • Test suites are pulled from this repo's release assets (generated-tests-<type>-<network>.tar.gz, default amsterdam-repricings-v5.2.0), overridable to any URL including GitHub Actions artifacts; filter narrows the suite per run.

Layout

  • .github/workflows/benchmarkoor-nethermind.yml — the dispatchable workflow (network/test-type/image/filter/rollback/diagnostics/... inputs)
  • benchmarkoor/*.yaml — benchmarkoor config fragments fetched at the pinned commit and merged in order (global, test source, datadir, per-strategy rollback)
  • benchmarkoor/README.md — full docs: defaults, ZFS bootstrap, datadir layout (--Init.BaseDbPath=/data/mainnet), profiling, caveats
  • scripts/genesisfiles/nethermind/generator-amsterdam-jochemnet.json — corrected jochemnet chainspec (main's copy scheduled an incomplete Amsterdam EIP set at a timestamp inside the snapshot's synthetic block range, causing InvalidBlockLevelAccessListHash on startup replay; this copy matches the ethpandaops devnet-7 chainspec, params/genesis/accounts-identical)

Dependencies

Two benchmarkoor capabilities live in a fork until upstreamed (kamilchodola/benchmarkoor@gas-benchmarks):

Upstream images ignore both options gracefully (stateful runs fall back to archive-import restores; diagnostics runs auto-default to the fork build). Once merged upstream, no changes needed here.

Validation

Run Scope Result
29346142578 + 29361760222 full 550-test stateful suite, bal-devnet-7 550/550 passed, 1 h 22 m
29402227698 11-test smoke 11/11 + per-test summary table
29404884422 1-test dotTrace smoke (bal-devnet-7-diag) test passed + .dtp snapshot + XML report artifacts

Client/fixture pairing note: the v5.2.0 fixtures carry bal-devnet-7 block access lists — nethermind:master rejects them (InvalidBlockLevelAccessList), so master runs need fixtures refilled against master.

…eckpoints)

Integrates ethpandaops/benchmarkoor for Nethermind only, running on the
stateful-generator self-hosted runner.

- container_runtime: podman with rollback_strategy:
  container-checkpoint-restore by default for stateful suites: pre-run
  steps (gas-bump/funding) are baked into a CRIU memory checkpoint +
  ZFS datadir snapshot, then every test gets an instant rollback/restore
  instead of a full container recreate.
- Bootstraps a file-backed zpool on the runner and seeds a dataset from
  /mnt/sda/<network>/nethermind when the snapshot is not already on ZFS
  (one-time rsync, reused across runs).
- Tests/opcodes pulled from gas-benchmarks release archives (default
  amsterdam-repricings-v5.2.0, jochemnet), chainspec from
  scripts/genesisfiles/nethermind at the triggering commit.
- Rollback strategy selectable (checkpoint-restore / container-recreate /
  rpc-debug-setHead / none) for A/B comparison; extra_run_config input
  deep-merges arbitrary benchmarkoor YAML last.
- Results uploaded as workflow artifact + job summary via the
  ethpandaops/benchmarkoor composite action, which also installs
  podman/CRIU/crun and starts the rootful podman socket.
The jochemnet nethermind snapshot is ~900G; the fixed 500G pool filled at
57% of the seed copy. Pool size now defaults to 'auto' (snapshot + 15% +
32G) with a free-space preflight, existing pools are grown in place
(sparse truncate + zpool online -e), and seed completion is tracked via
the gb:seeded ZFS user property so interrupted copies resume instead of
being mistaken for complete.

Also add --Init.BaseDbPath=/data to the default Nethermind flags: the
runner snapshots hold the nethermind_db content at their root, which the
stock --datadir=/data would miss.
…nnet)

Nethermind's BaseDbPath convention already contains the per-network dir
(mainnet config uses nethermind_db/mainnet; our own
scripts/nethermind/docker-compose.yaml uses /nethermind/data/mainnet).
With --Init.BaseDbPath=/data the client created an empty DB next to the
snapshot's mainnet/ dir and booted at genesis (chain head 0, endless
SYNCING). Point it at /data/mainnet instead.
…nly-scenarios

main's copy schedules the Amsterdam EIP set (missing 7954/7976/7981/8037)
at 0x697D1108, which falls inside the snapshot's synthetic block range —
the node then computes BAL hashes for stored blocks that have none and
rejects them (InvalidBlockLevelAccessListHash). The corrected file
activates 7708,7778,7843,7928,7954,7976,7981,8024,8037 at 0x697ddeff
(head block 24402727 timestamp + 1), matching the chainspec ethpandaops
use for this exact snapshot (verified params/genesis/accounts-identical
to skylenet's devnet-6 gist).
Full Nethermind output is always written to runs/<run_id>/container.log
in the results artifact; client_logs_to_stdout only duplicates it into
the workflow log, which drowns the job output during the startup block
replay. Re-enable per run via extra_run_config when live-debugging.
Needed to validate benchmarkoor branches (e.g. the restore_in_place
fork) on the runner before they land upstream.
cancel-in-progress: true killed a 3h validation run when the next
dispatch arrived on the same ref. Benchmarks should queue.
GitHub substitutes the default for explicitly-empty dispatch inputs, so
passing benchmarkoor_image='' still pulled ghcr master and silently
skipped the fork build. A non-empty git ref now implies image=''.
Stateful suites now get checkpoint-restore with restore_in_place out of
the box (rollback auto already maps compute to 'none', so compute runs
carry no checkpoint burden). Upstream benchmarkoor binaries ignore the
option until ethpandaops/benchmarkoor#282 merges and simply use the
export/import path; the fork ref enables the ~4s in-place restores now.
- Append a per-test throughput table (median/min/max + collapsible
  all-tests table) to the GitHub job summary, generated from the run's
  result.json by .github/scripts/benchmarkoor-pertest-summary.jq.
- diagnostics_mode input (dottrace/dotmemory/dotnet-trace) runs the
  client through the diag-image DIAG_WITH entrypoint; snapshots are
  collected via the fork's new instance-level extra_mounts option
  (bind mount at /nethermind/diag), uploaded as an artifact, and
  dotTrace snapshots are converted to XML on windows-latest with
  Reporter.exe (mirrors nethermind's run-expb-reproducible-benchmarks).
- trace_blocks input (implies dottrace) injects
  NETHERMIND_PROFILE_BLOCKS for the BlockProfiler plugin
  (NethermindEth/nethermind#12444): per-block dotTrace windows so only
  testing blocks are traced, excluding gas-bump and setup blocks;
  'auto' maps to the jochemnet testing blocks 24407730,24407731.
- Profiling runs resolve rollback auto -> rpc-debug-setHead (one live
  process; dotTrace is incompatible with CRIU restores) and default
  benchmarkoor to the fork's gas-benchmarks branch for extra_mounts.
The diag entrypoint's --save-to base produces a chunked snapshot: an
extensionless 'dottrace' index plus dottrace.XXXX chunk files. Match
the index (plus *.dtp/*.dtc) like repricing-nethermind does instead of
requiring a .dtp extension.
masterdiag profiles with dotTrace 2026.1.4; Reporter 2026.1.0.1 rejects
its snapshots ('unknown or unsupported snapshot format'). Also make the
XML step's exit explicit: fail only when snapshots existed but nothing
converted, instead of leaking the last Reporter exit code.
masterdiag's diag-entrypoint profiles with --profiling-type=timeline;
Reporter.exe cannot read Timeline snapshots (MFDTEM storage, 'Invalid
storage type'), verified by local bisection: default/sampling capture
produces MFDTPF storage that converts fine. In dottrace mode the
container entrypoint is now overridden with an expb-style wrapper:
default (sampling) profiling, .dtp snapshot naming, and --use-api only
when NETHERMIND_PROFILE_BLOCKS is set (MeasureProfiler per-block windows
need API mode; API mode suppresses whole-run collection, so trace_blocks
needs an image with the BlockProfiler plugin).
…onfig

benchmarkoor os.Expand()s the raw config text at load time, so the
bash wrapper's runtime variables ($tool, $c, $@) were blanked before
the container ever started ('dottrace tool not found in image').
Replace the shell wrapper with a flat argv entrypoint — the client
flags (benchmarkoor's command) are appended by the runtime,
ENTRYPOINT+CMD style — and decide --use-api at compose time.
The dottrace path (/opt/diag-tools/dottrace) is where nethermind
diag images install it (verified in masterdiag and bal-devnet-7-diag).
Comment thread .github/workflows/benchmarkoor-nethermind.yml Fixed
…e runs

rpc-debug-setHead keeps client caches warm across tests, so only the
first test in a profiling run measures cold state (observed ~4x speedup
on subsequent tests). Single-test filters give pure-cold profiles.
@kamilchodola
kamilchodola merged commit 8502c45 into main Jul 15, 2026
7 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