Skip to content

feat(builder): record build-loop state-read latency - #5025

Open
wbj-cb wants to merge 1 commit into
mainfrom
willjohnston/base-165-build-loop-io-metrics
Open

feat(builder): record build-loop state-read latency#5025
wbj-cb wants to merge 1 commit into
mainfrom
willjohnston/base-165-build-loop-io-metrics

Conversation

@wbj-cb

@wbj-cb wbj-cb commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Time spent blocked on state reads during block building was not measurable. Reth's existing state-provider instrumentation is constructed only on the engine's block-validation path, so enabling it on a sequencer reports validation cost and nothing about the loop that builds blocks.

What changed

  • Both payload builders (flashblocks and native) wrap their state provider in reth's InstrumentedStateProvider, reporting under sync.state_provider with a builder source label to distinguish it from the engine's engine label.
  • Gated on reth's existing --engine.state-provider-metrics rather than a new flag, so the builder and the validation path are instrumented by one switch. Off by default.
  • The wrapper is applied outside CachedStateProvider, so the recorded latency is the build loop's total per-read cost including cache hits. Inside, a working cache would show no improvement.
  • Adds state_provider_metrics to BuilderConfig and BaseBuilderConfig, resolved from the node config when the payload service is built.

Notes for reviewers

Scope is narrower than "all build-loop IO". Reth's per-job read cache (CachedReads) sits above this wrapper, so what is timed is reads that miss that cache rather than every state read the build loop issues. Those are the reads that can actually reach storage, but the measured population shrinks as CachedReads warms, so means are not directly comparable across configurations without accounting for it.

No production effect as merged. --engine.state-provider-metrics is currently off across the fleet, so this emits nothing until it is enabled. The gate is evaluated once per payload build, not per read.

@cb-heimdall

Copy link
Copy Markdown
Collaborator

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

@linear

linear Bot commented Sep 11, 2026

Copy link
Copy Markdown

BASE-165

@depot-code-access

depot-code-access Bot commented Sep 11, 2026

Copy link
Copy Markdown

✅ All benchmarks green — 14 within ±2% (deterministic instruction counts). View run

Benchmark details (14)
Benchmark Base (target) Head (this PR) Δ instructions
batch_queue/drain/drain_cached_span_batches 242,027 242,027 +0.0%
batch_transaction/encode_in_place/encode_in_place 4,199,759 4,199,759 +0.0%
batch_transaction/temporary_frame_buffers/temporary_frame_buffers 8,408,350 8,408,350 +0.0%
flashblock_decode/decode/brotli 3,296,484 3,296,484 +0.0%
flashblock_decode/decode/plain_json 2,280,194 2,280,194 +0.0%
flz/compress_len/real_contract_call 43,148 43,148 +0.0%
flz/compress_len/synthetic_0 38,205 38,205 +0.0%
flz/compress_len/synthetic_1 54,682 54,682 +0.0%
flz/compress_len/synthetic_2 147,976 147,976 +0.0%
flz/data_gas 43,059 43,059 +0.0%
flz/tx_estimated_size 43,056 43,056 +0.0%
frame_parse/decode/single_4kib 1,031 1,031 +0.0%
frame_parse/parse_frames/few_large 1,053,062 1,053,062 +0.0%
frame_parse/parse_frames/many_small 154,763 154,763 +0.0%

Comment thread crates/execution/node/src/node.rs Outdated
predicate_eval_hard_cutoff: self.predicate_eval_hard_cutoff,
resource_metering: self.resource_metering,
rejection_cache: self.rejection_cache,
state_provider_metrics: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low: state_provider_metrics not resolved from node config on this path.

The multiplex service builder reads ctx.config().engine.state_provider_metrics to propagate the flag, but this standalone build_payload_builder path hardcodes false. If someone uses BasePayloadBuilder without the multiplex layer, --engine.state-provider-metrics will have no effect on the builder.

Consider resolving from ctx here too:

Suggested change
state_provider_metrics: false,
state_provider_metrics: ctx.config().engine.state_provider_metrics,

