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).
This commit is contained in:
CPerezz 2026-04-30 13:40:22 +02:00
parent ae69e96efd
commit 812fa198c3
No known key found for this signature in database
GPG key ID: 62045F34B97177DD
3 changed files with 147 additions and 25 deletions

View file

@ -38,16 +38,10 @@ type ExecuteStats struct {
StorageCommits time.Duration // Time spent on the storage trie commit StorageCommits time.Duration // Time spent on the storage trie commit
CodeReads time.Duration // Time spent on the contract code read CodeReads time.Duration // Time spent on the contract code read
AccountLoaded int // Number of accounts loaded // Embedded state-mutation counts. Field promotion preserves access as
AccountUpdated int // Number of accounts updated // s.AccountLoaded etc. Note StorageUpdated/StorageDeleted are int64 here
AccountDeleted int // Number of accounts deleted // (snapshot from atomic.Int64 on StateDB).
StorageLoaded int // Number of storage slots loaded state.StateCounts
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 Execution time.Duration // Time spent on the EVM execution
Validation time.Duration // Time spent on the block validation 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 TotalTime time.Duration // The total time spent on block execution
MgasPerSecond float64 // The million gas processed per second 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 // Cache hit rates
StateReadCacheStats state.ReaderStats StateReadCacheStats state.ReaderStats
StatePrefetchCacheStats state.ReaderStats StatePrefetchCacheStats state.ReaderStats
@ -120,6 +121,10 @@ type slowBlockLog struct {
StateReads slowBlockReads `json:"state_reads"` StateReads slowBlockReads `json:"state_reads"`
StateWrites slowBlockWrites `json:"state_writes"` StateWrites slowBlockWrites `json:"state_writes"`
Cache slowBlockCache `json:"cache"` 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 { type slowBlockInfo struct {
@ -180,24 +185,33 @@ type slowBlockCodeCacheEntry struct {
MissBytes int64 `json:"miss_bytes"` 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 // durationToMs converts a time.Duration to milliseconds as a float64
// with sub-millisecond precision for accurate cross-client metrics. // with sub-millisecond precision for accurate cross-client metrics.
func durationToMs(d time.Duration) float64 { func durationToMs(d time.Duration) float64 {
return float64(d.Nanoseconds()) / 1e6 return float64(d.Nanoseconds()) / 1e6
} }
// logSlow prints the detailed execution statistics in JSON format if the block // buildSlowBlockLog constructs the slow-block log JSON struct from execution
// is regarded as slow. The JSON format is designed for cross-client compatibility // statistics. Pure function — no side effects, no logging — to make the JSON
// with other Ethereum execution clients. // shape directly testable.
func (s *ExecuteStats) logSlow(block *types.Block, slowBlockThreshold time.Duration) { func buildSlowBlockLog(s *ExecuteStats, block *types.Block) slowBlockLog {
// 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
}
logEntry := slowBlockLog{ logEntry := slowBlockLog{
Level: "warn", Level: "warn",
Msg: "Slow block", Msg: "Slow block",
@ -226,8 +240,8 @@ func (s *ExecuteStats) logSlow(block *types.Block, slowBlockThreshold time.Durat
StateWrites: slowBlockWrites{ StateWrites: slowBlockWrites{
Accounts: s.AccountUpdated, Accounts: s.AccountUpdated,
AccountsDeleted: s.AccountDeleted, AccountsDeleted: s.AccountDeleted,
StorageSlots: s.StorageUpdated, StorageSlots: int(s.StorageUpdated),
StorageSlotsDeleted: s.StorageDeleted, StorageSlotsDeleted: int(s.StorageDeleted),
Code: s.CodeUpdated, Code: s.CodeUpdated,
CodeBytes: s.CodeUpdateBytes, 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 { if err != nil {
log.Error("Failed to marshal slow block log", "error", err) log.Error("Failed to marshal slow block log", "error", err)
return return

View file

@ -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 <http://www.gnu.org/licenses/>.
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
}

View file

@ -223,6 +223,24 @@ func (s *StateDB) WithReader(reader Reader) *StateDB {
return cpy 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 // 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 // state trie concurrently while the state is mutated so that when we reach the
// commit phase, most of the needed data is already hot. // commit phase, most of the needed data is already hot.