Skip to content

feat(processing): parallel-safe gas-price aggregation, base-fee fallback#11542

Open
benaadams wants to merge 5 commits intomasterfrom
processing-stats
Open

feat(processing): parallel-safe gas-price aggregation, base-fee fallback#11542
benaadams wants to merge 5 commits intomasterfrom
processing-stats

Conversation

@benaadams
Copy link
Copy Markdown
Member

@benaadams benaadams commented May 10, 2026

Changes

  • Parallel BAL workers defer inline gas-price folds via Metrics.SuppressInlineGasPriceUpdate; the executor recomputes serially in AggregateBlockGasPriceMetrics after workers join, giving race-free min/max and deterministic avg/median.
  • SeedBlockGasPriceIfEmpty seeds all four aggregates with block.Header.BaseFeePerGas when no user tx contributed (empty / system-only blocks). Intentionally skips zero-baseFee chains (pre-EIP-1559, some rollups, genesis) - rendering "0.000" for those would be less informative than the prior blank output, so they retain the current behavior.
  • ProcessingStats auto-steps the gas-price unit GWei -> MWei -> KWei -> Wei when min would render 0.000 at :N3 precision, keeping low-base-fee values visible (e.g. 7 wei renders as "Gas KWei: 0.007" instead of "Gas GWei: 0.000").
  • Append execution-mode indicator to the "Block throughput" log line, after the ops count: chains for parallel BAL, link for sequential. Reads as ... | ops 1,516,057 🔗 (or ⛓️). Emoji at end-of-line of the lowest per-block row so cell-width variation across terminals cannot drift the mid-line pipes on the Block / Block-throughput rows above. The exec-mode flag is [ThreadStatic] so concurrent processing pipelines (tests, multi-context hosts) do not race on the same global; ProcessingStats reads on the same thread the executor wrote on.
  • UpdateBlockGasPrice and SeedBlockGasPriceIfEmpty use IsUint64 / .u0 fast path to avoid the multi-limb UInt256 -> double conversion in the hot tx-execution path; single cvtsi2sd instead of an out-of-line helper call.

Types of changes

What types of changes does your code introduce?

  • Bugfix (a non-breaking change that fixes an issue)
  • New feature (a non-breaking change that adds functionality)

Testing

Requires testing

  • Yes

If yes, did you write tests?

  • Yes

Notes on testing

Three new tests in Nethermind.Blockchain.Test/BlockProcessorTests.cs (each [NonParallelizable] since the fixture is Parallelizable(ParallelScope.All) and Metrics is static state):

  • Parallel_validation_aggregates_block_gas_price_metrics_after_processing - asserts Metrics.GetBlockGasPrices() is populated and LastBlockUsedParallelExecution is true after a parallel run with EIP-1559 txs at varied gas prices.
  • Parallel_validation_seeds_block_gas_price_with_baseFee_for_empty_block - empty block + 2 gwei baseFee seeds aggregates to 2.0.
  • Parallel_validation_does_not_seed_block_gas_price_for_zero_baseFee_empty_block - zero baseFee + empty block leaves aggregates uninitialized (GetBlockGasPrices() returns null).

Nethermind.Blockchain.Test/BlockProcessorTests: 43/43 pass.

…ack, exec-mode emoji

- Parallel BAL workers defer inline gas-price folds via Metrics.SuppressInlineGasPriceUpdate;
  the executor recomputes serially in AggregateBlockGasPriceMetrics after workers join,
  giving race-free min/max and deterministic avg/median.
- SeedBlockGasPriceIfEmpty seeds all four aggregates with block.Header.BaseFeePerGas when
  no user tx contributed (empty / system-only blocks), so the "Processed" log line and
  Prometheus gauges show a meaningful value rather than blanking.
- ProcessingStats auto-steps the gas-price unit GWei -> MWei -> KWei -> Wei when min would
  render 0.000 at :N3 precision, keeping low-base-fee values visible (e.g. 7 wei renders
  as "Gas KWei: 0.007" instead of "Gas GWei: 0.000").