Comment thread Cargo.toml Outdated
reth-eth-wire-types = { git = "https://github.com/base/reth", tag = "base-v2.5.2.3" }
reth-payload-builder = { git = "https://github.com/base/reth", tag = "base-v2.5.2.3" }
reth-execution-types = { git = "https://github.com/base/reth", tag = "base-v2.5.2.3" }
reth-engine-tree = { git = "https://github.com/base/reth", tag = "base-v2.5.2.3" }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: reth-engine-tree is a heavyweight dependency for a single type.

reth-engine-tree pulls in reth-db, reth-stages, reth-static-file, reth-prune, rayon, moka, and ~40 other transitive crates. It's being added to base-builder-core and base-execution-payload solely for InstrumentedStateProvider, which is likely a thin timing wrapper.

If InstrumentedStateProvider could be re-exported from a lighter leaf crate (e.g., reth-provider or a dedicated reth-state-instrumentation crate), both builder crates would avoid inheriting the full engine-tree dependency graph. Not a blocker, but worth tracking upstream.

@depot-code-access

depot-code-access Bot commented Sep 11, 2026

Copy link
Copy Markdown

Base Std historical fork tests

Fork Result Passed Failed Skipped base/base base-anvil base-std
Beryl pass 616 0 13 7ec9d742 98e7839c 520d069b
Cobalt pass 725 0 14 7ec9d742 98e7839c 4571b325

View run

Comment thread crates/execution/payload/Cargo.toml Outdated
reth-trie-parallel.workspace = true
reth-execution-types.workspace = true
reth-execution-cache.workspace = true
reth-engine-tree.workspace = true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: reth-engine-tree breaks the waterfall sort order.

Per project conventions, Cargo.toml dependencies are sorted by line length. reth-engine-tree (34 chars) is shorter than its neighbors reth-execution-cache (38 chars) and reth-payload-builder (38 chars). Same applies in crates/builder/core/Cargo.toml and the workspace root.

@github-actions

Copy link
Copy Markdown
Contributor

Review Summary — feat(builder): record build-loop state-read latency

Clean, well-scoped observability addition. The wrapping order, config gating, and threading through the multiplex path are all correct. No block-production risk: the instrumentation is gated on --engine.state-provider-metrics (off by default), and InstrumentedStateProvider is already battle-tested on reth's engine validation path.

Findings

# Severity File Issue
1 Low crates/execution/node/src/node.rs:1182 state_provider_metrics hardcoded to false in the standalone build_payload_builder path. The multiplex path reads from ctx.config().engine.state_provider_metrics, but this path doesn't — so the flag has no effect when BasePayloadBuilder is used without the multiplex layer.
2 Nit Cargo.toml, crates/builder/core/Cargo.toml, crates/execution/payload/Cargo.toml reth-engine-tree is a heavyweight dependency (~40 transitive crates including reth-db, reth-stages, rayon) pulled in for a single type. Worth tracking upstream to re-export InstrumentedStateProvider from a lighter crate.
3 Nit crates/execution/payload/Cargo.toml:23, crates/builder/core/Cargo.toml:31, Cargo.toml:395 reth-engine-tree breaks the waterfall (sort-by-line-length) ordering convention in all three manifests.

No critical or block-production findings.

@wbj-cb
wbj-cb force-pushed the willjohnston/base-165-build-loop-io-metrics branch from 3175a9b to bb4b743 Compare September 11, 2026 19:46
@github-actions

Copy link
Copy Markdown
Contributor

Review Summary — feat(builder): record build-loop state-read latency

Overall assessment: This PR is well-scoped and safe. The changes add optional InstrumentedStateProvider wrapping to both payload builders (flashblocks and native), gated behind the existing --engine.state-provider-metrics flag (off by default). No new error paths, panics, or fallible operations are introduced in block-production code.

Block production safety

This PR is block-production-sensitive (touches both payload builders). After tracing the changes against the block production review guide:

  • No new failure modes: The InstrumentedStateProvider wrapper is purely additive timing instrumentation. It delegates all state provider trait methods to the inner provider, adding only histogram recording. No ?, unwrap, or panic paths are introduced.
  • Gated and off by default: The wrapping is conditional on state_provider_metrics, which defaults to false. The flag is evaluated once per payload build, not per read.
  • No I/O boundary changes: No payload/data size limits or transport paths are affected.

