diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000000..d308228d379 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,209 @@ +version: 2.1 + +# Runs this fork's validation workflows through pinned act because GitHub Actions is disabled. +# Pull requests and branch pushes run the PR checks; merge queues add stage.yml. Event metadata is +# generated once per CircleCI revision and shared by all shards. +# +# The read-only GitHub token is exposed to PR code, so its CircleCI context must remain +# restricted to trusted contributors. Keep CircleCI auto-cancellation enabled. +# +# The act binary, checksum, and runner image are pinned. Compiler caches are no-ops because +# act's local cache is not reusable across these machines; Pages plumbing is skipped after the +# validation build. Lint and unit jobs are distributed; other workflows run up to four jobs. +# +# Excluded: pr-title.yml, label-pr.yml, and the privileged cyclops-audit.yml integration. + +jobs: + prepare-act-event: + machine: + image: ubuntu-2404:2025.09.1 + resource_class: medium + steps: + - checkout + - run: + name: Snapshot GitHub event + command: | + mkdir -p /tmp/act-event + if ! .circleci/prepare-act-event.sh; then + echo "GitHub event preparation failed; act shards will report the failure" | + tee /tmp/act-event/failed + fi + - persist_to_workspace: + root: /tmp/act-event + paths: + - . + + act: + parameters: + wf: + type: string + job: + type: string + default: "" + concurrent_jobs: + type: integer + default: 4 + machine: + image: ubuntu-2404:2025.09.1 + resource_class: xlarge + steps: + - checkout + - attach_workspace: + at: /tmp/act-event + - run: + name: Check event snapshot + command: | + if [ -f /tmp/act-event/failed ]; then + cat /tmp/act-event/failed >&2 + exit 1 + fi + - run: + name: Install pinned act + command: | + set -euo pipefail + readonly ACT_VERSION="0.2.89-action-cache-fix.1" + readonly ACT_RELEASE="v0.2.89-action-cache-fix.4" + readonly ACT_BINARY="act_Linux_x86_64" + readonly ACT_SHA256="28bfcf1502f59a834b14f2fb23c39e5b253c2234d875592b5aa0a175f9674dce" + curl --fail --silent --show-error --location \ + "https://github.com/ethereum-optimism/act/releases/download/${ACT_RELEASE}/${ACT_BINARY}" \ + --output "/tmp/${ACT_BINARY}" + echo "${ACT_SHA256} /tmp/${ACT_BINARY}" | sha256sum --check + sudo install -m 0755 "/tmp/${ACT_BINARY}" /usr/local/bin/act + test "$(act --version)" = "act version ${ACT_VERSION}" + - run: + name: Stub unsupported non-check actions + command: | + mkdir -p /tmp/noop-action + printf '%s\n' \ + 'name: noop' \ + 'description: disabled service plumbing under act' \ + 'inputs:' \ + ' cache-on-failure: {required: false}' \ + ' cache-targets: {required: false}' \ + 'runs:' \ + ' using: composite' \ + ' steps:' \ + ' - run: echo "Service plumbing disabled under act"' \ + ' shell: bash' \ + > /tmp/noop-action/action.yml + - run: + name: Run << parameters.wf >> via act + no_output_timeout: 45m + command: | + set -euo pipefail + event_path=/tmp/act-event/github-event.json + event_name="$(jq -er 'if .pull_request then "pull_request" elif .merge_group then "merge_group" elif .ref and .after then "push" else error("unsupported event") end' "$event_path")" + base_ref="$(jq -r '(.pull_request.base.ref // .merge_group.base_ref // "") | sub("^refs/heads/"; "")' "$event_path")" + target_job="<< parameters.job >>" + # Validate documentation without entering Pages deployment on branch pushes. + if [ "<< parameters.wf >>" = "book.yml" ]; then + target_job=build + fi + act_job=() + if [ -n "$target_job" ]; then + case "$target_job" in + crate-checks-*) + partition="${target_job#crate-checks-}" + if [[ ! "$partition" =~ ^[1-9][0-9]*$ ]]; then + echo "invalid crate-checks partition: $partition" >&2 + exit 1 + fi + act_job=(-j crate-checks --matrix "partition:${partition}") + ;; + *) + act_job=(-j "$target_job") + ;; + esac + fi + + act_env=(--env "RUSTC_WRAPPER=" --env "GITHUB_BASE_REF=${base_ref}") + if [ "<< parameters.wf >>" = "unit.yml" ] && [ "$target_job" = "state" ]; then + # EF tests use Rayon internally, so unrestricted nextest processes multiply + # runnable threads and starve valid_blocks past its upstream timeout. + act_env+=(--env "NEXTEST_TEST_THREADS=2") + fi + if [ "$event_name" != "pull_request" ]; then + act_env+=( + --env "GITHUB_REF=refs/heads/${CIRCLE_BRANCH}" + --env "GITHUB_SHA=${CIRCLE_SHA1}" + ) + fi + + act "$event_name" -W ".github/workflows/<< parameters.wf >>" \ + "${act_job[@]}" \ + --eventpath "$event_path" \ + -P "ubuntu-latest=catthehacker/ubuntu@sha256:b839c14c4410998529ec18f951262bdf87a2b23bc1467304d07b491b9455e074" \ + --concurrent-jobs << parameters.concurrent_jobs >> \ + "${act_env[@]}" \ + -s GITHUB_TOKEN="${MISE_GITHUB_TOKEN:?missing restricted read-only GitHub token}" \ + --local-repository "mozilla-actions/sccache-action@9e7fa8a12102821edf02ca5dbea1acd0f89a2696=/tmp/noop-action" \ + --local-repository "Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4=/tmp/noop-action" \ + --local-repository "actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d=/tmp/noop-action" \ + --local-repository "actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9=/tmp/noop-action" + +workflows: + reth-act: + jobs: + - prepare-act-event: + context: + - circleci-repo-readonly-authenticated-github-token + - act: + context: + - circleci-repo-readonly-authenticated-github-token + name: "act-<< matrix.wf >>" + requires: + - prepare-act-event + matrix: + parameters: + wf: + - book.yml + - compact.yml + - e2e.yml + - grafana.yml + - integration.yml + - lint-actions.yml + - stage.yml + - act: + wf: lint.yml + concurrent_jobs: 1 + context: + - circleci-repo-readonly-authenticated-github-token + name: "act-lint-<< matrix.job >>" + requires: + - prepare-act-event + matrix: + parameters: + job: + - clippy-binaries + - clippy + - wasm + - riscv + - crate-checks-1 + - crate-checks-2 + - crate-checks-3 + - msrv + - docs + - fmt + - udeps + - book + - typos + - check-toml + - grafana + - no-test-deps + - feature-propagation + - deny + - act: + wf: unit.yml + concurrent_jobs: 1 + context: + - circleci-repo-readonly-authenticated-github-token + name: "act-unit-<< matrix.job >>" + requires: + - prepare-act-event + matrix: + parameters: + job: + - test + - state + - doc diff --git a/.circleci/prepare-act-event.sh b/.circleci/prepare-act-event.sh new file mode 100755 index 00000000000..99cccb27f3c --- /dev/null +++ b/.circleci/prepare-act-event.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +set -euo pipefail + +readonly OUTPUT_DIR="${ACT_EVENT_OUTPUT_DIR:-/tmp/act-event}" +readonly EVENT_PATH="${OUTPUT_DIR}/github-event.json" + +mkdir -p "$OUTPUT_DIR" +rm -f "$EVENT_PATH" + +: "${CIRCLE_SHA1:?missing CircleCI revision}" +: "${CIRCLE_PROJECT_USERNAME:?missing CircleCI project owner}" +: "${CIRCLE_PROJECT_REPONAME:?missing CircleCI project repository}" + +repository="${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}" + +if [[ "${CIRCLE_BRANCH:-}" == gh-readonly-queue/* ]]; then + event_name="merge_group" +elif [[ -n "${CIRCLE_PULL_REQUEST:-}" ]]; then + event_name="pull_request" +else + event_name="push" +fi + +case "$event_name" in + pull_request) + : "${CIRCLE_PULL_REQUEST:?pull-request trigger must provide CIRCLE_PULL_REQUEST}" + pr_number="${CIRCLE_PULL_REQUEST##*/}" + if [[ ! "$pr_number" =~ ^[1-9][0-9]*$ ]]; then + echo "invalid pull-request URL: ${CIRCLE_PULL_REQUEST}" >&2 + exit 1 + fi + + pull_request_path="${OUTPUT_DIR}/pull-request.json" + curl --fail --silent --show-error --location \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${MISE_GITHUB_TOKEN:?missing restricted read-only GitHub token}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/repos/${repository}/pulls/${pr_number}" > "$pull_request_path" + + event_head_sha="$(jq -er '.head.sha' "$pull_request_path")" + if [[ "$event_head_sha" != "$CIRCLE_SHA1" ]]; then + echo "PR head does not match CircleCI revision: ${CIRCLE_SHA1} != ${event_head_sha}" >&2 + exit 1 + fi + + jq '{ + action: "synchronize", + number: .number, + pull_request: ., + repository: .base.repo, + sender: .user + }' "$pull_request_path" > "$EVENT_PATH" + base_ref="$(jq -er '.base.ref | select(length > 0)' "$pull_request_path")" + head_ref="$(jq -er '.head.ref | select(length > 0)' "$pull_request_path")" + echo "act event: pull_request #${pr_number}, ${head_ref} -> ${base_ref}" + ;; + push) + : "${CIRCLE_BRANCH:?push trigger must provide CIRCLE_BRANCH}" + jq -n \ + --arg ref "refs/heads/${CIRCLE_BRANCH}" \ + --arg after "$CIRCLE_SHA1" \ + '{ref: $ref, after: $after}' > "$EVENT_PATH" + echo "act event: push ${CIRCLE_BRANCH} at ${CIRCLE_SHA1}" + ;; + merge_group) + : "${CIRCLE_BRANCH:?merge-queue trigger must provide CIRCLE_BRANCH}" + if [[ ! "$CIRCLE_BRANCH" =~ ^gh-readonly-queue/(.+)/pr-([1-9][0-9]*)-[^/]+$ ]]; then + echo "merge_group requires a gh-readonly-queue//pr-- branch, got: ${CIRCLE_BRANCH}" >&2 + exit 1 + fi + base_ref="${BASH_REMATCH[1]}" + + if ! base_sha="$(git rev-parse "${CIRCLE_SHA1}^1" 2>/dev/null)"; then + git fetch --no-tags --depth=2 origin "$CIRCLE_SHA1" + base_sha="$(git rev-parse "${CIRCLE_SHA1}^1")" + fi + + jq -n \ + --arg head_sha "$CIRCLE_SHA1" \ + --arg head_ref "refs/heads/${CIRCLE_BRANCH}" \ + --arg base_sha "$base_sha" \ + --arg base_ref "refs/heads/${base_ref}" \ + --arg repository "$repository" \ + --arg repository_name "$CIRCLE_PROJECT_REPONAME" \ + --arg repository_owner "$CIRCLE_PROJECT_USERNAME" \ + '{ + action: "checks_requested", + merge_group: { + head_sha: $head_sha, + head_ref: $head_ref, + base_sha: $base_sha, + base_ref: $base_ref + }, + repository: { + full_name: $repository, + name: $repository_name, + owner: {login: $repository_owner} + } + }' > "$EVENT_PATH" + echo "act event: merge_group ${CIRCLE_BRANCH} -> ${base_ref} at ${base_sha}" + ;; +esac diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 65d6fc7e40d..6887f88baf4 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -38,6 +38,7 @@ crates/storage/libmdbx-rs/ @shekhirin crates/storage/nippy-jar/ @joshieDo @shekhirin crates/storage/provider/ @joshieDo @shekhirin crates/storage/storage-api/ @joshieDo +crates/storage/storage-overlay/ @joshieDo @shekhirin @mediocregopher crates/tasks/ @mattsse @DaniPopes crates/tokio-util/ @mattsse crates/tracing/ @mattsse @shekhirin diff --git a/.github/config/bench-metrics-targets.json b/.github/config/bench-metrics-targets.json index 4df96b969b5..aa9c839221b 100644 --- a/.github/config/bench-metrics-targets.json +++ b/.github/config/bench-metrics-targets.json @@ -14,11 +14,60 @@ } ], "histograms": [ + { + "query": "reth_consensus_engine_beacon_new_payload_thread_user_cpu_seconds", + "target": "decrease", + "floor_pct": 5.0, + "min_value_pct_total_latency": 0.0 + }, + { + "query": "reth_consensus_engine_beacon_new_payload_thread_system_cpu_seconds", + "target": "decrease", + "floor_pct": 5.0, + "min_value_pct_total_latency": 0.0 + }, + { + "query": "reth_consensus_engine_beacon_new_payload_thread_minor_page_faults", + "target": "decrease", + "floor_pct": 5.0, + "min_value_pct_total_latency": 0.0 + }, + { + "query": "reth_consensus_engine_beacon_new_payload_thread_major_page_faults", + "target": "decrease", + "floor_pct": 5.0, + "min_value_pct_total_latency": 0.0 + }, + { + "query": "reth_consensus_engine_beacon_new_payload_thread_voluntary_context_switches", + "target": "decrease", + "floor_pct": 5.0, + "min_value_pct_total_latency": 0.0 + }, + { + "query": "reth_consensus_engine_beacon_new_payload_thread_involuntary_context_switches", + "target": "decrease", + "floor_pct": 5.0, + "min_value_pct_total_latency": 0.0 + }, + { + "query": "reth_consensus_engine_beacon_new_payload_thread_block_input_operations", + "target": "decrease", + "floor_pct": 5.0, + "min_value_pct_total_latency": 0.0 + }, + { + "query": "reth_consensus_engine_beacon_new_payload_thread_block_output_operations", + "target": "decrease", + "floor_pct": 5.0, + "min_value_pct_total_latency": 0.0 + }, { "query": "reth_sync_block_validation_state_root_histogram", "target": "decrease", "floor_pct": 5.0, - "min_value_pct_total_latency": 5.0 + "min_value_pct_total_latency": 5.0, + "unit": "seconds" }, { "query": "reth_trie_proof_task_account_worker_idle_time_seconds", @@ -38,6 +87,13 @@ "floor_pct": 7.5, "min_value_pct_total_latency": 15.0 }, + { + "query": "reth_tree_root_sparse_trie_prune_duration_histogram", + "target": "decrease", + "floor_pct": 5.0, + "min_value_pct_total_latency": 0.0, + "unit": "seconds" + }, { "query": "reth_tree_root_hashing_task_idle_time_seconds", "target": "increase", @@ -60,13 +116,15 @@ "query": "reth_storage_providers_database_save_blocks_commit_mdbx", "target": "decrease", "floor_pct": 5.0, - "min_value_pct_total_latency": 13.6 + "min_value_pct_total_latency": 13.6, + "unit": "seconds" }, { "query": "reth_storage_providers_database_save_blocks_commit_rocksdb", "target": "decrease", "floor_pct": 2.5, - "min_value_pct_total_latency": 0.8 + "min_value_pct_total_latency": 0.8, + "unit": "seconds" }, { "query": "reth_storage_providers_database_save_blocks_mdbx", @@ -79,19 +137,22 @@ "query": "reth_storage_providers_database_save_blocks_rocksdb", "target": "decrease", "floor_pct": 2.5, - "min_value_pct_total_latency": 14.2 + "min_value_pct_total_latency": 14.2, + "unit": "seconds" }, { "query": "reth_storage_providers_database_save_blocks_sf", "target": "decrease", "floor_pct": 2.5, - "min_value_pct_total_latency": 5.4 + "min_value_pct_total_latency": 5.4, + "unit": "seconds" }, { "query": "reth_sync_block_validation_deferred_trie_compute_duration", "target": "decrease", "floor_pct": 2.5, - "min_value_pct_total_latency": 1.4 + "min_value_pct_total_latency": 1.4, + "unit": "seconds" }, { "query": "reth_executor_named_worker_queue_wait_duration_seconds", diff --git a/.github/scripts/bench-job-summary.js b/.github/scripts/bench-job-summary.js index 6187aad008f..e79085eb2fd 100644 --- a/.github/scripts/bench-job-summary.js +++ b/.github/scripts/bench-job-summary.js @@ -157,5 +157,5 @@ module.exports = async function ({ core, context, chartSha, grafanaUrl, logsUrl, if (errors.trim()) md += '\n' + errors + '\n'; } catch {} - await core.summary.addRaw(md).write(); + await core.summary.addRaw(md).write({ overwrite: true }); }; diff --git a/.github/scripts/bench-reth-summary.py b/.github/scripts/bench-reth-summary.py index 9b942a183af..7d1ad07a2f4 100755 --- a/.github/scripts/bench-reth-summary.py +++ b/.github/scripts/bench-reth-summary.py @@ -1659,7 +1659,9 @@ def generate_comparison_table( big_blocks: bool = False, warmup_blocks: str | None = None, wait_time: str | None = None, + block_time: str | None = None, bal_mode: str | None = None, + mode: str = "engine", run_pairs: int | None = None, ) -> str: """Generate a markdown comparison table between baseline and feature.""" @@ -1715,13 +1717,15 @@ def pct(base: float, feat: float) -> float: f"| Persist Wait | {fmt_ms(run1['mean_persist_ms'])} | {fmt_ms(run2['mean_persist_ms'])} | {change_str(persist_pct, persist_ci_pct, persist_floor, lower_is_better=True, informational=persist_informational)} |", "", ] - meta_parts = [f"{n} {'big blocks' if big_blocks else 'blocks'}"] + meta_parts = [f"{n} {'big blocks' if big_blocks else 'blocks'}", f"mode: {mode}"] if warmup_blocks: meta_parts.append(f"{warmup_blocks} warmup") if run_pairs: meta_parts.append(f"{run_pairs} run pairs") if wait_time: meta_parts.append(f"wait time: {wait_time}") + if block_time: + meta_parts.append(f"block time: {block_time}") display_mode = display_bal_mode(bal_mode) if big_blocks and display_mode: meta_parts.append(f"BAL: {display_mode}") @@ -1969,7 +1973,9 @@ def main(): parser.add_argument("--big-blocks", action="store_true", default=False, help="Big blocks mode") parser.add_argument("--warmup-blocks", default=None, help="Number of warmup blocks") parser.add_argument("--wait-time", default=None, help="Wait time interval used between blocks") + parser.add_argument("--block-time", default=None, help="RPC mode local block interval") parser.add_argument("--bal-mode", default=None, help="BAL mode (true, feature, baseline)") + parser.add_argument("--mode", choices=("engine", "rpc"), default="engine", help="Benchmark execution mode") parser.add_argument("--benchmark-id", default=os.environ.get("BENCH_ID"), help="Benchmark ID used for OTLP labels") parser.add_argument("--grafana-url", default=None, help="Grafana dashboard URL for this benchmark run") parser.add_argument("--logs-url", default=None, help="Grafana Explore URL for benchmark logs") @@ -2033,7 +2039,9 @@ def main(): big_blocks=args.big_blocks, warmup_blocks=args.warmup_blocks, wait_time=args.wait_time, + block_time=args.block_time, bal_mode=bal_mode, + mode=args.mode, run_pairs=args.run_pairs, ) print( @@ -2084,7 +2092,9 @@ def main(): "warmup_blocks": args.warmup_blocks, "run_pairs": args.run_pairs, "wait_time": args.wait_time, + "block_time": args.block_time, "bal_mode": bal_mode, + "mode": args.mode, "baseline": { "name": baseline_name, "ref": baseline_ref, diff --git a/.github/scripts/bench-txgen-extract.sh b/.github/scripts/bench-txgen-extract.sh index 39131424021..a513dcfd1d4 100755 --- a/.github/scripts/bench-txgen-extract.sh +++ b/.github/scripts/bench-txgen-extract.sh @@ -1,11 +1,11 @@ #!/usr/bin/env bash # -# Pre-extracts txgen payloads (normal or big blocks) so they can be reused +# Pre-extracts txgen payloads or transactions so they can be reused # across multiple benchmark runs instead of re-fetching from the remote RPC # every time. # # Starts a throwaway reth node to discover the snapshot's chain tip, extracts -# payloads from BENCH_RPC_URL, then stops the node and recovers the snapshot. +# data from BENCH_RPC_URL, then stops the node and recovers the snapshot. # # Usage: bench-txgen-extract.sh # @@ -13,15 +13,17 @@ # /all-blocks.ndjson (or all-big-blocks.ndjson) # /warmup-blocks.ndjson # /benchmark-blocks.ndjson +# In RPC mode, the corresponding names end in `-transactions.ndjson`. # # Required env: SCHELK_MOUNT, BENCH_RPC_URL, BENCH_BLOCKS, BENCH_WARMUP_BLOCKS -# Optional env: BENCH_BIG_BLOCKS, BENCH_BIG_BLOCKS_TARGET_GAS, BENCH_BAL +# Optional env: BENCH_EXECUTION_MODE, BENCH_BIG_BLOCKS, BENCH_BIG_BLOCKS_TARGET_GAS, BENCH_BAL set -euxo pipefail BINARY="$1" OUTPUT_DIR="$2" BIG_BLOCKS="${BENCH_BIG_BLOCKS:-false}" +EXECUTION_MODE="${BENCH_EXECUTION_MODE:-engine}" BAL_MODE="${BENCH_BAL:-false}" INCLUDE_BAL=false if [ "$BAL_MODE" != "false" ] && [ -n "$BAL_MODE" ]; then @@ -123,19 +125,44 @@ fi ALL_BLOCKS="$OUTPUT_DIR/all-blocks.ndjson" WARMUP_FILE="$OUTPUT_DIR/warmup-blocks.ndjson" BENCHMARK_FILE="$OUTPUT_DIR/benchmark-blocks.ndjson" - -if [ "$BIG_BLOCKS" = "true" ]; then - ALL_BLOCKS="$OUTPUT_DIR/all-big-blocks.ndjson" - WARMUP_FILE="$OUTPUT_DIR/warmup-big-blocks.ndjson" - BENCHMARK_FILE="$OUTPUT_DIR/measured-big-blocks.ndjson" -fi - EXTRACT_FROM=$(( HEAD_DEC + 1 )) TXGEN_EXTRACT_ARGS=() if [ "$INCLUDE_BAL" = "true" ]; then TXGEN_EXTRACT_ARGS+=(--bal) fi -if [ "$BIG_BLOCKS" = "true" ]; then + +if [ "$EXECUTION_MODE" = "rpc" ]; then + ALL_BLOCKS="$OUTPUT_DIR/all-transactions.ndjson" + WARMUP_FILE="$OUTPUT_DIR/warmup-transactions.ndjson" + BENCHMARK_FILE="$OUTPUT_DIR/benchmark-transactions.ndjson" + if [ "$BIG_BLOCKS" = "true" ] || [ "$INCLUDE_BAL" = "true" ]; then + echo "::error::RPC mode does not support big blocks or BAL" + exit 1 + fi + + EXTRACT_TO=$(( HEAD_DEC + TOTAL )) + echo "Extracting transactions from blocks ${EXTRACT_FROM}..${EXTRACT_TO} for RPC benchmark (${WARMUP} warmup blocks, ${BLOCKS} measured blocks)" + if [ "$WARMUP" -gt 0 ] 2>/dev/null; then + "$TXGEN_ETHEREUM" extract \ + --rpc "$BENCH_RPC_URL" \ + --from "$EXTRACT_FROM" \ + --to "$(( HEAD_DEC + WARMUP ))" \ + --format transactions \ + -o "$WARMUP_FILE" + else + : > "$WARMUP_FILE" + fi + "$TXGEN_ETHEREUM" extract \ + --rpc "$BENCH_RPC_URL" \ + --from "$(( HEAD_DEC + WARMUP + 1 ))" \ + --to "$EXTRACT_TO" \ + --format transactions \ + -o "$BENCHMARK_FILE" + cat "$WARMUP_FILE" "$BENCHMARK_FILE" > "$ALL_BLOCKS" +elif [ "$BIG_BLOCKS" = "true" ]; then + ALL_BLOCKS="$OUTPUT_DIR/all-big-blocks.ndjson" + WARMUP_FILE="$OUTPUT_DIR/warmup-big-blocks.ndjson" + BENCHMARK_FILE="$OUTPUT_DIR/measured-big-blocks.ndjson" echo "Extracting ${TOTAL} big blocks from ${EXTRACT_FROM} for txgen benchmark (${WARMUP} warmup, ${BLOCKS} measured, bal=${INCLUDE_BAL})" "$TXGEN_ETHEREUM" extract-big-blocks \ --rpc "$BENCH_RPC_URL" \ @@ -155,13 +182,16 @@ else -o "$ALL_BLOCKS" fi -# Split into warmup and measured files -if [ "$WARMUP" -gt 0 ] 2>/dev/null; then - head -n "$WARMUP" "$ALL_BLOCKS" > "$WARMUP_FILE" -else - : > "$WARMUP_FILE" +# Block output has one line per source block. Transaction output is extracted +# separately above because it has one line per transaction. +if [ "$EXECUTION_MODE" = "engine" ]; then + if [ "$WARMUP" -gt 0 ] 2>/dev/null; then + head -n "$WARMUP" "$ALL_BLOCKS" > "$WARMUP_FILE" + else + : > "$WARMUP_FILE" + fi + awk -v warmup="$WARMUP" 'NR > warmup { print }' "$ALL_BLOCKS" > "$BENCHMARK_FILE" fi -awk -v warmup="$WARMUP" 'NR > warmup { print }' "$ALL_BLOCKS" > "$BENCHMARK_FILE" if [ "$INCLUDE_BAL" = "true" ] && [ "$BAL_MODE" != "true" ]; then echo "Writing no-BAL payload variants for selective BAL mode (${BAL_MODE})" @@ -170,4 +200,4 @@ if [ "$INCLUDE_BAL" = "true" ] && [ "$BAL_MODE" != "true" ]; then done fi -echo "Extraction complete: $(wc -l < "$ALL_BLOCKS") payloads in ${OUTPUT_DIR}" +echo "Extraction complete: $(wc -l < "$ALL_BLOCKS") ${EXECUTION_MODE} records in ${OUTPUT_DIR}" diff --git a/.github/scripts/bench-txgen-report-to-reth-csv.py b/.github/scripts/bench-txgen-report-to-reth-csv.py index 6501abe617a..a9847014e45 100755 --- a/.github/scripts/bench-txgen-report-to-reth-csv.py +++ b/.github/scripts/bench-txgen-report-to-reth-csv.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Convert txgen `bench send-blocks` JSON into legacy benchmark CSVs. +"""Convert txgen `bench send-blocks` or `bench send` JSON into legacy benchmark CSVs. The benchmark rendering pipeline still consumes `combined_latency.csv` and `total_gas.csv`. This adapter lets the txgen runner reuse the existing @@ -18,12 +18,17 @@ def opt_int(value, default=None): return int(value) -def block_latency_us(block: dict) -> tuple[int, int, int]: +def block_latency_us(block: dict, fallback_block_time_ms: int = 0) -> tuple[int, int, int]: # txgen currently records server newPayload latency in microseconds but # client-side forkchoiceUpdated latency in milliseconds. new_payload_us = opt_int(block.get("new_payload_server_latency_us")) if new_payload_us is None: - new_payload_us = opt_int(block.get("new_payload_ms"), 0) * 1000 + new_payload_ms = opt_int(block.get("new_payload_ms")) + if new_payload_ms is None: + # `bench send` has no Engine API timings. Use locally mined block + # time so the existing gas/latency summaries remain meaningful. + new_payload_ms = opt_int(block.get("block_time_ms"), fallback_block_time_ms) + new_payload_us = new_payload_ms * 1000 fcu_us = opt_int(block.get("forkchoice_updated_ms"), 0) * 1000 return new_payload_us, fcu_us, new_payload_us + fcu_us @@ -45,6 +50,11 @@ def main() -> None: if not blocks: raise SystemExit(f"txgen report {report_path} does not contain any blocks") + fallback_block_time_ms = next( + (int(block["block_time_ms"]) for block in blocks if block.get("block_time_ms") is not None), + 0, + ) + combined_path = output_dir / "combined_latency.csv" with combined_path.open("w", newline="") as f: writer = csv.DictWriter( @@ -64,7 +74,7 @@ def main() -> None: ) writer.writeheader() for block in blocks: - new_payload_us, fcu_us, total_us = block_latency_us(block) + new_payload_us, fcu_us, total_us = block_latency_us(block, fallback_block_time_ms) writer.writerow( { "block_number": block["number"], @@ -89,7 +99,7 @@ def main() -> None: ) writer.writeheader() for block in blocks: - _, _, total_us = block_latency_us(block) + _, _, total_us = block_latency_us(block, fallback_block_time_ms) elapsed_us += total_us writer.writerow( { diff --git a/.github/scripts/bench-txgen-run.sh b/.github/scripts/bench-txgen-run.sh index 98ea31f04c3..b709cf7c544 100755 --- a/.github/scripts/bench-txgen-run.sh +++ b/.github/scripts/bench-txgen-run.sh @@ -1,14 +1,15 @@ #!/usr/bin/env bash # -# Runs a single txgen-backed Engine API benchmark cycle: -# mount snapshot → start node → extract source blocks → warmup → send-blocks → +# Runs a single txgen-backed benchmark cycle: +# mount snapshot → start node → extract source blocks → warmup → replay → # convert txgen JSON report into legacy benchmark CSVs. # # Usage: bench-txgen-run.sh