feat(cli): make benchmark reports comparable across runs and machines - #596
Conversation
🤖 Kimi Code Review
General
Security/Memory: No unsafe code introduced. File I/O in build.rs is limited to reading the lockfile; paths are constructed from Automated review by Kimi (Moonshot AI) · kimi-k2.5 · custom prompt |
🤖 Codex Code ReviewFindings
No consensus-path, fork-choice, attestation-validation, SSZ, or XMSS-processing logic is touched here, so I did not find consensus/security regressions in the Ethereum client code itself. I could not run the Rust tests in this sandbox because Cargo/rustup attempted to write under read-only Automated review by OpenAI Codex · gpt-5.4 · custom prompt |
🤖 Claude Code ReviewReview: PR 596 — benchmark report comparability (stats, provenance, JSON)This is benchmarking-tool code, not consensus-critical logic, so the risk profile is low, but there's one finding that materially affects CI cost/time and directly contradicts the PR's own claims. 1. CI smoke step does not reuse the previous build, contrary to its comment (
|
605531f to
254cf06
Compare
254cf06 to
9226605
Compare
9226605 to
e1bea8a
Compare
9df5f5f to
3d98b58
Compare
Per-iteration rows show what one build cost; comparing an optimization against a baseline needs three more things, which this adds. Aggregate statistics per phase — count, min, mean, p50, p90, max, and a coefficient of variation flagged above 10% so a noisy run is not read as a result. Percentiles are nearest-rank, without interpolation: sample counts are small, so an exact observed value beats a blend of two. Outliers are never discarded, and the raw per-iteration rows stay above the summary. Build provenance — build.rs resolves the leansig and leanVM revisions from Cargo.lock into the report. leansig is pinned to a moving branch and leanVM does the signature aggregation, so either one moves the measured crypto; two reports that disagree on them are not comparable, and without this the report cannot say so. The per-[[package]] parse collects `name` and `source` before extracting the rev, so it does not depend on TOML field order. Machine-readable output — `--format json` with a schema_version, and `--output <path>` to write it alongside a human-readable run. Logs already go to stderr, so the JSON pipes straight into jq. CI gains a seconds-fast mock smoke step that asserts the contract, so a change to the report shape cannot land unnoticed.
3d98b58 to
0b049b6
Compare
…ass#591) ## 🗒️ Description / Motivation The binary has only ever run the node, so an invocation is a bare list of node flags. The offline block-building benchmark adds a second entry point, which means the node first needs a name of its own. `node` is an ordinary clap sub-command on a top-level parser that owns the binary's name, version and about. `NodeOptions` (renamed from `CliOptions`) becomes a plain `clap::Args` group and keeps every field exactly as it is — no `Option<T>`, no `required = true`, no unwrap helper on the node path, which is what the review of lambdaclass#497 objected to. The flat `ethlambda --genesis ...` form keeps working, because that is what the Dockerfile, lean-quickstart, the hive shim and the devnet skills all pass. clap has no `default_subcommand`, so exactly one thing sits in front of the parser: a command line that names no sub-command gets `node` inserted. ## What Changed | File | Change | |------|--------| | `bin/ethlambda/src/command.rs` | New. Top-level `Cli` parser + `Command` sub-command enum, and `default_subcommand`, which inserts `node` unless the first token is a sub-command, `-h/--help/-V/--version`, or clap's generated `help` | | `bin/ethlambda/src/cli.rs` | `clap::Parser` → `clap::Args`, and `CliOptions` renamed to `NodeOptions`; the `#[command(...)]` attribute moves to the top-level parser. No field changes | | `bin/ethlambda/src/main.rs` | Parses through `command::parse()` and matches on `Command` | `command.rs` also carries a test-only `parse_node_options` helper. Merging `main` brought lambdaclass#579's `cli.rs` tests, which called `CliOptions::parse_from` — a `clap::Parser` method the group lost when it became `clap::Args`. Git merged both sides cleanly, so nothing flagged it; the test build was broken until `a3d7e52`, and both test modules now parse a node command line through the real dispatch. ## Correctness / Behavior Guarantees - **Every existing invocation keeps working**, and clap owns everything a reader should not have to trust us for: `--help` lists the sub-commands itself, usage lines name the sub-command, and an unknown sub-command produces clap's error rather than a stray-positional one. - `NodeOptions` declares no positional arguments, so the first token after the program name is either a flag or a sub-command — a flag *value* never lands there and is never mistaken for one. A leading flag therefore means the flat node form. - **`--version` after node flags still works and still prints the same string.** It used to live on the node options, so it was accepted anywhere; `propagate_version` keeps that, and `display_name = "ethlambda"` keeps the output byte-identical rather than `ethlambda-node`. All three forms are asserted equal. - **One deliberate change:** a bare `ethlambda` now prints clap's top-level help, listing the sub-commands, instead of a missing-argument list. It still exits non-zero, and the test asserts both. ## Tests Added / Run Unit tests in `command.rs` pin: the flat parse; the two forms agreeing field for field; a `--node-id` value that is literally `node`; a trailing `node` token still rejected; a second `node` token rejected; missing required flags in both forms; the bare invocation's error kind and non-zero exit; `--help`/`--version` staying top-level; `--version` printing one identical string across all three forms; and `--help` listing the sub-commands. `make fmt`, `make lint`, `make test` (574 tests, 30 suites) — all clean. ## Related Issues / PRs - Replaces the CLI approach reviewed in the now-closed lambdaclass#497 - Design doc in lambdaclass#594; the benchmark stacks on this in lambdaclass#595 → lambdaclass#596 - Related to lambdaclass#465 ## ✅ Verification Checklist - [x] Ran `make fmt` — clean - [x] Ran `make lint` (clippy with `-D warnings`) — clean - [x] Ran `make test` (`cargo test --workspace --profile release-fast`) — all passing --------- Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com>
## 🗒️ Description / Motivation Documents the block-building benchmark: what it measures, how to run it, how to read a report, and what it cannot do yet. This replaces the design plan this PR originally carried. Per review, a `docs/plans/` file written as "this is how the benchmark was originally designed" goes stale the moment the benchmark changes and then actively misleads, so the page now describes the tool rather than a schedule for building it. The plan served its purpose — it was the shared reference while lambdaclass#595 and lambdaclass#596 were reviewed — and is not something the tree should keep. > **Restacked.** Now based on lambdaclass#596, so the page documents behaviour that actually exists > rather than behaviour that is still in review. Base moves to `main` once the two land. ## What Changed | File | Change | |------|--------| | `docs/benchmarking.md` | New. Running it (flag table with defaults), what the measured span includes and deliberately excludes, how phase times come from the existing histogram, reading the per-iteration and summary tables, comparing two runs, current limitations, the CI smoke step | | `docs/SUMMARY.md` | Listed under Development | | `docs/plans/block-building-benchmark.md` | Removed | | `bin/ethlambda/src/benchmark/mod.rs` | Module doc points at the new page | ## Correctness / Behavior Guarantees Documentation only — the one code change is a doc-comment path. Two things the plan file never stated, both of which a reader needs: - **When two reports may not be compared at all.** The header carries the resolved leanSig and leanVM revisions plus the machine fields; leanSig tracks a moving branch and leanVM performs the aggregation, so either one moving changes the measured crypto. - **What is not supported yet** — real crypto, the seal phase, replay from a datadir — written as current limitations rather than as milestones, so the page does not promise a schedule it cannot keep. ## Tests Added / Run - `make docs` builds the site with the new page in place; no dangling references to the removed plan file anywhere in the tree. - `make fmt`, `make lint`, `make test` (622 tests) — all clean. ## Related Issues / PRs - Stacked on lambdaclass#596, which stacks on lambdaclass#595 - Related to lambdaclass#465 ## ✅ Verification Checklist - [x] Ran `make fmt` — clean - [x] Ran `make lint` (clippy with `-D warnings`) — clean - [x] Ran `make test` (`cargo test --workspace --profile release-fast`) — all passing --------- Co-authored-by: Tomás Grüner <47506558+MegaRedHand@users.noreply.github.com>
…er the main merge Merging main brought in the offline block-building benchmark (#595, #596), which was written against the old 52-byte pubkey. It is not a textual conflict, so the merge landed silently and only broke at compile time: error[E0308]: mismatched types --> bin/ethlambda/src/benchmark/corpus.rs:127:5 | expected an array with a size of 32, found one with a size of 52 `synthetic_pubkey` now sizes its buffer from `PUBLIC_KEY_SIZE`, so the next scheme change moves it rather than breaking the build again. The same merge left three references to a dependency this branch removed: - The benchmark report embedded a resolved `leansig` revision read from Cargo.lock. leanVM internalized XMSS, so no `leansig` package resolves any more and every report would have printed `leansig=unknown`. The header now carries the single leanVM revision that pins the whole signature stack. - `rand` was a dependency of `ethlambda-crypto` and a dev-dependency of `ethlambda-blockchain` and `ethlambda-storage` only for leanSig keygen in tests this branch rewrote. Dropped from all three; `ethlambda-types` keeps its own, which was already unused before this branch. - CLAUDE.md still described 52-byte keys, 2536-byte signatures, and a `leansig` dependency. Also corrects the `SingleMessageAggregate` / `MultiMessageAggregate` doc comments, which claimed the proof bytes are leanVM's `to_bytes()` form with participant pubkeys embedded. Every producer and consumer uses `to_bytes_without_pubkeys()`: pubkeys stay off the wire and the verifier rebuilds the set from the aggregation bits, which is the property that makes the framing match main. Fixture-driven spec tests still fail (122 forkchoice, 73 stf, 7 ssz, 3 signature), all at deserialization: leanSpec's latest released fixtures are still on the 52-byte scheme. Every other test in the workspace passes.
🗒️ Description / Motivation
Per-iteration rows show what one build cost. Comparing an optimization against a baseline
needs three more things, and this adds them: aggregate statistics, build provenance, and
machine-readable output.
Third of three (design doc → harness → this).
What Changed
bin/ethlambda/src/benchmark/report.rsStats/Summaryplusstats(),percentile()and the aggregate table: count, min, mean, p50, p90, max per phase, and a CV flagged above 10%.schema_version+to_json().Environmentgains the two resolved crypto revisionsbin/ethlambda/build.rsCargo.lockintorustc-envvars. The per-[[package]]parse collectsnameandsourcebefore extracting the rev, so it does not depend on TOML field orderbin/ethlambda/src/benchmark/mod.rs--format human|jsonand--output <path>bin/ethlambda/Cargo.toml,Cargo.lockserde_json.github/workflows/ci.ymlCorrectness / Behavior Guarantees
observed value beats a blend of two neighbours.
so a heavy tail stays visible instead of being averaged away. A CV above 10% is flagged
so a noisy run is not read as a result.
branch and leanVM does the signature aggregation, so either revision moving moves the
measured crypto. Two reports that disagree on them are not comparable, and without this
the report cannot say so.
unnoticed. Logs already go to stderr, so the JSON pipes straight into
jq.build.rsonly adds env vars consumed by the report.Tests Added / Run
report.rs: percentile on a single sample and on odd/even lengths;stats()against aknown set whose population stddev gives CV = 0.4;
stats()on empty input is zeroed.jq -e '.schema_version == 1 and (.samples | length == 3)'passes, and reports carryboth resolved revisions.
make fmt,make lint,make test(580 tests, 30 suites) — all clean.Related Issues / PRs
✅ Verification Checklist
make fmt— cleanmake lint(clippy with-D warnings) — cleanmake test(cargo test --workspace --profile release-fast) — all passing