Existing inline findings (from prior review run)

Three inline comments were previously posted and remain valid:

  1. crates/execution/node/src/node.rsstate_provider_metrics hardcoded to false on the standalone BasePayloadBuilder path, meaning --engine.state-provider-metrics has no effect when the multiplex layer is not used.
  2. Cargo.tomlreth-engine-tree is a heavyweight dependency pulled in for a single type. Worth tracking upstream for a lighter re-export.
  3. crates/execution/payload/Cargo.tomlreth-engine-tree breaks the waterfall sort convention.

No new findings

The remaining changes (config plumbing, builder method signatures, Default impls, Debug impls, test updates) are mechanically correct and consistent across both builders.

@wbj-cb
wbj-cb force-pushed the willjohnston/base-165-build-loop-io-metrics branch from bb4b743 to 89a00c6 Compare September 11, 2026 19:50
@github-actions

Copy link
Copy Markdown
Contributor

Review Summary

This PR adds InstrumentedStateProvider wrapping to both payload builders (flashblocks and native) to measure per-read state fetch latency during block building. The feature is gated behind reth's existing --engine.state-provider-metrics flag and is off by default.

Block-production sensitivity

This PR touches both payload builder paths (flashblocks and native), which are block-production-sensitive. However, the instrumentation is:

  • Off by default (state_provider_metrics: false)
  • A transparent timing wrapper that delegates all calls to the underlying provider without altering error semantics or control flow
  • Evaluated once per payload build (the config check), not per read

No block-production halt/stall risk when disabled. When enabled, the wrapper adds timing overhead but does not introduce new error paths, panics, or failure modes beyond what the underlying state provider already produces.

Findings

No new findings beyond what has already been raised in existing inline comments. The prior comments on node.rs (hardcoded false instead of reading from engine config), reth-engine-tree dependency weight, and Cargo.toml sort order are the substantive items.

Wrapping order note

The InstrumentedStateProvider wraps outside CachedStateProvider (execution cache) but inside the per-job CachedReads. This means it times reads that reach the execution cache layer (including execution-cache hits) but not reads served entirely by the per-job CachedReads. The PR description acknowledges this layering, and the positioning is reasonable for measuring IO-bound cost during block building.

@wbj-cb
wbj-cb force-pushed the willjohnston/base-165-build-loop-io-metrics branch from 89a00c6 to d400ac7 Compare September 11, 2026 20:01
@github-actions

Copy link
Copy Markdown
Contributor

Review Summary

PR: feat(builder): record build-loop state-read latency

This PR adds optional InstrumentedStateProvider wrapping to both the flashblocks and native payload builders, gated by reth's existing --engine.state-provider-metrics flag. The change is well-scoped and low-risk.

Block-production sensitivity

This PR touches payload builder paths, making it block-production-sensitive. However, no Critical findings were identified:

  • No new error paths: The only new code is a conditional Box::new(InstrumentedStateProvider::new(...)) wrapping, which cannot fail or panic.
  • Off by default: state_provider_metrics defaults to false everywhere. The gate is evaluated once per payload build, not per state read. Production is unaffected until operators explicitly enable the flag.
  • No I/O boundary changes: No new data transport, serialization, or size-limit concerns.
  • Proven wrapper: InstrumentedStateProvider is already used by reth's engine validation path.

Correctness

  • Both builder paths (flashblocks in crates/builder/core/src/flashblocks/payload.rs and native in crates/execution/payload/src/builder.rs) apply the wrapper consistently, outside CachedStateProvider — so the measured latency includes the execution cache but not reth's per-job CachedReads. This is intentional per the PR description.
  • The flag is correctly resolved from ctx.config().engine.state_provider_metrics in both the multiplex service builder (service_builder.rs:102) and the standalone node path (node.rs:1182).
  • The args.rs hardcode of false is the initial default, appropriately overridden later by the service builder.

No new findings

