core, core/state: instrument BAL slow-block metrics

Populates per-block state read/write counts in slow-block JSON for BAL
blocks (which #34892 left as TBD), and adds reader-level read timing.
Builds on top of bal-devnet-3 — most of the PR's earlier slow-block log
infrastructure was adapted into upstream by that commit, so this change
is now scoped to the metric population that the BAL alone can derive.

- BAL helpers: BlockAccessList.{UniqueAccountCount, UniqueStorageSlotCount,
  WrittenCounts}. WrittenCounts walks the BAL once and returns the
  block-aggregate write counts.

- Reader-level read timing: *reader times all synchronous Account/Storage/
  Code/CodeSize calls via atomic counters; exposed via ReadTimes()
  ReadDurations and the new state.ReadTimer interface. Replaces StateDB-
  level AccountReads/StorageReads/CodeReads tracking (the StateDB shouldn't
  time its dependencies — the reader is where the I/O happens).

- Reader-level code-load dedup: *reader.codeLoaded sync.Map records the
  first-seen byte length per address; CodeLoads() returns (count, bytes).
  Exposed via state.CodeLoadTracker. Replaces StateDB CodeLoaded/
  CodeLoadBytes tracking and the SnapshotCodeLoads aggregation pattern.

- BALStateTransition: caches BlockAccessList.WrittenCounts() once at
  construction; tracks accountDeleted/storageDeleted atomics for the
  parallel root-pass (the BAL alone can't distinguish a selfdestruct from
  a balance/nonce reset). Exposes Deletions() DeletionCounts. Drops the
  older accountUpdated/storageUpdated/codeUpdated/codeUpdateBytes counters
  (now derived from WrittenCounts).

- BAL block stats path (blockchain.go): populates StateCounts directly —
  AccountUpdated = WrittenCounts.Accounts - Deletions.Accounts (same for
  storage). AccountLoaded/StorageLoaded come from BAL. CodeLoaded/
  CodeLoadBytes come from the shared *reader (deduplicated across phase
  StateDBs naturally because they share one reader instance).

- Non-BAL block stats path: read durations come from the reader; counts
  from StateDB fields. StorageUpdated/StorageDeleted unified to int width.

- Hard type assertions: state.ReadTimer / state.CodeLoadTracker /
  state.ReaderStater consumers use direct casts (no silent zero
  fallback) — every Reader chain in production satisfies these
  interfaces.

- Meter alignment: account/storage Updated meters subtract Deletions to
  avoid double-reporting blocks under both Update and Delete dashboards.
This commit is contained in:
CPerezz 2026-05-07 00:12:51 +02:00
parent 2e771eaacf
commit 3f5e27e7b0
No known key found for this signature in database
GPG key ID: 62045F34B97177DD
11 changed files with 277 additions and 98 deletions

View file

@ -656,6 +656,20 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block *
writeTime := time.Since(writeStart) writeTime := time.Since(writeStart)
var stats ExecuteStats var stats ExecuteStats
wc := stateTransition.WrittenCounts()
d := stateTransition.Deletions()
codeLoaded, codeLoadBytes := prefetchReader.(state.CodeLoadTracker).CodeLoads()
stats.AccountLoaded = al.UniqueAccountCount()
stats.AccountUpdated = wc.Accounts - d.Accounts
stats.AccountDeleted = d.Accounts
stats.StorageLoaded = al.UniqueStorageSlotCount()
stats.StorageUpdated = wc.StorageSlots - d.Storage
stats.StorageDeleted = d.Storage
stats.CodeLoaded = codeLoaded
stats.CodeLoadBytes = codeLoadBytes
stats.CodeUpdated = wc.Codes
stats.CodeUpdateBytes = wc.CodeBytes
stats.ExecWall = res.ExecTime stats.ExecWall = res.ExecTime
stats.PostProcess = res.PostProcessTime stats.PostProcess = res.PostProcessTime
@ -666,12 +680,13 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block *
stats.DatabaseCommit = m.TrieDBCommits stats.DatabaseCommit = m.TrieDBCommits
stats.Prefetch = m.StatePrefetch stats.Prefetch = m.StatePrefetch
} }
readerReads := prefetchReader.(state.ReadTimer).ReadTimes()
stats.AccountReads = readerReads.Account
stats.StorageReads = readerReads.Storage
stats.CodeReads = readerReads.Code
stats.Prefetch = prefetchReader.(state.PrefetcherMetricer).Metrics().Elapsed stats.Prefetch = prefetchReader.(state.PrefetcherMetricer).Metrics().Elapsed
if r, ok := prefetchReader.(state.ReaderStater); ok { stats.StateReadCacheStats = prefetchReader.(state.ReaderStater).GetStats()
stats.StateReadCacheStats = r.GetStats()
}
elapsed := time.Since(startTime) + 1 // prevent zero division elapsed := time.Since(startTime) + 1 // prevent zero division
stats.TotalTime = elapsed stats.TotalTime = elapsed
@ -2435,13 +2450,14 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
proctime = time.Since(startTime) // processing + validation + cross validation proctime = time.Since(startTime) // processing + validation + cross validation
stats = &ExecuteStats{} stats = &ExecuteStats{}
) )
// Update the metrics touched during block processing and validation reads := statedb.Reader().(state.ReadTimer).ReadTimes()
stats.AccountReads = statedb.AccountReads // Account reads are complete(in processing) codeLoaded, codeLoadBytes := statedb.Reader().(state.CodeLoadTracker).CodeLoads()
stats.StorageReads = statedb.StorageReads // Storage reads are complete(in processing) stats.AccountReads = reads.Account
stats.AccountUpdates = statedb.AccountUpdates // Account updates are complete(in validation) stats.StorageReads = reads.Storage
stats.StorageUpdates = statedb.StorageUpdates // Storage updates are complete(in validation) stats.CodeReads = reads.Code
stats.AccountHashes = statedb.AccountHashes // Account hashes are complete(in validation) stats.AccountUpdates = statedb.AccountUpdates
stats.CodeReads = statedb.CodeReads stats.StorageUpdates = statedb.StorageUpdates
stats.AccountHashes = statedb.AccountHashes
stats.AccountLoaded = statedb.AccountLoaded stats.AccountLoaded = statedb.AccountLoaded
stats.AccountUpdated = statedb.AccountUpdated stats.AccountUpdated = statedb.AccountUpdated
@ -2449,15 +2465,14 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash,
stats.StorageLoaded = statedb.StorageLoaded stats.StorageLoaded = statedb.StorageLoaded
stats.StorageUpdated = int(statedb.StorageUpdated.Load()) stats.StorageUpdated = int(statedb.StorageUpdated.Load())
stats.StorageDeleted = int(statedb.StorageDeleted.Load()) stats.StorageDeleted = int(statedb.StorageDeleted.Load())
stats.CodeLoaded = codeLoaded
stats.CodeLoaded = statedb.CodeLoaded stats.CodeLoadBytes = codeLoadBytes
stats.CodeLoadBytes = statedb.CodeLoadBytes
stats.CodeUpdated = statedb.CodeUpdated stats.CodeUpdated = statedb.CodeUpdated
stats.CodeUpdateBytes = statedb.CodeUpdateBytes stats.CodeUpdateBytes = statedb.CodeUpdateBytes
stats.Execution = ptime - (statedb.AccountReads + statedb.StorageReads + statedb.CodeReads) // The time spent on EVM processing stats.Execution = ptime - (reads.Account + reads.Storage + reads.Code)
stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) // The time spent on block validation stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates)
stats.CrossValidation = xvtime // The time spent on stateless cross validation stats.CrossValidation = xvtime
// Write the block to the chain and get the status. // Write the block to the chain and get the status.
var status WriteStatus var status WriteStatus