- Append execution-mode indicator to the "Processed" log line: chains for parallel BAL,
  link for sequential. Emoji at end-of-line so cell-width variation across terminals
  cannot drift the mid-line pipes against the Blocks/Throughput rows below.
- UpdateBlockGasPrice and SeedBlockGasPriceIfEmpty use IsUint64 / .u0 fast path to avoid
  the multi-limb UInt256 -> double conversion in the hot tx-execution path; single
  cvtsi2sd instead of an out-of-line helper call.
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 10, 2026

Claude finished @benaadams's task in 6m 38s —— View job


PR Review: feat(processing): parallel-safe gas-price aggregation, base-fee fallback

  • Load repo rules (coding-style, robustness, performance)
  • Read changed files
  • Review correctness and edge cases
  • Review thread safety / race conditions
  • Review performance hot path
  • Post findings

Summary

The design is sound. The race-free aggregation strategy (suppress inline updates via [ThreadStatic] flag on workers + serial post-pass after join) is correct, well-commented, and avoids the previous unsynchronised min/max/avg writes on parallel workers. The IsUint64 / .u0 fast path, base-fee fallback, gas-unit auto-scaling, and exec-mode emoji are all reasonable. No Critical or High issues.


Medium

SeedBlockGasPriceIfEmpty with baseFee = 0 shows zeros instead of blank

SeedBlockGasPriceIfEmpty accepts any IsUint64 value including zero. When baseFee = 0 (pre-EIP-1559 chains, some rollups with explicitly zero base fee, or genesis) and no user txs ran, all four metrics are seeded to 0. ProcessingStats then enters the display branch (because BlockMinGasPrice == 0 != float.MaxValue) and renders:

⛽ Gas Wei: 0.000 .. 0.000 (0.000) .. 0.000

Before this PR, an empty block showed nothing (blank). The "meaningful value" goal of the PR is defeated for zero-baseFee blocks. A simple guard would fix it:

internal static void SeedBlockGasPriceIfEmpty(in UInt256 baseFee)
{
    if (BlockMinGasPrice != float.MaxValue) return;
    if (!baseFee.IsUint64 || baseFee.IsZero) return;   // ← add IsZero guard
    ...
}

Fix this →


Low

LastBlockUsedParallelExecution should be volatile

LastBlockUsedParallelExecution (ParallelBlockValidationTransactionsExecutor.cs:44) is a public static bool that is written on the block-processing thread and read from ProcessingStats. In the normal code path both happen on the same thread so there is no practical race today. However, as a public static field, external monitoring/metrics code could read it from a different thread without a memory barrier — the JIT is free to cache the field in a register. Using volatile is the correct annotation for this pattern.

// before
public static bool LastBlockUsedParallelExecution { get; private set; }

// after
public static volatile bool LastBlockUsedParallelExecution;  // or use Volatile.Read/Write at call sites

UpdateBlockGasPrice silently drops txs with effectiveGasPrice > ulong.MaxValue

Metrics.cs:247: if (!effectiveGasPrice.IsUint64) return; is stricter than the old guard (effectiveGasPrice[2] | effectiveGasPrice[3]) == 0 (which accepted 65–127-bit values). Any tx with an effective gas price above ulong.MaxValue (~18 GWei at 1 wei/unit — far beyond any realistic value) is silently excluded from all four aggregates. The comment justifies this, and the practical impact is zero on mainnet or any real chain, but it is a silent behavioural narrowing worth noting.


ProcessingStats hard-codes a reference to ParallelBlockValidationTransactionsExecutor

ProcessingStats.cs:369 always reads BlockProcessor.ParallelBlockValidationTransactionsExecutor.LastBlockUsedParallelExecution. Nodes that do not use this executor (e.g. custom integrations or test harnesses) will always show 🔗 regardless of actual execution mode. This is a tight coupling — consider an interface or an injected delegate if more configurability is needed in the future.


Correctness confirmations