The existing inline comments from prior review runs cover the relevant observations (heavyweight dependency, sort order). No additional issues identified.

Times the payload builder's account, storage, and code reads so time blocked
on state during block building can be measured. Off by default.

Reth's InstrumentedStateProvider is constructed in exactly one place, the
engine's newPayload validation path, hardcoded to a source label of "engine".
Enabling --engine.state-provider-metrics on a sequencer therefore measures
block validation and reports nothing about the loop that builds blocks. Its
constructor already accepts a source label, so both Base payload builders now
wrap their own state provider and report under sync.state_provider with a
"builder" label.

The wrapper is applied outside CachedStateProvider so the recorded latency is
the build loop's total per-read cost, cache hits included. Wrapping it inside
would measure only reads that fall through to the database, and a working
cache would then show no improvement.

Scope, deliberately narrow. The reth per-job read cache (CachedReads) sits
above this wrapper, so what is timed is reads that miss that cache rather than
every state read the build loop issues. Those are the reads that can actually
touch storage, but the measured population shrinks as CachedReads warms, so
means are not directly comparable across configurations without accounting for
it.

The consumer today is the snapshot benchmark harness, which scrapes the
Prometheus endpoint directly and can group on the source label. Datadog
currently applies an empty tag allowlist to the reth metric families for
cardinality control, so builder and engine series would aggregate together
there; separating them in dashboards needs a tag-configuration change that is
not part of this commit.

Driven by reth's existing --engine.state-provider-metrics rather than a new
flag, so the builder and the validation path are instrumented by one switch.

Co-authored-by: OpenCode <opencode-noreply@coinbase.com>
@wbj-cb
wbj-cb force-pushed the willjohnston/base-165-build-loop-io-metrics branch from d400ac7 to 3ad4a73 Compare September 11, 2026 20:48
Comment on lines +1540 to +1541
Arc, Mutex,
atomic::{AtomicUsize, Ordering},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low: Unused imports will fail CI.

AtomicUsize and Ordering are imported but never used in the test module. With -D warnings in CI, this will cause a compile error.

Suggested change
Arc, Mutex,
atomic::{AtomicUsize, Ordering},
sync::Arc,

(Keep Mutex on a separate line if it's used elsewhere in the block, or merge as appropriate.)

@github-actions

Copy link
Copy Markdown
Contributor

<!-- CLAUDE_REVIEW_SUMMARY -->

Review Summary

PR: feat(builder): record build-loop state-read latency

This PR adds optional InstrumentedStateProvider wrapping to both the flashblocks and native payload builders, gated by reth's existing --engine.state-provider-metrics flag. The design is clean: the flag is resolved from the node config at service-builder time, the wrapper is applied outside CachedStateProvider so both cache hits and read-throughs are timed, and the test validates the wrapping order.

Findings

Severity File Issue
Low crates/execution/payload/src/builder.rs:1541 Unused imports AtomicUsize and Ordering in the test module will fail CI with -D warnings

Block Production Assessment

This PR touches block-production-sensitive paths (both payload builders' state provider setup). However, the change is low-risk:

  • The InstrumentedStateProvider is a transparent timing wrapper — it delegates all operations without introducing new error types or panics
  • The wrapping is gated by a boolean flag that defaults to false and is off across the fleet
  • The flag is evaluated once per payload build, not per read
  • No new I/O boundaries are introduced

No critical block-production findings.

@depot-code-access

Copy link
Copy Markdown

Caution

This PR may regress performance. 1 benchmark(s) slower by more than 15% beyond the noise band: historical_provider/storage_before_change/v2 (+20.5%).

Benchmark results (advisory)

Median time on the PR head versus the base branch, measured on the same host. Wall-clock, so a change is only flagged when it clears ±15% and the confidence intervals do not overlap. Only benchmarks past the ±15% threshold (plus new or dropped ones) are listed. This check never blocks a merge.

Benchmark Base Head Δ median
historical_provider/storage_before_change/v2 2.46 µs 2.97 µs +20.5% ⚠️ slower

9 benchmark(s) within ±15% omitted.

View run · Re-run benchmarks

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