View file

@ -38,17 +38,16 @@ 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
// TODO: code bytes loaded AccountLoaded int
AccountLoaded int // Number of accounts loaded AccountUpdated int
AccountUpdated int // Number of accounts updated AccountDeleted int
AccountDeleted int // Number of accounts deleted StorageLoaded int
StorageLoaded int // Number of storage slots loaded StorageUpdated int
StorageUpdated int // Number of storage slots updated StorageDeleted int
StorageDeleted int // Number of storage slots deleted CodeLoaded int
CodeLoaded int // Number of contract code loaded CodeLoadBytes int
CodeLoadBytes int // Number of bytes read from contract code CodeUpdated int
CodeUpdated int // Number of contract code written (CREATE/CREATE2 + EIP-7702) CodeUpdateBytes int
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
@ -304,8 +303,8 @@ func (s *ExecuteStats) logSlow(block *types.Block, slowBlockThreshold time.Durat
} }
func (s *ExecuteStats) reportBALMetrics() { func (s *ExecuteStats) reportBALMetrics() {
accountCommitTimer.Update(s.AccountCommits) // Account commits are complete, we can mark them accountCommitTimer.Update(s.AccountCommits)
storageCommitTimer.Update(s.StorageCommits) // Storage commits are complete, we can mark them storageCommitTimer.Update(s.StorageCommits)
if m := s.balTransitionStats; m != nil { if m := s.balTransitionStats; m != nil {
stateTriePrefetchTimer.Update(m.StatePrefetch) stateTriePrefetchTimer.Update(m.StatePrefetch)

View file

@ -14,16 +14,13 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
// ProcessResultWithMetrics wraps ProcessResult with some metrics that are // ProcessResultWithMetrics wraps ProcessResult with timing breakdown for BAL block processing.
// emitted when executing blocks containing access lists.
type ProcessResultWithMetrics struct { type ProcessResultWithMetrics struct {
ProcessResult *ProcessResult ProcessResult *ProcessResult
PreProcessTime time.Duration PreProcessTime time.Duration
StateTransitionMetrics *state.BALStateTransitionMetrics StateTransitionMetrics *state.BALStateTransitionMetrics
// the time it took to execute all txs in the block ExecTime time.Duration
ExecTime time.Duration PostProcessTime time.Duration
PostProcessTime time.Duration
// TODO: have the prefetch metric in here as well?
} }
// ParallelStateProcessor is used to execute and verify blocks containing // ParallelStateProcessor is used to execute and verify blocks containing

View file

@ -3,6 +3,7 @@ package state
import ( import (
"maps" "maps"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -15,30 +16,28 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
// BALStateTransition is responsible for performing the state root update // BALStateTransition performs the state root update and commit for EIP-7928
// and commit for EIP 7928 access-list-containing blocks. An instance of // access-list-containing blocks. One instance per block.
// this object is only used for a single block.
type BALStateTransition struct { type BALStateTransition struct {
accessList bal.AccessListReader accessList bal.AccessListReader
written bal.WrittenCounts
db Database db Database
reader Reader reader Reader
stateTrie Trie stateTrie Trie
parentRoot common.Hash parentRoot common.Hash
// the computed state root of the block
rootHash common.Hash rootHash common.Hash
// the state modifications performed by the block diffs bal.StateMutations
diffs bal.StateMutations
// a map of common.Address -> *types.StateAccount containing the block
// prestate of all accounts that will be modified
prestates sync.Map
prestates sync.Map
postStates map[common.Address]*types.StateAccount postStates map[common.Address]*types.StateAccount
// a map of common.Address -> Trie containing the account tries for all tries sync.Map
// accounts with mutated storage deletions map[common.Address]struct{}
tries sync.Map //map[common.Address]Trie
deletions map[common.Address]struct{} // Deletion counters; not derivable from the BAL alone (selfdestruct vs
// balance/nonce reset is indistinguishable without prestate).
accountDeleted int
storageDeleted atomic.Int64
stateUpdate *stateUpdate stateUpdate *stateUpdate
@ -52,6 +51,19 @@ func (s *BALStateTransition) Metrics() *BALStateTransitionMetrics {
return &s.metrics return &s.metrics
} }
// DeletionCounts holds per-block deletion counters from the parallel root-pass.
type DeletionCounts struct {
Accounts int
Storage int
}
func (s *BALStateTransition) Deletions() DeletionCounts {
return DeletionCounts{
Accounts: s.accountDeleted,
Storage: int(s.storageDeleted.Load()),
}
}
type BALStateTransitionMetrics struct { type BALStateTransitionMetrics struct {
// trie hashing metrics // trie hashing metrics
AccountUpdate time.Duration AccountUpdate time.Duration
@ -75,6 +87,7 @@ func NewBALStateTransition(block *types.Block, prefetchReader Reader, db Databas
return &BALStateTransition{ return &BALStateTransition{
accessList: bal.NewAccessListReader(*block.AccessList()), accessList: bal.NewAccessListReader(*block.AccessList()),
written: block.AccessList().WrittenCounts(),
db: db, db: db,
reader: prefetchReader, reader: prefetchReader,
stateTrie: stateTrie, stateTrie: stateTrie,
@ -90,6 +103,11 @@ func NewBALStateTransition(block *types.Block, prefetchReader Reader, db Databas
}, nil }, nil
} }
// WrittenCounts returns the cached BAL write counts (computed once per block).
func (s *BALStateTransition) WrittenCounts() bal.WrittenCounts {
return s.written
}
func (s *BALStateTransition) Error() error { func (s *BALStateTransition) Error() error {
return s.err return s.err
} }
@ -334,15 +352,11 @@ func (s *BALStateTransition) CommitWithUpdate(block uint64, deleteEmptyObjects b
return common.Hash{}, nil, err return common.Hash{}, nil, err
} }
/* storageDeleted := s.storageDeleted.Load()
TODO: derive these from the BAL accountUpdatedMeter.Mark(int64(s.written.Accounts - s.accountDeleted))
^ I think even then, there is a semantic difference with how these metrics were calculated previously storageUpdatedMeter.Mark(int64(s.written.StorageSlots) - storageDeleted)
I don't know if it makes sense to recompute those, or just derive new ones from the BAL accountDeletedMeter.Mark(int64(s.accountDeleted))
accountUpdatedMeter.Mark(int64(s.accountUpdated)) storageDeletedMeter.Mark(storageDeleted)
storageUpdatedMeter.Mark(s.storageUpdated.Load())
accountDeletedMeter.Mark(int64(s.accountDeleted))
storageDeletedMeter.Mark(s.storageDeleted.Load())
*/
accountTrieUpdatedMeter.Mark(int64(accountTrieNodesUpdated)) accountTrieUpdatedMeter.Mark(int64(accountTrieNodesUpdated))
accountTrieDeletedMeter.Mark(int64(accountTrieNodesDeleted)) accountTrieDeletedMeter.Mark(int64(accountTrieNodesDeleted))
storageTriesUpdatedMeter.Mark(int64(storageTrieNodesUpdated)) storageTriesUpdatedMeter.Mark(int64(storageTrieNodesUpdated))
@ -477,6 +491,7 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash {
return common.Hash{} return common.Hash{}
} }
s.deletions[mutatedAddr] = struct{}{} s.deletions[mutatedAddr] = struct{}{}
s.accountDeleted++
} else { } else {
acct, code := s.updateAccount(mutatedAddr) acct, code := s.updateAccount(mutatedAddr)

View file

@ -18,6 +18,10 @@ package state
import ( import (
"errors" "errors"
"sync"
"sync/atomic"
"time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/overlay" "github.com/ethereum/go-ethereum/core/overlay"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
@ -28,8 +32,6 @@ import (
"github.com/ethereum/go-ethereum/trie/transitiontrie" "github.com/ethereum/go-ethereum/trie/transitiontrie"
"github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb"
"github.com/ethereum/go-ethereum/triedb/database" "github.com/ethereum/go-ethereum/triedb/database"
"sync"
"sync/atomic"
) )
// ContractCodeReader defines the interface for accessing contract code. // ContractCodeReader defines the interface for accessing contract code.
@ -525,14 +527,18 @@ func (r *stateReaderWithStats) GetStateStats() StateReaderStats {
} }
} }
// reader aggregates a code reader and a state reader into a single object.
type reader struct { type reader struct {
ContractCodeReader ContractCodeReader
StateReader StateReader
PrefetcherMetricer PrefetcherMetricer
accountReadNS atomic.Int64
storageReadNS atomic.Int64
codeReadNS atomic.Int64
codeLoaded sync.Map // common.Address → int (first-seen len(code))
} }
// newReader constructs a reader with the supplied code reader and state reader.
func newReader(codeReader ContractCodeReader, stateReader StateReader) *reader { func newReader(codeReader ContractCodeReader, stateReader StateReader) *reader {
return &reader{ return &reader{
ContractCodeReader: codeReader, ContractCodeReader: codeReader,
@ -548,6 +554,53 @@ func newReaderWithPrefetch(codeReader ContractCodeReader, stateReader StateReade
} }
} }
func (r *reader) Account(addr common.Address) (*types.StateAccount, error) {
defer func(start time.Time) { r.accountReadNS.Add(int64(time.Since(start))) }(time.Now())
return r.StateReader.Account(addr)
}
func (r *reader) Storage(addr common.Address, slot common.Hash) (common.Hash, error) {
defer func(start time.Time) { r.storageReadNS.Add(int64(time.Since(start))) }(time.Now())
return r.StateReader.Storage(addr, slot)
}
func (r *reader) Code(addr common.Address, codeHash common.Hash) []byte {
defer func(start time.Time) { r.codeReadNS.Add(int64(time.Since(start))) }(time.Now())
code := r.ContractCodeReader.Code(addr, codeHash)
if len(code) > 0 {
r.codeLoaded.LoadOrStore(addr, len(code))
}
return code
}
func (r *reader) CodeSize(addr common.Address, codeHash common.Hash) (int, error) {
defer func(start time.Time) { r.codeReadNS.Add(int64(time.Since(start))) }(time.Now())
size, err := r.ContractCodeReader.CodeSize(addr, codeHash)
if err == nil && size > 0 {
r.codeLoaded.LoadOrStore(addr, size)
}
return size, err
}
func (r *reader) ReadTimes() ReadDurations {
return ReadDurations{
Account: time.Duration(r.accountReadNS.Load()),
Storage: time.Duration(r.storageReadNS.Load()),
Code: time.Duration(r.codeReadNS.Load()),
}
}
// CodeLoads returns the count of unique contracts whose code was fetched and
// the sum of their first-seen byte lengths. Call after Reader use has quiesced.
func (r *reader) CodeLoads() (count, bytes int) {
r.codeLoaded.Range(func(_, v any) bool {
count++
bytes += v.(int)
return true
})
return
}
// GetCodeStats returns the statistics of code access. // GetCodeStats returns the statistics of code access.
func (r *reader) GetCodeStats() ContractCodeReaderStats { func (r *reader) GetCodeStats() ContractCodeReaderStats {
if stater, ok := r.ContractCodeReader.(ContractCodeReaderStater); ok { if stater, ok := r.ContractCodeReader.(ContractCodeReaderStater); ok {
@ -571,3 +624,20 @@ func (r *reader) GetStats() ReaderStats {
StateStats: r.GetStateStats(), 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
}
// 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()
}
}

View file

@ -382,3 +382,21 @@ func (r *readerTracker) TouchStorage(addr common.Address, slot common.Hash) {
} }
list[slot] = struct{}{} list[slot] = struct{}{}
} }
func (r *readerTracker) ReadTimes() ReadDurations {
return r.Reader.(ReadTimer).ReadTimes()
}
func (r *readerTracker) CodeLoads() (count, bytes int) {
return r.Reader.(CodeLoadTracker).CodeLoads()
}
// 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{}
}

View file

@ -263,3 +263,31 @@ func TestTrackerSurvivesStateDBCache(t *testing.T) {
t.Fatal("slot must be tracked on cache hit (storage)") 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)
}
}

View file

@ -16,6 +16,25 @@
package state package state
import "time"
// ReadDurations groups cumulative read durations by category.
type ReadDurations struct {
Account time.Duration
Storage time.Duration
Code time.Duration
}
// ReadTimer exposes a Reader's cumulative read durations.
type ReadTimer interface {
ReadTimes() ReadDurations
}
// CodeLoadTracker exposes a Reader's deduplicated code-load count and bytes.
type CodeLoadTracker interface {
CodeLoads() (count, bytes int)
}
// ContractCodeReaderStats aggregates statistics for the contract code reader. // ContractCodeReaderStats aggregates statistics for the contract code reader.
type ContractCodeReaderStats struct { type ContractCodeReaderStats struct {
CacheHit int64 // Number of cache hits CacheHit int64 // Number of cache hits

View file

@ -21,7 +21,6 @@ import (
"fmt" "fmt"
"maps" "maps"
"slices" "slices"
"time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
@ -226,13 +225,11 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash {
} }
s.db.StorageLoaded++ s.db.StorageLoaded++
start := time.Now()
value, err := s.db.reader.Storage(s.address, key) value, err := s.db.reader.Storage(s.address, key)
if err != nil { if err != nil {
s.db.setError(err) s.db.setError(err)
return common.Hash{} return common.Hash{}
} }
s.db.StorageReads += time.Since(start)
// Schedule the resolved storage slots for prefetching if it's enabled. // Schedule the resolved storage slots for prefetching if it's enabled.
if s.db.prefetcher != nil && s.data.Root != types.EmptyRootHash { if s.db.prefetcher != nil && s.data.Root != types.EmptyRootHash {
@ -644,12 +641,6 @@ func (s *stateObject) Code() []byte {
if bytes.Equal(s.CodeHash(), types.EmptyCodeHash.Bytes()) { if bytes.Equal(s.CodeHash(), types.EmptyCodeHash.Bytes()) {
return nil return nil
} }
defer func(start time.Time) {
s.db.CodeLoaded += 1
s.db.CodeReads += time.Since(start)
s.db.CodeLoadBytes += len(s.code)
}(time.Now())
code := s.db.reader.Code(s.address, common.BytesToHash(s.CodeHash())) code := s.db.reader.Code(s.address, common.BytesToHash(s.CodeHash()))
if len(code) == 0 { if len(code) == 0 {
s.db.setError(fmt.Errorf("code is not found %x", s.CodeHash())) s.db.setError(fmt.Errorf("code is not found %x", s.CodeHash()))
@ -668,11 +659,6 @@ func (s *stateObject) CodeSize() int {
if bytes.Equal(s.CodeHash(), types.EmptyCodeHash.Bytes()) { if bytes.Equal(s.CodeHash(), types.EmptyCodeHash.Bytes()) {
return 0 return 0
} }
defer func(start time.Time) {
s.db.CodeLoaded += 1
s.db.CodeReads += time.Since(start)
}(time.Now())
size, err := s.db.reader.CodeSize(s.address, common.BytesToHash(s.CodeHash())) size, err := s.db.reader.CodeSize(s.address, common.BytesToHash(s.CodeHash()))
if err != nil { if err != nil {
s.db.setError(err) s.db.setError(err)

View file

@ -156,33 +156,24 @@ type StateDB struct {
// State witness if cross validation is needed // State witness if cross validation is needed
witness *stateless.Witness witness *stateless.Witness
// Measurements gathered during execution for debugging purposes // Per-block counters surfaced in ExecuteStats; read durations and code-loads
AccountReads time.Duration // are tracked on the reader (see ReadTimer, CodeLoadTracker).
AccountHashes time.Duration AccountHashes time.Duration
AccountUpdates time.Duration AccountUpdates time.Duration
AccountCommits time.Duration AccountCommits time.Duration
StorageReads time.Duration
StorageUpdates time.Duration StorageUpdates time.Duration
StorageCommits time.Duration StorageCommits time.Duration
DatabaseCommits time.Duration DatabaseCommits time.Duration
CodeReads time.Duration
AccountLoaded int // Number of accounts retrieved from the database during the state transition AccountLoaded int
AccountUpdated int // Number of accounts updated during the state transition AccountUpdated int
AccountDeleted int // Number of accounts deleted during the state transition AccountDeleted int
StorageLoaded int // Number of storage slots retrieved from the database during the state transition StorageLoaded int
StorageUpdated atomic.Int64 // Number of storage slots updated during the state transition StorageUpdated atomic.Int64
StorageDeleted atomic.Int64 // Number of storage slots deleted during the state transition StorageDeleted atomic.Int64
CodeUpdated int
// CodeLoadBytes is the total number of bytes read from contract code. CodeUpdateBytes int
// This value may be smaller than the actual number of bytes read, since
// some APIs (e.g. CodeSize) may load the entire code from either the
// cache or the database when the size is not available in the cache.
CodeLoaded int // Number of contract code loaded during the state transition
CodeLoadBytes int // Total bytes of resolved code
CodeUpdated int // Number of contracts with code changes that persisted
CodeUpdateBytes int // Total bytes of persisted code written
} }
// New creates a new state from a given trie. // New creates a new state from a given trie.
@ -635,13 +626,11 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject {
s.AccountLoaded++ s.AccountLoaded++
start := time.Now()
acct, err := s.reader.Account(addr) acct, err := s.reader.Account(addr)
if err != nil { if err != nil {
s.setError(fmt.Errorf("getStateObject (%x) error: %w", addr.Bytes(), err)) s.setError(fmt.Errorf("getStateObject (%x) error: %w", addr.Bytes(), err))
return nil return nil
} }
s.AccountReads += time.Since(start)
// Short circuit if the account is not found // Short circuit if the account is not found
if acct == nil { if acct == nil {

View file

@ -44,6 +44,49 @@ import (
// BlockAccessList is the encoding format of AccessListBuilder. // BlockAccessList is the encoding format of AccessListBuilder.
type BlockAccessList []AccountAccess 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
}
// WrittenCounts groups per-block aggregate write counts derived from the BAL.
type WrittenCounts struct {
Accounts int
StorageSlots int
Codes int
CodeBytes int
}
// WrittenCounts walks the BAL once and returns the aggregate write counts.
func (e BlockAccessList) WrittenCounts() WrittenCounts {
var w WrittenCounts
for i := range e {
a := &e[i]
if len(a.StorageChanges) > 0 || len(a.BalanceChanges) > 0 ||
len(a.NonceChanges) > 0 || len(a.CodeChanges) > 0 {
w.Accounts++
}
w.StorageSlots += len(a.StorageChanges)
if n := len(a.CodeChanges); n > 0 {
w.Codes++
w.CodeBytes += len(a.CodeChanges[n-1].Code)
}
}
return w
}
func (e BlockAccessList) EncodeRLP(_w io.Writer) error { func (e BlockAccessList) EncodeRLP(_w io.Writer) error {
w := rlp.NewEncoderBuffer(_w) w := rlp.NewEncoderBuffer(_w)
l := w.List() l := w.List()