Concern Verdict
[ThreadStatic] suppression correctly isolates worker threads ✓ correct, each thread has independent state
finally restore of SuppressInlineGasPriceUpdate always runs
AggregateBlockGasPriceMetrics uses same tx.CalculateEffectiveGasPrice as TransactionProcessor TransactionProcessor.CalculateEffectiveGasPrice is a thin wrapper over the same call
AggregateBlockGasPriceMetrics iterates block.Transactions in canonical order (matches sequential path) ✓ consistent running avg/median
ResetBlockStats called exactly once per block in ParallelBlockValidationTransactionsExecutor path
!balManager.Enabled path (delegates to inner) still calls ResetBlockStats ✓ via BlockValidationTransactionsExecutor.ProcessTransactions
Gas-unit auto-scaling thresholds correctly prevent 0.000 at :N3 ✓ — threshold at 0.0005 GWei means the scaled value always rounds to ≥ 0.001
float.Sign(gasPrice - BlockEstMedianGasPrice) returns int, widened to float
baseFee.IsUint64 guard in UpdateBlockGasPrice covers all real-world gas prices ✓ (ulong.MaxValue ≈ 1.8 × 10¹⁰ GWei)

Comment thread src/Nethermind/Nethermind.Evm/Metrics.cs Outdated
…xec-mode flag

- SeedBlockGasPriceIfEmpty: skip seeding when baseFee is zero (pre-EIP-1559 chains,
  some rollups, genesis). Without this guard, empty zero-baseFee blocks rendered
  "Gas Wei: 0.000 .. 0.000 (0.000) .. 0.000" in the "Processed" log line, defeating
  the PR's goal of showing a meaningful value vs the prior blank output.
- LastBlockUsedParallelExecution: change auto-property to a volatile field. Written
  on the block-processing thread and read from ProcessingStats; cross-thread monitoring
  readers (Prometheus scrapers, external dashboards) need a memory barrier to avoid
  the JIT caching the field in a register.
@benaadams
Copy link
Copy Markdown
Member Author

@claude re-review

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 10, 2026

Claude finished @benaadams's task in 3m 4s —— View job


PR Re-Review: feat(processing): parallel-safe gas-price aggregation, base-fee fallback

  • Load repo rules
  • Read changed files and compare against prior review findings
  • Re-review for correctness, thread safety, performance
  • Post updated findings

Summary

Both issues raised in the prior review have been addressed. The PR is ready to merge.


Prior findings: resolved

Finding Severity Status
SeedBlockGasPriceIfEmpty shows zeros for zero-baseFee blocks Medium ✅ Fixed — baseFee.IsZero guard added (Metrics.cs:262), comment explains the rationale
LastBlockUsedParallelExecution missing volatile Low ✅ Fixed — field is now public static volatile bool with a comment explaining cross-thread monitoring visibility
Silent drop for effectiveGasPrice > ulong.MaxValue Low ✅ Acknowledged — IsUint64 guard is in UpdateBlockGasPrice with an explanatory comment (ulong.MaxValue ≈ 1.8×10¹⁰ GWei, far above any real gas price)
ProcessingStats tight coupling to ParallelBlockValidationTransactionsExecutor Low Acknowledged (unchanged) — acceptable for current scope

Fresh review: no new findings

Verified end-to-end correctness:

  • Race-free parallel path: SuppressInlineGasPriceUpdate is [ThreadStatic], set to true inside ParallelUnbalancedWork.For lambda, restored in finally — each worker thread has independent state, no shared-state race.
  • AggregateBlockGasPriceMetrics parity with sequential path: iterates block.Transactions in canonical order, skips IsSystem() txs (matching the ExecutionOptions.SkipValidation gate in TransactionProcessor.UpdateMetrics), uses the same tx.CalculateEffectiveGasPrice formula.
  • All three dispatch paths correctly seed/aggregate: !balManager.Enabled → delegated to inner (which calls both ResetBlockStats and SeedBlockGasPriceIfEmpty); parallel=trueAggregateBlockGasPriceMetrics (which ends with SeedBlockGasPriceIfEmpty); parallel=false → inline updates + explicit SeedBlockGasPriceIfEmpty.
  • Gas-unit thresholds: 0.0005/5e-7/5e-10 GWei boundaries correctly ensure min * scale ≥ 0.001 at :N3 precision.
  • gasMed = Math.Max(gasMin, BlockEstMedianGasPrice): correctly clamps the running estimator to ≥ min when the estimator hasn't converged on very short blocks.
  • in UInt256 in UpdateMetrics call site: avoids a 32-byte copy in a per-tx hot path — correct minor optimization.

