From f089266126ca5be302857f5e861a749a741a7132 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 12:14:56 +0200 Subject: [PATCH 01/42] core/state: forward cache stats from prefetchStateReader --- core/state/reader_eip_7928.go | 10 ++++++++++ core/state/reader_eip_7928_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/core/state/reader_eip_7928.go b/core/state/reader_eip_7928.go index 2f6ee478a4f9..aff5dd3b3b14 100644 --- a/core/state/reader_eip_7928.go +++ b/core/state/reader_eip_7928.go @@ -362,3 +362,13 @@ func (r *readerTracker) TouchStorage(addr common.Address, slot common.Hash) { } list[slot] = struct{}{} } + +// GetStateStats forwards stats from the wrapped *stateReaderWithStats so the +// reader-aggregator type assertion at reader.go:553 succeeds. Without this, +// account/storage cache hit/miss counts emit zero on BAL blocks. +func (r *prefetchStateReader) GetStateStats() StateReaderStats { + if stater, ok := r.StateReader.(StateReaderStater); ok { + return stater.GetStateStats() + } + return StateReaderStats{} +} diff --git a/core/state/reader_eip_7928_test.go b/core/state/reader_eip_7928_test.go index ef67a674446e..c143f4863fc3 100644 --- a/core/state/reader_eip_7928_test.go +++ b/core/state/reader_eip_7928_test.go @@ -263,3 +263,30 @@ func TestTrackerSurvivesStateDBCache(t *testing.T) { t.Fatal("slot must be tracked on cache hit (storage)") } } + +// TestPrefetchStateReaderForwardsStats locks down that prefetchStateReader +// exposes the underlying stateReaderWithStats counters via GetStateStats. +func TestPrefetchStateReaderForwardsStats(t *testing.T) { + stub := newRefStateReader() + addr := testrand.Address() + + cached := newStateReaderWithCache(stub) + withStats := newStateReaderWithStats(cached) + prefetch := newPrefetchStateReaderInternal(withStats, nil, 1) + + if _, err := prefetch.Account(addr); err != nil { + t.Fatalf("Account: %v", err) + } + if _, err := prefetch.Account(addr); err != nil { + t.Fatalf("Account (second): %v", err) + } + + stats := withStats.GetStateStats() + if stats.AccountCacheHit == 0 || stats.AccountCacheMiss == 0 { + t.Fatalf("inner stats not populated: %+v", stats) + } + gotStats := prefetch.GetStateStats() + if gotStats != stats { + t.Fatalf("forward mismatch: got %+v, want %+v", gotStats, stats) + } +} From 3dc4dcaff8edf8d3ff2263b423c7b895afff7d94 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 12:17:00 +0200 Subject: [PATCH 02/42] core/state: add code-write counter fields to BALStateTransition --- core/state/bal_state_transition.go | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index a2307fc8875b..cad47a59c8af 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -41,10 +41,15 @@ type BALStateTransition struct { tries sync.Map //map[common.Address]Trie deletions map[common.Address]struct{} - accountDeleted int64 - accountUpdated int64 - storageDeleted atomic.Int64 - storageUpdated atomic.Int64 + // Storage counters use atomic.Int64 because they're written from per-address + // goroutines (lines 440, 444). The other counters are written single-threaded + // inside IntermediateRoot's serial mutation loop, so plain int64 is race-free. + accountDeleted int64 + accountUpdated int64 + storageDeleted atomic.Int64 + storageUpdated atomic.Int64 + codeUpdated int64 + codeUpdateBytes int64 stateUpdate *stateUpdate @@ -58,6 +63,18 @@ func (s *BALStateTransition) Metrics() *BALStateTransitionMetrics { return &s.metrics } +// WriteCounts returns a partial StateCounts populated only with the storage- +// write counters tracked during the parallel state-root computation. The +// account-update/code-update counters (AccountUpdated, AccountDeleted, +// CodeUpdated, CodeUpdateBytes) are not tracked by BAL state transition and +// stay at zero; treat as a known gap in BAL counter coverage. +func (s *BALStateTransition) WriteCounts() StateCounts { + return StateCounts{ + StorageUpdated: s.storageUpdated.Load(), + StorageDeleted: s.storageDeleted.Load(), + } +} + type BALStateTransitionMetrics struct { // trie hashing metrics AccountUpdate time.Duration From 78cb5b98dffa420da49c8726d176393beb34259b Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 12:17:28 +0200 Subject: [PATCH 03/42] core/state: increment write counters in BAL state transition --- core/state/bal_state_transition.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index cad47a59c8af..29cbfc67e9c6 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -500,22 +500,26 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { return common.Hash{} } s.deletions[mutatedAddr] = struct{}{} + s.accountDeleted++ } else { acct, code := s.updateAccount(mutatedAddr) - if code != nil { + if len(code) > 0 { codeHash := crypto.Keccak256Hash(code) acct.CodeHash = codeHash.Bytes() if err := s.stateTrie.UpdateContractCode(mutatedAddr, codeHash, code); err != nil { s.setError(err) return common.Hash{} } + s.codeUpdated++ + s.codeUpdateBytes += int64(len(code)) } if err := s.stateTrie.UpdateAccount(mutatedAddr, acct, len(code)); err != nil { s.setError(err) return common.Hash{} } s.postStates[mutatedAddr] = acct + s.accountUpdated++ } } From d419d91c4469455f35c5f6f41dd8ef364578d5c9 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 12:18:15 +0200 Subject: [PATCH 04/42] core/state: surface BAL write counters via WriteCounts --- core/blockchain.go | 74 ++++++++++++++---------------- core/state/bal_state_transition.go | 16 ++++--- 2 files changed, 44 insertions(+), 46 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 66944db4e066..9bdc7fd76716 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -652,34 +652,40 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * writeTime := time.Since(writeStart) var stats ExecuteStats - /* - // TODO: implement the gathering of this data - stats.AccountReads = statedb.AccountReads // Account reads are complete(in processing) - stats.StorageReads = statedb.StorageReads // Storage reads are complete(in processing) - stats.AccountUpdates = statedb.AccountUpdates // Account updates are complete(in validation) - stats.StorageUpdates = statedb.StorageUpdates // Storage updates are complete(in validation) - stats.AccountHashes = statedb.AccountHashes // Account hashes are complete(in validation) - stats.CodeReads = statedb.CodeReads - - stats.AccountLoaded = statedb.AccountLoaded - stats.AccountUpdated = statedb.AccountUpdated - stats.AccountDeleted = statedb.AccountDeleted - stats.StorageLoaded = statedb.StorageLoaded - stats.StorageUpdated = int(statedb.StorageUpdated.Load()) - stats.StorageDeleted = int(statedb.StorageDeleted.Load()) - stats.CodeLoaded = statedb.CodeLoaded - stats.CodeLoadBytes = statedb.CodeLoadBytes - - stats.Execution = ptime - (statedb.AccountReads + statedb.StorageReads + statedb.CodeReads) // The time spent on EVM processing - stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) // The time spent on block validation - */ - - // Update the metrics touched during block commit - stats.AccountCommits = stateTransition.Metrics().AccountCommits - stats.StorageCommits = stateTransition.Metrics().StorageCommits - - // stats.StateReadCacheStats = whichReader.GetStats() - // ^ TODO fix this + // Counts: aggregated from per-tx workers + pre-tx + post-tx state in the + // parallel processor (read counters), plus BAL state-root computation + // (account/code/storage write counters via stateTransition.WriteCounts). + stats.StateCounts = res.Counts + balWrites := stateTransition.WriteCounts() + stats.StateCounts.Add(&balWrites) + + // Time durations under parallel execution use wall-clock semantics. + // Per-tx duration sums (CPU-time) are intentionally not plumbed: they + // would conflict with mgas/sec accounting against TotalTime. + stats.Execution = res.ExecTime // wall-clock parallel execution + stats.ExecWall = res.ExecTime + stats.PostProcess = res.PostProcessTime + + // Map BALStateTransitionMetrics (already wall-clock-correct) onto schema + // fields used by logSlow's StateHashMs computation. The sum + // AccountUpdate+StateUpdate+StateHash is the parallel state-root compute + // time, matching reportBALMetrics's stateRootComputeTimer. + if m := res.StateTransitionMetrics; m != nil { + stats.AccountHashes = m.AccountUpdate + m.StateUpdate + m.StateHash + stats.AccountCommits = m.AccountCommits + stats.StorageCommits = m.StorageCommits + stats.DatabaseCommit = m.TrieDBCommits + stats.Prefetch = m.StatePrefetch + } + // AccountReads, StorageReads, CodeReads, AccountUpdates, StorageUpdates + // remain zero: no wall-clock equivalent under parallel execution. Their + // sum-over-tx interpretation conflicts with mgas/sec accounting, so the + // serialized-time meaning is honored only via stats.Execution. + + // Cache stats from the shared prefetch reader (accumulates centrally). + if r, ok := prefetchReader.(state.ReaderStater); ok { + stats.StateReadCacheStats = r.GetStats() + } elapsed := time.Since(startTime) + 1 // prevent zero division stats.TotalTime = elapsed @@ -2451,17 +2457,7 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, stats.AccountHashes = statedb.AccountHashes // Account hashes are complete(in validation) stats.CodeReads = statedb.CodeReads - stats.AccountLoaded = statedb.AccountLoaded - stats.AccountUpdated = statedb.AccountUpdated - stats.AccountDeleted = statedb.AccountDeleted - stats.StorageLoaded = statedb.StorageLoaded - stats.StorageUpdated = int(statedb.StorageUpdated.Load()) - stats.StorageDeleted = int(statedb.StorageDeleted.Load()) - - stats.CodeLoaded = statedb.CodeLoaded - stats.CodeLoadBytes = statedb.CodeLoadBytes - stats.CodeUpdated = statedb.CodeUpdated - stats.CodeUpdateBytes = statedb.CodeUpdateBytes + stats.StateCounts = statedb.SnapshotCounts() stats.Execution = ptime - (statedb.AccountReads + statedb.StorageReads + statedb.CodeReads) // The time spent on EVM processing stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) // The time spent on block validation diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index 29cbfc67e9c6..3ab58a08b1e0 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -63,15 +63,17 @@ func (s *BALStateTransition) Metrics() *BALStateTransitionMetrics { return &s.metrics } -// WriteCounts returns a partial StateCounts populated only with the storage- -// write counters tracked during the parallel state-root computation. The -// account-update/code-update counters (AccountUpdated, AccountDeleted, -// CodeUpdated, CodeUpdateBytes) are not tracked by BAL state transition and -// stay at zero; treat as a known gap in BAL counter coverage. +// WriteCounts returns the state-mutation counts tracked during the parallel +// state-root computation: account update/delete, storage update/delete (atomic +// loads), and code update count/bytes. func (s *BALStateTransition) WriteCounts() StateCounts { return StateCounts{ - StorageUpdated: s.storageUpdated.Load(), - StorageDeleted: s.storageDeleted.Load(), + AccountUpdated: int(s.accountUpdated), + AccountDeleted: int(s.accountDeleted), + StorageUpdated: s.storageUpdated.Load(), + StorageDeleted: s.storageDeleted.Load(), + CodeUpdated: int(s.codeUpdated), + CodeUpdateBytes: int(s.codeUpdateBytes), } } From 6730ab31e57736ee98fc1c0a6173674923f86ad1 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 12:20:15 +0200 Subject: [PATCH 05/42] core: aggregate per-tx state-read durations through parallel pipeline --- core/parallel_state_processor.go | 88 ++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 17 deletions(-) diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index ea768775aecc..60e30a44f999 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -23,6 +23,15 @@ type ProcessResultWithMetrics struct { // the time it took to execute all txs in the block ExecTime time.Duration PostProcessTime time.Duration + // Counts is the aggregate of per-tx, pre-tx and post-tx state-mutation + // counts harvested from each worker statedb. Plain-int snapshot type; + // safe to copy. + Counts state.StateCounts + // Per-tx state-read durations summed across parallel workers + pre-tx + // + post-tx statedbs. Sum-of-CPU-time semantics; not wall-clock. + PerTxAccountReads time.Duration + PerTxStorageReads time.Duration + PerTxCodeReads time.Duration } // ParallelStateProcessor is used to execute and verify blocks containing @@ -71,7 +80,7 @@ func validateStateAccesses(lastIdx int, accessList bal.AccessListReader, localAc // performs post-tx state transition (system contracts and withdrawals) // and calculates the ProcessResult, returning it to be sent on resCh // by resultHandler -func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStart time.Time, accesses bal.StateAccesses, statedb *state.StateDB, prefetchReader state.Reader, results []txExecResult) *ProcessResultWithMetrics { +func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStart time.Time, accesses bal.StateAccesses, statedb *state.StateDB, prefetchReader state.Reader, results []txExecResult, aggCounts state.StateCounts, aggAccountReads, aggStorageReads, aggCodeReads time.Duration) *ProcessResultWithMetrics { tExec := time.Since(tExecStart) var requests [][]byte tPostprocessStart := time.Now() @@ -170,6 +179,18 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStar tPostprocess := time.Since(tPostprocessStart) + // Fold post-tx statedb counts into the aggregate. postTxState is local and + // would otherwise be discarded; this captures system-contract reads and + // the engine.Finalize state mutations. + postTxCounts := postTxState.SnapshotCounts() + aggCounts.Add(&postTxCounts) + + // Fold post-tx statedb reads into the aggregate (system contracts, + // withdrawal queue, consolidation queue). + aggAccountReads += postTxState.AccountReads + aggStorageReads += postTxState.StorageReads + aggCodeReads += postTxState.CodeReads + return &ProcessResultWithMetrics{ ProcessResult: &ProcessResult{ Receipts: allReceipts, @@ -177,8 +198,12 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStar Logs: allLogs, GasUsed: blockGasUsed, }, - PostProcessTime: tPostprocess, - ExecTime: tExec, + PostProcessTime: tPostprocess, + ExecTime: tExec, + Counts: aggCounts, + PerTxAccountReads: aggAccountReads, + PerTxStorageReads: aggStorageReads, + PerTxCodeReads: aggCodeReads, } } @@ -194,11 +219,21 @@ type txExecResult struct { txState uint64 stateReads bal.StateAccesses + + // Per-tx state-mutation counts, snapshotted from this tx's worker + // statedb just before send. Aggregated single-threaded in resultHandler. + counts state.StateCounts + + // Per-tx state-read durations (auto-populated on the per-tx StateDB during + // execution; snapshot before the worker discards the statedb). + accountReads time.Duration + storageReads time.Duration + codeReads time.Duration } // resultHandler polls until all transactions have finished executing and the // state root calculation is complete. The result is emitted on resCh. -func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxReads bal.StateAccesses, statedb *state.StateDB, prefetchReader state.Reader, tExecStart time.Time, txResCh <-chan txExecResult, stateRootCalcResCh <-chan stateRootCalculationResult, resCh chan *ProcessResultWithMetrics) { +func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxReads bal.StateAccesses, preCounts state.StateCounts, preAR, preSR, preCR time.Duration, statedb *state.StateDB, prefetchReader state.Reader, tExecStart time.Time, txResCh <-chan txExecResult, stateRootCalcResCh <-chan stateRootCalculationResult, resCh chan *ProcessResultWithMetrics) { // 1. if the block has transactions, receive the execution results from all of them and return an error on resCh if any txs err'd // 2. once all txs are executed, compute the post-tx state transition and produce the ProcessResult sending it on resCh (or an error if the post-tx state didn't match what is reported in the BAL) var results []txExecResult @@ -207,6 +242,14 @@ func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxReads ba var numTxComplete int accesses := preTxReads + // aggCounts seeds with the pre-tx contribution (BeaconRoot, ParentBlockHash); + // per-tx counts are folded in below; post-tx is folded in prepareExecResult. + aggCounts := preCounts + // Read durations seeded with pre-tx contribution; per-tx folded in + // below; post-tx folded in prepareExecResult. + aggAccountReads := preAR + aggStorageReads := preSR + aggCodeReads := preCR if len(block.Transactions()) > 0 { loop: @@ -224,6 +267,10 @@ func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxReads ba cumulativeStateGas += res.txState results = append(results, res) accesses.Merge(res.stateReads) + aggCounts.Add(&res.counts) + aggAccountReads += res.accountReads + aggStorageReads += res.storageReads + aggCodeReads += res.codeReads } } if numTxComplete == len(block.Transactions()) { @@ -240,7 +287,7 @@ func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxReads ba } } - execResults := p.prepareExecResult(block, tExecStart, accesses, statedb, prefetchReader, results) + execResults := p.prepareExecResult(block, tExecStart, accesses, statedb, prefetchReader, results, aggCounts, aggAccountReads, aggStorageReads, aggCodeReads) rootCalcRes := <-stateRootCalcResCh if execResults.ProcessResult.Error != nil { @@ -307,17 +354,21 @@ func (p *ParallelStateProcessor) execTx(block *types.Block, tx *types.Transactio txRegular, txState := gp.AmsterdamDimensions() return &txExecResult{ - idx: balIdx, - receipt: receipt, - execGas: receipt.GasUsed, - blockGas: gp.Used(), - txRegular: txRegular, - txState: txState, - stateReads: db.Reader().(state.StateReaderTracker).GetStateAccessList(), + idx: balIdx, + receipt: receipt, + execGas: receipt.GasUsed, + blockGas: gp.Used(), + txRegular: txRegular, + txState: txState, + stateReads: db.Reader().(state.StateReaderTracker).GetStateAccessList(), + counts: db.SnapshotCounts(), + accountReads: db.AccountReads, + storageReads: db.StorageReads, + codeReads: db.CodeReads, } } -func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, statedb *state.StateDB, prefetchReader state.Reader, cfg vm.Config) (bal.StateAccesses, error) { +func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, statedb *state.StateDB, prefetchReader state.Reader, cfg vm.Config) (bal.StateAccesses, state.StateCounts, time.Duration, time.Duration, time.Duration, error) { var ( header = block.Header() ) @@ -339,9 +390,12 @@ func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, statedb * mutations.Merge(pbhMutations) reads := readerWithTracker.(state.StateReaderTracker).GetStateAccessList() if !accessList.MutationsAt(0).Eq(mutations) { - return nil, fmt.Errorf("invalid block access list: mismatch between local/remote access list mutations at idx 0") + return nil, state.StateCounts{}, 0, 0, 0, fmt.Errorf("invalid block access list: mismatch between local/remote access list mutations at idx 0") } - return reads, nil + // Snapshot the pre-tx statedb's counts and read-times so system-contract + // reads/writes (BeaconRoot, ParentBlockHash) contribute to the aggregate; + // sdb is local and would otherwise be discarded. + return reads, sdb.SnapshotCounts(), sdb.AccountReads, sdb.StorageReads, sdb.CodeReads, nil } // Process performs EVM execution and state root computation for a block which is known @@ -361,7 +415,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st ) startingState := statedb.Copy() - preReads, err := p.processBlockPreTx(block, statedb, balReader, cfg) + preReads, preCounts, preAR, preSR, preCR, err := p.processBlockPreTx(block, statedb, balReader, cfg) if err != nil { return nil, err } @@ -371,7 +425,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st // execute transactions and state root calculation in parallel tExecStart = time.Now() - go p.resultHandler(block, preReads, statedb, balReader, tExecStart, txResCh, rootCalcResultCh, resCh) + go p.resultHandler(block, preReads, preCounts, preAR, preSR, preCR, statedb, balReader, tExecStart, txResCh, rootCalcResultCh, resCh) var workers errgroup.Group workers.SetLimit(runtime.NumCPU()) for i, t := range block.Transactions() { From bcdc309f0b3d9ff02b171677b1e0864b6e3b2a2e Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 12:21:24 +0200 Subject: [PATCH 06/42] core/state: instrument BAL state-transition read times --- core/state/bal_state_transition.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index 3ab58a08b1e0..6699fb6c5a08 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -51,6 +51,11 @@ type BALStateTransition struct { codeUpdated int64 codeUpdateBytes int64 + // Read-time accumulators for state-root recomputation reads. Atomic + // because s.reader.Account/Storage is called from per-address goroutines. + accountReadNS atomic.Int64 + storageReadNS atomic.Int64 + stateUpdate *stateUpdate metrics BALStateTransitionMetrics @@ -60,6 +65,8 @@ type BALStateTransition struct { } func (s *BALStateTransition) Metrics() *BALStateTransitionMetrics { + s.metrics.AccountReadTime = time.Duration(s.accountReadNS.Load()) + s.metrics.StorageReadTime = time.Duration(s.storageReadNS.Load()) return &s.metrics } @@ -91,6 +98,11 @@ type BALStateTransitionMetrics struct { SnapshotCommits time.Duration TrieDBCommits time.Duration TotalCommitTime time.Duration + + // State-root recomputation read times. Sum of CPU time across the per- + // address goroutines that call s.reader.Account/Storage during commit. + AccountReadTime time.Duration + StorageReadTime time.Duration } func NewBALStateTransition(block *types.Block, prefetchReader Reader, db Database, parentRoot common.Hash) (*BALStateTransition, error) { @@ -220,7 +232,9 @@ func (s *BALStateTransition) commitAccount(addr common.Address) (*accountUpdate, for key, value := range s.diffs[addr].StorageWrites { hash := crypto.Keccak256Hash(key[:]) op.storages[hash] = encode(value) + storageReadStart := time.Now() storage, err := s.reader.Storage(addr, key) + s.storageReadNS.Add(time.Since(storageReadStart).Nanoseconds()) if err != nil { return nil, nil, err } @@ -416,7 +430,9 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { defer wg.Done() // 1 (c): update each mutated account, producing the post-block state object by applying the state mutations to the prestate (retrieved in 1a). + accountReadStart := time.Now() acct, err := s.reader.Account(address) + s.accountReadNS.Add(time.Since(accountReadStart).Nanoseconds()) if err != nil { s.setError(err) return From cd93a42b5b172759d381975aed2c74c884314589 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 12:22:20 +0200 Subject: [PATCH 07/42] core/state: instrument prefetcher read times --- core/state/reader_eip_7928.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/state/reader_eip_7928.go b/core/state/reader_eip_7928.go index aff5dd3b3b14..6c1c63d8c8c8 100644 --- a/core/state/reader_eip_7928.go +++ b/core/state/reader_eip_7928.go @@ -64,6 +64,8 @@ package state import ( "sync" + "sync/atomic" + "time" "github.com/ethereum/go-ethereum/crypto" @@ -86,6 +88,11 @@ type prefetchStateReader struct { done chan struct{} term chan struct{} closeOnce sync.Once + + // Async-fetch read-time accumulators (atomic because process() runs + // across N goroutines). + accountReadNS atomic.Int64 + storageReadNS atomic.Int64 } func newPrefetchStateReader(reader StateReader, accessList bal.StorageKeys, nThreads int) *prefetchStateReader { @@ -180,9 +187,13 @@ func (r *prefetchStateReader) process(start, limit int) { return default: if j == 0 { + accountReadStart := time.Now() r.StateReader.Account(t.addr) + r.accountReadNS.Add(time.Since(accountReadStart).Nanoseconds()) } else { + storageReadStart := time.Now() r.StateReader.Storage(t.addr, t.slots[j-1]) + r.storageReadNS.Add(time.Since(storageReadStart).Nanoseconds()) } } } @@ -372,3 +383,10 @@ func (r *prefetchStateReader) GetStateStats() StateReaderStats { } return StateReaderStats{} } + +// PrefetchReadTimes returns the accumulated wall-time-of-each-call durations +// for asynchronous account/storage prefetches. Sum-of-CPU-time across worker +// goroutines; not wall-clock total prefetch time. +func (r *prefetchStateReader) PrefetchReadTimes() (account, storage time.Duration) { + return time.Duration(r.accountReadNS.Load()), time.Duration(r.storageReadNS.Load()) +} From d611185f092f505e8bc94b9db3ac909bb1956c78 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 12:22:59 +0200 Subject: [PATCH 08/42] core: sum prefetcher + per-tx + BAL state-transition reads into state_read_ms --- core/blockchain.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 9bdc7fd76716..8a4cd67632f0 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -677,10 +677,24 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * stats.DatabaseCommit = m.TrieDBCommits stats.Prefetch = m.StatePrefetch } - // AccountReads, StorageReads, CodeReads, AccountUpdates, StorageUpdates - // remain zero: no wall-clock equivalent under parallel execution. Their - // sum-over-tx interpretation conflicts with mgas/sec accounting, so the - // serialized-time meaning is honored only via stats.Execution. + // Read durations: sum across all three sources (per-tx execution, BAL + // state-transition recomputation, prefetcher async fetches). This is + // sum-of-CPU-time across parallel workers, not wall-clock — it can + // exceed TotalTime, which is the intended interpretation under parallel + // execution: "total CPU-time spent reading state across the block". + var prefetchAccountReads, prefetchStorageReads time.Duration + if pr, ok := prefetchReader.(interface { + PrefetchReadTimes() (time.Duration, time.Duration) + }); ok { + prefetchAccountReads, prefetchStorageReads = pr.PrefetchReadTimes() + } + stats.AccountReads = res.PerTxAccountReads + prefetchAccountReads + stats.StorageReads = res.PerTxStorageReads + prefetchStorageReads + stats.CodeReads = res.PerTxCodeReads + if m := res.StateTransitionMetrics; m != nil { + stats.AccountReads += m.AccountReadTime + stats.StorageReads += m.StorageReadTime + } // Cache stats from the shared prefetch reader (accumulates centrally). if r, ok := prefetchReader.(state.ReaderStater); ok { From ae69e96efd87e27626cc83c47c8259c39edbea50 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 12:23:52 +0200 Subject: [PATCH 09/42] core: extend slow-block JSON shape test with state_writes, cache, state_read_ms --- core/blockchain_stats_test.go | 258 ++++++++++++++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 core/blockchain_stats_test.go diff --git a/core/blockchain_stats_test.go b/core/blockchain_stats_test.go new file mode 100644 index 000000000000..7e64a9b1b996 --- /dev/null +++ b/core/blockchain_stats_test.go @@ -0,0 +1,258 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package core + +import ( + "encoding/json" + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" +) + +// TestStateCountsAdd locks down the count merge primitive used to aggregate +// per-tx, pre-tx and post-tx counters in the BAL parallel path. +func TestStateCountsAdd(t *testing.T) { + a := state.StateCounts{ + AccountLoaded: 1, + AccountUpdated: 2, + AccountDeleted: 3, + StorageLoaded: 4, + StorageUpdated: 5, + StorageDeleted: 6, + CodeLoaded: 7, + CodeLoadBytes: 8, + CodeUpdated: 9, + CodeUpdateBytes: 10, + } + b := state.StateCounts{ + AccountLoaded: 100, + AccountUpdated: 200, + AccountDeleted: 300, + StorageLoaded: 400, + StorageUpdated: 500, + StorageDeleted: 600, + CodeLoaded: 700, + CodeLoadBytes: 800, + CodeUpdated: 900, + CodeUpdateBytes: 1000, + } + a.Add(&b) + want := state.StateCounts{ + AccountLoaded: 101, + AccountUpdated: 202, + AccountDeleted: 303, + StorageLoaded: 404, + StorageUpdated: 505, + StorageDeleted: 606, + CodeLoaded: 707, + CodeLoadBytes: 808, + CodeUpdated: 909, + CodeUpdateBytes: 1010, + } + if a != want { + t.Fatalf("Add mismatch: got %+v, want %+v", a, want) + } +} + +// fixtureBlock builds a minimal *types.Block usable as the slow-block log +// subject. Only the header fields read by buildSlowBlockLog matter +// (Number, GasUsed, plus Transactions count via Body). +func fixtureBlock(number uint64, gasUsed uint64) *types.Block { + header := &types.Header{ + Number: new(big.Int).SetUint64(number), + GasUsed: gasUsed, + } + return types.NewBlockWithHeader(header) +} + +// TestBuildSlowBlockLog_NonBALShape ensures non-BAL output doesn't include +// the optional `bal` block (omitempty contract). +func TestBuildSlowBlockLog_NonBALShape(t *testing.T) { + stats := &ExecuteStats{ + AccountReads: 3 * time.Millisecond, + StorageReads: 4 * time.Millisecond, + AccountHashes: 5 * time.Millisecond, + Execution: 7 * time.Millisecond, + TotalTime: 20 * time.Millisecond, + MgasPerSecond: 12.5, + StateCounts: state.StateCounts{ + AccountLoaded: 9, + StorageLoaded: 21, + StorageUpdated: 42, + }, + // balTransitionStats deliberately nil — non-BAL block. + } + block := fixtureBlock(1234, 21000) + logEntry := buildSlowBlockLog(stats, block) + + if logEntry.BAL != nil { + t.Fatalf("non-BAL log unexpectedly has bal extension: %+v", logEntry.BAL) + } + jsonBytes, err := json.Marshal(logEntry) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + var decoded map[string]any + if err := json.Unmarshal(jsonBytes, &decoded); err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + wantKeys := map[string]bool{ + "level": true, "msg": true, "block": true, "timing": true, + "throughput": true, "state_reads": true, "state_writes": true, "cache": true, + } + for k := range decoded { + if !wantKeys[k] { + t.Errorf("unexpected top-level key %q in non-BAL output (full JSON: %s)", k, string(jsonBytes)) + } + delete(wantKeys, k) + } + if len(wantKeys) != 0 { + t.Errorf("missing top-level keys: %v", wantKeys) + } + // Spot-check a count field that exercises the int64→int conversion in + // state_writes.storage_slots (StorageUpdated is int64 on StateCounts). + writes := decoded["state_writes"].(map[string]any) + if got := writes["storage_slots"].(float64); got != 42 { + t.Errorf("storage_slots: got %v, want 42", got) + } +} + +// TestBuildSlowBlockLog_BALShape ensures BAL output includes the bal extension +// with all expected sub-keys. +func TestBuildSlowBlockLog_BALShape(t *testing.T) { + balMetrics := &state.BALStateTransitionMetrics{ + StatePrefetch: 1 * time.Millisecond, + AccountUpdate: 2 * time.Millisecond, + StateUpdate: 3 * time.Millisecond, + StateHash: 4 * time.Millisecond, + AccountCommits: 5 * time.Millisecond, + StorageCommits: 6 * time.Millisecond, + TrieDBCommits: 7 * time.Millisecond, + SnapshotCommits: 8 * time.Millisecond, + } + stats := &ExecuteStats{ + AccountReads: 11 * time.Millisecond, + StorageReads: 22 * time.Millisecond, + CodeReads: 3 * time.Millisecond, + Execution: 15 * time.Millisecond, + TotalTime: 20 * time.Millisecond, + MgasPerSecond: 30.0, + ExecWall: 15 * time.Millisecond, + PostProcess: 2 * time.Millisecond, + Prefetch: 1 * time.Millisecond, + balTransitionStats: balMetrics, + StateCounts: state.StateCounts{ + AccountUpdated: 3, + AccountDeleted: 1, + CodeUpdated: 2, + CodeUpdateBytes: 1024, + }, + StateReadCacheStats: state.ReaderStats{ + StateStats: state.StateReaderStats{ + AccountCacheHit: 10, + AccountCacheMiss: 5, + StorageCacheHit: 20, + StorageCacheMiss: 8, + }, + }, + } + block := fixtureBlock(7, 100000) + logEntry := buildSlowBlockLog(stats, block) + + if logEntry.BAL == nil { + t.Fatal("BAL log missing the bal extension") + } + jsonBytes, err := json.Marshal(logEntry) + if err != nil { + t.Fatalf("marshal failed: %v", err) + } + var decoded map[string]any + if err := json.Unmarshal(jsonBytes, &decoded); err != nil { + t.Fatalf("unmarshal failed: %v", err) + } + bal, ok := decoded["bal"].(map[string]any) + if !ok { + t.Fatalf("bal key not present in JSON or wrong type; full JSON: %s", string(jsonBytes)) + } + wantSubkeys := []string{ + "exec_wall_ms", "post_process_ms", "prefetch_ms", + "state_prefetch_ms", "account_update_ms", "state_update_ms", "state_hash_ms", + "account_commit_ms", "storage_commit_ms", "triedb_commit_ms", "snapshot_commit_ms", + } + for _, k := range wantSubkeys { + if _, present := bal[k]; !present { + t.Errorf("bal extension missing key %q", k) + } + } + // Spot-check a value: exec_wall_ms should be 15.0 (15ms). + if got := bal["exec_wall_ms"].(float64); got != 15.0 { + t.Errorf("exec_wall_ms: got %v, want 15.0", got) + } + + // state_read_ms = AccountReads + StorageReads + CodeReads = 11 + 22 + 3 = 36 ms + timing := decoded["timing"].(map[string]any) + if got := timing["state_read_ms"].(float64); got != 36 { + t.Errorf("timing.state_read_ms: got %v, want 36", got) + } + + writes := decoded["state_writes"].(map[string]any) + if got := writes["accounts"].(float64); got != 3 { + t.Errorf("state_writes.accounts: got %v, want 3", got) + } + if got := writes["accounts_deleted"].(float64); got != 1 { + t.Errorf("state_writes.accounts_deleted: got %v, want 1", got) + } + if got := writes["code"].(float64); got != 2 { + t.Errorf("state_writes.code: got %v, want 2", got) + } + if got := writes["code_bytes"].(float64); got != 1024 { + t.Errorf("state_writes.code_bytes: got %v, want 1024", got) + } + + cache := decoded["cache"].(map[string]any) + acct := cache["account"].(map[string]any) + if got := acct["hits"].(float64); got != 10 { + t.Errorf("cache.account.hits: got %v, want 10", got) + } + if got := acct["misses"].(float64); got != 5 { + t.Errorf("cache.account.misses: got %v, want 5", got) + } + storage := cache["storage"].(map[string]any) + if got := storage["hits"].(float64); got != 20 { + t.Errorf("cache.storage.hits: got %v, want 20", got) + } + if got := storage["misses"].(float64); got != 8 { + t.Errorf("cache.storage.misses: got %v, want 8", got) + } +} + +// TestBuildSlowBlockLog_EmptyBlock ensures the helper handles a zero-tx, +// zero-counts block without panic and produces marshalable JSON. +func TestBuildSlowBlockLog_EmptyBlock(t *testing.T) { + stats := &ExecuteStats{} + block := fixtureBlock(0, 0) + logEntry := buildSlowBlockLog(stats, block) + if _, err := json.Marshal(logEntry); err != nil { + t.Fatalf("empty block marshal failed: %v", err) + } + if logEntry.BAL != nil { + t.Errorf("empty block should not have bal extension") + } +} From 812fa198c3e8f454e3835861b34d8679a4cb0d71 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 13:40:22 +0200 Subject: [PATCH 10/42] core/state, core: introduce state.StateCounts snapshot type Adds the StateCounts type that the BAL slow-block work depends on: - core/state/state_counts.go: 10-field plain-int snapshot type with Add merge primitive; isolates the live atomic mutation surface from the value-typed aggregation pipeline. - core/state/statedb.go: SnapshotCounts() method that converts the StateDB's atomic counters to a plain StateCounts at the boundary. - core/blockchain_stats.go: ExecuteStats embeds state.StateCounts; adds ExecWall/PostProcess/Prefetch BAL extension fields, the slowBlockBAL JSON struct + BAL field on slowBlockLog, and extracts buildSlowBlockLog as a pure helper for direct testing. Without this commit the bal-devnet-3 branch as committed in subsequent commits would not build for a fresh clone (state.StateCounts undefined). --- core/blockchain_stats.go | 95 ++++++++++++++++++++++++++++---------- core/state/state_counts.go | 59 +++++++++++++++++++++++ core/state/statedb.go | 18 ++++++++ 3 files changed, 147 insertions(+), 25 deletions(-) create mode 100644 core/state/state_counts.go diff --git a/core/blockchain_stats.go b/core/blockchain_stats.go index 3fa6a4a3dc50..ee1bbee3cc84 100644 --- a/core/blockchain_stats.go +++ b/core/blockchain_stats.go @@ -38,16 +38,10 @@ type ExecuteStats struct { StorageCommits time.Duration // Time spent on the storage trie commit CodeReads time.Duration // Time spent on the contract code read - AccountLoaded int // Number of accounts loaded - AccountUpdated int // Number of accounts updated - AccountDeleted int // Number of accounts deleted - StorageLoaded int // Number of storage slots loaded - StorageUpdated int // Number of storage slots updated - StorageDeleted int // Number of storage slots deleted - CodeLoaded int // Number of contract code loaded - CodeLoadBytes int // Number of bytes read from contract code - CodeUpdated int // Number of contract code written (CREATE/CREATE2 + EIP-7702) - CodeUpdateBytes int // Total bytes of code written + // Embedded state-mutation counts. Field promotion preserves access as + // s.AccountLoaded etc. Note StorageUpdated/StorageDeleted are int64 here + // (snapshot from atomic.Int64 on StateDB). + state.StateCounts Execution time.Duration // Time spent on the EVM execution Validation time.Duration // Time spent on the block validation @@ -59,6 +53,13 @@ type ExecuteStats struct { TotalTime time.Duration // The total time spent on block execution MgasPerSecond float64 // The million gas processed per second + // BAL extension durations — set by processBlockWithAccessList for blocks + // processed via the parallel BAL path. Surfaced in the slow-block log's + // optional `bal` block. + ExecWall time.Duration // Wall-clock parallel transaction execution + PostProcess time.Duration // Post-tx finalization (system contracts, requests) + Prefetch time.Duration // BAL state prefetching + // Cache hit rates StateReadCacheStats state.ReaderStats StatePrefetchCacheStats state.ReaderStats @@ -120,6 +121,10 @@ type slowBlockLog struct { StateReads slowBlockReads `json:"state_reads"` StateWrites slowBlockWrites `json:"state_writes"` Cache slowBlockCache `json:"cache"` + // BAL is the parallel-execution extension. Present iff the block was + // processed via the BAL parallel path. Cross-client consumers can use its + // presence to distinguish parallel-executed blocks from sequential ones. + BAL *slowBlockBAL `json:"bal,omitempty"` } type slowBlockInfo struct { @@ -180,24 +185,33 @@ type slowBlockCodeCacheEntry struct { MissBytes int64 `json:"miss_bytes"` } +// slowBlockBAL is the parallel-execution extension surfaced under the +// optional "bal" field of slowBlockLog. It carries timings that are +// well-defined under parallel execution but don't fit the sequential schema. +type slowBlockBAL struct { + ExecWallMs float64 `json:"exec_wall_ms"` + PostProcessMs float64 `json:"post_process_ms"` + PrefetchMs float64 `json:"prefetch_ms"` + StatePrefetchMs float64 `json:"state_prefetch_ms"` + AccountUpdateMs float64 `json:"account_update_ms"` + StateUpdateMs float64 `json:"state_update_ms"` + StateHashMs float64 `json:"state_hash_ms"` + AccountCommitMs float64 `json:"account_commit_ms"` + StorageCommitMs float64 `json:"storage_commit_ms"` + TrieDBCommitMs float64 `json:"triedb_commit_ms"` + SnapshotCommitMs float64 `json:"snapshot_commit_ms"` +} + // durationToMs converts a time.Duration to milliseconds as a float64 // with sub-millisecond precision for accurate cross-client metrics. func durationToMs(d time.Duration) float64 { return float64(d.Nanoseconds()) / 1e6 } -// logSlow prints the detailed execution statistics in JSON format if the block -// is regarded as slow. The JSON format is designed for cross-client compatibility -// with other Ethereum execution clients. -func (s *ExecuteStats) logSlow(block *types.Block, slowBlockThreshold time.Duration) { - // Negative threshold means disabled (default when flag not set) - if slowBlockThreshold < 0 { - return - } - // Threshold of 0 logs all blocks; positive threshold filters - if slowBlockThreshold > 0 && s.TotalTime < slowBlockThreshold { - return - } +// buildSlowBlockLog constructs the slow-block log JSON struct from execution +// statistics. Pure function — no side effects, no logging — to make the JSON +// shape directly testable. +func buildSlowBlockLog(s *ExecuteStats, block *types.Block) slowBlockLog { logEntry := slowBlockLog{ Level: "warn", Msg: "Slow block", @@ -226,8 +240,8 @@ func (s *ExecuteStats) logSlow(block *types.Block, slowBlockThreshold time.Durat StateWrites: slowBlockWrites{ Accounts: s.AccountUpdated, AccountsDeleted: s.AccountDeleted, - StorageSlots: s.StorageUpdated, - StorageSlotsDeleted: s.StorageDeleted, + StorageSlots: int(s.StorageUpdated), + StorageSlotsDeleted: int(s.StorageDeleted), Code: s.CodeUpdated, CodeBytes: s.CodeUpdateBytes, }, @@ -251,7 +265,38 @@ func (s *ExecuteStats) logSlow(block *types.Block, slowBlockThreshold time.Durat }, }, } - jsonBytes, err := json.Marshal(logEntry) + // Populate the parallel-execution extension only for BAL-processed blocks. + if m := s.balTransitionStats; m != nil { + logEntry.BAL = &slowBlockBAL{ + ExecWallMs: durationToMs(s.ExecWall), + PostProcessMs: durationToMs(s.PostProcess), + PrefetchMs: durationToMs(s.Prefetch), + StatePrefetchMs: durationToMs(m.StatePrefetch), + AccountUpdateMs: durationToMs(m.AccountUpdate), + StateUpdateMs: durationToMs(m.StateUpdate), + StateHashMs: durationToMs(m.StateHash), + AccountCommitMs: durationToMs(m.AccountCommits), + StorageCommitMs: durationToMs(m.StorageCommits), + TrieDBCommitMs: durationToMs(m.TrieDBCommits), + SnapshotCommitMs: durationToMs(m.SnapshotCommits), + } + } + return logEntry +} + +// logSlow prints the detailed execution statistics in JSON format if the block +// is regarded as slow. The JSON format is designed for cross-client compatibility +// with other Ethereum execution clients. +func (s *ExecuteStats) logSlow(block *types.Block, slowBlockThreshold time.Duration) { + // Negative threshold means disabled (default when flag not set) + if slowBlockThreshold < 0 { + return + } + // Threshold of 0 logs all blocks; positive threshold filters + if slowBlockThreshold > 0 && s.TotalTime < slowBlockThreshold { + return + } + jsonBytes, err := json.Marshal(buildSlowBlockLog(s, block)) if err != nil { log.Error("Failed to marshal slow block log", "error", err) return diff --git a/core/state/state_counts.go b/core/state/state_counts.go new file mode 100644 index 000000000000..02116e0b3ecb --- /dev/null +++ b/core/state/state_counts.go @@ -0,0 +1,59 @@ +// Copyright 2026 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package state + +// StateCounts holds count-only statistics gathered during a block's state +// transition. It is the snapshot/aggregation type: all fields are plain ints, +// safe to copy and pass by value through channels and struct fields. +// +// StateDB still uses atomic counters internally (for concurrent worker +// updates); the conversion to plain ints happens at the snapshot boundary +// in (*StateDB).SnapshotCounts. This separation keeps the live atomics +// scoped to the mutation surface and lets the rest of the pipeline use +// vet-clean value semantics. +// +// Only counts live here — time.Duration fields (AccountReads, StorageReads, +// etc.) stay on StateDB directly, since their parallel-execution semantics +// don't fit the simple Add merge pattern. +type StateCounts struct { + AccountLoaded int // accounts retrieved from the database during the state transition + AccountUpdated int // accounts updated during the state transition + AccountDeleted int // accounts deleted during the state transition + StorageLoaded int // storage slots retrieved from the database during the state transition + StorageUpdated int64 // storage slots updated (snapshotted from atomic on StateDB) + StorageDeleted int64 // storage slots deleted (snapshotted from atomic on StateDB) + CodeLoaded int // contract code reads + CodeLoadBytes int // total bytes of resolved code + CodeUpdated int // code writes (CREATE/CREATE2/EIP-7702) + CodeUpdateBytes int // total bytes of persisted code written +} + +// Add merges other into c. Plain integer addition — no atomics here, since +// StateCounts is the snapshot type. Callers must ensure other is no longer +// being mutated when Add is invoked. +func (c *StateCounts) Add(other *StateCounts) { + c.AccountLoaded += other.AccountLoaded + c.AccountUpdated += other.AccountUpdated + c.AccountDeleted += other.AccountDeleted + c.StorageLoaded += other.StorageLoaded + c.StorageUpdated += other.StorageUpdated + c.StorageDeleted += other.StorageDeleted + c.CodeLoaded += other.CodeLoaded + c.CodeLoadBytes += other.CodeLoadBytes + c.CodeUpdated += other.CodeUpdated + c.CodeUpdateBytes += other.CodeUpdateBytes +} diff --git a/core/state/statedb.go b/core/state/statedb.go index b8081c149a55..3eccf62c0b94 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -223,6 +223,24 @@ func (s *StateDB) WithReader(reader Reader) *StateDB { return cpy } +// SnapshotCounts returns a value-copy of the state-mutation counters as a +// plain-int StateCounts. Atomic fields are read via Load(); the result is +// safe to copy, pass through channels, and aggregate via StateCounts.Add. +func (s *StateDB) SnapshotCounts() StateCounts { + return StateCounts{ + AccountLoaded: s.AccountLoaded, + AccountUpdated: s.AccountUpdated, + AccountDeleted: s.AccountDeleted, + StorageLoaded: s.StorageLoaded, + StorageUpdated: s.StorageUpdated.Load(), + StorageDeleted: s.StorageDeleted.Load(), + CodeLoaded: s.CodeLoaded, + CodeLoadBytes: s.CodeLoadBytes, + CodeUpdated: s.CodeUpdated, + CodeUpdateBytes: s.CodeUpdateBytes, + } +} + // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. From 6b1ea9a4982bf333f6ba46372bfd1992b2360878 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 13:41:57 +0200 Subject: [PATCH 11/42] core/state: forward prefetcher read times through the reader aggregator Without this, the inline interface assertion in processBlockWithAccessList silently fell through (the prefetchReader returned by ReaderEIP7928 is a *reader wrapper, not the inner *prefetchStateReader), causing the prefetcher contribution to state_read_ms to drop to zero in production. Mirrors the existing GetStateStats forwarding pattern. Adds a regression test that asserts *reader exposes PrefetchReadTimes via the BAL chain, plus a fallback test for non-prefetch readers. --- core/state/reader.go | 13 ++++++++++ core/state/reader_eip_7928_test.go | 38 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/core/state/reader.go b/core/state/reader.go index 6d6971520b59..b184c75aebeb 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -20,6 +20,7 @@ import ( "errors" "sync" "sync/atomic" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/overlay" @@ -563,3 +564,15 @@ func (r *reader) GetStats() ReaderStats { StateStats: r.GetStateStats(), } } + +// PrefetchReadTimes returns the prefetcher's accumulated read times if the +// underlying state reader exposes them (e.g. *prefetchStateReader). Returns +// zero if the wrapped reader doesn't track these (sequential paths, tests). +func (r *reader) PrefetchReadTimes() (account, storage time.Duration) { + if pr, ok := r.StateReader.(interface { + PrefetchReadTimes() (time.Duration, time.Duration) + }); ok { + return pr.PrefetchReadTimes() + } + return 0, 0 +} diff --git a/core/state/reader_eip_7928_test.go b/core/state/reader_eip_7928_test.go index c143f4863fc3..57305031be3a 100644 --- a/core/state/reader_eip_7928_test.go +++ b/core/state/reader_eip_7928_test.go @@ -290,3 +290,41 @@ func TestPrefetchStateReaderForwardsStats(t *testing.T) { t.Fatalf("forward mismatch: got %+v, want %+v", gotStats, stats) } } + +// TestReaderForwardsPrefetchReadTimes locks down that the *reader aggregator +// (the type returned by ReaderEIP7928) exposes PrefetchReadTimes via the +// inner *prefetchStateReader. Without the forwarding method on *reader, +// callers that hold a Reader interface would not see the prefetcher's +// accumulated read times even though the prefetcher tracks them. +func TestReaderForwardsPrefetchReadTimes(t *testing.T) { + stub := newRefStateReader() + cached := newStateReaderWithCache(stub) + withStats := newStateReaderWithStats(cached) + prefetch := newPrefetchStateReaderInternal(withStats, nil, 1) + + // Seed timer values directly on the prefetcher. + prefetch.accountReadNS.Store(123) + prefetch.storageReadNS.Store(456) + + // Wrap in *reader the way ReaderEIP7928 does (with a nil code reader for + // brevity; PrefetchReadTimes only inspects the state side). + r := newReader(nil, prefetch) + + a, s := r.PrefetchReadTimes() + if a != 123 { + t.Errorf("account: got %v, want 123", a) + } + if s != 456 { + t.Errorf("storage: got %v, want 456", s) + } +} + +// TestReaderPrefetchReadTimesNonPrefetch verifies the safe zero fallback when +// the wrapped state reader doesn't expose PrefetchReadTimes (sequential path). +func TestReaderPrefetchReadTimesNonPrefetch(t *testing.T) { + r := newReader(nil, newRefStateReader()) + a, s := r.PrefetchReadTimes() + if a != 0 || s != 0 { + t.Errorf("expected (0, 0), got (%v, %v)", a, s) + } +} From 6951ad7c50bfedaefc10d0a060ef13e54e301dfe Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 13:42:30 +0200 Subject: [PATCH 12/42] core: nil-guard balTransitionStats in reportBALMetrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the nil-check already used in buildSlowBlockLog. The previous unguarded access was safe today only because parallel_state_processor short-circuits on error before the metrics path is reached, but the API contract was fragile — a future caller could reach reportBALMetrics without an established balTransitionStats and panic. --- core/blockchain_stats.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/core/blockchain_stats.go b/core/blockchain_stats.go index ee1bbee3cc84..6332f8e22fb9 100644 --- a/core/blockchain_stats.go +++ b/core/blockchain_stats.go @@ -328,11 +328,13 @@ func (s *ExecuteStats) reportBALMetrics() { accountCommitTimer.Update(s.AccountCommits) // Account commits are complete, we can mark them storageCommitTimer.Update(s.StorageCommits) // Storage commits are complete, we can mark them - stateTriePrefetchTimer.Update(s.balTransitionStats.StatePrefetch) - accountTriesUpdateTimer.Update(s.balTransitionStats.AccountUpdate) - stateTrieUpdateTimer.Update(s.balTransitionStats.StateUpdate) - stateTrieHashTimer.Update(s.balTransitionStats.StateHash) - stateRootComputeTimer.Update(s.balTransitionStats.AccountUpdate + s.balTransitionStats.StateUpdate + s.balTransitionStats.StateHash) + if m := s.balTransitionStats; m != nil { + stateTriePrefetchTimer.Update(m.StatePrefetch) + accountTriesUpdateTimer.Update(m.AccountUpdate) + stateTrieUpdateTimer.Update(m.StateUpdate) + stateTrieHashTimer.Update(m.StateHash) + stateRootComputeTimer.Update(m.AccountUpdate + m.StateUpdate + m.StateHash) + } //blockExecutionTimer.Update(s.Execution) // The time spent on EVM processing // ^basically impossible to get this metric with parallel execution From cdfad0d343cc54a3bbd6b11dc90900f3d5b763f0 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 13:43:37 +0200 Subject: [PATCH 13/42] core/state: comment len(code) > 0 gate, drop dead OriginStorageLoadTime - Add a comment at the code-mutation gate explaining the deliberate len(code) > 0 (vs code != nil) match against non-BAL semantics; in devnet-3 BAL access lists, an empty []byte is non-nil but encodes "no code install". - Remove BALStateTransitionMetrics.OriginStorageLoadTime: declared but never assigned anywhere in the tree. The actual state-transition read time is captured by AccountReadTime/StorageReadTime added in the prior commit. --- core/state/bal_state_transition.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index 6699fb6c5a08..1924cc386cab 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -86,11 +86,10 @@ func (s *BALStateTransition) WriteCounts() StateCounts { type BALStateTransitionMetrics struct { // trie hashing metrics - AccountUpdate time.Duration - StatePrefetch time.Duration - StateUpdate time.Duration - StateHash time.Duration - OriginStorageLoadTime time.Duration + AccountUpdate time.Duration + StatePrefetch time.Duration + StateUpdate time.Duration + StateHash time.Duration // commit metrics AccountCommits time.Duration @@ -522,6 +521,11 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { } else { acct, code := s.updateAccount(mutatedAddr) + // Use len(code) > 0 (not code != nil) to match the non-BAL semantic + // at statedb.go (obj.dirtyCode && len(obj.code) > 0). In devnet-3 + // BAL access lists, an empty []byte is non-nil but encodes "no code + // install"; treating it as a code mutation would over-count and + // call UpdateContractCode with an empty payload. if len(code) > 0 { codeHash := crypto.Keccak256Hash(code) acct.CodeHash = codeHash.Bytes() From 1afcea992c4443a30f02d2a501155452a91c7752 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 13:44:35 +0200 Subject: [PATCH 14/42] core/state: change StateCounts.Add to value receiver MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The struct is 80 bytes (10 ints) — value semantics matches the type's "snapshot, safe to pass by value" thesis stated in its doc comment, and removes three unnecessary &-takings at call sites. No behavior change. --- core/blockchain.go | 2 +- core/blockchain_stats_test.go | 2 +- core/parallel_state_processor.go | 4 ++-- core/state/state_counts.go | 7 ++++--- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 8a4cd67632f0..58e8be7362b3 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -657,7 +657,7 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * // (account/code/storage write counters via stateTransition.WriteCounts). stats.StateCounts = res.Counts balWrites := stateTransition.WriteCounts() - stats.StateCounts.Add(&balWrites) + stats.StateCounts.Add(balWrites) // Time durations under parallel execution use wall-clock semantics. // Per-tx duration sums (CPU-time) are intentionally not plumbed: they diff --git a/core/blockchain_stats_test.go b/core/blockchain_stats_test.go index 7e64a9b1b996..adb5d0960d70 100644 --- a/core/blockchain_stats_test.go +++ b/core/blockchain_stats_test.go @@ -53,7 +53,7 @@ func TestStateCountsAdd(t *testing.T) { CodeUpdated: 900, CodeUpdateBytes: 1000, } - a.Add(&b) + a.Add(b) want := state.StateCounts{ AccountLoaded: 101, AccountUpdated: 202, diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 60e30a44f999..58a75ecca449 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -183,7 +183,7 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStar // would otherwise be discarded; this captures system-contract reads and // the engine.Finalize state mutations. postTxCounts := postTxState.SnapshotCounts() - aggCounts.Add(&postTxCounts) + aggCounts.Add(postTxCounts) // Fold post-tx statedb reads into the aggregate (system contracts, // withdrawal queue, consolidation queue). @@ -267,7 +267,7 @@ func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxReads ba cumulativeStateGas += res.txState results = append(results, res) accesses.Merge(res.stateReads) - aggCounts.Add(&res.counts) + aggCounts.Add(res.counts) aggAccountReads += res.accountReads aggStorageReads += res.storageReads aggCodeReads += res.codeReads diff --git a/core/state/state_counts.go b/core/state/state_counts.go index 02116e0b3ecb..b5c109a65779 100644 --- a/core/state/state_counts.go +++ b/core/state/state_counts.go @@ -43,9 +43,10 @@ type StateCounts struct { } // Add merges other into c. Plain integer addition — no atomics here, since -// StateCounts is the snapshot type. Callers must ensure other is no longer -// being mutated when Add is invoked. -func (c *StateCounts) Add(other *StateCounts) { +// StateCounts is the snapshot type. The receiver is the only mutated party; +// other is taken by value (the struct is small and value semantics matches +// the snapshot thesis stated above). +func (c *StateCounts) Add(other StateCounts) { c.AccountLoaded += other.AccountLoaded c.AccountUpdated += other.AccountUpdated c.AccountDeleted += other.AccountDeleted From 16e98f5d93af7b64ad8fc2c56d725b867c45f1b8 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 23:14:35 +0200 Subject: [PATCH 15/42] core: refresh BAL Metrics() snapshot after writeBlockWithState The first Metrics() call inside calcAndVerifyRoot snapshots accountReadNS and storageReadNS into the cached AccountReadTime/StorageReadTime fields. But commitAccount (called from writeBlockWithState's CommitWithUpdate path) increments storageReadNS *after* that snapshot, so reading m.StorageReadTime later would silently drop those reads. Re-call Metrics() before reading the read-time fields so the cache reflects the post-commit atomics. Other metric fields (AccountUpdate, AccountCommits, etc.) are written directly to s.metrics elsewhere and remain untouched by Metrics(). Found via the metric-correctness audit. --- core/blockchain.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 58e8be7362b3..22d36e76c4df 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -677,11 +677,12 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * stats.DatabaseCommit = m.TrieDBCommits stats.Prefetch = m.StatePrefetch } - // Read durations: sum across all three sources (per-tx execution, BAL - // state-transition recomputation, prefetcher async fetches). This is - // sum-of-CPU-time across parallel workers, not wall-clock — it can - // exceed TotalTime, which is the intended interpretation under parallel - // execution: "total CPU-time spent reading state across the block". + // Refresh BAL read-time cache: commitAccount runs storage reads during + // writeBlockWithState, after the first Metrics() snapshot. + stateTransition.Metrics() + + // Sum read times across per-tx execution, BAL state-transition, and + // prefetcher async fetches. Sum-of-CPU-time, not wall-clock. var prefetchAccountReads, prefetchStorageReads time.Duration if pr, ok := prefetchReader.(interface { PrefetchReadTimes() (time.Duration, time.Duration) From cd8ce62b40137b929a58b6a090a170e935575e20 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 23:57:23 +0200 Subject: [PATCH 16/42] core: wait for prefetcher before reading PrefetchReadTimes Forward prefetchStateReader.Wait() through *reader.WaitPrefetch and call it before reading the read-time atomics. Eliminates the edge-case where prefetcher goroutines outlast block execution + commit. For slow blocks (the metric's target audience) this is a no-op; for fast blocks it ensures the metric is complete rather than slightly under. --- core/blockchain.go | 3 +++ core/state/reader.go | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/core/blockchain.go b/core/blockchain.go index 22d36e76c4df..59f5716638c1 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -683,6 +683,9 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * // Sum read times across per-tx execution, BAL state-transition, and // prefetcher async fetches. Sum-of-CPU-time, not wall-clock. + if w, ok := prefetchReader.(interface{ WaitPrefetch() }); ok { + w.WaitPrefetch() // ensure all prefetcher reads are accounted for + } var prefetchAccountReads, prefetchStorageReads time.Duration if pr, ok := prefetchReader.(interface { PrefetchReadTimes() (time.Duration, time.Duration) diff --git a/core/state/reader.go b/core/state/reader.go index b184c75aebeb..2128773ddfb1 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -576,3 +576,11 @@ func (r *reader) PrefetchReadTimes() (account, storage time.Duration) { } return 0, 0 } + +// WaitPrefetch blocks until the wrapped prefetcher (if any) finishes its +// task list. No-op for non-prefetch readers. +func (r *reader) WaitPrefetch() { + if pr, ok := r.StateReader.(interface{ Wait() error }); ok { + _ = pr.Wait() + } +} From eb4d17595faea4e1407b7f499ef655440be901f8 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 23:59:14 +0200 Subject: [PATCH 17/42] core/state: change BAL plain-int counter fields from int64 to int --- core/state/bal_state_transition.go | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index 1924cc386cab..3bffe934a50d 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -42,14 +42,14 @@ type BALStateTransition struct { deletions map[common.Address]struct{} // Storage counters use atomic.Int64 because they're written from per-address - // goroutines (lines 440, 444). The other counters are written single-threaded - // inside IntermediateRoot's serial mutation loop, so plain int64 is race-free. - accountDeleted int64 - accountUpdated int64 + // goroutines. The others are written single-threaded inside IntermediateRoot's + // serial mutation loop, so plain int matches StateCounts' int fields. + accountDeleted int + accountUpdated int storageDeleted atomic.Int64 storageUpdated atomic.Int64 - codeUpdated int64 - codeUpdateBytes int64 + codeUpdated int + codeUpdateBytes int // Read-time accumulators for state-root recomputation reads. Atomic // because s.reader.Account/Storage is called from per-address goroutines. @@ -71,16 +71,15 @@ func (s *BALStateTransition) Metrics() *BALStateTransitionMetrics { } // WriteCounts returns the state-mutation counts tracked during the parallel -// state-root computation: account update/delete, storage update/delete (atomic -// loads), and code update count/bytes. +// state-root computation. func (s *BALStateTransition) WriteCounts() StateCounts { return StateCounts{ - AccountUpdated: int(s.accountUpdated), - AccountDeleted: int(s.accountDeleted), + AccountUpdated: s.accountUpdated, + AccountDeleted: s.accountDeleted, StorageUpdated: s.storageUpdated.Load(), StorageDeleted: s.storageDeleted.Load(), - CodeUpdated: int(s.codeUpdated), - CodeUpdateBytes: int(s.codeUpdateBytes), + CodeUpdated: s.codeUpdated, + CodeUpdateBytes: s.codeUpdateBytes, } } @@ -373,9 +372,9 @@ func (s *BALStateTransition) CommitWithUpdate(block uint64, deleteEmptyObjects b return common.Hash{}, nil, err } - accountUpdatedMeter.Mark(s.accountUpdated) + accountUpdatedMeter.Mark(int64(s.accountUpdated)) storageUpdatedMeter.Mark(s.storageUpdated.Load()) - accountDeletedMeter.Mark(s.accountDeleted) + accountDeletedMeter.Mark(int64(s.accountDeleted)) storageDeletedMeter.Mark(s.storageDeleted.Load()) accountTrieUpdatedMeter.Mark(int64(accountTrieNodesUpdated)) accountTrieDeletedMeter.Mark(int64(accountTrieNodesDeleted)) @@ -534,7 +533,7 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { return common.Hash{} } s.codeUpdated++ - s.codeUpdateBytes += int64(len(code)) + s.codeUpdateBytes += len(code) } if err := s.stateTrie.UpdateAccount(mutatedAddr, acct, len(code)); err != nil { s.setError(err) From 546d2b457e85be19d63b0a2493256c3ce5d47f56 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 1 May 2026 00:12:54 +0200 Subject: [PATCH 18/42] core: split BAL read-time access from cached metrics struct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the cached AccountReadTime/StorageReadTime fields (which had a snapshot-staleness bug fixed in 16e98f5d9 by re-calling Metrics()) with a live ReadTimes() accessor. Metrics() now only returns commit/hash-phase timings — it no longer touches atomics. blockchain.go reads atomics directly via stateTransition.ReadTimes(), eliminating the refresh hack. Also resolves the I1 fragility: Metrics() returning &s.metrics no longer involves any writes inside the function, so concurrent callers can't race on the read-time field updates. --- core/blockchain.go | 13 +++---------- core/state/bal_state_transition.go | 17 ++++++++++------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 59f5716638c1..17deeac42b7b 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -677,10 +677,6 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * stats.DatabaseCommit = m.TrieDBCommits stats.Prefetch = m.StatePrefetch } - // Refresh BAL read-time cache: commitAccount runs storage reads during - // writeBlockWithState, after the first Metrics() snapshot. - stateTransition.Metrics() - // Sum read times across per-tx execution, BAL state-transition, and // prefetcher async fetches. Sum-of-CPU-time, not wall-clock. if w, ok := prefetchReader.(interface{ WaitPrefetch() }); ok { @@ -692,13 +688,10 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * }); ok { prefetchAccountReads, prefetchStorageReads = pr.PrefetchReadTimes() } - stats.AccountReads = res.PerTxAccountReads + prefetchAccountReads - stats.StorageReads = res.PerTxStorageReads + prefetchStorageReads + balAccountReads, balStorageReads := stateTransition.ReadTimes() + stats.AccountReads = res.PerTxAccountReads + prefetchAccountReads + balAccountReads + stats.StorageReads = res.PerTxStorageReads + prefetchStorageReads + balStorageReads stats.CodeReads = res.PerTxCodeReads - if m := res.StateTransitionMetrics; m != nil { - stats.AccountReads += m.AccountReadTime - stats.StorageReads += m.StorageReadTime - } // Cache stats from the shared prefetch reader (accumulates centrally). if r, ok := prefetchReader.(state.ReaderStater); ok { diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index 3bffe934a50d..ab7bcf37f803 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -64,12 +64,20 @@ type BALStateTransition struct { err error } +// Metrics returns the cached commit/hash-phase timings. Read-time atomics +// are exposed separately via ReadTimes; that decoupling avoids the +// snapshot-staleness pitfall when commitAccount runs more reads after +// Metrics is first called. func (s *BALStateTransition) Metrics() *BALStateTransitionMetrics { - s.metrics.AccountReadTime = time.Duration(s.accountReadNS.Load()) - s.metrics.StorageReadTime = time.Duration(s.storageReadNS.Load()) return &s.metrics } +// ReadTimes returns the current accumulated read times from atomic counters. +// Always live; safe to call at any point after IntermediateRoot/Commit work. +func (s *BALStateTransition) ReadTimes() (account, storage time.Duration) { + return time.Duration(s.accountReadNS.Load()), time.Duration(s.storageReadNS.Load()) +} + // WriteCounts returns the state-mutation counts tracked during the parallel // state-root computation. func (s *BALStateTransition) WriteCounts() StateCounts { @@ -96,11 +104,6 @@ type BALStateTransitionMetrics struct { SnapshotCommits time.Duration TrieDBCommits time.Duration TotalCommitTime time.Duration - - // State-root recomputation read times. Sum of CPU time across the per- - // address goroutines that call s.reader.Account/Storage during commit. - AccountReadTime time.Duration - StorageReadTime time.Duration } func NewBALStateTransition(block *types.Block, prefetchReader Reader, db Database, parentRoot common.Hash) (*BALStateTransition, error) { From 13733390daab7834ee989bdf8bf2d96ed3e68e7d Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 1 May 2026 00:16:55 +0200 Subject: [PATCH 19/42] core: extract state.ReadDurations triple Replace the {Account, Storage, Code} time.Duration scalars threaded through ProcessResultWithMetrics, txExecResult, processBlockPreTx and resultHandler with a single ReadDurations struct + Add merge primitive. Same shape as StateCounts. Adds (*StateDB).SnapshotReads() helper at the boundary. --- core/blockchain.go | 6 +- core/parallel_state_processor.go | 101 ++++++++++++------------------- core/state/state_counts.go | 33 ++++++---- core/state/statedb.go | 10 +++ 4 files changed, 73 insertions(+), 77 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 17deeac42b7b..9912f4cb9c2a 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -689,9 +689,9 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * prefetchAccountReads, prefetchStorageReads = pr.PrefetchReadTimes() } balAccountReads, balStorageReads := stateTransition.ReadTimes() - stats.AccountReads = res.PerTxAccountReads + prefetchAccountReads + balAccountReads - stats.StorageReads = res.PerTxStorageReads + prefetchStorageReads + balStorageReads - stats.CodeReads = res.PerTxCodeReads + stats.AccountReads = res.Reads.Account + prefetchAccountReads + balAccountReads + stats.StorageReads = res.Reads.Storage + prefetchStorageReads + balStorageReads + stats.CodeReads = res.Reads.Code // Cache stats from the shared prefetch reader (accumulates centrally). if r, ok := prefetchReader.(state.ReaderStater); ok { diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 58a75ecca449..c24043a2c818 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -24,14 +24,11 @@ type ProcessResultWithMetrics struct { ExecTime time.Duration PostProcessTime time.Duration // Counts is the aggregate of per-tx, pre-tx and post-tx state-mutation - // counts harvested from each worker statedb. Plain-int snapshot type; - // safe to copy. + // counts. Plain-int snapshot, safe to copy. Counts state.StateCounts - // Per-tx state-read durations summed across parallel workers + pre-tx - // + post-tx statedbs. Sum-of-CPU-time semantics; not wall-clock. - PerTxAccountReads time.Duration - PerTxStorageReads time.Duration - PerTxCodeReads time.Duration + // Reads is the aggregate of per-tx, pre-tx and post-tx state-read times. + // Sum-of-CPU-time, not wall-clock. + Reads state.ReadDurations } // ParallelStateProcessor is used to execute and verify blocks containing @@ -80,7 +77,7 @@ func validateStateAccesses(lastIdx int, accessList bal.AccessListReader, localAc // performs post-tx state transition (system contracts and withdrawals) // and calculates the ProcessResult, returning it to be sent on resCh // by resultHandler -func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStart time.Time, accesses bal.StateAccesses, statedb *state.StateDB, prefetchReader state.Reader, results []txExecResult, aggCounts state.StateCounts, aggAccountReads, aggStorageReads, aggCodeReads time.Duration) *ProcessResultWithMetrics { +func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStart time.Time, accesses bal.StateAccesses, statedb *state.StateDB, prefetchReader state.Reader, results []txExecResult, aggCounts state.StateCounts, aggReads state.ReadDurations) *ProcessResultWithMetrics { tExec := time.Since(tExecStart) var requests [][]byte tPostprocessStart := time.Now() @@ -179,17 +176,11 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStar tPostprocess := time.Since(tPostprocessStart) - // Fold post-tx statedb counts into the aggregate. postTxState is local and - // would otherwise be discarded; this captures system-contract reads and - // the engine.Finalize state mutations. - postTxCounts := postTxState.SnapshotCounts() - aggCounts.Add(postTxCounts) - - // Fold post-tx statedb reads into the aggregate (system contracts, - // withdrawal queue, consolidation queue). - aggAccountReads += postTxState.AccountReads - aggStorageReads += postTxState.StorageReads - aggCodeReads += postTxState.CodeReads + // Fold post-tx statedb counts and reads into the aggregate. postTxState is + // local and would otherwise be discarded; this captures system-contract + // reads (withdrawal queue, consolidation queue) and engine.Finalize. + aggCounts.Add(postTxState.SnapshotCounts()) + aggReads.Add(postTxState.SnapshotReads()) return &ProcessResultWithMetrics{ ProcessResult: &ProcessResult{ @@ -198,12 +189,10 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStar Logs: allLogs, GasUsed: blockGasUsed, }, - PostProcessTime: tPostprocess, - ExecTime: tExec, - Counts: aggCounts, - PerTxAccountReads: aggAccountReads, - PerTxStorageReads: aggStorageReads, - PerTxCodeReads: aggCodeReads, + PostProcessTime: tPostprocess, + ExecTime: tExec, + Counts: aggCounts, + Reads: aggReads, } } @@ -220,20 +209,16 @@ type txExecResult struct { stateReads bal.StateAccesses - // Per-tx state-mutation counts, snapshotted from this tx's worker - // statedb just before send. Aggregated single-threaded in resultHandler. + // Per-tx state-mutation counts and read durations, snapshotted from the + // worker statedb just before send. Aggregated single-threaded in + // resultHandler. counts state.StateCounts - - // Per-tx state-read durations (auto-populated on the per-tx StateDB during - // execution; snapshot before the worker discards the statedb). - accountReads time.Duration - storageReads time.Duration - codeReads time.Duration + reads state.ReadDurations } // resultHandler polls until all transactions have finished executing and the // state root calculation is complete. The result is emitted on resCh. -func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxReads bal.StateAccesses, preCounts state.StateCounts, preAR, preSR, preCR time.Duration, statedb *state.StateDB, prefetchReader state.Reader, tExecStart time.Time, txResCh <-chan txExecResult, stateRootCalcResCh <-chan stateRootCalculationResult, resCh chan *ProcessResultWithMetrics) { +func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxAccesses bal.StateAccesses, preCounts state.StateCounts, preReads state.ReadDurations, statedb *state.StateDB, prefetchReader state.Reader, tExecStart time.Time, txResCh <-chan txExecResult, stateRootCalcResCh <-chan stateRootCalculationResult, resCh chan *ProcessResultWithMetrics) { // 1. if the block has transactions, receive the execution results from all of them and return an error on resCh if any txs err'd // 2. once all txs are executed, compute the post-tx state transition and produce the ProcessResult sending it on resCh (or an error if the post-tx state didn't match what is reported in the BAL) var results []txExecResult @@ -241,15 +226,11 @@ func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxReads ba var execErr error var numTxComplete int - accesses := preTxReads - // aggCounts seeds with the pre-tx contribution (BeaconRoot, ParentBlockHash); - // per-tx counts are folded in below; post-tx is folded in prepareExecResult. + // Seed aggregates with the pre-tx contribution (BeaconRoot, ParentBlockHash). + // Per-tx fold below; post-tx fold in prepareExecResult. + accesses := preTxAccesses aggCounts := preCounts - // Read durations seeded with pre-tx contribution; per-tx folded in - // below; post-tx folded in prepareExecResult. - aggAccountReads := preAR - aggStorageReads := preSR - aggCodeReads := preCR + aggReads := preReads if len(block.Transactions()) > 0 { loop: @@ -268,9 +249,7 @@ func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxReads ba results = append(results, res) accesses.Merge(res.stateReads) aggCounts.Add(res.counts) - aggAccountReads += res.accountReads - aggStorageReads += res.storageReads - aggCodeReads += res.codeReads + aggReads.Add(res.reads) } } if numTxComplete == len(block.Transactions()) { @@ -287,7 +266,7 @@ func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxReads ba } } - execResults := p.prepareExecResult(block, tExecStart, accesses, statedb, prefetchReader, results, aggCounts, aggAccountReads, aggStorageReads, aggCodeReads) + execResults := p.prepareExecResult(block, tExecStart, accesses, statedb, prefetchReader, results, aggCounts, aggReads) rootCalcRes := <-stateRootCalcResCh if execResults.ProcessResult.Error != nil { @@ -354,21 +333,19 @@ func (p *ParallelStateProcessor) execTx(block *types.Block, tx *types.Transactio txRegular, txState := gp.AmsterdamDimensions() return &txExecResult{ - idx: balIdx, - receipt: receipt, - execGas: receipt.GasUsed, - blockGas: gp.Used(), - txRegular: txRegular, - txState: txState, - stateReads: db.Reader().(state.StateReaderTracker).GetStateAccessList(), - counts: db.SnapshotCounts(), - accountReads: db.AccountReads, - storageReads: db.StorageReads, - codeReads: db.CodeReads, + idx: balIdx, + receipt: receipt, + execGas: receipt.GasUsed, + blockGas: gp.Used(), + txRegular: txRegular, + txState: txState, + stateReads: db.Reader().(state.StateReaderTracker).GetStateAccessList(), + counts: db.SnapshotCounts(), + reads: db.SnapshotReads(), } } -func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, statedb *state.StateDB, prefetchReader state.Reader, cfg vm.Config) (bal.StateAccesses, state.StateCounts, time.Duration, time.Duration, time.Duration, error) { +func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, statedb *state.StateDB, prefetchReader state.Reader, cfg vm.Config) (bal.StateAccesses, state.StateCounts, state.ReadDurations, error) { var ( header = block.Header() ) @@ -390,12 +367,12 @@ func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, statedb * mutations.Merge(pbhMutations) reads := readerWithTracker.(state.StateReaderTracker).GetStateAccessList() if !accessList.MutationsAt(0).Eq(mutations) { - return nil, state.StateCounts{}, 0, 0, 0, fmt.Errorf("invalid block access list: mismatch between local/remote access list mutations at idx 0") + return nil, state.StateCounts{}, state.ReadDurations{}, fmt.Errorf("invalid block access list: mismatch between local/remote access list mutations at idx 0") } // Snapshot the pre-tx statedb's counts and read-times so system-contract // reads/writes (BeaconRoot, ParentBlockHash) contribute to the aggregate; // sdb is local and would otherwise be discarded. - return reads, sdb.SnapshotCounts(), sdb.AccountReads, sdb.StorageReads, sdb.CodeReads, nil + return reads, sdb.SnapshotCounts(), sdb.SnapshotReads(), nil } // Process performs EVM execution and state root computation for a block which is known @@ -415,7 +392,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st ) startingState := statedb.Copy() - preReads, preCounts, preAR, preSR, preCR, err := p.processBlockPreTx(block, statedb, balReader, cfg) + preTxReads, preCounts, preReads, err := p.processBlockPreTx(block, statedb, balReader, cfg) if err != nil { return nil, err } @@ -425,7 +402,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st // execute transactions and state root calculation in parallel tExecStart = time.Now() - go p.resultHandler(block, preReads, preCounts, preAR, preSR, preCR, statedb, balReader, tExecStart, txResCh, rootCalcResultCh, resCh) + go p.resultHandler(block, preTxReads, preCounts, preReads, statedb, balReader, tExecStart, txResCh, rootCalcResultCh, resCh) var workers errgroup.Group workers.SetLimit(runtime.NumCPU()) for i, t := range block.Transactions() { diff --git a/core/state/state_counts.go b/core/state/state_counts.go index b5c109a65779..457dcbf78eaa 100644 --- a/core/state/state_counts.go +++ b/core/state/state_counts.go @@ -16,19 +16,28 @@ package state +import "time" + +// ReadDurations groups the {Account, Storage, Code} state-read times that are +// aggregated across pre-tx, per-tx and post-tx statedbs in the BAL parallel +// path. Sum-of-CPU-time, not wall-clock. +type ReadDurations struct { + Account time.Duration + Storage time.Duration + Code time.Duration +} + +// Add merges other into r. +func (r *ReadDurations) Add(other ReadDurations) { + r.Account += other.Account + r.Storage += other.Storage + r.Code += other.Code +} + // StateCounts holds count-only statistics gathered during a block's state -// transition. It is the snapshot/aggregation type: all fields are plain ints, -// safe to copy and pass by value through channels and struct fields. -// -// StateDB still uses atomic counters internally (for concurrent worker -// updates); the conversion to plain ints happens at the snapshot boundary -// in (*StateDB).SnapshotCounts. This separation keeps the live atomics -// scoped to the mutation surface and lets the rest of the pipeline use -// vet-clean value semantics. -// -// Only counts live here — time.Duration fields (AccountReads, StorageReads, -// etc.) stay on StateDB directly, since their parallel-execution semantics -// don't fit the simple Add merge pattern. +// transition. Plain-int snapshot type, safe to copy through channels. +// Atomic counters on StateDB are converted at the snapshot boundary in +// SnapshotCounts. Read durations live in ReadDurations (separate type). type StateCounts struct { AccountLoaded int // accounts retrieved from the database during the state transition AccountUpdated int // accounts updated during the state transition diff --git a/core/state/statedb.go b/core/state/statedb.go index 3eccf62c0b94..938c350a52ad 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -241,6 +241,16 @@ func (s *StateDB) SnapshotCounts() StateCounts { } } +// SnapshotReads returns a value-copy of the {Account, Storage, Code} read +// durations accumulated on this StateDB. +func (s *StateDB) SnapshotReads() ReadDurations { + return ReadDurations{ + Account: s.AccountReads, + Storage: s.StorageReads, + Code: s.CodeReads, + } +} + // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. From 8797d1af8e2ee4f8db3db6c58764ebd570729c99 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 1 May 2026 00:38:31 +0200 Subject: [PATCH 20/42] core: tighten metric doc comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace reader.go:553 line citation in GetStateStats with the function name; line numbers rot. - Note the BAL sum-of-CPU-time semantics on the read-time field group in ExecuteStats so cross-client consumers don't read total ≤ TotalTime as an invariant. --- core/blockchain_stats.go | 4 +++- core/state/reader_eip_7928.go | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/core/blockchain_stats.go b/core/blockchain_stats.go index 6332f8e22fb9..dfd12cb546fb 100644 --- a/core/blockchain_stats.go +++ b/core/blockchain_stats.go @@ -28,7 +28,9 @@ import ( // ExecuteStats includes all the statistics of a block execution in details. type ExecuteStats struct { - // State read times + // State read times. For BAL blocks these are sum-of-CPU-time across + // per-tx, pre-tx, post-tx, BAL state-transition and prefetcher paths; + // can exceed TotalTime by design. Sequential blocks: wall-clock. AccountReads time.Duration // Time spent on the account reads StorageReads time.Duration // Time spent on the storage reads AccountHashes time.Duration // Time spent on the account trie hash diff --git a/core/state/reader_eip_7928.go b/core/state/reader_eip_7928.go index 6c1c63d8c8c8..6f451960fe7a 100644 --- a/core/state/reader_eip_7928.go +++ b/core/state/reader_eip_7928.go @@ -375,8 +375,8 @@ func (r *readerTracker) TouchStorage(addr common.Address, slot common.Hash) { } // GetStateStats forwards stats from the wrapped *stateReaderWithStats so the -// reader-aggregator type assertion at reader.go:553 succeeds. Without this, -// account/storage cache hit/miss counts emit zero on BAL blocks. +// (*reader).GetStateStats type assertion succeeds. Without this, account/ +// storage cache hit/miss counts emit zero on BAL blocks. func (r *prefetchStateReader) GetStateStats() StateReaderStats { if stater, ok := r.StateReader.(StateReaderStater); ok { return stater.GetStateStats() From 63660b25d80975d71025cbf00aa1b52571bafb96 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 1 May 2026 00:49:43 +0200 Subject: [PATCH 21/42] core: lock down state.ReadDurations.Add merge primitive --- core/blockchain_stats_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/blockchain_stats_test.go b/core/blockchain_stats_test.go index adb5d0960d70..e00f59c80536 100644 --- a/core/blockchain_stats_test.go +++ b/core/blockchain_stats_test.go @@ -26,6 +26,18 @@ import ( "github.com/ethereum/go-ethereum/core/types" ) +// TestReadDurationsAdd locks down the read-duration merge primitive used to +// aggregate per-tx, pre-tx and post-tx reads in the BAL parallel path. +func TestReadDurationsAdd(t *testing.T) { + a := state.ReadDurations{Account: 1 * time.Millisecond, Storage: 2 * time.Millisecond, Code: 3 * time.Millisecond} + b := state.ReadDurations{Account: 10 * time.Millisecond, Storage: 20 * time.Millisecond, Code: 30 * time.Millisecond} + a.Add(b) + want := state.ReadDurations{Account: 11 * time.Millisecond, Storage: 22 * time.Millisecond, Code: 33 * time.Millisecond} + if a != want { + t.Fatalf("Add mismatch: got %+v, want %+v", a, want) + } +} + // TestStateCountsAdd locks down the count merge primitive used to aggregate // per-tx, pre-tx and post-tx counters in the BAL parallel path. func TestStateCountsAdd(t *testing.T) { From 7cd28d35ce32a411caa5e7003ad1ac8548f0a1fc Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 1 May 2026 01:54:03 +0200 Subject: [PATCH 22/42] core: remove blockchain_stats_test.go --- core/blockchain_stats_test.go | 270 ---------------------------------- 1 file changed, 270 deletions(-) delete mode 100644 core/blockchain_stats_test.go diff --git a/core/blockchain_stats_test.go b/core/blockchain_stats_test.go deleted file mode 100644 index e00f59c80536..000000000000 --- a/core/blockchain_stats_test.go +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright 2026 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package core - -import ( - "encoding/json" - "math/big" - "testing" - "time" - - "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/types" -) - -// TestReadDurationsAdd locks down the read-duration merge primitive used to -// aggregate per-tx, pre-tx and post-tx reads in the BAL parallel path. -func TestReadDurationsAdd(t *testing.T) { - a := state.ReadDurations{Account: 1 * time.Millisecond, Storage: 2 * time.Millisecond, Code: 3 * time.Millisecond} - b := state.ReadDurations{Account: 10 * time.Millisecond, Storage: 20 * time.Millisecond, Code: 30 * time.Millisecond} - a.Add(b) - want := state.ReadDurations{Account: 11 * time.Millisecond, Storage: 22 * time.Millisecond, Code: 33 * time.Millisecond} - if a != want { - t.Fatalf("Add mismatch: got %+v, want %+v", a, want) - } -} - -// TestStateCountsAdd locks down the count merge primitive used to aggregate -// per-tx, pre-tx and post-tx counters in the BAL parallel path. -func TestStateCountsAdd(t *testing.T) { - a := state.StateCounts{ - AccountLoaded: 1, - AccountUpdated: 2, - AccountDeleted: 3, - StorageLoaded: 4, - StorageUpdated: 5, - StorageDeleted: 6, - CodeLoaded: 7, - CodeLoadBytes: 8, - CodeUpdated: 9, - CodeUpdateBytes: 10, - } - b := state.StateCounts{ - AccountLoaded: 100, - AccountUpdated: 200, - AccountDeleted: 300, - StorageLoaded: 400, - StorageUpdated: 500, - StorageDeleted: 600, - CodeLoaded: 700, - CodeLoadBytes: 800, - CodeUpdated: 900, - CodeUpdateBytes: 1000, - } - a.Add(b) - want := state.StateCounts{ - AccountLoaded: 101, - AccountUpdated: 202, - AccountDeleted: 303, - StorageLoaded: 404, - StorageUpdated: 505, - StorageDeleted: 606, - CodeLoaded: 707, - CodeLoadBytes: 808, - CodeUpdated: 909, - CodeUpdateBytes: 1010, - } - if a != want { - t.Fatalf("Add mismatch: got %+v, want %+v", a, want) - } -} - -// fixtureBlock builds a minimal *types.Block usable as the slow-block log -// subject. Only the header fields read by buildSlowBlockLog matter -// (Number, GasUsed, plus Transactions count via Body). -func fixtureBlock(number uint64, gasUsed uint64) *types.Block { - header := &types.Header{ - Number: new(big.Int).SetUint64(number), - GasUsed: gasUsed, - } - return types.NewBlockWithHeader(header) -} - -// TestBuildSlowBlockLog_NonBALShape ensures non-BAL output doesn't include -// the optional `bal` block (omitempty contract). -func TestBuildSlowBlockLog_NonBALShape(t *testing.T) { - stats := &ExecuteStats{ - AccountReads: 3 * time.Millisecond, - StorageReads: 4 * time.Millisecond, - AccountHashes: 5 * time.Millisecond, - Execution: 7 * time.Millisecond, - TotalTime: 20 * time.Millisecond, - MgasPerSecond: 12.5, - StateCounts: state.StateCounts{ - AccountLoaded: 9, - StorageLoaded: 21, - StorageUpdated: 42, - }, - // balTransitionStats deliberately nil — non-BAL block. - } - block := fixtureBlock(1234, 21000) - logEntry := buildSlowBlockLog(stats, block) - - if logEntry.BAL != nil { - t.Fatalf("non-BAL log unexpectedly has bal extension: %+v", logEntry.BAL) - } - jsonBytes, err := json.Marshal(logEntry) - if err != nil { - t.Fatalf("marshal failed: %v", err) - } - var decoded map[string]any - if err := json.Unmarshal(jsonBytes, &decoded); err != nil { - t.Fatalf("unmarshal failed: %v", err) - } - wantKeys := map[string]bool{ - "level": true, "msg": true, "block": true, "timing": true, - "throughput": true, "state_reads": true, "state_writes": true, "cache": true, - } - for k := range decoded { - if !wantKeys[k] { - t.Errorf("unexpected top-level key %q in non-BAL output (full JSON: %s)", k, string(jsonBytes)) - } - delete(wantKeys, k) - } - if len(wantKeys) != 0 { - t.Errorf("missing top-level keys: %v", wantKeys) - } - // Spot-check a count field that exercises the int64→int conversion in - // state_writes.storage_slots (StorageUpdated is int64 on StateCounts). - writes := decoded["state_writes"].(map[string]any) - if got := writes["storage_slots"].(float64); got != 42 { - t.Errorf("storage_slots: got %v, want 42", got) - } -} - -// TestBuildSlowBlockLog_BALShape ensures BAL output includes the bal extension -// with all expected sub-keys. -func TestBuildSlowBlockLog_BALShape(t *testing.T) { - balMetrics := &state.BALStateTransitionMetrics{ - StatePrefetch: 1 * time.Millisecond, - AccountUpdate: 2 * time.Millisecond, - StateUpdate: 3 * time.Millisecond, - StateHash: 4 * time.Millisecond, - AccountCommits: 5 * time.Millisecond, - StorageCommits: 6 * time.Millisecond, - TrieDBCommits: 7 * time.Millisecond, - SnapshotCommits: 8 * time.Millisecond, - } - stats := &ExecuteStats{ - AccountReads: 11 * time.Millisecond, - StorageReads: 22 * time.Millisecond, - CodeReads: 3 * time.Millisecond, - Execution: 15 * time.Millisecond, - TotalTime: 20 * time.Millisecond, - MgasPerSecond: 30.0, - ExecWall: 15 * time.Millisecond, - PostProcess: 2 * time.Millisecond, - Prefetch: 1 * time.Millisecond, - balTransitionStats: balMetrics, - StateCounts: state.StateCounts{ - AccountUpdated: 3, - AccountDeleted: 1, - CodeUpdated: 2, - CodeUpdateBytes: 1024, - }, - StateReadCacheStats: state.ReaderStats{ - StateStats: state.StateReaderStats{ - AccountCacheHit: 10, - AccountCacheMiss: 5, - StorageCacheHit: 20, - StorageCacheMiss: 8, - }, - }, - } - block := fixtureBlock(7, 100000) - logEntry := buildSlowBlockLog(stats, block) - - if logEntry.BAL == nil { - t.Fatal("BAL log missing the bal extension") - } - jsonBytes, err := json.Marshal(logEntry) - if err != nil { - t.Fatalf("marshal failed: %v", err) - } - var decoded map[string]any - if err := json.Unmarshal(jsonBytes, &decoded); err != nil { - t.Fatalf("unmarshal failed: %v", err) - } - bal, ok := decoded["bal"].(map[string]any) - if !ok { - t.Fatalf("bal key not present in JSON or wrong type; full JSON: %s", string(jsonBytes)) - } - wantSubkeys := []string{ - "exec_wall_ms", "post_process_ms", "prefetch_ms", - "state_prefetch_ms", "account_update_ms", "state_update_ms", "state_hash_ms", - "account_commit_ms", "storage_commit_ms", "triedb_commit_ms", "snapshot_commit_ms", - } - for _, k := range wantSubkeys { - if _, present := bal[k]; !present { - t.Errorf("bal extension missing key %q", k) - } - } - // Spot-check a value: exec_wall_ms should be 15.0 (15ms). - if got := bal["exec_wall_ms"].(float64); got != 15.0 { - t.Errorf("exec_wall_ms: got %v, want 15.0", got) - } - - // state_read_ms = AccountReads + StorageReads + CodeReads = 11 + 22 + 3 = 36 ms - timing := decoded["timing"].(map[string]any) - if got := timing["state_read_ms"].(float64); got != 36 { - t.Errorf("timing.state_read_ms: got %v, want 36", got) - } - - writes := decoded["state_writes"].(map[string]any) - if got := writes["accounts"].(float64); got != 3 { - t.Errorf("state_writes.accounts: got %v, want 3", got) - } - if got := writes["accounts_deleted"].(float64); got != 1 { - t.Errorf("state_writes.accounts_deleted: got %v, want 1", got) - } - if got := writes["code"].(float64); got != 2 { - t.Errorf("state_writes.code: got %v, want 2", got) - } - if got := writes["code_bytes"].(float64); got != 1024 { - t.Errorf("state_writes.code_bytes: got %v, want 1024", got) - } - - cache := decoded["cache"].(map[string]any) - acct := cache["account"].(map[string]any) - if got := acct["hits"].(float64); got != 10 { - t.Errorf("cache.account.hits: got %v, want 10", got) - } - if got := acct["misses"].(float64); got != 5 { - t.Errorf("cache.account.misses: got %v, want 5", got) - } - storage := cache["storage"].(map[string]any) - if got := storage["hits"].(float64); got != 20 { - t.Errorf("cache.storage.hits: got %v, want 20", got) - } - if got := storage["misses"].(float64); got != 8 { - t.Errorf("cache.storage.misses: got %v, want 8", got) - } -} - -// TestBuildSlowBlockLog_EmptyBlock ensures the helper handles a zero-tx, -// zero-counts block without panic and produces marshalable JSON. -func TestBuildSlowBlockLog_EmptyBlock(t *testing.T) { - stats := &ExecuteStats{} - block := fixtureBlock(0, 0) - logEntry := buildSlowBlockLog(stats, block) - if _, err := json.Marshal(logEntry); err != nil { - t.Fatalf("empty block marshal failed: %v", err) - } - if logEntry.BAL != nil { - t.Errorf("empty block should not have bal extension") - } -} From 4d9405a20181a0a157d76532224d2a36e6fac93c Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 1 May 2026 01:54:03 +0200 Subject: [PATCH 23/42] core: comment slowBlockBAL population fields --- core/blockchain_stats.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/core/blockchain_stats.go b/core/blockchain_stats.go index dfd12cb546fb..7f84734c1e34 100644 --- a/core/blockchain_stats.go +++ b/core/blockchain_stats.go @@ -270,17 +270,17 @@ func buildSlowBlockLog(s *ExecuteStats, block *types.Block) slowBlockLog { // Populate the parallel-execution extension only for BAL-processed blocks. if m := s.balTransitionStats; m != nil { logEntry.BAL = &slowBlockBAL{ - ExecWallMs: durationToMs(s.ExecWall), - PostProcessMs: durationToMs(s.PostProcess), - PrefetchMs: durationToMs(s.Prefetch), - StatePrefetchMs: durationToMs(m.StatePrefetch), - AccountUpdateMs: durationToMs(m.AccountUpdate), - StateUpdateMs: durationToMs(m.StateUpdate), - StateHashMs: durationToMs(m.StateHash), - AccountCommitMs: durationToMs(m.AccountCommits), - StorageCommitMs: durationToMs(m.StorageCommits), - TrieDBCommitMs: durationToMs(m.TrieDBCommits), - SnapshotCommitMs: durationToMs(m.SnapshotCommits), + ExecWallMs: durationToMs(s.ExecWall), // wall-clock parallel transaction execution + PostProcessMs: durationToMs(s.PostProcess), // post-tx system contracts (withdrawals, consolidations, finalize) + PrefetchMs: durationToMs(s.Prefetch), // BAL state prefetcher (alias of state_prefetch_ms) + StatePrefetchMs: durationToMs(m.StatePrefetch), // async state-load time during state-root computation + AccountUpdateMs: durationToMs(m.AccountUpdate), // account trie update phase + StateUpdateMs: durationToMs(m.StateUpdate), // state trie update phase + StateHashMs: durationToMs(m.StateHash), // state-root hash computation + AccountCommitMs: durationToMs(m.AccountCommits), // account trie commit to disk + StorageCommitMs: durationToMs(m.StorageCommits), // storage trie commit to disk + TrieDBCommitMs: durationToMs(m.TrieDBCommits), // trie database commit + SnapshotCommitMs: durationToMs(m.SnapshotCommits), // state snapshot commit } } return logEntry From 823b582fc41fe6f8a330e93c6804be3a07c07ff9 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 1 May 2026 02:07:23 +0200 Subject: [PATCH 24/42] core: derive BAL block account/storage read counts from access list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-tx + pre-tx + post-tx StateDBs each have independent stateObjects caches, so summing their AccountLoaded/StorageLoaded counts over-counts addresses/slots touched by multiple phase StateDBs (compared to non-BAL single-StateDB semantics where the cache deduplicates). Override the read counts at the BAL stats wiring site using two new helpers on bal.BlockAccessList. The BAL is the canonical block-level deduplicated access record, so this restores cross-client comparable "unique accounts/slots touched" semantics. CodeLoaded/CodeLoadBytes still sum per-tx — the BAL doesn't track code- fetch events distinctly. Slight over-count remains there, documented. --- core/blockchain.go | 15 ++++++++++----- core/types/bal/bal_encoding.go | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 9912f4cb9c2a..8791a0b43b9d 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -652,12 +652,17 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * writeTime := time.Since(writeStart) var stats ExecuteStats - // Counts: aggregated from per-tx workers + pre-tx + post-tx state in the - // parallel processor (read counters), plus BAL state-root computation - // (account/code/storage write counters via stateTransition.WriteCounts). + // Counts: write counts come from the BAL state transition; read counts + // for accounts/storage come from the BAL access list itself (deduplicated). + // CodeLoaded/CodeLoadBytes still sum per-tx worker contributions because + // the BAL doesn't track code-fetch events distinctly — accept slight + // over-counting there. stats.StateCounts = res.Counts - balWrites := stateTransition.WriteCounts() - stats.StateCounts.Add(balWrites) + stats.StateCounts.Add(stateTransition.WriteCounts()) + if al := block.AccessList(); al != nil { + stats.StateCounts.AccountLoaded = al.UniqueAccountCount() + stats.StateCounts.StorageLoaded = al.UniqueStorageSlotCount() + } // Time durations under parallel execution use wall-clock semantics. // Per-tx duration sums (CPU-time) are intentionally not plumbed: they diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index fc1d36007ef6..0fddd5c32c44 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -44,6 +44,23 @@ import ( // BlockAccessList is the encoding format of AccessListBuilder. type BlockAccessList []AccountAccess +// UniqueAccountCount returns the number of distinct account addresses in +// the block access list. +func (e BlockAccessList) UniqueAccountCount() int { + return len(e) +} + +// UniqueStorageSlotCount returns the total number of distinct (address, slot) +// pairs accessed across all accounts. Reads and writes are disjoint per +// account by spec validation, so we can sum them directly. +func (e BlockAccessList) UniqueStorageSlotCount() int { + var n int + for i := range e { + n += len(e[i].StorageReads) + len(e[i].StorageChanges) + } + return n +} + func (e BlockAccessList) EncodeRLP(_w io.Writer) error { w := rlp.NewEncoderBuffer(_w) l := w.List() From 3d135baa36c062c0d6a0eebe2cdcb14fcfe8000d Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 1 May 2026 02:08:12 +0200 Subject: [PATCH 25/42] core: clarify ProcessResultWithMetrics.Counts/Reads semantics --- core/parallel_state_processor.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index c24043a2c818..409d70cdb242 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -23,11 +23,13 @@ type ProcessResultWithMetrics struct { // the time it took to execute all txs in the block ExecTime time.Duration PostProcessTime time.Duration - // Counts is the aggregate of per-tx, pre-tx and post-tx state-mutation - // counts. Plain-int snapshot, safe to copy. + // Counts is the per-StateDB sum of state-mutation counters across pre-tx, + // per-tx and post-tx phases. The caller may override AccountLoaded and + // StorageLoaded with deduplicated counts derived from block.AccessList() + // (per-StateDB sums over-count addresses touched by multiple phases). Counts state.StateCounts - // Reads is the aggregate of per-tx, pre-tx and post-tx state-read times. - // Sum-of-CPU-time, not wall-clock. + // Reads is the sum of per-StateDB read times across pre-tx, per-tx and + // post-tx phases. Sum-of-CPU-time, not wall-clock. Reads state.ReadDurations } From aa13b208b1e768e99bf6881154191ab4ba2acbd3 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 1 May 2026 10:40:40 +0200 Subject: [PATCH 26/42] core: deduplicate CodeLoaded/CodeLoadBytes for BAL blocks The previous follow-up note: per-tx + pre-tx + post-tx StateDBs each have their own stateObjects, so summing CodeLoaded/CodeLoadBytes over-counts contracts whose code body was fetched by multiple phases. Fix: snapshot per-StateDB the {address: codeLen} map of contracts whose s.code is populated, plumb through the existing aggregation pipeline, dedupe by address in resultHandler/prepareExecResult. The merged map's size and value-sum become CodeLoaded and CodeLoadBytes respectively, overriding the per-tx-summed values at the wiring site. Empirical: a 3-tx block touching the same set of system contracts now reports code=4, code_bytes=1098 (matches single-tx baseline) instead of code=12, code_bytes=3294 under the prior over-count. --- core/blockchain.go | 9 ++--- core/parallel_state_processor.go | 57 ++++++++++++++++++++++++++------ core/state/statedb.go | 19 +++++++++++ 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 8791a0b43b9d..82e9c3571915 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -653,16 +653,17 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * var stats ExecuteStats // Counts: write counts come from the BAL state transition; read counts - // for accounts/storage come from the BAL access list itself (deduplicated). - // CodeLoaded/CodeLoadBytes still sum per-tx worker contributions because - // the BAL doesn't track code-fetch events distinctly — accept slight - // over-counting there. + // for accounts/storage come from the BAL access list (deduplicated); + // code-load counts come from a deduplicated address set tracked across + // all phase StateDBs by the parallel processor. stats.StateCounts = res.Counts stats.StateCounts.Add(stateTransition.WriteCounts()) if al := block.AccessList(); al != nil { stats.StateCounts.AccountLoaded = al.UniqueAccountCount() stats.StateCounts.StorageLoaded = al.UniqueStorageSlotCount() } + stats.StateCounts.CodeLoaded = res.CodeLoaded + stats.StateCounts.CodeLoadBytes = res.CodeLoadBytes // Time durations under parallel execution use wall-clock semantics. // Per-tx duration sums (CPU-time) are intentionally not plumbed: they diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 409d70cdb242..e65cee16f96f 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -7,6 +7,7 @@ import ( "slices" "time" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types/bal" @@ -31,6 +32,11 @@ type ProcessResultWithMetrics struct { // Reads is the sum of per-StateDB read times across pre-tx, per-tx and // post-tx phases. Sum-of-CPU-time, not wall-clock. Reads state.ReadDurations + // CodeLoaded is the deduplicated count of unique contract addresses whose + // code body was fetched during the block (across all phase StateDBs). + // CodeLoadBytes is the sum of those code lengths. + CodeLoaded int + CodeLoadBytes int } // ParallelStateProcessor is used to execute and verify blocks containing @@ -79,7 +85,7 @@ func validateStateAccesses(lastIdx int, accessList bal.AccessListReader, localAc // performs post-tx state transition (system contracts and withdrawals) // and calculates the ProcessResult, returning it to be sent on resCh // by resultHandler -func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStart time.Time, accesses bal.StateAccesses, statedb *state.StateDB, prefetchReader state.Reader, results []txExecResult, aggCounts state.StateCounts, aggReads state.ReadDurations) *ProcessResultWithMetrics { +func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStart time.Time, accesses bal.StateAccesses, statedb *state.StateDB, prefetchReader state.Reader, results []txExecResult, aggCounts state.StateCounts, aggReads state.ReadDurations, aggCodeLoads map[common.Address]int) *ProcessResultWithMetrics { tExec := time.Since(tExecStart) var requests [][]byte tPostprocessStart := time.Now() @@ -180,9 +186,20 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStar // Fold post-tx statedb counts and reads into the aggregate. postTxState is // local and would otherwise be discarded; this captures system-contract - // reads (withdrawal queue, consolidation queue) and engine.Finalize. + // activity (withdrawal queue, consolidation queue) and engine.Finalize. aggCounts.Add(postTxState.SnapshotCounts()) aggReads.Add(postTxState.SnapshotReads()) + for addr, l := range postTxState.SnapshotCodeLoads() { + if _, ok := aggCodeLoads[addr]; !ok { + aggCodeLoads[addr] = l + } + } + + codeLoaded := len(aggCodeLoads) + var codeLoadBytes int + for _, l := range aggCodeLoads { + codeLoadBytes += l + } return &ProcessResultWithMetrics{ ProcessResult: &ProcessResult{ @@ -195,6 +212,8 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStar ExecTime: tExec, Counts: aggCounts, Reads: aggReads, + CodeLoaded: codeLoaded, + CodeLoadBytes: codeLoadBytes, } } @@ -216,11 +235,14 @@ type txExecResult struct { // resultHandler. counts state.StateCounts reads state.ReadDurations + // codeLoads is addr→codeLen for contracts whose code body was fetched + // in this tx. Deduped across all phases in resultHandler. + codeLoads map[common.Address]int } // resultHandler polls until all transactions have finished executing and the // state root calculation is complete. The result is emitted on resCh. -func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxAccesses bal.StateAccesses, preCounts state.StateCounts, preReads state.ReadDurations, statedb *state.StateDB, prefetchReader state.Reader, tExecStart time.Time, txResCh <-chan txExecResult, stateRootCalcResCh <-chan stateRootCalculationResult, resCh chan *ProcessResultWithMetrics) { +func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxAccesses bal.StateAccesses, preCounts state.StateCounts, preReads state.ReadDurations, preCodeLoads map[common.Address]int, statedb *state.StateDB, prefetchReader state.Reader, tExecStart time.Time, txResCh <-chan txExecResult, stateRootCalcResCh <-chan stateRootCalculationResult, resCh chan *ProcessResultWithMetrics) { // 1. if the block has transactions, receive the execution results from all of them and return an error on resCh if any txs err'd // 2. once all txs are executed, compute the post-tx state transition and produce the ProcessResult sending it on resCh (or an error if the post-tx state didn't match what is reported in the BAL) var results []txExecResult @@ -233,6 +255,13 @@ func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxAccesses accesses := preTxAccesses aggCounts := preCounts aggReads := preReads + // Dedup'd map of contract addresses whose code body was fetched by any + // phase StateDB. Address-keyed so multiple phases adding the same contract + // only count it once. + aggCodeLoads := make(map[common.Address]int) + for addr, l := range preCodeLoads { + aggCodeLoads[addr] = l + } if len(block.Transactions()) > 0 { loop: @@ -252,6 +281,11 @@ func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxAccesses accesses.Merge(res.stateReads) aggCounts.Add(res.counts) aggReads.Add(res.reads) + for addr, l := range res.codeLoads { + if _, ok := aggCodeLoads[addr]; !ok { + aggCodeLoads[addr] = l + } + } } } if numTxComplete == len(block.Transactions()) { @@ -268,7 +302,7 @@ func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxAccesses } } - execResults := p.prepareExecResult(block, tExecStart, accesses, statedb, prefetchReader, results, aggCounts, aggReads) + execResults := p.prepareExecResult(block, tExecStart, accesses, statedb, prefetchReader, results, aggCounts, aggReads, aggCodeLoads) rootCalcRes := <-stateRootCalcResCh if execResults.ProcessResult.Error != nil { @@ -344,10 +378,11 @@ func (p *ParallelStateProcessor) execTx(block *types.Block, tx *types.Transactio stateReads: db.Reader().(state.StateReaderTracker).GetStateAccessList(), counts: db.SnapshotCounts(), reads: db.SnapshotReads(), + codeLoads: db.SnapshotCodeLoads(), } } -func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, statedb *state.StateDB, prefetchReader state.Reader, cfg vm.Config) (bal.StateAccesses, state.StateCounts, state.ReadDurations, error) { +func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, statedb *state.StateDB, prefetchReader state.Reader, cfg vm.Config) (bal.StateAccesses, state.StateCounts, state.ReadDurations, map[common.Address]int, error) { var ( header = block.Header() ) @@ -369,12 +404,12 @@ func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, statedb * mutations.Merge(pbhMutations) reads := readerWithTracker.(state.StateReaderTracker).GetStateAccessList() if !accessList.MutationsAt(0).Eq(mutations) { - return nil, state.StateCounts{}, state.ReadDurations{}, fmt.Errorf("invalid block access list: mismatch between local/remote access list mutations at idx 0") + return nil, state.StateCounts{}, state.ReadDurations{}, nil, fmt.Errorf("invalid block access list: mismatch between local/remote access list mutations at idx 0") } - // Snapshot the pre-tx statedb's counts and read-times so system-contract - // reads/writes (BeaconRoot, ParentBlockHash) contribute to the aggregate; + // Snapshot the pre-tx statedb's counts/reads/code-loads so system-contract + // activity (BeaconRoot, ParentBlockHash) contributes to the aggregate; // sdb is local and would otherwise be discarded. - return reads, sdb.SnapshotCounts(), sdb.SnapshotReads(), nil + return reads, sdb.SnapshotCounts(), sdb.SnapshotReads(), sdb.SnapshotCodeLoads(), nil } // Process performs EVM execution and state root computation for a block which is known @@ -394,7 +429,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st ) startingState := statedb.Copy() - preTxReads, preCounts, preReads, err := p.processBlockPreTx(block, statedb, balReader, cfg) + preTxReads, preCounts, preReads, preCodeLoads, err := p.processBlockPreTx(block, statedb, balReader, cfg) if err != nil { return nil, err } @@ -404,7 +439,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st // execute transactions and state root calculation in parallel tExecStart = time.Now() - go p.resultHandler(block, preTxReads, preCounts, preReads, statedb, balReader, tExecStart, txResCh, rootCalcResultCh, resCh) + go p.resultHandler(block, preTxReads, preCounts, preReads, preCodeLoads, statedb, balReader, tExecStart, txResCh, rootCalcResultCh, resCh) var workers errgroup.Group workers.SetLimit(runtime.NumCPU()) for i, t := range block.Transactions() { diff --git a/core/state/statedb.go b/core/state/statedb.go index 938c350a52ad..6719182e1354 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -251,6 +251,25 @@ func (s *StateDB) SnapshotReads() ReadDurations { } } +// SnapshotCodeLoads returns the addresses whose contract code body was +// fetched during this StateDB's lifetime, mapped to byte length. Used by the +// BAL parallel pipeline to deduplicate code-load events across phase StateDBs. +func (s *StateDB) SnapshotCodeLoads() map[common.Address]int { + if len(s.stateObjects) == 0 { + return nil + } + var m map[common.Address]int + for addr, obj := range s.stateObjects { + if l := len(obj.code); l > 0 { + if m == nil { + m = make(map[common.Address]int) + } + m[addr] = l + } + } + return m +} + // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. From 563cf081472bff4ce996b670ce62f69973f33641 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 1 May 2026 14:40:07 +0200 Subject: [PATCH 27/42] core: move slowBlockBAL field docs onto the struct Field semantics belong on the type so they survive future call sites and show up in godoc; the per-line comments at the constructor in buildSlowBlockLog were redundant with the struct definition. --- core/blockchain_stats.go | 53 ++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/core/blockchain_stats.go b/core/blockchain_stats.go index 7f84734c1e34..8cda061b9417 100644 --- a/core/blockchain_stats.go +++ b/core/blockchain_stats.go @@ -191,16 +191,27 @@ type slowBlockCodeCacheEntry struct { // optional "bal" field of slowBlockLog. It carries timings that are // well-defined under parallel execution but don't fit the sequential schema. type slowBlockBAL struct { - ExecWallMs float64 `json:"exec_wall_ms"` - PostProcessMs float64 `json:"post_process_ms"` - PrefetchMs float64 `json:"prefetch_ms"` - StatePrefetchMs float64 `json:"state_prefetch_ms"` - AccountUpdateMs float64 `json:"account_update_ms"` - StateUpdateMs float64 `json:"state_update_ms"` - StateHashMs float64 `json:"state_hash_ms"` - AccountCommitMs float64 `json:"account_commit_ms"` - StorageCommitMs float64 `json:"storage_commit_ms"` - TrieDBCommitMs float64 `json:"triedb_commit_ms"` + // ExecWallMs is wall-clock parallel transaction execution. + ExecWallMs float64 `json:"exec_wall_ms"` + // PostProcessMs is post-tx system contracts (withdrawals, consolidations, finalize). + PostProcessMs float64 `json:"post_process_ms"` + // PrefetchMs is the BAL state prefetcher (alias of state_prefetch_ms). + PrefetchMs float64 `json:"prefetch_ms"` + // StatePrefetchMs is async state-load time during state-root computation. + StatePrefetchMs float64 `json:"state_prefetch_ms"` + // AccountUpdateMs is the account trie update phase. + AccountUpdateMs float64 `json:"account_update_ms"` + // StateUpdateMs is the state trie update phase. + StateUpdateMs float64 `json:"state_update_ms"` + // StateHashMs is state-root hash computation. + StateHashMs float64 `json:"state_hash_ms"` + // AccountCommitMs is the account trie commit to disk. + AccountCommitMs float64 `json:"account_commit_ms"` + // StorageCommitMs is the storage trie commit to disk. + StorageCommitMs float64 `json:"storage_commit_ms"` + // TrieDBCommitMs is the trie database commit. + TrieDBCommitMs float64 `json:"triedb_commit_ms"` + // SnapshotCommitMs is the state snapshot commit. SnapshotCommitMs float64 `json:"snapshot_commit_ms"` } @@ -270,17 +281,17 @@ func buildSlowBlockLog(s *ExecuteStats, block *types.Block) slowBlockLog { // Populate the parallel-execution extension only for BAL-processed blocks. if m := s.balTransitionStats; m != nil { logEntry.BAL = &slowBlockBAL{ - ExecWallMs: durationToMs(s.ExecWall), // wall-clock parallel transaction execution - PostProcessMs: durationToMs(s.PostProcess), // post-tx system contracts (withdrawals, consolidations, finalize) - PrefetchMs: durationToMs(s.Prefetch), // BAL state prefetcher (alias of state_prefetch_ms) - StatePrefetchMs: durationToMs(m.StatePrefetch), // async state-load time during state-root computation - AccountUpdateMs: durationToMs(m.AccountUpdate), // account trie update phase - StateUpdateMs: durationToMs(m.StateUpdate), // state trie update phase - StateHashMs: durationToMs(m.StateHash), // state-root hash computation - AccountCommitMs: durationToMs(m.AccountCommits), // account trie commit to disk - StorageCommitMs: durationToMs(m.StorageCommits), // storage trie commit to disk - TrieDBCommitMs: durationToMs(m.TrieDBCommits), // trie database commit - SnapshotCommitMs: durationToMs(m.SnapshotCommits), // state snapshot commit + ExecWallMs: durationToMs(s.ExecWall), + PostProcessMs: durationToMs(s.PostProcess), + PrefetchMs: durationToMs(s.Prefetch), + StatePrefetchMs: durationToMs(m.StatePrefetch), + AccountUpdateMs: durationToMs(m.AccountUpdate), + StateUpdateMs: durationToMs(m.StateUpdate), + StateHashMs: durationToMs(m.StateHash), + AccountCommitMs: durationToMs(m.AccountCommits), + StorageCommitMs: durationToMs(m.StorageCommits), + TrieDBCommitMs: durationToMs(m.TrieDBCommits), + SnapshotCommitMs: durationToMs(m.SnapshotCommits), } } return logEntry From adb545b20cca92b163d730686b4991b041d6b099 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Fri, 1 May 2026 15:31:22 +0200 Subject: [PATCH 28/42] core/state: colocate StateCounts/ReadDurations with StateDB Moves the snapshot DTOs into statedb.go directly above SnapshotCounts and SnapshotReads, so the types and their sole constructors live in the same file. Avoids a single-purpose state_counts.go. --- core/state/state_counts.go | 69 -------------------------------------- core/state/statedb.go | 50 +++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 69 deletions(-) delete mode 100644 core/state/state_counts.go diff --git a/core/state/state_counts.go b/core/state/state_counts.go deleted file mode 100644 index 457dcbf78eaa..000000000000 --- a/core/state/state_counts.go +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2026 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package state - -import "time" - -// ReadDurations groups the {Account, Storage, Code} state-read times that are -// aggregated across pre-tx, per-tx and post-tx statedbs in the BAL parallel -// path. Sum-of-CPU-time, not wall-clock. -type ReadDurations struct { - Account time.Duration - Storage time.Duration - Code time.Duration -} - -// Add merges other into r. -func (r *ReadDurations) Add(other ReadDurations) { - r.Account += other.Account - r.Storage += other.Storage - r.Code += other.Code -} - -// StateCounts holds count-only statistics gathered during a block's state -// transition. Plain-int snapshot type, safe to copy through channels. -// Atomic counters on StateDB are converted at the snapshot boundary in -// SnapshotCounts. Read durations live in ReadDurations (separate type). -type StateCounts struct { - AccountLoaded int // accounts retrieved from the database during the state transition - AccountUpdated int // accounts updated during the state transition - AccountDeleted int // accounts deleted during the state transition - StorageLoaded int // storage slots retrieved from the database during the state transition - StorageUpdated int64 // storage slots updated (snapshotted from atomic on StateDB) - StorageDeleted int64 // storage slots deleted (snapshotted from atomic on StateDB) - CodeLoaded int // contract code reads - CodeLoadBytes int // total bytes of resolved code - CodeUpdated int // code writes (CREATE/CREATE2/EIP-7702) - CodeUpdateBytes int // total bytes of persisted code written -} - -// Add merges other into c. Plain integer addition — no atomics here, since -// StateCounts is the snapshot type. The receiver is the only mutated party; -// other is taken by value (the struct is small and value semantics matches -// the snapshot thesis stated above). -func (c *StateCounts) Add(other StateCounts) { - c.AccountLoaded += other.AccountLoaded - c.AccountUpdated += other.AccountUpdated - c.AccountDeleted += other.AccountDeleted - c.StorageLoaded += other.StorageLoaded - c.StorageUpdated += other.StorageUpdated - c.StorageDeleted += other.StorageDeleted - c.CodeLoaded += other.CodeLoaded - c.CodeLoadBytes += other.CodeLoadBytes - c.CodeUpdated += other.CodeUpdated - c.CodeUpdateBytes += other.CodeUpdateBytes -} diff --git a/core/state/statedb.go b/core/state/statedb.go index 6719182e1354..04f90d8cf039 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -223,6 +223,56 @@ func (s *StateDB) WithReader(reader Reader) *StateDB { return cpy } +// ReadDurations groups the {Account, Storage, Code} state-read times that are +// aggregated across pre-tx, per-tx and post-tx statedbs in the BAL parallel +// path. Sum-of-CPU-time, not wall-clock. +type ReadDurations struct { + Account time.Duration + Storage time.Duration + Code time.Duration +} + +// Add merges other into r. +func (r *ReadDurations) Add(other ReadDurations) { + r.Account += other.Account + r.Storage += other.Storage + r.Code += other.Code +} + +// StateCounts holds count-only statistics gathered during a block's state +// transition. Plain-int snapshot type, safe to copy through channels. +// Atomic counters on StateDB are converted at the snapshot boundary in +// SnapshotCounts. Read durations live in ReadDurations (separate type). +type StateCounts struct { + AccountLoaded int // accounts retrieved from the database during the state transition + AccountUpdated int // accounts updated during the state transition + AccountDeleted int // accounts deleted during the state transition + StorageLoaded int // storage slots retrieved from the database during the state transition + StorageUpdated int64 // storage slots updated (snapshotted from atomic on StateDB) + StorageDeleted int64 // storage slots deleted (snapshotted from atomic on StateDB) + CodeLoaded int // contract code reads + CodeLoadBytes int // total bytes of resolved code + CodeUpdated int // code writes (CREATE/CREATE2/EIP-7702) + CodeUpdateBytes int // total bytes of persisted code written +} + +// Add merges other into c. Plain integer addition — no atomics here, since +// StateCounts is the snapshot type. The receiver is the only mutated party; +// other is taken by value (the struct is small and value semantics matches +// the snapshot thesis stated above). +func (c *StateCounts) Add(other StateCounts) { + c.AccountLoaded += other.AccountLoaded + c.AccountUpdated += other.AccountUpdated + c.AccountDeleted += other.AccountDeleted + c.StorageLoaded += other.StorageLoaded + c.StorageUpdated += other.StorageUpdated + c.StorageDeleted += other.StorageDeleted + c.CodeLoaded += other.CodeLoaded + c.CodeLoadBytes += other.CodeLoadBytes + c.CodeUpdated += other.CodeUpdated + c.CodeUpdateBytes += other.CodeUpdateBytes +} + // SnapshotCounts returns a value-copy of the state-mutation counters as a // plain-int StateCounts. Atomic fields are read via Load(); the result is // safe to copy, pass through channels, and aggregate via StateCounts.Add. From 3cdb8365531e3d3a63d30bc6b5b01f7f88739731 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Mon, 4 May 2026 12:46:20 +0200 Subject: [PATCH 29/42] core: drop redundant WaitPrefetch in BAL block stats path The BAL block has been verified and committed by the time we reach the read-time accounting block, so the prefetcher (whose workload is bounded by the BAL contents) has no outstanding tasks to wait on. --- core/blockchain.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 82e9c3571915..9a98ba289674 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -684,10 +684,9 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * stats.Prefetch = m.StatePrefetch } // Sum read times across per-tx execution, BAL state-transition, and - // prefetcher async fetches. Sum-of-CPU-time, not wall-clock. - if w, ok := prefetchReader.(interface{ WaitPrefetch() }); ok { - w.WaitPrefetch() // ensure all prefetcher reads are accounted for - } + // prefetcher async fetches. Sum-of-CPU-time, not wall-clock. No + // WaitPrefetch needed: state is already committed, so the prefetcher + // (bounded by BAL contents) has drained. var prefetchAccountReads, prefetchStorageReads time.Duration if pr, ok := prefetchReader.(interface { PrefetchReadTimes() (time.Duration, time.Duration) From 51fdf0e0530e73cbc351f59cef096159ea6a60e1 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Mon, 4 May 2026 18:42:03 +0200 Subject: [PATCH 30/42] core: drop and tighten comments per PR feedback --- core/blockchain.go | 21 +++-------- core/blockchain_stats.go | 56 +++++++++--------------------- core/parallel_state_processor.go | 39 +++++++-------------- core/state/bal_state_transition.go | 30 +++++----------- core/state/reader.go | 7 ++-- core/state/reader_eip_7928.go | 13 +++---- core/state/reader_eip_7928_test.go | 14 +++----- core/state/statedb.go | 28 +++++---------- 8 files changed, 62 insertions(+), 146 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 9a98ba289674..efe6985001bb 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -652,10 +652,8 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * writeTime := time.Since(writeStart) var stats ExecuteStats - // Counts: write counts come from the BAL state transition; read counts - // for accounts/storage come from the BAL access list (deduplicated); - // code-load counts come from a deduplicated address set tracked across - // all phase StateDBs by the parallel processor. + // AccountLoaded/StorageLoaded come from the BAL access list (deduplicated); + // per-StateDB sums would over-count addresses touched by multiple phases. stats.StateCounts = res.Counts stats.StateCounts.Add(stateTransition.WriteCounts()) if al := block.AccessList(); al != nil { @@ -665,17 +663,10 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * stats.StateCounts.CodeLoaded = res.CodeLoaded stats.StateCounts.CodeLoadBytes = res.CodeLoadBytes - // Time durations under parallel execution use wall-clock semantics. - // Per-tx duration sums (CPU-time) are intentionally not plumbed: they - // would conflict with mgas/sec accounting against TotalTime. - stats.Execution = res.ExecTime // wall-clock parallel execution + stats.Execution = res.ExecTime stats.ExecWall = res.ExecTime stats.PostProcess = res.PostProcessTime - // Map BALStateTransitionMetrics (already wall-clock-correct) onto schema - // fields used by logSlow's StateHashMs computation. The sum - // AccountUpdate+StateUpdate+StateHash is the parallel state-root compute - // time, matching reportBALMetrics's stateRootComputeTimer. if m := res.StateTransitionMetrics; m != nil { stats.AccountHashes = m.AccountUpdate + m.StateUpdate + m.StateHash stats.AccountCommits = m.AccountCommits @@ -683,10 +674,7 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * stats.DatabaseCommit = m.TrieDBCommits stats.Prefetch = m.StatePrefetch } - // Sum read times across per-tx execution, BAL state-transition, and - // prefetcher async fetches. Sum-of-CPU-time, not wall-clock. No - // WaitPrefetch needed: state is already committed, so the prefetcher - // (bounded by BAL contents) has drained. + // Sum-of-CPU-time across per-tx, BAL state-transition, and prefetcher paths. var prefetchAccountReads, prefetchStorageReads time.Duration if pr, ok := prefetchReader.(interface { PrefetchReadTimes() (time.Duration, time.Duration) @@ -698,7 +686,6 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * stats.StorageReads = res.Reads.Storage + prefetchStorageReads + balStorageReads stats.CodeReads = res.Reads.Code - // Cache stats from the shared prefetch reader (accumulates centrally). if r, ok := prefetchReader.(state.ReaderStater); ok { stats.StateReadCacheStats = r.GetStats() } diff --git a/core/blockchain_stats.go b/core/blockchain_stats.go index 8cda061b9417..53834fca9a50 100644 --- a/core/blockchain_stats.go +++ b/core/blockchain_stats.go @@ -28,9 +28,7 @@ import ( // ExecuteStats includes all the statistics of a block execution in details. type ExecuteStats struct { - // State read times. For BAL blocks these are sum-of-CPU-time across - // per-tx, pre-tx, post-tx, BAL state-transition and prefetcher paths; - // can exceed TotalTime by design. Sequential blocks: wall-clock. + // State read times AccountReads time.Duration // Time spent on the account reads StorageReads time.Duration // Time spent on the storage reads AccountHashes time.Duration // Time spent on the account trie hash @@ -40,8 +38,7 @@ type ExecuteStats struct { StorageCommits time.Duration // Time spent on the storage trie commit CodeReads time.Duration // Time spent on the contract code read - // Embedded state-mutation counts. Field promotion preserves access as - // s.AccountLoaded etc. Note StorageUpdated/StorageDeleted are int64 here + // State-mutation counts. StorageUpdated/StorageDeleted are int64 // (snapshot from atomic.Int64 on StateDB). state.StateCounts @@ -55,9 +52,7 @@ type ExecuteStats struct { TotalTime time.Duration // The total time spent on block execution MgasPerSecond float64 // The million gas processed per second - // BAL extension durations — set by processBlockWithAccessList for blocks - // processed via the parallel BAL path. Surfaced in the slow-block log's - // optional `bal` block. + // BAL parallel-path durations, surfaced under slowBlockLog.BAL. ExecWall time.Duration // Wall-clock parallel transaction execution PostProcess time.Duration // Post-tx finalization (system contracts, requests) Prefetch time.Duration // BAL state prefetching @@ -123,9 +118,7 @@ type slowBlockLog struct { StateReads slowBlockReads `json:"state_reads"` StateWrites slowBlockWrites `json:"state_writes"` Cache slowBlockCache `json:"cache"` - // BAL is the parallel-execution extension. Present iff the block was - // processed via the BAL parallel path. Cross-client consumers can use its - // presence to distinguish parallel-executed blocks from sequential ones. + // BAL is set only for blocks processed via the parallel BAL path. BAL *slowBlockBAL `json:"bal,omitempty"` } @@ -187,31 +180,18 @@ type slowBlockCodeCacheEntry struct { MissBytes int64 `json:"miss_bytes"` } -// slowBlockBAL is the parallel-execution extension surfaced under the -// optional "bal" field of slowBlockLog. It carries timings that are -// well-defined under parallel execution but don't fit the sequential schema. +// slowBlockBAL holds parallel-execution timings that don't fit the sequential schema. type slowBlockBAL struct { - // ExecWallMs is wall-clock parallel transaction execution. - ExecWallMs float64 `json:"exec_wall_ms"` - // PostProcessMs is post-tx system contracts (withdrawals, consolidations, finalize). - PostProcessMs float64 `json:"post_process_ms"` - // PrefetchMs is the BAL state prefetcher (alias of state_prefetch_ms). - PrefetchMs float64 `json:"prefetch_ms"` - // StatePrefetchMs is async state-load time during state-root computation. - StatePrefetchMs float64 `json:"state_prefetch_ms"` - // AccountUpdateMs is the account trie update phase. - AccountUpdateMs float64 `json:"account_update_ms"` - // StateUpdateMs is the state trie update phase. - StateUpdateMs float64 `json:"state_update_ms"` - // StateHashMs is state-root hash computation. - StateHashMs float64 `json:"state_hash_ms"` - // AccountCommitMs is the account trie commit to disk. - AccountCommitMs float64 `json:"account_commit_ms"` - // StorageCommitMs is the storage trie commit to disk. - StorageCommitMs float64 `json:"storage_commit_ms"` - // TrieDBCommitMs is the trie database commit. - TrieDBCommitMs float64 `json:"triedb_commit_ms"` - // SnapshotCommitMs is the state snapshot commit. + ExecWallMs float64 `json:"exec_wall_ms"` + PostProcessMs float64 `json:"post_process_ms"` + PrefetchMs float64 `json:"prefetch_ms"` + StatePrefetchMs float64 `json:"state_prefetch_ms"` + AccountUpdateMs float64 `json:"account_update_ms"` + StateUpdateMs float64 `json:"state_update_ms"` + StateHashMs float64 `json:"state_hash_ms"` + AccountCommitMs float64 `json:"account_commit_ms"` + StorageCommitMs float64 `json:"storage_commit_ms"` + TrieDBCommitMs float64 `json:"triedb_commit_ms"` SnapshotCommitMs float64 `json:"snapshot_commit_ms"` } @@ -221,9 +201,8 @@ func durationToMs(d time.Duration) float64 { return float64(d.Nanoseconds()) / 1e6 } -// buildSlowBlockLog constructs the slow-block log JSON struct from execution -// statistics. Pure function — no side effects, no logging — to make the JSON -// shape directly testable. +// buildSlowBlockLog builds the slow-block JSON payload. Split out from logSlow +// so the JSON shape is directly testable. func buildSlowBlockLog(s *ExecuteStats, block *types.Block) slowBlockLog { logEntry := slowBlockLog{ Level: "warn", @@ -278,7 +257,6 @@ func buildSlowBlockLog(s *ExecuteStats, block *types.Block) slowBlockLog { }, }, } - // Populate the parallel-execution extension only for BAL-processed blocks. if m := s.balTransitionStats; m != nil { logEntry.BAL = &slowBlockBAL{ ExecWallMs: durationToMs(s.ExecWall), diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index e65cee16f96f..6d6ba2ba32ed 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -24,17 +24,14 @@ type ProcessResultWithMetrics struct { // the time it took to execute all txs in the block ExecTime time.Duration PostProcessTime time.Duration - // Counts is the per-StateDB sum of state-mutation counters across pre-tx, - // per-tx and post-tx phases. The caller may override AccountLoaded and - // StorageLoaded with deduplicated counts derived from block.AccessList() - // (per-StateDB sums over-count addresses touched by multiple phases). + // Counts sums state-mutation counters across pre-tx, per-tx and post-tx + // StateDBs. AccountLoaded/StorageLoaded are NOT deduplicated here — the + // caller overrides them from block.AccessList(). Counts state.StateCounts - // Reads is the sum of per-StateDB read times across pre-tx, per-tx and - // post-tx phases. Sum-of-CPU-time, not wall-clock. + // Reads sums per-StateDB read times (sum-of-CPU-time, not wall-clock). Reads state.ReadDurations - // CodeLoaded is the deduplicated count of unique contract addresses whose - // code body was fetched during the block (across all phase StateDBs). - // CodeLoadBytes is the sum of those code lengths. + // CodeLoaded/CodeLoadBytes are deduplicated by contract address across + // all phase StateDBs. CodeLoaded int CodeLoadBytes int } @@ -184,9 +181,7 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStar tPostprocess := time.Since(tPostprocessStart) - // Fold post-tx statedb counts and reads into the aggregate. postTxState is - // local and would otherwise be discarded; this captures system-contract - // activity (withdrawal queue, consolidation queue) and engine.Finalize. + // Fold post-tx counts/reads in: postTxState is local and otherwise discarded. aggCounts.Add(postTxState.SnapshotCounts()) aggReads.Add(postTxState.SnapshotReads()) for addr, l := range postTxState.SnapshotCodeLoads() { @@ -230,14 +225,10 @@ type txExecResult struct { stateReads bal.StateAccesses - // Per-tx state-mutation counts and read durations, snapshotted from the - // worker statedb just before send. Aggregated single-threaded in - // resultHandler. - counts state.StateCounts - reads state.ReadDurations - // codeLoads is addr→codeLen for contracts whose code body was fetched - // in this tx. Deduped across all phases in resultHandler. - codeLoads map[common.Address]int + // Per-tx counts/reads/code-loads, aggregated single-threaded in resultHandler. + counts state.StateCounts + reads state.ReadDurations + codeLoads map[common.Address]int // addr → code len, deduped across phases } // resultHandler polls until all transactions have finished executing and the @@ -251,13 +242,9 @@ func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxAccesses var numTxComplete int // Seed aggregates with the pre-tx contribution (BeaconRoot, ParentBlockHash). - // Per-tx fold below; post-tx fold in prepareExecResult. accesses := preTxAccesses aggCounts := preCounts aggReads := preReads - // Dedup'd map of contract addresses whose code body was fetched by any - // phase StateDB. Address-keyed so multiple phases adding the same contract - // only count it once. aggCodeLoads := make(map[common.Address]int) for addr, l := range preCodeLoads { aggCodeLoads[addr] = l @@ -406,9 +393,7 @@ func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, statedb * if !accessList.MutationsAt(0).Eq(mutations) { return nil, state.StateCounts{}, state.ReadDurations{}, nil, fmt.Errorf("invalid block access list: mismatch between local/remote access list mutations at idx 0") } - // Snapshot the pre-tx statedb's counts/reads/code-loads so system-contract - // activity (BeaconRoot, ParentBlockHash) contributes to the aggregate; - // sdb is local and would otherwise be discarded. + // Snapshot pre-tx counts/reads/code-loads: sdb is local and otherwise discarded. return reads, sdb.SnapshotCounts(), sdb.SnapshotReads(), sdb.SnapshotCodeLoads(), nil } diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index ab7bcf37f803..3dbaedc3446a 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -41,20 +41,16 @@ type BALStateTransition struct { tries sync.Map //map[common.Address]Trie deletions map[common.Address]struct{} - // Storage counters use atomic.Int64 because they're written from per-address - // goroutines. The others are written single-threaded inside IntermediateRoot's - // serial mutation loop, so plain int matches StateCounts' int fields. + // Storage/read counters are atomic — written from per-address goroutines. + // account/code counters are plain int — written single-threaded. accountDeleted int accountUpdated int storageDeleted atomic.Int64 storageUpdated atomic.Int64 codeUpdated int codeUpdateBytes int - - // Read-time accumulators for state-root recomputation reads. Atomic - // because s.reader.Account/Storage is called from per-address goroutines. - accountReadNS atomic.Int64 - storageReadNS atomic.Int64 + accountReadNS atomic.Int64 + storageReadNS atomic.Int64 stateUpdate *stateUpdate @@ -64,22 +60,16 @@ type BALStateTransition struct { err error } -// Metrics returns the cached commit/hash-phase timings. Read-time atomics -// are exposed separately via ReadTimes; that decoupling avoids the -// snapshot-staleness pitfall when commitAccount runs more reads after -// Metrics is first called. func (s *BALStateTransition) Metrics() *BALStateTransitionMetrics { return &s.metrics } -// ReadTimes returns the current accumulated read times from atomic counters. -// Always live; safe to call at any point after IntermediateRoot/Commit work. +// ReadTimes returns the accumulated state-read times. func (s *BALStateTransition) ReadTimes() (account, storage time.Duration) { return time.Duration(s.accountReadNS.Load()), time.Duration(s.storageReadNS.Load()) } -// WriteCounts returns the state-mutation counts tracked during the parallel -// state-root computation. +// WriteCounts returns the state-mutation counts from the parallel state-root pass. func (s *BALStateTransition) WriteCounts() StateCounts { return StateCounts{ AccountUpdated: s.accountUpdated, @@ -523,11 +513,9 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { } else { acct, code := s.updateAccount(mutatedAddr) - // Use len(code) > 0 (not code != nil) to match the non-BAL semantic - // at statedb.go (obj.dirtyCode && len(obj.code) > 0). In devnet-3 - // BAL access lists, an empty []byte is non-nil but encodes "no code - // install"; treating it as a code mutation would over-count and - // call UpdateContractCode with an empty payload. + // Empty []byte is non-nil but means "no code install" in devnet-3 + // BAL access lists; matches the obj.dirtyCode && len(obj.code) > 0 + // gate in statedb.go. if len(code) > 0 { codeHash := crypto.Keccak256Hash(code) acct.CodeHash = codeHash.Bytes() diff --git a/core/state/reader.go b/core/state/reader.go index 2128773ddfb1..9cfaab9b7c46 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -565,9 +565,7 @@ func (r *reader) GetStats() ReaderStats { } } -// PrefetchReadTimes returns the prefetcher's accumulated read times if the -// underlying state reader exposes them (e.g. *prefetchStateReader). Returns -// zero if the wrapped reader doesn't track these (sequential paths, tests). +// PrefetchReadTimes forwards to the wrapped prefetcher, or returns zero. func (r *reader) PrefetchReadTimes() (account, storage time.Duration) { if pr, ok := r.StateReader.(interface { PrefetchReadTimes() (time.Duration, time.Duration) @@ -577,8 +575,7 @@ func (r *reader) PrefetchReadTimes() (account, storage time.Duration) { return 0, 0 } -// WaitPrefetch blocks until the wrapped prefetcher (if any) finishes its -// task list. No-op for non-prefetch readers. +// WaitPrefetch blocks until the wrapped prefetcher drains; no-op otherwise. func (r *reader) WaitPrefetch() { if pr, ok := r.StateReader.(interface{ Wait() error }); ok { _ = pr.Wait() diff --git a/core/state/reader_eip_7928.go b/core/state/reader_eip_7928.go index 6f451960fe7a..53949619a981 100644 --- a/core/state/reader_eip_7928.go +++ b/core/state/reader_eip_7928.go @@ -89,8 +89,7 @@ type prefetchStateReader struct { term chan struct{} closeOnce sync.Once - // Async-fetch read-time accumulators (atomic because process() runs - // across N goroutines). + // Atomic — process() runs across N goroutines. accountReadNS atomic.Int64 storageReadNS atomic.Int64 } @@ -374,9 +373,8 @@ func (r *readerTracker) TouchStorage(addr common.Address, slot common.Hash) { list[slot] = struct{}{} } -// GetStateStats forwards stats from the wrapped *stateReaderWithStats so the -// (*reader).GetStateStats type assertion succeeds. Without this, account/ -// storage cache hit/miss counts emit zero on BAL blocks. +// GetStateStats forwards stats from the wrapped reader; without this, BAL +// blocks would emit zero cache hit/miss counts. func (r *prefetchStateReader) GetStateStats() StateReaderStats { if stater, ok := r.StateReader.(StateReaderStater); ok { return stater.GetStateStats() @@ -384,9 +382,8 @@ func (r *prefetchStateReader) GetStateStats() StateReaderStats { return StateReaderStats{} } -// PrefetchReadTimes returns the accumulated wall-time-of-each-call durations -// for asynchronous account/storage prefetches. Sum-of-CPU-time across worker -// goroutines; not wall-clock total prefetch time. +// PrefetchReadTimes returns sum-of-CPU-time across worker goroutines (not +// wall-clock). func (r *prefetchStateReader) PrefetchReadTimes() (account, storage time.Duration) { return time.Duration(r.accountReadNS.Load()), time.Duration(r.storageReadNS.Load()) } diff --git a/core/state/reader_eip_7928_test.go b/core/state/reader_eip_7928_test.go index 57305031be3a..a6f4eb8ecd4c 100644 --- a/core/state/reader_eip_7928_test.go +++ b/core/state/reader_eip_7928_test.go @@ -291,23 +291,17 @@ func TestPrefetchStateReaderForwardsStats(t *testing.T) { } } -// TestReaderForwardsPrefetchReadTimes locks down that the *reader aggregator -// (the type returned by ReaderEIP7928) exposes PrefetchReadTimes via the -// inner *prefetchStateReader. Without the forwarding method on *reader, -// callers that hold a Reader interface would not see the prefetcher's -// accumulated read times even though the prefetcher tracks them. +// TestReaderForwardsPrefetchReadTimes locks down that *reader exposes the +// inner prefetcher's read-time counters via PrefetchReadTimes. func TestReaderForwardsPrefetchReadTimes(t *testing.T) { stub := newRefStateReader() cached := newStateReaderWithCache(stub) withStats := newStateReaderWithStats(cached) prefetch := newPrefetchStateReaderInternal(withStats, nil, 1) - // Seed timer values directly on the prefetcher. prefetch.accountReadNS.Store(123) prefetch.storageReadNS.Store(456) - // Wrap in *reader the way ReaderEIP7928 does (with a nil code reader for - // brevity; PrefetchReadTimes only inspects the state side). r := newReader(nil, prefetch) a, s := r.PrefetchReadTimes() @@ -319,8 +313,8 @@ func TestReaderForwardsPrefetchReadTimes(t *testing.T) { } } -// TestReaderPrefetchReadTimesNonPrefetch verifies the safe zero fallback when -// the wrapped state reader doesn't expose PrefetchReadTimes (sequential path). +// TestReaderPrefetchReadTimesNonPrefetch verifies the zero fallback when the +// wrapped reader doesn't expose PrefetchReadTimes. func TestReaderPrefetchReadTimesNonPrefetch(t *testing.T) { r := newReader(nil, newRefStateReader()) a, s := r.PrefetchReadTimes() diff --git a/core/state/statedb.go b/core/state/statedb.go index 04f90d8cf039..7f97822ca088 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -223,9 +223,8 @@ func (s *StateDB) WithReader(reader Reader) *StateDB { return cpy } -// ReadDurations groups the {Account, Storage, Code} state-read times that are -// aggregated across pre-tx, per-tx and post-tx statedbs in the BAL parallel -// path. Sum-of-CPU-time, not wall-clock. +// ReadDurations groups the {Account, Storage, Code} state-read times. +// Sum-of-CPU-time when aggregated across BAL phase statedbs. type ReadDurations struct { Account time.Duration Storage time.Duration @@ -239,10 +238,8 @@ func (r *ReadDurations) Add(other ReadDurations) { r.Code += other.Code } -// StateCounts holds count-only statistics gathered during a block's state -// transition. Plain-int snapshot type, safe to copy through channels. -// Atomic counters on StateDB are converted at the snapshot boundary in -// SnapshotCounts. Read durations live in ReadDurations (separate type). +// StateCounts is a plain-int snapshot of state-mutation counters. Atomic +// fields on StateDB are Load()'d at the SnapshotCounts boundary. type StateCounts struct { AccountLoaded int // accounts retrieved from the database during the state transition AccountUpdated int // accounts updated during the state transition @@ -256,10 +253,7 @@ type StateCounts struct { CodeUpdateBytes int // total bytes of persisted code written } -// Add merges other into c. Plain integer addition — no atomics here, since -// StateCounts is the snapshot type. The receiver is the only mutated party; -// other is taken by value (the struct is small and value semantics matches -// the snapshot thesis stated above). +// Add merges other into c. func (c *StateCounts) Add(other StateCounts) { c.AccountLoaded += other.AccountLoaded c.AccountUpdated += other.AccountUpdated @@ -273,9 +267,7 @@ func (c *StateCounts) Add(other StateCounts) { c.CodeUpdateBytes += other.CodeUpdateBytes } -// SnapshotCounts returns a value-copy of the state-mutation counters as a -// plain-int StateCounts. Atomic fields are read via Load(); the result is -// safe to copy, pass through channels, and aggregate via StateCounts.Add. +// SnapshotCounts returns a plain-int copy of the state-mutation counters. func (s *StateDB) SnapshotCounts() StateCounts { return StateCounts{ AccountLoaded: s.AccountLoaded, @@ -291,8 +283,7 @@ func (s *StateDB) SnapshotCounts() StateCounts { } } -// SnapshotReads returns a value-copy of the {Account, Storage, Code} read -// durations accumulated on this StateDB. +// SnapshotReads returns the {Account, Storage, Code} read durations. func (s *StateDB) SnapshotReads() ReadDurations { return ReadDurations{ Account: s.AccountReads, @@ -301,9 +292,8 @@ func (s *StateDB) SnapshotReads() ReadDurations { } } -// SnapshotCodeLoads returns the addresses whose contract code body was -// fetched during this StateDB's lifetime, mapped to byte length. Used by the -// BAL parallel pipeline to deduplicate code-load events across phase StateDBs. +// SnapshotCodeLoads returns addresses whose code body was fetched, mapped to +// byte length. Used to deduplicate code-load events across BAL phase StateDBs. func (s *StateDB) SnapshotCodeLoads() map[common.Address]int { if len(s.stateObjects) == 0 { return nil From f5d50d065b97c36012ccdab30f8ac9afcaec3d20 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Tue, 5 May 2026 10:36:37 -0400 Subject: [PATCH 31/42] fix for code mutation --- core/state/bal_state_transition.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index 3dbaedc3446a..7a30c96be58c 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -513,10 +513,7 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { } else { acct, code := s.updateAccount(mutatedAddr) - // Empty []byte is non-nil but means "no code install" in devnet-3 - // BAL access lists; matches the obj.dirtyCode && len(obj.code) > 0 - // gate in statedb.go. - if len(code) > 0 { + if code != nil { codeHash := crypto.Keccak256Hash(code) acct.CodeHash = codeHash.Bytes() if err := s.stateTrie.UpdateContractCode(mutatedAddr, codeHash, code); err != nil { From b01202e3fe6f9e4ec0883da66f3fe30dfd5051ef Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Tue, 5 May 2026 11:33:39 -0400 Subject: [PATCH 32/42] the start of some changes I was experimenting with. broken --- core/blockchain.go | 2 ++ core/state/database.go | 21 +++++++++++++++++++++ core/state/reader.go | 7 ------- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index efe6985001bb..95240293d6b8 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -681,6 +681,8 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * }); ok { prefetchAccountReads, prefetchStorageReads = pr.PrefetchReadTimes() } + prefetchAccountReads, prefetchStorageReads = prefetchReader.(*.PrefetchReadTimes() + balAccountReads, balStorageReads := stateTransition.ReadTimes() stats.AccountReads = res.Reads.Account + prefetchAccountReads + balAccountReads stats.StorageReads = res.Reads.Storage + prefetchStorageReads + balStorageReads diff --git a/core/state/database.go b/core/state/database.go index 7760cc4b5c5e..da44d383abfb 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -18,6 +18,7 @@ package state import ( "fmt" + "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/overlay" @@ -240,6 +241,26 @@ func (db *CachingDB) ReadersWithCacheStats(stateRoot common.Hash) (Reader, Reade return ra, rb, nil } +type ReaderEIP7928Metrics struct { + // the total amount of time it took to complete the scheduled workload + WallElapsed time.Duration + // the aggregated total time spent on state loading by all workers + TotalElapsed time.Duration + // the amount of accounts loaded + Accounts int + // the amount of storage slots loaded + Storages int + // number of accounts with code loaded + Codes int + // total amount of code bytes loaded + CodeBytes int +} + +type ReaderEIP7928 interface { + Reader + Metrics() *ReaderEIP7928Metrics +} + // ReaderEIP7928 creates a state reader with the manner of Block-level accessList. func (db *CachingDB) ReaderEIP7928(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int) (Reader, error) { base, err := db.StateReader(stateRoot) diff --git a/core/state/reader.go b/core/state/reader.go index 9cfaab9b7c46..e6a6b5022c26 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -574,10 +574,3 @@ func (r *reader) PrefetchReadTimes() (account, storage time.Duration) { } return 0, 0 } - -// WaitPrefetch blocks until the wrapped prefetcher drains; no-op otherwise. -func (r *reader) WaitPrefetch() { - if pr, ok := r.StateReader.(interface{ Wait() error }); ok { - _ = pr.Wait() - } -} From 90175152bbeb88288769337eea4ecc3eb359e81e Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 6 May 2026 12:00:33 -0400 Subject: [PATCH 33/42] more changes (wip) --- core/blockchain.go | 25 +++---------- core/blockchain_stats.go | 4 +- core/state/bal_state_transition.go | 52 ++++---------------------- core/state/database.go | 22 ----------- core/state/reader.go | 16 +------- core/state/reader_eip_7928.go | 38 +++++++++++++++---- core/state/statedb.go | 59 +++--------------------------- 7 files changed, 51 insertions(+), 165 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 95240293d6b8..9a9ebe11963d 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -655,13 +655,6 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * // AccountLoaded/StorageLoaded come from the BAL access list (deduplicated); // per-StateDB sums would over-count addresses touched by multiple phases. stats.StateCounts = res.Counts - stats.StateCounts.Add(stateTransition.WriteCounts()) - if al := block.AccessList(); al != nil { - stats.StateCounts.AccountLoaded = al.UniqueAccountCount() - stats.StateCounts.StorageLoaded = al.UniqueStorageSlotCount() - } - stats.StateCounts.CodeLoaded = res.CodeLoaded - stats.StateCounts.CodeLoadBytes = res.CodeLoadBytes stats.Execution = res.ExecTime stats.ExecWall = res.ExecTime @@ -674,19 +667,13 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * stats.DatabaseCommit = m.TrieDBCommits stats.Prefetch = m.StatePrefetch } - // Sum-of-CPU-time across per-tx, BAL state-transition, and prefetcher paths. - var prefetchAccountReads, prefetchStorageReads time.Duration - if pr, ok := prefetchReader.(interface { - PrefetchReadTimes() (time.Duration, time.Duration) - }); ok { - prefetchAccountReads, prefetchStorageReads = pr.PrefetchReadTimes() - } - prefetchAccountReads, prefetchStorageReads = prefetchReader.(*.PrefetchReadTimes() - balAccountReads, balStorageReads := stateTransition.ReadTimes() - stats.AccountReads = res.Reads.Account + prefetchAccountReads + balAccountReads - stats.StorageReads = res.Reads.Storage + prefetchStorageReads + balStorageReads - stats.CodeReads = res.Reads.Code + stats.Prefetch = prefetchReader.(state.PrefetcherMetricer).Metrics().Elapsed + /* + stats.AccountReads = res.Reads.Account + prefetchAccountReads + balAccountReads + stats.StorageReads = res.Reads.Storage + prefetchStorageReads + balStorageReads + stats.CodeReads = res.Reads.Code + */ if r, ok := prefetchReader.(state.ReaderStater); ok { stats.StateReadCacheStats = r.GetStats() diff --git a/core/blockchain_stats.go b/core/blockchain_stats.go index 53834fca9a50..b5278fb13865 100644 --- a/core/blockchain_stats.go +++ b/core/blockchain_stats.go @@ -38,9 +38,7 @@ type ExecuteStats struct { StorageCommits time.Duration // Time spent on the storage trie commit CodeReads time.Duration // Time spent on the contract code read - // State-mutation counts. StorageUpdated/StorageDeleted are int64 - // (snapshot from atomic.Int64 on StateDB). - state.StateCounts + // TODO: where is code bytes loaded metric? Execution time.Duration // Time spent on the EVM execution Validation time.Duration // Time spent on the block validation diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index 7a30c96be58c..881bd337f4e8 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -3,7 +3,6 @@ package state import ( "maps" "sync" - "sync/atomic" "time" "github.com/ethereum/go-ethereum/common" @@ -41,17 +40,6 @@ type BALStateTransition struct { tries sync.Map //map[common.Address]Trie deletions map[common.Address]struct{} - // Storage/read counters are atomic — written from per-address goroutines. - // account/code counters are plain int — written single-threaded. - accountDeleted int - accountUpdated int - storageDeleted atomic.Int64 - storageUpdated atomic.Int64 - codeUpdated int - codeUpdateBytes int - accountReadNS atomic.Int64 - storageReadNS atomic.Int64 - stateUpdate *stateUpdate metrics BALStateTransitionMetrics @@ -64,23 +52,6 @@ func (s *BALStateTransition) Metrics() *BALStateTransitionMetrics { return &s.metrics } -// ReadTimes returns the accumulated state-read times. -func (s *BALStateTransition) ReadTimes() (account, storage time.Duration) { - return time.Duration(s.accountReadNS.Load()), time.Duration(s.storageReadNS.Load()) -} - -// WriteCounts returns the state-mutation counts from the parallel state-root pass. -func (s *BALStateTransition) WriteCounts() StateCounts { - return StateCounts{ - AccountUpdated: s.accountUpdated, - AccountDeleted: s.accountDeleted, - StorageUpdated: s.storageUpdated.Load(), - StorageDeleted: s.storageDeleted.Load(), - CodeUpdated: s.codeUpdated, - CodeUpdateBytes: s.codeUpdateBytes, - } -} - type BALStateTransitionMetrics struct { // trie hashing metrics AccountUpdate time.Duration @@ -223,9 +194,7 @@ func (s *BALStateTransition) commitAccount(addr common.Address) (*accountUpdate, for key, value := range s.diffs[addr].StorageWrites { hash := crypto.Keccak256Hash(key[:]) op.storages[hash] = encode(value) - storageReadStart := time.Now() storage, err := s.reader.Storage(addr, key) - s.storageReadNS.Add(time.Since(storageReadStart).Nanoseconds()) if err != nil { return nil, nil, err } @@ -365,10 +334,13 @@ func (s *BALStateTransition) CommitWithUpdate(block uint64, deleteEmptyObjects b return common.Hash{}, nil, err } - accountUpdatedMeter.Mark(int64(s.accountUpdated)) - storageUpdatedMeter.Mark(s.storageUpdated.Load()) - accountDeletedMeter.Mark(int64(s.accountDeleted)) - storageDeletedMeter.Mark(s.storageDeleted.Load()) + /* + TODO: derive these from the BAL + accountUpdatedMeter.Mark(int64(s.accountUpdated)) + storageUpdatedMeter.Mark(s.storageUpdated.Load()) + accountDeletedMeter.Mark(int64(s.accountDeleted)) + storageDeletedMeter.Mark(s.storageDeleted.Load()) + */ accountTrieUpdatedMeter.Mark(int64(accountTrieNodesUpdated)) accountTrieDeletedMeter.Mark(int64(accountTrieNodesDeleted)) storageTriesUpdatedMeter.Mark(int64(storageTrieNodesUpdated)) @@ -421,9 +393,7 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { defer wg.Done() // 1 (c): update each mutated account, producing the post-block state object by applying the state mutations to the prestate (retrieved in 1a). - accountReadStart := time.Now() acct, err := s.reader.Account(address) - s.accountReadNS.Add(time.Since(accountReadStart).Nanoseconds()) if err != nil { s.setError(err) return @@ -450,12 +420,8 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { if val != (common.Hash{}) { updateKeys = append(updateKeys, key[:]) updateValues = append(updateValues, common.TrimLeftZeroes(val[:])) - - s.storageUpdated.Add(1) } else { deleteKeys = append(deleteKeys, key[:]) - - s.storageDeleted.Add(1) } } if err := tr.UpdateStorageBatch(address, updateKeys, updateValues); err != nil { @@ -509,7 +475,6 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { return common.Hash{} } s.deletions[mutatedAddr] = struct{}{} - s.accountDeleted++ } else { acct, code := s.updateAccount(mutatedAddr) @@ -520,15 +485,12 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { s.setError(err) return common.Hash{} } - s.codeUpdated++ - s.codeUpdateBytes += len(code) } if err := s.stateTrie.UpdateAccount(mutatedAddr, acct, len(code)); err != nil { s.setError(err) return common.Hash{} } s.postStates[mutatedAddr] = acct - s.accountUpdated++ } } diff --git a/core/state/database.go b/core/state/database.go index da44d383abfb..7c85d39a0541 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -18,8 +18,6 @@ package state import ( "fmt" - "time" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/overlay" "github.com/ethereum/go-ethereum/core/rawdb" @@ -241,26 +239,6 @@ func (db *CachingDB) ReadersWithCacheStats(stateRoot common.Hash) (Reader, Reade return ra, rb, nil } -type ReaderEIP7928Metrics struct { - // the total amount of time it took to complete the scheduled workload - WallElapsed time.Duration - // the aggregated total time spent on state loading by all workers - TotalElapsed time.Duration - // the amount of accounts loaded - Accounts int - // the amount of storage slots loaded - Storages int - // number of accounts with code loaded - Codes int - // total amount of code bytes loaded - CodeBytes int -} - -type ReaderEIP7928 interface { - Reader - Metrics() *ReaderEIP7928Metrics -} - // ReaderEIP7928 creates a state reader with the manner of Block-level accessList. func (db *CachingDB) ReaderEIP7928(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int) (Reader, error) { base, err := db.StateReader(stateRoot) diff --git a/core/state/reader.go b/core/state/reader.go index e6a6b5022c26..4f3aa63667df 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -18,10 +18,6 @@ package state import ( "errors" - "sync" - "sync/atomic" - "time" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/overlay" "github.com/ethereum/go-ethereum/core/types" @@ -32,6 +28,8 @@ import ( "github.com/ethereum/go-ethereum/trie/transitiontrie" "github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb/database" + "sync" + "sync/atomic" ) // ContractCodeReader defines the interface for accessing contract code. @@ -564,13 +562,3 @@ func (r *reader) GetStats() ReaderStats { StateStats: r.GetStateStats(), } } - -// PrefetchReadTimes forwards to the wrapped prefetcher, or returns zero. -func (r *reader) PrefetchReadTimes() (account, storage time.Duration) { - if pr, ok := r.StateReader.(interface { - PrefetchReadTimes() (time.Duration, time.Duration) - }); ok { - return pr.PrefetchReadTimes() - } - return 0, 0 -} diff --git a/core/state/reader_eip_7928.go b/core/state/reader_eip_7928.go index 53949619a981..2212c7ac1515 100644 --- a/core/state/reader_eip_7928.go +++ b/core/state/reader_eip_7928.go @@ -64,7 +64,6 @@ package state import ( "sync" - "sync/atomic" "time" "github.com/ethereum/go-ethereum/crypto" @@ -88,10 +87,33 @@ type prefetchStateReader struct { done chan struct{} term chan struct{} closeOnce sync.Once + start time.Time + metrics PrefetchMetrics +} + +type PrefetchMetrics struct { + // the total amount of time it took to complete the scheduled workload + Elapsed time.Duration + // the aggregated total time spent on state loading by all workers + // TODO (jwasinger): add back in after i finish initial commit(s) with only the changes I think will be ultimately merged + // TotalElapsed time.Duration + + /* + // TODO: source these from the other reader where they are implemented + // the amount of accounts loaded + Accounts int + // the amount of storage slots loaded + Storages int + // number of accounts with code loaded + Codes int + // total amount of code bytes loaded + CodeBytes int + } + */ +} - // Atomic — process() runs across N goroutines. - accountReadNS atomic.Int64 - storageReadNS atomic.Int64 +type PrefetcherMetricer interface { + Metrics() PrefetchMetrics } func newPrefetchStateReader(reader StateReader, accessList bal.StorageKeys, nThreads int) *prefetchStateReader { @@ -186,13 +208,9 @@ func (r *prefetchStateReader) process(start, limit int) { return default: if j == 0 { - accountReadStart := time.Now() r.StateReader.Account(t.addr) - r.accountReadNS.Add(time.Since(accountReadStart).Nanoseconds()) } else { - storageReadStart := time.Now() r.StateReader.Storage(t.addr, t.slots[j-1]) - r.storageReadNS.Add(time.Since(storageReadStart).Nanoseconds()) } } } @@ -373,6 +391,9 @@ func (r *readerTracker) TouchStorage(addr common.Address, slot common.Hash) { list[slot] = struct{}{} } +/* +// TODO: ensure these are accounted for + // GetStateStats forwards stats from the wrapped reader; without this, BAL // blocks would emit zero cache hit/miss counts. func (r *prefetchStateReader) GetStateStats() StateReaderStats { @@ -387,3 +408,4 @@ func (r *prefetchStateReader) GetStateStats() StateReaderStats { func (r *prefetchStateReader) PrefetchReadTimes() (account, storage time.Duration) { return time.Duration(r.accountReadNS.Load()), time.Duration(r.storageReadNS.Load()) } +*/ diff --git a/core/state/statedb.go b/core/state/statedb.go index 7f97822ca088..230c90454143 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -223,6 +223,10 @@ func (s *StateDB) WithReader(reader Reader) *StateDB { return cpy } +/* + +// TODO (jwasinger): add these back in a subsequent commit + // ReadDurations groups the {Account, Storage, Code} state-read times. // Sum-of-CPU-time when aggregated across BAL phase statedbs. type ReadDurations struct { @@ -237,60 +241,7 @@ func (r *ReadDurations) Add(other ReadDurations) { r.Storage += other.Storage r.Code += other.Code } - -// StateCounts is a plain-int snapshot of state-mutation counters. Atomic -// fields on StateDB are Load()'d at the SnapshotCounts boundary. -type StateCounts struct { - AccountLoaded int // accounts retrieved from the database during the state transition - AccountUpdated int // accounts updated during the state transition - AccountDeleted int // accounts deleted during the state transition - StorageLoaded int // storage slots retrieved from the database during the state transition - StorageUpdated int64 // storage slots updated (snapshotted from atomic on StateDB) - StorageDeleted int64 // storage slots deleted (snapshotted from atomic on StateDB) - CodeLoaded int // contract code reads - CodeLoadBytes int // total bytes of resolved code - CodeUpdated int // code writes (CREATE/CREATE2/EIP-7702) - CodeUpdateBytes int // total bytes of persisted code written -} - -// Add merges other into c. -func (c *StateCounts) Add(other StateCounts) { - c.AccountLoaded += other.AccountLoaded - c.AccountUpdated += other.AccountUpdated - c.AccountDeleted += other.AccountDeleted - c.StorageLoaded += other.StorageLoaded - c.StorageUpdated += other.StorageUpdated - c.StorageDeleted += other.StorageDeleted - c.CodeLoaded += other.CodeLoaded - c.CodeLoadBytes += other.CodeLoadBytes - c.CodeUpdated += other.CodeUpdated - c.CodeUpdateBytes += other.CodeUpdateBytes -} - -// SnapshotCounts returns a plain-int copy of the state-mutation counters. -func (s *StateDB) SnapshotCounts() StateCounts { - return StateCounts{ - AccountLoaded: s.AccountLoaded, - AccountUpdated: s.AccountUpdated, - AccountDeleted: s.AccountDeleted, - StorageLoaded: s.StorageLoaded, - StorageUpdated: s.StorageUpdated.Load(), - StorageDeleted: s.StorageDeleted.Load(), - CodeLoaded: s.CodeLoaded, - CodeLoadBytes: s.CodeLoadBytes, - CodeUpdated: s.CodeUpdated, - CodeUpdateBytes: s.CodeUpdateBytes, - } -} - -// SnapshotReads returns the {Account, Storage, Code} read durations. -func (s *StateDB) SnapshotReads() ReadDurations { - return ReadDurations{ - Account: s.AccountReads, - Storage: s.StorageReads, - Code: s.CodeReads, - } -} +*/ // SnapshotCodeLoads returns addresses whose code body was fetched, mapped to // byte length. Used to deduplicate code-load events across BAL phase StateDBs. From 052a24c353938496ec113573990e764347a8fc94 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 6 May 2026 12:32:45 -0400 Subject: [PATCH 34/42] it builds --- core/blockchain.go | 6 --- core/blockchain_stats.go | 12 +++++- core/parallel_state_processor.go | 70 ++++---------------------------- 3 files changed, 20 insertions(+), 68 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 9a9ebe11963d..4009a5414e60 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -652,10 +652,6 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * writeTime := time.Since(writeStart) var stats ExecuteStats - // AccountLoaded/StorageLoaded come from the BAL access list (deduplicated); - // per-StateDB sums would over-count addresses touched by multiple phases. - stats.StateCounts = res.Counts - stats.Execution = res.ExecTime stats.ExecWall = res.ExecTime stats.PostProcess = res.PostProcessTime @@ -2449,8 +2445,6 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, stats.AccountHashes = statedb.AccountHashes // Account hashes are complete(in validation) stats.CodeReads = statedb.CodeReads - stats.StateCounts = statedb.SnapshotCounts() - stats.Execution = ptime - (statedb.AccountReads + statedb.StorageReads + statedb.CodeReads) // The time spent on EVM processing stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) // The time spent on block validation stats.CrossValidation = xvtime // The time spent on stateless cross validation diff --git a/core/blockchain_stats.go b/core/blockchain_stats.go index b5278fb13865..d2d28a1c792e 100644 --- a/core/blockchain_stats.go +++ b/core/blockchain_stats.go @@ -38,7 +38,17 @@ type ExecuteStats struct { StorageCommits time.Duration // Time spent on the storage trie commit CodeReads time.Duration // Time spent on the contract code read - // TODO: where is code bytes loaded metric? + // TODO: code bytes loaded + AccountLoaded int // Number of accounts loaded + AccountUpdated int // Number of accounts updated + AccountDeleted int // Number of accounts deleted + StorageLoaded int // Number of storage slots loaded + StorageUpdated int // Number of storage slots updated + StorageDeleted int // Number of storage slots deleted + CodeLoaded int // Number of contract code loaded + CodeLoadBytes int // Number of bytes read from contract code + CodeUpdated int // Number of contract code written (CREATE/CREATE2 + EIP-7702) + CodeUpdateBytes int // Total bytes of code written Execution time.Duration // Time spent on the EVM execution Validation time.Duration // Time spent on the block validation diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 6d6ba2ba32ed..2c6aaf02f62f 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -7,7 +7,6 @@ import ( "slices" "time" - "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types/bal" @@ -24,16 +23,7 @@ type ProcessResultWithMetrics struct { // the time it took to execute all txs in the block ExecTime time.Duration PostProcessTime time.Duration - // Counts sums state-mutation counters across pre-tx, per-tx and post-tx - // StateDBs. AccountLoaded/StorageLoaded are NOT deduplicated here — the - // caller overrides them from block.AccessList(). - Counts state.StateCounts - // Reads sums per-StateDB read times (sum-of-CPU-time, not wall-clock). - Reads state.ReadDurations - // CodeLoaded/CodeLoadBytes are deduplicated by contract address across - // all phase StateDBs. - CodeLoaded int - CodeLoadBytes int + // TODO: have the prefetch metric in here as well? } // ParallelStateProcessor is used to execute and verify blocks containing @@ -82,7 +72,7 @@ func validateStateAccesses(lastIdx int, accessList bal.AccessListReader, localAc // performs post-tx state transition (system contracts and withdrawals) // and calculates the ProcessResult, returning it to be sent on resCh // by resultHandler -func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStart time.Time, accesses bal.StateAccesses, statedb *state.StateDB, prefetchReader state.Reader, results []txExecResult, aggCounts state.StateCounts, aggReads state.ReadDurations, aggCodeLoads map[common.Address]int) *ProcessResultWithMetrics { +func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStart time.Time, accesses bal.StateAccesses, statedb *state.StateDB, prefetchReader state.Reader, results []txExecResult) *ProcessResultWithMetrics { tExec := time.Since(tExecStart) var requests [][]byte tPostprocessStart := time.Now() @@ -181,21 +171,6 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStar tPostprocess := time.Since(tPostprocessStart) - // Fold post-tx counts/reads in: postTxState is local and otherwise discarded. - aggCounts.Add(postTxState.SnapshotCounts()) - aggReads.Add(postTxState.SnapshotReads()) - for addr, l := range postTxState.SnapshotCodeLoads() { - if _, ok := aggCodeLoads[addr]; !ok { - aggCodeLoads[addr] = l - } - } - - codeLoaded := len(aggCodeLoads) - var codeLoadBytes int - for _, l := range aggCodeLoads { - codeLoadBytes += l - } - return &ProcessResultWithMetrics{ ProcessResult: &ProcessResult{ Receipts: allReceipts, @@ -205,10 +180,6 @@ func (p *ParallelStateProcessor) prepareExecResult(block *types.Block, tExecStar }, PostProcessTime: tPostprocess, ExecTime: tExec, - Counts: aggCounts, - Reads: aggReads, - CodeLoaded: codeLoaded, - CodeLoadBytes: codeLoadBytes, } } @@ -224,31 +195,18 @@ type txExecResult struct { txState uint64 stateReads bal.StateAccesses - - // Per-tx counts/reads/code-loads, aggregated single-threaded in resultHandler. - counts state.StateCounts - reads state.ReadDurations - codeLoads map[common.Address]int // addr → code len, deduped across phases } // resultHandler polls until all transactions have finished executing and the // state root calculation is complete. The result is emitted on resCh. -func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxAccesses bal.StateAccesses, preCounts state.StateCounts, preReads state.ReadDurations, preCodeLoads map[common.Address]int, statedb *state.StateDB, prefetchReader state.Reader, tExecStart time.Time, txResCh <-chan txExecResult, stateRootCalcResCh <-chan stateRootCalculationResult, resCh chan *ProcessResultWithMetrics) { +func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxAccesses bal.StateAccesses, statedb *state.StateDB, prefetchReader state.Reader, tExecStart time.Time, txResCh <-chan txExecResult, stateRootCalcResCh <-chan stateRootCalculationResult, resCh chan *ProcessResultWithMetrics) { // 1. if the block has transactions, receive the execution results from all of them and return an error on resCh if any txs err'd // 2. once all txs are executed, compute the post-tx state transition and produce the ProcessResult sending it on resCh (or an error if the post-tx state didn't match what is reported in the BAL) var results []txExecResult var cumulativeStateGas, cumulativeRegularGas uint64 var execErr error var numTxComplete int - - // Seed aggregates with the pre-tx contribution (BeaconRoot, ParentBlockHash). accesses := preTxAccesses - aggCounts := preCounts - aggReads := preReads - aggCodeLoads := make(map[common.Address]int) - for addr, l := range preCodeLoads { - aggCodeLoads[addr] = l - } if len(block.Transactions()) > 0 { loop: @@ -266,13 +224,6 @@ func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxAccesses cumulativeStateGas += res.txState results = append(results, res) accesses.Merge(res.stateReads) - aggCounts.Add(res.counts) - aggReads.Add(res.reads) - for addr, l := range res.codeLoads { - if _, ok := aggCodeLoads[addr]; !ok { - aggCodeLoads[addr] = l - } - } } } if numTxComplete == len(block.Transactions()) { @@ -289,7 +240,7 @@ func (p *ParallelStateProcessor) resultHandler(block *types.Block, preTxAccesses } } - execResults := p.prepareExecResult(block, tExecStart, accesses, statedb, prefetchReader, results, aggCounts, aggReads, aggCodeLoads) + execResults := p.prepareExecResult(block, tExecStart, accesses, statedb, prefetchReader, results) rootCalcRes := <-stateRootCalcResCh if execResults.ProcessResult.Error != nil { @@ -363,13 +314,10 @@ func (p *ParallelStateProcessor) execTx(block *types.Block, tx *types.Transactio txRegular: txRegular, txState: txState, stateReads: db.Reader().(state.StateReaderTracker).GetStateAccessList(), - counts: db.SnapshotCounts(), - reads: db.SnapshotReads(), - codeLoads: db.SnapshotCodeLoads(), } } -func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, statedb *state.StateDB, prefetchReader state.Reader, cfg vm.Config) (bal.StateAccesses, state.StateCounts, state.ReadDurations, map[common.Address]int, error) { +func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, statedb *state.StateDB, prefetchReader state.Reader, cfg vm.Config) (bal.StateAccesses, error) { var ( header = block.Header() ) @@ -391,10 +339,10 @@ func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, statedb * mutations.Merge(pbhMutations) reads := readerWithTracker.(state.StateReaderTracker).GetStateAccessList() if !accessList.MutationsAt(0).Eq(mutations) { - return nil, state.StateCounts{}, state.ReadDurations{}, nil, fmt.Errorf("invalid block access list: mismatch between local/remote access list mutations at idx 0") + return nil, fmt.Errorf("invalid block access list: mismatch between local/remote access list mutations at idx 0") } // Snapshot pre-tx counts/reads/code-loads: sdb is local and otherwise discarded. - return reads, sdb.SnapshotCounts(), sdb.SnapshotReads(), sdb.SnapshotCodeLoads(), nil + return reads, nil } // Process performs EVM execution and state root computation for a block which is known @@ -414,7 +362,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st ) startingState := statedb.Copy() - preTxReads, preCounts, preReads, preCodeLoads, err := p.processBlockPreTx(block, statedb, balReader, cfg) + preTxReads, err := p.processBlockPreTx(block, statedb, balReader, cfg) if err != nil { return nil, err } @@ -424,7 +372,7 @@ func (p *ParallelStateProcessor) Process(block *types.Block, stateTransition *st // execute transactions and state root calculation in parallel tExecStart = time.Now() - go p.resultHandler(block, preTxReads, preCounts, preReads, preCodeLoads, statedb, balReader, tExecStart, txResCh, rootCalcResultCh, resCh) + go p.resultHandler(block, preTxReads, statedb, balReader, tExecStart, txResCh, rootCalcResultCh, resCh) var workers errgroup.Group workers.SetLimit(runtime.NumCPU()) for i, t := range block.Transactions() { From 5c101bba03fd3b28afbd8372d33549acca39b5b3 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 6 May 2026 12:51:10 -0400 Subject: [PATCH 35/42] try fix --- core/state/database.go | 2 +- core/state/reader.go | 9 +++++++++ core/state/reader_eip_7928.go | 5 +++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/core/state/database.go b/core/state/database.go index 7c85d39a0541..5628fb211c6d 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -251,7 +251,7 @@ func (db *CachingDB) ReaderEIP7928(stateRoot common.Hash, accessList map[common. // Construct the state reader with background prefetching pr := newPrefetchStateReader(r, accessList, threads) - return newReader(db.codedb.Reader(), pr), nil + return newReaderWithPrefetch(db.codedb.Reader(), pr, pr), nil } // OpenTrie opens the main account trie at a specific root hash. diff --git a/core/state/reader.go b/core/state/reader.go index 4f3aa63667df..692536350de9 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -529,6 +529,7 @@ func (r *stateReaderWithStats) GetStateStats() StateReaderStats { type reader struct { ContractCodeReader StateReader + PrefetcherMetricer } // newReader constructs a reader with the supplied code reader and state reader. @@ -539,6 +540,14 @@ func newReader(codeReader ContractCodeReader, stateReader StateReader) *reader { } } +func newReaderWithPrefetch(codeReader ContractCodeReader, stateReader StateReader, metricer PrefetcherMetricer) *reader { + return &reader{ + ContractCodeReader: codeReader, + StateReader: stateReader, + PrefetcherMetricer: metricer, + } +} + // GetCodeStats returns the statistics of code access. func (r *reader) GetCodeStats() ContractCodeReaderStats { if stater, ok := r.ContractCodeReader.(ContractCodeReaderStater); ok { diff --git a/core/state/reader_eip_7928.go b/core/state/reader_eip_7928.go index 2212c7ac1515..bdd0ecf09fff 100644 --- a/core/state/reader_eip_7928.go +++ b/core/state/reader_eip_7928.go @@ -139,6 +139,11 @@ func newPrefetchStateReaderInternal(reader StateReader, tasks []*fetchTask, nThr return r } +func (r *prefetchStateReader) Metrics() PrefetchMetrics { + // TODO (jwasinger) actually implement this + return PrefetchMetrics{} +} + func (r *prefetchStateReader) Close() { r.closeOnce.Do(func() { close(r.term) From 10314a1fe1a0b2c43ad538231b1614763af39be2 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 6 May 2026 13:31:26 -0400 Subject: [PATCH 36/42] add --bal.prefetchworkers flag to parameterize state loading concurrency when executing with BALs --- cmd/geth/main.go | 1 + cmd/utils/flags.go | 9 +++++++++ core/blockchain.go | 4 +++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index da1623be7c34..ad8ab1fc1e74 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -159,6 +159,7 @@ var ( utils.BeaconCheckpointFlag, utils.BeaconCheckpointFileFlag, utils.LogSlowBlockFlag, + utils.PrefetchWorkersFlag, }, utils.NetworkFlags, utils.DatabaseFlags) rpcFlags = []cli.Flag{ diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 7d8c47e4f752..27e840ee6492 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -28,6 +28,7 @@ import ( "net/http" "os" "path/filepath" + "runtime" godebug "runtime/debug" "strconv" "strings" @@ -713,6 +714,13 @@ var ( Category: flags.MiscCategory, } + PrefetchWorkersFlag = &cli.UintFlag{ + Name: "bal.prefetchworkers", + Usage: "The number of concurrent state loading tasks to perform when prefetching BAL state. Default to the number of cpus", + Value: uint(runtime.NumCPU()), + Category: flags.MiscCategory, + } + // RPC settings IPCDisabledFlag = &cli.BoolFlag{ Name: "ipcdisable", @@ -2459,6 +2467,7 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockCh TrienodeHistory: ctx.Int64(TrienodeHistoryFlag.Name), NodeFullValueCheckpoint: uint32(ctx.Uint(TrienodeHistoryFullValueCheckpointFlag.Name)), + PrefetchWorkers: int(ctx.Uint(PrefetchWorkersFlag.Name)), // Disable transaction indexing/unindexing. TxLookupLimit: -1, diff --git a/core/blockchain.go b/core/blockchain.go index 4009a5414e60..33172aff913c 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -211,6 +211,8 @@ type BlockChainConfig struct { Overrides *ChainOverrides // Optional chain config overrides VmConfig vm.Config // Config options for the EVM Interpreter + PrefetchWorkers int // number of concurrent go-routines for BAL state prefetching + // TxLookupLimit specifies the maximum number of blocks from head for which // transaction hashes will be indexed. // @@ -597,7 +599,7 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * useAsyncReads := bc.cfg.BALExecutionMode != bal.BALExecutionNoBatchIO al := block.AccessList() // TODO: make the return of this method not be a pointer accessListReader := bal.NewAccessListReader(*al) - prefetchReader, err := sdb.ReaderEIP7928(parentRoot, accessListReader.StorageKeys(useAsyncReads), runtime.NumCPU()) + prefetchReader, err := sdb.ReaderEIP7928(parentRoot, accessListReader.StorageKeys(useAsyncReads), bc.cfg.PrefetchWorkers) if err != nil { return nil, err } From 457491107ab592919d027e7554de0732701cad32 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 6 May 2026 13:37:25 -0400 Subject: [PATCH 37/42] fix --- tests/block_test_util.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/block_test_util.go b/tests/block_test_util.go index 657cb55f940a..ab3a0e3b75b0 100644 --- a/tests/block_test_util.go +++ b/tests/block_test_util.go @@ -162,6 +162,7 @@ func (t *BlockTest) createTestBlockChain(config *params.ChainConfig, snapshotter }, StatelessSelfValidation: witness, NoPrefetch: true, + PrefetchWorkers: 100, // note: this is totally unrelated to NoPrefetch, just for BAL execution } if snapshotter { options.SnapshotLimit = 1 From ac8354d4ca4bf203b48dd8922cfae4402fdbd037 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 6 May 2026 13:48:19 -0400 Subject: [PATCH 38/42] add --bal.blockingprefetch: if enabled, will ensure that when executing with a BAL, state loading tasks are completed before tx and state root calculation. can be used with --bal.prefetchworkers --- cmd/geth/main.go | 1 + cmd/utils/flags.go | 9 ++++++++- core/blockchain.go | 6 ++++-- core/state/database.go | 7 ++++++- tests/block_test_util.go | 1 + 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index ad8ab1fc1e74..56ec5b154162 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -160,6 +160,7 @@ var ( utils.BeaconCheckpointFileFlag, utils.LogSlowBlockFlag, utils.PrefetchWorkersFlag, + utils.BlockingPrefetch, }, utils.NetworkFlags, utils.DatabaseFlags) rpcFlags = []cli.Flag{ diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 27e840ee6492..a1f15d52db84 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -721,6 +721,12 @@ var ( Category: flags.MiscCategory, } + BlockingPrefetch = &cli.BoolFlag{ + Name: "bal.blockingprefetch", + Usage: "only relevant when executing in parallel with a BAL: if true, the prefetcher will block tx/state-root calculation until all scheduled fetching tasks have completed.", + Category: flags.MiscCategory, + } + // RPC settings IPCDisabledFlag = &cli.BoolFlag{ Name: "ipcdisable", @@ -2467,7 +2473,8 @@ func MakeChain(ctx *cli.Context, stack *node.Node, readonly bool) (*core.BlockCh TrienodeHistory: ctx.Int64(TrienodeHistoryFlag.Name), NodeFullValueCheckpoint: uint32(ctx.Uint(TrienodeHistoryFullValueCheckpointFlag.Name)), - PrefetchWorkers: int(ctx.Uint(PrefetchWorkersFlag.Name)), + PrefetchWorkers: int(ctx.Uint(PrefetchWorkersFlag.Name)), + BlockingPrefetch: ctx.Bool(BlockingPrefetch.Name), // Disable transaction indexing/unindexing. TxLookupLimit: -1, diff --git a/core/blockchain.go b/core/blockchain.go index 33172aff913c..d55f1baecb26 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -211,7 +211,9 @@ type BlockChainConfig struct { Overrides *ChainOverrides // Optional chain config overrides VmConfig vm.Config // Config options for the EVM Interpreter - PrefetchWorkers int // number of concurrent go-routines for BAL state prefetching + // BAL-related + PrefetchWorkers int // number of concurrent go-routines for BAL state prefetching + BlockingPrefetch bool // whether the prefetch should block further execution until it finishes // TxLookupLimit specifies the maximum number of blocks from head for which // transaction hashes will be indexed. @@ -599,7 +601,7 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * useAsyncReads := bc.cfg.BALExecutionMode != bal.BALExecutionNoBatchIO al := block.AccessList() // TODO: make the return of this method not be a pointer accessListReader := bal.NewAccessListReader(*al) - prefetchReader, err := sdb.ReaderEIP7928(parentRoot, accessListReader.StorageKeys(useAsyncReads), bc.cfg.PrefetchWorkers) + prefetchReader, err := sdb.ReaderEIP7928(parentRoot, accessListReader.StorageKeys(useAsyncReads), bc.cfg.PrefetchWorkers, bc.cfg.BlockingPrefetch) if err != nil { return nil, err } diff --git a/core/state/database.go b/core/state/database.go index 5628fb211c6d..c7b91c90c5fc 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -240,7 +240,7 @@ func (db *CachingDB) ReadersWithCacheStats(stateRoot common.Hash) (Reader, Reade } // ReaderEIP7928 creates a state reader with the manner of Block-level accessList. -func (db *CachingDB) ReaderEIP7928(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int) (Reader, error) { +func (db *CachingDB) ReaderEIP7928(stateRoot common.Hash, accessList map[common.Address][]common.Hash, threads int, block bool) (Reader, error) { base, err := db.StateReader(stateRoot) if err != nil { return nil, err @@ -250,6 +250,11 @@ func (db *CachingDB) ReaderEIP7928(stateRoot common.Hash, accessList map[common. // Construct the state reader with background prefetching pr := newPrefetchStateReader(r, accessList, threads) + if block { + if err := pr.Wait(); err != nil { + panic("wat do") + } + } return newReaderWithPrefetch(db.codedb.Reader(), pr, pr), nil } diff --git a/tests/block_test_util.go b/tests/block_test_util.go index ab3a0e3b75b0..4f8b5f18ed99 100644 --- a/tests/block_test_util.go +++ b/tests/block_test_util.go @@ -162,6 +162,7 @@ func (t *BlockTest) createTestBlockChain(config *params.ChainConfig, snapshotter }, StatelessSelfValidation: witness, NoPrefetch: true, + BlockingPrefetch: true, PrefetchWorkers: 100, // note: this is totally unrelated to NoPrefetch, just for BAL execution } if snapshotter { From aa8745521f14f5b08ddf7b39047ad17fdb469b2e Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 6 May 2026 14:28:25 -0400 Subject: [PATCH 39/42] core/state: note that error case is unreachable --- core/state/database.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/state/database.go b/core/state/database.go index c7b91c90c5fc..fb878b78c991 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -252,7 +252,7 @@ func (db *CachingDB) ReaderEIP7928(stateRoot common.Hash, accessList map[common. pr := newPrefetchStateReader(r, accessList, threads) if block { if err := pr.Wait(); err != nil { - panic("wat do") + panic("unreachable") } } From b7118dccfeaffc9ad94f55104c316fce151da906 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 6 May 2026 14:43:40 -0400 Subject: [PATCH 40/42] cleanup --- core/blockchain.go | 18 +++++--- core/parallel_state_processor.go | 1 - core/state/bal_state_transition.go | 12 +++--- core/state/reader_eip_7928.go | 35 ---------------- core/state/reader_eip_7928_test.go | 67 ++---------------------------- core/state/statedb.go | 38 ----------------- core/types/bal/bal_encoding.go | 17 -------- 7 files changed, 23 insertions(+), 165 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index d55f1baecb26..71746c221ca0 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -656,7 +656,6 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * writeTime := time.Since(writeStart) var stats ExecuteStats - stats.Execution = res.ExecTime stats.ExecWall = res.ExecTime stats.PostProcess = res.PostProcessTime @@ -669,11 +668,6 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * } stats.Prefetch = prefetchReader.(state.PrefetcherMetricer).Metrics().Elapsed - /* - stats.AccountReads = res.Reads.Account + prefetchAccountReads + balAccountReads - stats.StorageReads = res.Reads.Storage + prefetchStorageReads + balStorageReads - stats.CodeReads = res.Reads.Code - */ if r, ok := prefetchReader.(state.ReaderStater); ok { stats.StateReadCacheStats = r.GetStats() @@ -2449,6 +2443,18 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, stats.AccountHashes = statedb.AccountHashes // Account hashes are complete(in validation) stats.CodeReads = statedb.CodeReads + stats.AccountLoaded = statedb.AccountLoaded + stats.AccountUpdated = statedb.AccountUpdated + stats.AccountDeleted = statedb.AccountDeleted + stats.StorageLoaded = statedb.StorageLoaded + stats.StorageUpdated = int(statedb.StorageUpdated.Load()) + stats.StorageDeleted = int(statedb.StorageDeleted.Load()) + + stats.CodeLoaded = statedb.CodeLoaded + stats.CodeLoadBytes = statedb.CodeLoadBytes + stats.CodeUpdated = statedb.CodeUpdated + stats.CodeUpdateBytes = statedb.CodeUpdateBytes + stats.Execution = ptime - (statedb.AccountReads + statedb.StorageReads + statedb.CodeReads) // The time spent on EVM processing stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) // The time spent on block validation stats.CrossValidation = xvtime // The time spent on stateless cross validation diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index 2c6aaf02f62f..e2507dda7056 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -341,7 +341,6 @@ func (p *ParallelStateProcessor) processBlockPreTx(block *types.Block, statedb * if !accessList.MutationsAt(0).Eq(mutations) { return nil, fmt.Errorf("invalid block access list: mismatch between local/remote access list mutations at idx 0") } - // Snapshot pre-tx counts/reads/code-loads: sdb is local and otherwise discarded. return reads, nil } diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index 881bd337f4e8..2178169c7f6e 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -335,11 +335,13 @@ func (s *BALStateTransition) CommitWithUpdate(block uint64, deleteEmptyObjects b } /* - TODO: derive these from the BAL - accountUpdatedMeter.Mark(int64(s.accountUpdated)) - storageUpdatedMeter.Mark(s.storageUpdated.Load()) - accountDeletedMeter.Mark(int64(s.accountDeleted)) - storageDeletedMeter.Mark(s.storageDeleted.Load()) + TODO: derive these from the BAL + ^ I think even then, there is a semantic difference with how these metrics were calculated previously + I don't know if it makes sense to recompute those, or just derive new ones from the BAL + accountUpdatedMeter.Mark(int64(s.accountUpdated)) + storageUpdatedMeter.Mark(s.storageUpdated.Load()) + accountDeletedMeter.Mark(int64(s.accountDeleted)) + storageDeletedMeter.Mark(s.storageDeleted.Load()) */ accountTrieUpdatedMeter.Mark(int64(accountTrieNodesUpdated)) accountTrieDeletedMeter.Mark(int64(accountTrieNodesDeleted)) diff --git a/core/state/reader_eip_7928.go b/core/state/reader_eip_7928.go index bdd0ecf09fff..e3f917029566 100644 --- a/core/state/reader_eip_7928.go +++ b/core/state/reader_eip_7928.go @@ -94,22 +94,6 @@ type prefetchStateReader struct { type PrefetchMetrics struct { // the total amount of time it took to complete the scheduled workload Elapsed time.Duration - // the aggregated total time spent on state loading by all workers - // TODO (jwasinger): add back in after i finish initial commit(s) with only the changes I think will be ultimately merged - // TotalElapsed time.Duration - - /* - // TODO: source these from the other reader where they are implemented - // the amount of accounts loaded - Accounts int - // the amount of storage slots loaded - Storages int - // number of accounts with code loaded - Codes int - // total amount of code bytes loaded - CodeBytes int - } - */ } type PrefetcherMetricer interface { @@ -395,22 +379,3 @@ func (r *readerTracker) TouchStorage(addr common.Address, slot common.Hash) { } list[slot] = struct{}{} } - -/* -// TODO: ensure these are accounted for - -// GetStateStats forwards stats from the wrapped reader; without this, BAL -// blocks would emit zero cache hit/miss counts. -func (r *prefetchStateReader) GetStateStats() StateReaderStats { - if stater, ok := r.StateReader.(StateReaderStater); ok { - return stater.GetStateStats() - } - return StateReaderStats{} -} - -// PrefetchReadTimes returns sum-of-CPU-time across worker goroutines (not -// wall-clock). -func (r *prefetchStateReader) PrefetchReadTimes() (account, storage time.Duration) { - return time.Duration(r.accountReadNS.Load()), time.Duration(r.storageReadNS.Load()) -} -*/ diff --git a/core/state/reader_eip_7928_test.go b/core/state/reader_eip_7928_test.go index a6f4eb8ecd4c..8e30bc5f7788 100644 --- a/core/state/reader_eip_7928_test.go +++ b/core/state/reader_eip_7928_test.go @@ -209,10 +209,10 @@ func TestReaderWithTracker(t *testing.T) { // transactions read without hitting the reader, causing the BAL to be incomplete. func TestTrackerSurvivesStateDBCache(t *testing.T) { var ( - sdb = NewDatabaseForTesting() - statedb, _ = New(types.EmptyRootHash, sdb) - addr = common.HexToAddress("0xaaaa") - slot = common.HexToHash("0x01") + sdb = NewDatabaseForTesting() + statedb, _ = New(types.EmptyRootHash, sdb) + addr = common.HexToAddress("0xaaaa") + slot = common.HexToHash("0x01") ) // Set up committed state with one account that has a storage slot. statedb.SetBalance(addr, uint256.NewInt(1e18), tracing.BalanceChangeUnspecified) @@ -263,62 +263,3 @@ func TestTrackerSurvivesStateDBCache(t *testing.T) { t.Fatal("slot must be tracked on cache hit (storage)") } } - -// TestPrefetchStateReaderForwardsStats locks down that prefetchStateReader -// exposes the underlying stateReaderWithStats counters via GetStateStats. -func TestPrefetchStateReaderForwardsStats(t *testing.T) { - stub := newRefStateReader() - addr := testrand.Address() - - cached := newStateReaderWithCache(stub) - withStats := newStateReaderWithStats(cached) - prefetch := newPrefetchStateReaderInternal(withStats, nil, 1) - - if _, err := prefetch.Account(addr); err != nil { - t.Fatalf("Account: %v", err) - } - if _, err := prefetch.Account(addr); err != nil { - t.Fatalf("Account (second): %v", err) - } - - stats := withStats.GetStateStats() - if stats.AccountCacheHit == 0 || stats.AccountCacheMiss == 0 { - t.Fatalf("inner stats not populated: %+v", stats) - } - gotStats := prefetch.GetStateStats() - if gotStats != stats { - t.Fatalf("forward mismatch: got %+v, want %+v", gotStats, stats) - } -} - -// TestReaderForwardsPrefetchReadTimes locks down that *reader exposes the -// inner prefetcher's read-time counters via PrefetchReadTimes. -func TestReaderForwardsPrefetchReadTimes(t *testing.T) { - stub := newRefStateReader() - cached := newStateReaderWithCache(stub) - withStats := newStateReaderWithStats(cached) - prefetch := newPrefetchStateReaderInternal(withStats, nil, 1) - - prefetch.accountReadNS.Store(123) - prefetch.storageReadNS.Store(456) - - r := newReader(nil, prefetch) - - a, s := r.PrefetchReadTimes() - if a != 123 { - t.Errorf("account: got %v, want 123", a) - } - if s != 456 { - t.Errorf("storage: got %v, want 456", s) - } -} - -// TestReaderPrefetchReadTimesNonPrefetch verifies the zero fallback when the -// wrapped reader doesn't expose PrefetchReadTimes. -func TestReaderPrefetchReadTimesNonPrefetch(t *testing.T) { - r := newReader(nil, newRefStateReader()) - a, s := r.PrefetchReadTimes() - if a != 0 || s != 0 { - t.Errorf("expected (0, 0), got (%v, %v)", a, s) - } -} diff --git a/core/state/statedb.go b/core/state/statedb.go index 230c90454143..b8081c149a55 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -223,44 +223,6 @@ func (s *StateDB) WithReader(reader Reader) *StateDB { return cpy } -/* - -// TODO (jwasinger): add these back in a subsequent commit - -// ReadDurations groups the {Account, Storage, Code} state-read times. -// Sum-of-CPU-time when aggregated across BAL phase statedbs. -type ReadDurations struct { - Account time.Duration - Storage time.Duration - Code time.Duration -} - -// Add merges other into r. -func (r *ReadDurations) Add(other ReadDurations) { - r.Account += other.Account - r.Storage += other.Storage - r.Code += other.Code -} -*/ - -// SnapshotCodeLoads returns addresses whose code body was fetched, mapped to -// byte length. Used to deduplicate code-load events across BAL phase StateDBs. -func (s *StateDB) SnapshotCodeLoads() map[common.Address]int { - if len(s.stateObjects) == 0 { - return nil - } - var m map[common.Address]int - for addr, obj := range s.stateObjects { - if l := len(obj.code); l > 0 { - if m == nil { - m = make(map[common.Address]int) - } - m[addr] = l - } - } - return m -} - // StartPrefetcher initializes a new trie prefetcher to pull in nodes from the // state trie concurrently while the state is mutated so that when we reach the // commit phase, most of the needed data is already hot. diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index 0fddd5c32c44..fc1d36007ef6 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -44,23 +44,6 @@ import ( // BlockAccessList is the encoding format of AccessListBuilder. type BlockAccessList []AccountAccess -// UniqueAccountCount returns the number of distinct account addresses in -// the block access list. -func (e BlockAccessList) UniqueAccountCount() int { - return len(e) -} - -// UniqueStorageSlotCount returns the total number of distinct (address, slot) -// pairs accessed across all accounts. Reads and writes are disjoint per -// account by spec validation, so we can sum them directly. -func (e BlockAccessList) UniqueStorageSlotCount() int { - var n int - for i := range e { - n += len(e[i].StorageReads) + len(e[i].StorageChanges) - } - return n -} - func (e BlockAccessList) EncodeRLP(_w io.Writer) error { w := rlp.NewEncoderBuffer(_w) l := w.List() From da9e5177f92f7364945c32dd938216f5f2e9cd2e Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 6 May 2026 15:03:49 -0400 Subject: [PATCH 41/42] set prefetcher metrics upon completion --- core/state/reader_eip_7928.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/core/state/reader_eip_7928.go b/core/state/reader_eip_7928.go index e3f917029566..fc560f919d29 100644 --- a/core/state/reader_eip_7928.go +++ b/core/state/reader_eip_7928.go @@ -118,6 +118,7 @@ func newPrefetchStateReaderInternal(reader StateReader, tasks []*fetchTask, nThr nThreads: nThreads, done: make(chan struct{}), term: make(chan struct{}), + start: time.Now(), } go r.prefetch() return r @@ -145,7 +146,10 @@ func (r *prefetchStateReader) Wait() error { } func (r *prefetchStateReader) prefetch() { - defer close(r.done) + defer func() { + r.metrics = PrefetchMetrics{time.Since(r.start)} + close(r.done) + }() if len(r.tasks) == 0 { return From 2685212d75ee16205650ada2abc814a17cef1d29 Mon Sep 17 00:00:00 2001 From: Jared Wasinger Date: Wed, 6 May 2026 15:18:54 -0400 Subject: [PATCH 42/42] cleanup --- core/blockchain_stats.go | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/core/blockchain_stats.go b/core/blockchain_stats.go index d2d28a1c792e..e413fb3ef44e 100644 --- a/core/blockchain_stats.go +++ b/core/blockchain_stats.go @@ -304,26 +304,6 @@ func (s *ExecuteStats) logSlow(block *types.Block, slowBlockThreshold time.Durat } func (s *ExecuteStats) reportBALMetrics() { - /* - if s.AccountLoaded != 0 { - accountReadTimer.Update(s.AccountReads) - accountReadSingleTimer.Update(s.AccountReads / time.Duration(s.AccountLoaded)) - } - if s.StorageLoaded != 0 { - storageReadTimer.Update(s.StorageReads) - storageReadSingleTimer.Update(s.StorageReads / time.Duration(s.StorageLoaded)) - } - if s.CodeLoaded != 0 { - codeReadTimer.Update(s.CodeReads) - codeReadSingleTimer.Update(s.CodeReads / time.Duration(s.CodeLoaded)) - codeReadBytesTimer.Update(time.Duration(s.CodeLoadBytes)) - } - // TODO: implement these ^ - */ - //accountUpdateTimer.Update(s.AccountUpdates) // Account updates are complete(in validation) - //storageUpdateTimer.Update(s.StorageUpdates) // Storage updates are complete(in validation) - //accountHashTimer.Update(s.AccountHashes) // Account hashes are complete(in validation) - accountCommitTimer.Update(s.AccountCommits) // Account commits are complete, we can mark them storageCommitTimer.Update(s.StorageCommits) // Storage commits are complete, we can mark them @@ -335,12 +315,6 @@ func (s *ExecuteStats) reportBALMetrics() { stateRootComputeTimer.Update(m.AccountUpdate + m.StateUpdate + m.StateHash) } - //blockExecutionTimer.Update(s.Execution) // The time spent on EVM processing - // ^basically impossible to get this metric with parallel execution - - //blockValidationTimer.Update(s.Validation) // The time spent on block validation - //blockCrossValidationTimer.Update(s.CrossValidation) // The time spent on stateless cross validation - blockWriteTimer.Update(s.BlockWrite) // The time spent on block write blockInsertTimer.Update(s.TotalTime) // The total time spent on block execution chainMgaspsMeter.Update(time.Duration(s.MgasPerSecond)) // TODO(rjl493456442) generalize the ResettingTimer