Skip to content

feat(checkpoint-restore): add restore_in_place option#282

Open
kamilchodola wants to merge 1 commit into
ethpandaops:masterfrom
kamilchodola:feat/checkpoint-restore-in-place
Open

feat(checkpoint-restore): add restore_in_place option#282
kamilchodola wants to merge 1 commit into
ethpandaops:masterfrom
kamilchodola:feat/checkpoint-restore-in-place

Conversation

@kamilchodola

Copy link
Copy Markdown

feat(checkpoint-restore): add restore_in_place option

What

Adds an opt-in checkpoint_restore_strategy_options.restore_in_place flag to the container-checkpoint-restore rollback strategy. When enabled, the checkpoint is taken locally (Keep, no Export) so the CRIU images stay in the container's storage directory, and the same container — same name, mounts, and IP — is restored in place before every test, with a zero-timeout stop between tests. Default (false) keeps the current export/import behavior unchanged.

Why

On the export/import path, every test pays for extracting the checkpoint archive into container storage plus creating and wiring a fresh container — CRIU itself is a small fraction of the per-test restore:

export/import (current) restore_in_place
checkpoint (once) 23.1 s 12.4 s
per-test podman restore 15.0 s 4.1 s
…of which CRIU page restore 2.9 s 4.0 s
…of which podman overhead (archive extract + container create) ~12 s ~0.1 s

Measured on a Nethermind instance with a ~7.5 GB memory image (1.89 M pages) over the jochemnet stateful suite (gas-benchmarks repricing tests), identical 11-test filter, same host, tmpfs_threshold active on the export/import side. For a 550-test suite this saves ~100 minutes of pure restore overhead per run.

Podman semantics (verified empirically)

The design relies on podman restoring a stopped container repeatedly from kept checkpoint data. Verified on podman 5.x + CRIU (ubuntu-24.04 and a self-hosted runner): podman container checkpoint <ctr> → (podman container restore --keep <ctr>podman kill / podman stop -t 0)×N loops indefinitely; the process resumes from the identical memory state every time (counter-in-a-loop probe), the Checkpointed=false state flag after the first restore does not block subsequent restores, and the container IP is stable across restores.

Notes:

  • --name is not passed on the in-place restore (import-only option).
  • tmpfs_threshold / tmpfs_max_size only concern the export archive and are documented as not applying in this mode.
  • The datadir rollback (ZFS or copy-based) is unchanged: the mount path is identical across restores, so the rolled-back content is picked up transparently; restart_container composes with this option as before.

Changes

  • pkg/podman: CheckpointContainerLocal + RestoreContainerInPlace (with CRIU stats logging, mirroring the existing methods)
  • pkg/runner/strategy_checkpoint.go: branch the checkpoint phase, per-test restore, and per-test teardown (stop instead of remove)
  • pkg/config: option field + GetCheckpointRestoreInPlace getter + unit tests
  • docs/configuration.md, config.example.yaml: documentation
  • ui: restore_in_place in CheckpointRestoreStrategyOptions type and the RunConfiguration panel

Restore the checkpointed container in place between tests instead of
importing the checkpoint archive into a fresh container. The checkpoint
is taken locally (Keep, no Export) so the checkpoint data stays in the
container's storage directory, and the same container — same name,
mounts, and IP — is restored repeatedly, with a zero-timeout stop
between tests.

This skips the per-test archive extraction and container creation of
the export/import path. On a ~7.9G Nethermind checkpoint the per-test
podman restore drops from ~15s (of which CRIU page restore was only
~2.9s — the rest was extracting the archive into container storage and
creating the container) to roughly the CRIU restore time.

Repeated in-place restores are safe: podman accepts a restore of a
stopped container whenever its kept checkpoint data is present, restores
resume from the identical memory state each time, and the container IP
is stable (verified on podman 5.x + CRIU 4.x: checkpoint, restore
--keep, kill, restore --keep, ... loops indefinitely).

tmpfs_threshold / tmpfs_max_size only apply to the export archive and
are ignored in this mode. Default remains the export/import path
(restore_in_place: false).
kamilchodola added a commit to NethermindEth/gas-benchmarks that referenced this pull request Jul 14, 2026
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.
kamilchodola added a commit to NethermindEth/gas-benchmarks that referenced this pull request Jul 15, 2026
… per-test summary, dotTrace profiling) (#159)

* feat: add Benchmarkoor - Nethermind workflow (podman + CRIU memory checkpoints)

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.

* fix: auto-size and grow the file-backed zpool, resume interrupted seeds

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.

* fix: Nethermind BaseDbPath must include the network subdir (/data/mainnet)

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.

* fix: take corrected jochemnet chainspec from fix/hook-block-testing-only-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).

* chore: stop mirroring client logs into the CI job log

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.

* feat: allow building benchmarkoor from a custom git repo

Needed to validate benchmarkoor branches (e.g. the restore_in_place
fork) on the runner before they land upstream.

* fix: queue new dispatches instead of cancelling in-progress benchmarks

cancel-in-progress: true killed a 3h validation run when the next
dispatch arrived on the same ref. Benchmarks should queue.

* fix: force source build when benchmarkoor_git_ref is set

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=''.

* feat: make restore_in_place the built-in default for stateful runs

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.

* feat: per-test MGas/s job summary, dotTrace + trace_blocks profiling

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

* fix: match chunked dotTrace snapshot index in XML conversion

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.

* fix: match Reporter version to the diag image's dotTrace CLI

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.

* fix: bypass masterdiag's timeline profiling for dottrace XML conversion

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

* fix: flat-array dottrace entrypoint — benchmarkoor expands $vars in config

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

* docs: diagnostics-run numbers are not comparable to checkpoint-restore 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.
@skylenet
skylenet self-requested a review July 16, 2026 14:07
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.

1 participant