@benaadams benaadams marked this pull request as ready for review May 10, 2026 13:48
@benaadams benaadams requested a review from MarekM25 as a code owner May 10, 2026 13:48
Copilot AI review requested due to automatic review settings May 10, 2026 13:48
@benaadams benaadams requested a review from flcl42 as a code owner May 10, 2026 13:48
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 10, 2026

Claude finished @benaadams's task in 3m 49s —— View job


PR Re-Review: feat(processing): parallel-safe gas-price aggregation, base-fee fallback

  • Load repo rules (coding-style, robustness, performance)
  • Read changed files
  • Review correctness and edge cases
  • Review thread safety / race conditions
  • Review performance hot path
  • Post findings

Summary

All findings from previous reviews are resolved or acknowledged. Fresh pass found no new Critical, High, or Medium issues. The PR is ready to merge.


Prior findings: all resolved or acknowledged

Finding Severity Status
SeedBlockGasPriceIfEmpty shows zeros for zero-baseFee blocks Medium ✅ Fixed — baseFee.IsZero guard at Metrics.cs:262
LastBlockUsedParallelExecution missing volatile Low ✅ Fixed — public static volatile bool with cross-thread visibility comment at ParallelBlockValidationTransactionsExecutor.cs:45
Silent drop for effectiveGasPrice > ulong.MaxValue Low ✅ Acknowledged — comment at Metrics.cs:244 explains rationale
ProcessingStats tight coupling to ParallelBlockValidationTransactionsExecutor Low ✅ Acknowledged — acceptable for current scope

Fresh review: no new issues

Thread safety

  • SuppressInlineGasPriceUpdate is [ThreadStatic]; each parallel worker has independent state. The finally block restores the block-processing thread's flag correctly after ParallelUnbalancedWork.For returns, so AggregateBlockGasPriceMetrics runs with suppression off. ✓
  • AggregateBlockGasPriceMetrics is called after all parallel workers have joined (after ParallelUnbalancedWork.For returns and before the method returns), guaranteeing exclusive access to BlockMinGasPrice / BlockTransactions / etc. ✓
  • LastBlockUsedParallelExecution is set before the expensive ProcessTransactionsParallel/ProcessTransactionsSequential dispatch — acceptable; "last block wins" for multi-block batches is documented. ✓

Correctness

  • UpdateMetrics pattern-match (opts is Commit or None or BuildUp) vs the prior HasFlag approach: the new form correctly excludes Restore-flagged invocations (trace/eth_call), which is more correct — block gas metrics should not reflect simulated txs. Behaviorally equivalent for all real block-processing paths. ✓
  • AggregateBlockGasPriceMetrics skips tx.IsSystem() txs, matching the ExecutionOptions.SkipValidation gate in TransactionProcessor.UpdateMetrics. ✓
  • ResetBlockStats is called exactly once per block in all dispatch paths: direct BlockValidationTransactionsExecutor path, !balManager.Enabled inner delegate path, and both branches of ParallelBlockValidationTransactionsExecutor. ✓
  • float gasMin / gasMax / gasMed in GenerateReport are all sampled after the block-processing thread has written them — ThreadPool scheduling ensures visibility on x86; consistent with existing patterns in the file. ✓

Performance

  • in UInt256 parameter on UpdateBlockGasPrice and SeedBlockGasPriceIfEmpty avoids 32-byte copies per-call. ✓
  • .u0 fast path in UpdateBlockGasPrice avoids the multi-limb UInt256→double helper; IsUint64 guard is a branch the JIT can optimize away for the common case. ✓
  • UpdateMetrics is private static — no virtual dispatch or closure allocation on the hot tx-execution path. ✓

Gas-unit auto-scaling thresholds
Scale boundaries 0.0005 / 5e-7 / 5e-10 GWei correctly ensure min * scale ≥ 0.001 at :N3 for the minimum representable values at each unit boundary. The edge case at 5e-10f exactly is a floating-point rounding curiosity (0.0005 may round to 0.000 or 0.001) but has no practical consequence since gas prices at sub-picowei resolution don't exist on any real chain. ✓

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adjusts block-processing telemetry to make per-block gas-price metrics deterministic under parallel Block Access List (BAL) execution, and improves reporting output for empty/system-only blocks and very low gas prices.

