From 812fa198c3e8f454e3835861b34d8679a4cb0d71 Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 30 Apr 2026 13:40:22 +0200 Subject: [PATCH] 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 3fa6a4a3dc..ee1bbee3cc 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 0000000000..02116e0b3e --- /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 b8081c149a..3eccf62c0b 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.