Changes:

  • Suppress inline gas-price aggregation during parallel tx execution and recompute gas-price aggregates serially after workers join.
  • Seed per-block gas-price aggregates from BaseFeePerGas when no user txs contributed (to avoid blank metrics/log output).
  • Improve processing log output: adaptive gas-price units and an execution-mode indicator (parallel vs sequential).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/Nethermind/Nethermind.Evm/TransactionProcessing/TransactionProcessor.cs Routes per-tx metric updates through a suppression-aware helper (for parallel execution).
src/Nethermind/Nethermind.Evm/Metrics.cs Adds thread-static suppression flag, fast-path gas-price folding, and empty-block base-fee seeding.
src/Nethermind/Nethermind.Consensus/Processing/ProcessingStats.cs Updates “Processed” logging: adaptive gas units and an execution-mode marker.
src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.ParallelBlockValidationTransactionsExecutor.cs Adds serial post-pass aggregation for gas-price metrics in the parallel path and exposes last execution mode.
src/Nethermind/Nethermind.Consensus/Processing/BlockProcessor.BlockValidationTransactionsExecutor.cs Seeds gas-price metrics from base fee when no user tx updated the aggregates.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +245 to +253
internal static void UpdateBlockGasPrice(in UInt256 effectiveGasPrice)
{
if (!effectiveGasPrice.IsUint64) return;
float gasPrice = (float)(effectiveGasPrice.u0 / 1_000_000_000.0);
BlockMinGasPrice = Math.Min(gasPrice, BlockMinGasPrice);
BlockMaxGasPrice = Math.Max(gasPrice, BlockMaxGasPrice);
BlockAveGasPrice = (BlockAveGasPrice * BlockTransactions + gasPrice) / (BlockTransactions + 1);
BlockEstMedianGasPrice += BlockAveGasPrice * 0.01f * float.Sign(gasPrice - BlockEstMedianGasPrice);
BlockTransactions++;
Copy link
Copy Markdown
Member Author

@benaadams benaadams May 10, 2026

Choose a reason for hiding this comment

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

= 18.4 ETH per gas unit is really not valid for these metrics

That would mean an on target (gas price maintaining) block of 30M (for 60M max) would use 553402322 ETH which is x5 larger than the entire supply of ETH

Comment thread src/Nethermind/Nethermind.Evm/Metrics.cs
benaadams added 2 commits May 10, 2026 16:57
Was appended to the end of the Processed row; now appended after the ops
count on the Block throughput row (the lowest of the per-block rows).

Same alignment rationale (variable-width emoji at end-of-line so it cannot
drift mid-line pipes), but now the rows immediately above it (Block / Block
throughput) cannot be drifted, instead of the rows below the Processed row.
Reads as ", ... | ops   1,516,057 \U0001f517" or "... ⛓️".
…e, tests

PR #11542 review feedback:

- LastBlockUsedParallelExecution was public static volatile and shared
  across all BlockProcessor instances; concurrent processing pipelines
  (tests, multi-context hosts) would race on it. Switched to ThreadStatic
  with a private setter via property accessor. ProcessingStats reads on
  the same thread that writes (the block processing thread), so no read
  semantics regress; the only consumer was the log line, no Prometheus
  scraper read this field.

- AggregateBlockGasPriceMetrics: added a cost note explaining why a
  serial post-pass (rather than per-worker thread-local accumulators
  merged after join) is the right tradeoff. avg/median don't compose
  losslessly across partitions, and the recompute cost is bounded:
  ~150 txs/block * (one Min/Add UInt256 op + a few float ops) ~ <20us
  vs ~100ms block work.

- New tests in BlockProcessorTests covering the new paths:
  * Parallel_validation_aggregates_block_gas_price_metrics_after_processing
    - asserts Metrics.GetBlockGasPrices() is populated and
      LastBlockUsedParallelExecution is true after a parallel run.
  * Parallel_validation_seeds_block_gas_price_with_baseFee_for_empty_block
    - empty block + non-zero baseFee seeds aggregates to baseFee gwei.
  * Parallel_validation_does_not_seed_block_gas_price_for_zero_baseFee_empty_block
    - zero baseFee + empty block leaves aggregates uninitialized.
  All three tests are [NonParallelizable] because the fixture is
  Parallelizable(ParallelScope.All) and Metrics is static state.
@benaadams
Copy link
Copy Markdown
Member Author

@claude re-review

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 10, 2026

Claude finished @benaadams's task in 4m 9s —— View job


PR Re-Review: feat(processing): parallel-safe gas-price aggregation, base-fee fallback

  • Load repo rules (coding-style, robustness, performance)
  • Read changed files (fresh pass)
  • Review correctness and edge cases
  • Review thread safety / race conditions
  • Review performance hot path
  • Reconcile with prior findings and Copilot comments
  • Post findings

Summary

One new Medium finding from the latest commit (c904158). All earlier issues are resolved or acknowledged.


Prior findings: all resolved or acknowledged

Finding Severity Status
SeedBlockGasPriceIfEmpty shows zeros for zero-baseFee blocks Medium ✅ Fixed — baseFee.IsZero guard at Metrics.cs:262
LastBlockUsedParallelExecution missing volatile Low ✅ Addressed — switched to [ThreadStatic]; see new finding below
Silent drop for effectiveGasPrice > ulong.MaxValue Low ✅ Acknowledged — comment at Metrics.cs:244 + benaadams confirmed correct
ProcessingStats tight coupling to ParallelBlockValidationTransactionsExecutor Low ✅ Acknowledged — acceptable for current scope

Medium — [ThreadStatic] on LastBlockUsedParallelExecution breaks the cross-thread read in GenerateReport

ProcessingStats.cs:369 reads LastBlockUsedParallelExecution inside GenerateReport. The problem is that GenerateReport runs on a thread pool thread — CaptureReportData at ProcessingStats.cs:153 hands off via ThreadPool.UnsafeQueueUserWorkItem. Since LastBlockUsedParallelExecution is [ThreadStatic], the thread pool thread reads its own TLS slot, which is always false (the bool default). The exec-mode emoji therefore permanently shows 🔗 (sequential), regardless of actual execution mode.

The commit message for c904158 says "ProcessingStats reads on the same thread that writes" — but this is not the case; the GenerateReport path is explicitly offloaded to the thread pool.

The test Parallel_validation_aggregates_block_gas_price_metrics_after_processing does not catch this because the test calls executor.ProcessTransactions and then reads LastBlockUsedParallelExecution directly on the test thread (the writing thread), bypassing the UpdateStats → CaptureReportData → GenerateReport path entirely.

Fix: snapshot the flag on the block-processing thread (during UpdateStats, which is called on that thread) and carry it in BlockData, then consume data.UsedParallelExecution in GenerateReport. See inline comment.


Fresh review: correctness confirmations

Concern Verdict
SuppressInlineGasPriceUpdate is [ThreadStatic] — each parallel worker has independent state
finally restores SuppressInlineGasPriceUpdate on the block-processing thread after ParallelUnbalancedWork.For
AggregateBlockGasPriceMetrics runs serially after all workers join, before ProcessTransactions returns
AggregateBlockGasPriceMetrics skips tx.IsSystem() txs, matching TransactionProcessor.UpdateMetrics
ResetBlockStats called exactly once per block across all three dispatch paths
opts is Commit or None or BuildUp pattern was already the existing behavior on master (unchanged by this PR)
Gas-unit auto-scaling thresholds 0.0005 / 5e-7 / 5e-10 prevent 0.000 at :N3
IsZero guard in SeedBlockGasPriceIfEmpty prevents zeroed-out metrics on pre-EIP-1559 empty blocks
IsUint64 / .u0 fast path avoids multi-limb UInt256→double in hot tx-execution path
Three new [NonParallelizable] tests cover parallel aggregation, baseFee seeding, and zero-baseFee skip

Comment thread src/Nethermind/Nethermind.Consensus/Processing/ProcessingStats.cs Outdated
…pool report

The previous fix made LastBlockUsedParallelExecution [ThreadStatic] to
avoid cross-instance races, but ProcessingStats.GenerateReport runs on a
thread-pool thread (queued via UnsafeQueueUserWorkItem in
CaptureReportData). The ThreadStatic read on that thread always returns
the default false, so the emoji would permanently show sequential.

Snapshot the flag in UpdateStats (block-processing thread, same thread the
executor wrote on) into BlockData.UsedParallelExecution; GenerateReport
reads from BlockData. Also clear the field in BlockDataPolicy.Return so
pooled BlockData instances don't carry stale state across blocks.

Caught by claude[bot] in PR #11542 review.
@benaadams
Copy link
Copy Markdown
Member Author

@claude re-review

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented May 10, 2026

Claude finished @benaadams's task in 2m 17s —— View job


PR Re-Review: feat(processing): parallel-safe gas-price aggregation, base-fee fallback

  • Load repo rules (coding-style, robustness, performance)
  • Read changed files (fresh pass, including latest commit b72f3d0)
  • Review thread safety — verify [ThreadStatic] exec-mode fix
  • Review correctness and edge cases
  • Review performance hot path
  • Post findings

Summary

All findings from previous reviews are resolved or acknowledged. The b72f3d0 fix correctly addresses the Medium issue from the last review. The PR is ready to merge.


Prior findings: all resolved or acknowledged

Finding Severity Status
SeedBlockGasPriceIfEmpty shows zeros for zero-baseFee blocks Medium ✅ Fixed — baseFee.IsZero guard at Metrics.cs:262
[ThreadStatic] exec-mode read broken on thread-pool thread Medium ✅ Fixed — b72f3d0 snapshots LastBlockUsedParallelExecution into BlockData.UsedParallelExecution in UpdateStats (block-processing thread); GenerateReport reads from data.UsedParallelExecution; BlockDataPolicy.Return clears it on recycle
Silent drop for effectiveGasPrice > ulong.MaxValue Low ✅ Acknowledged — comment at Metrics.cs:244 explains ulong.MaxValue ≈ 1.8×10¹⁰ GWei, far above any realistic price
ProcessingStats tight coupling to ParallelBlockValidationTransactionsExecutor Low ✅ Acknowledged — acceptable for current scope

Fix verification (b72f3d0)

The [ThreadStatic] snapshot fix is correct and complete:

  • Write path (ProcessTransactions): _lastBlockUsedParallelExecution (TLS) written on the block-processing thread. ✓
  • Snapshot (UpdateStats, same thread): blockData.UsedParallelExecution = ...LastBlockUsedParallelExecution captures the TLS value before UnsafeQueueUserWorkItem. The queue call establishes happens-before, so the field is safely visible to GenerateReport. ✓
  • Read path (GenerateReport, thread-pool thread): reads data.UsedParallelExecution — the snapshotted regular field, not the TLS. Emoji is now correct. ✓
  • Pool recycle (BlockDataPolicy.Return): data.UsedParallelExecution = false prevents stale state leaking across blocks. ✓

Fresh review: no new issues

Concern Verdict
SuppressInlineGasPriceUpdate [ThreadStatic] — each parallel worker has independent state
finally restores suppression flag on every thread including the block-processing thread
AggregateBlockGasPriceMetrics runs serially after all workers join, with suppression off
UpdateMetrics (hot path) checks SuppressInlineGasPriceUpdate before calling UpdateBlockGasPrice
AggregateBlockGasPriceMetrics skips tx.IsSystem() txs, matching TransactionProcessor.UpdateMetrics
ResetBlockStats called exactly once per block across all three dispatch paths
BlockData.UsedParallelExecution cleared in BlockDataPolicy.Return before pool recycle
baseFee.IsZero guard prevents "Gas Wei: 0.000 .. 0.000" on pre-EIP-1559 empty blocks
Gas-unit auto-scaling thresholds prevent 0.000 at :N3 for all real-world gas prices
Three [NonParallelizable] tests cover parallel aggregation, baseFee seeding, and zero-baseFee skip

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants