From fd76921afab5788a658a1d34227be4e34728c4bc Mon Sep 17 00:00:00 2001 From: CPerezz Date: Thu, 7 May 2026 00:12:51 +0200 Subject: [PATCH] core, core/state: instrument BAL slow-block metrics MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- core/blockchain.go | 49 ++++++++++++------- core/blockchain_stats.go | 25 +++++----- core/parallel_state_processor.go | 9 ++-- core/state/bal_state_transition.go | 61 ++++++++++++++--------- core/state/reader.go | 78 ++++++++++++++++++++++++++++-- core/state/reader_eip_7928.go | 18 +++++++ core/state/reader_eip_7928_test.go | 28 +++++++++++ core/state/reader_stater.go | 19 ++++++++ core/state/state_object.go | 14 ------ core/state/statedb.go | 31 ++++-------- core/types/bal/bal_encoding.go | 43 ++++++++++++++++ 11 files changed, 277 insertions(+), 98 deletions(-) diff --git a/core/blockchain.go b/core/blockchain.go index 71746c221c..7b6b36334d 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -656,6 +656,20 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * writeTime := time.Since(writeStart) 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.PostProcess = res.PostProcessTime @@ -666,12 +680,13 @@ func (bc *BlockChain) processBlockWithAccessList(parentRoot common.Hash, block * stats.DatabaseCommit = m.TrieDBCommits 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 - if r, ok := prefetchReader.(state.ReaderStater); ok { - stats.StateReadCacheStats = r.GetStats() - } + stats.StateReadCacheStats = prefetchReader.(state.ReaderStater).GetStats() elapsed := time.Since(startTime) + 1 // prevent zero division 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 stats = &ExecuteStats{} ) - // Update the metrics touched during block processing and validation - 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 + reads := statedb.Reader().(state.ReadTimer).ReadTimes() + codeLoaded, codeLoadBytes := statedb.Reader().(state.CodeLoadTracker).CodeLoads() + stats.AccountReads = reads.Account + stats.StorageReads = reads.Storage + stats.CodeReads = reads.Code + stats.AccountUpdates = statedb.AccountUpdates + stats.StorageUpdates = statedb.StorageUpdates + stats.AccountHashes = statedb.AccountHashes stats.AccountLoaded = statedb.AccountLoaded stats.AccountUpdated = statedb.AccountUpdated @@ -2449,15 +2465,14 @@ func (bc *BlockChain) ProcessBlock(ctx context.Context, parentRoot common.Hash, 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.CodeLoaded = codeLoaded + stats.CodeLoadBytes = 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 + stats.Execution = ptime - (reads.Account + reads.Storage + reads.Code) + stats.Validation = vtime - (statedb.AccountHashes + statedb.AccountUpdates + statedb.StorageUpdates) + stats.CrossValidation = xvtime // Write the block to the chain and get the status. var status WriteStatus diff --git a/core/blockchain_stats.go b/core/blockchain_stats.go index e413fb3ef4..b1dfca1751 100644 --- a/core/blockchain_stats.go +++ b/core/blockchain_stats.go @@ -38,17 +38,16 @@ type ExecuteStats struct { StorageCommits time.Duration // Time spent on the storage trie commit CodeReads time.Duration // Time spent on the contract code read - // 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 + AccountLoaded int + AccountUpdated int + AccountDeleted int + StorageLoaded int + StorageUpdated int + StorageDeleted int + CodeLoaded int + CodeLoadBytes int + CodeUpdated int + CodeUpdateBytes int Execution time.Duration // Time spent on the EVM execution 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() { - accountCommitTimer.Update(s.AccountCommits) // Account commits are complete, we can mark them - storageCommitTimer.Update(s.StorageCommits) // Storage commits are complete, we can mark them + accountCommitTimer.Update(s.AccountCommits) + storageCommitTimer.Update(s.StorageCommits) if m := s.balTransitionStats; m != nil { stateTriePrefetchTimer.Update(m.StatePrefetch) diff --git a/core/parallel_state_processor.go b/core/parallel_state_processor.go index e2507dda70..260c3d5b3f 100644 --- a/core/parallel_state_processor.go +++ b/core/parallel_state_processor.go @@ -14,16 +14,13 @@ import ( "golang.org/x/sync/errgroup" ) -// ProcessResultWithMetrics wraps ProcessResult with some metrics that are -// emitted when executing blocks containing access lists. +// ProcessResultWithMetrics wraps ProcessResult with timing breakdown for BAL block processing. type ProcessResultWithMetrics struct { ProcessResult *ProcessResult PreProcessTime time.Duration StateTransitionMetrics *state.BALStateTransitionMetrics - // the time it took to execute all txs in the block - ExecTime time.Duration - PostProcessTime time.Duration - // TODO: have the prefetch metric in here as well? + ExecTime time.Duration + PostProcessTime time.Duration } // ParallelStateProcessor is used to execute and verify blocks containing diff --git a/core/state/bal_state_transition.go b/core/state/bal_state_transition.go index 2178169c7f..2446d49828 100644 --- a/core/state/bal_state_transition.go +++ b/core/state/bal_state_transition.go @@ -3,6 +3,7 @@ package state import ( "maps" "sync" + "sync/atomic" "time" "github.com/ethereum/go-ethereum/common" @@ -15,30 +16,28 @@ import ( "golang.org/x/sync/errgroup" ) -// BALStateTransition is responsible for performing the state root update -// and commit for EIP 7928 access-list-containing blocks. An instance of -// this object is only used for a single block. +// BALStateTransition performs the state root update and commit for EIP-7928 +// access-list-containing blocks. One instance per block. type BALStateTransition struct { accessList bal.AccessListReader + written bal.WrittenCounts db Database reader Reader stateTrie Trie parentRoot common.Hash - // the computed state root of the block rootHash common.Hash - // the state modifications performed by the block - diffs bal.StateMutations - - // a map of common.Address -> *types.StateAccount containing the block - // prestate of all accounts that will be modified - prestates sync.Map + diffs bal.StateMutations + prestates sync.Map postStates map[common.Address]*types.StateAccount - // a map of common.Address -> Trie containing the account tries for all - // accounts with mutated storage - tries sync.Map //map[common.Address]Trie - deletions map[common.Address]struct{} + tries sync.Map + 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 @@ -52,6 +51,19 @@ func (s *BALStateTransition) Metrics() *BALStateTransitionMetrics { 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 { // trie hashing metrics AccountUpdate time.Duration @@ -75,6 +87,7 @@ func NewBALStateTransition(block *types.Block, prefetchReader Reader, db Databas return &BALStateTransition{ accessList: bal.NewAccessListReader(*block.AccessList()), + written: block.AccessList().WrittenCounts(), db: db, reader: prefetchReader, stateTrie: stateTrie, @@ -90,6 +103,11 @@ func NewBALStateTransition(block *types.Block, prefetchReader Reader, db Databas }, 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 { return s.err } @@ -334,15 +352,11 @@ func (s *BALStateTransition) CommitWithUpdate(block uint64, deleteEmptyObjects b return common.Hash{}, nil, err } - /* - 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()) - */ + storageDeleted := s.storageDeleted.Load() + accountUpdatedMeter.Mark(int64(s.written.Accounts - s.accountDeleted)) + storageUpdatedMeter.Mark(int64(s.written.StorageSlots) - storageDeleted) + accountDeletedMeter.Mark(int64(s.accountDeleted)) + storageDeletedMeter.Mark(storageDeleted) accountTrieUpdatedMeter.Mark(int64(accountTrieNodesUpdated)) accountTrieDeletedMeter.Mark(int64(accountTrieNodesDeleted)) storageTriesUpdatedMeter.Mark(int64(storageTrieNodesUpdated)) @@ -477,6 +491,7 @@ func (s *BALStateTransition) IntermediateRoot(_ bool) common.Hash { return common.Hash{} } s.deletions[mutatedAddr] = struct{}{} + s.accountDeleted++ } else { acct, code := s.updateAccount(mutatedAddr) diff --git a/core/state/reader.go b/core/state/reader.go index 692536350d..58af034057 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -18,6 +18,10 @@ 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" @@ -28,8 +32,6 @@ 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. @@ -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 { ContractCodeReader StateReader 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 { return &reader{ 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. func (r *reader) GetCodeStats() ContractCodeReaderStats { if stater, ok := r.ContractCodeReader.(ContractCodeReaderStater); ok { @@ -571,3 +624,20 @@ 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 +} + +// 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 9bcf43fe89..b651d20c3f 100644 --- a/core/state/reader_eip_7928.go +++ b/core/state/reader_eip_7928.go @@ -382,3 +382,21 @@ func (r *readerTracker) TouchStorage(addr common.Address, slot common.Hash) { } 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{} +} + diff --git a/core/state/reader_eip_7928_test.go b/core/state/reader_eip_7928_test.go index 8e30bc5f77..01b5317567 100644 --- a/core/state/reader_eip_7928_test.go +++ b/core/state/reader_eip_7928_test.go @@ -263,3 +263,31 @@ 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) + } +} + diff --git a/core/state/reader_stater.go b/core/state/reader_stater.go index 5294275953..1b5e7aa469 100644 --- a/core/state/reader_stater.go +++ b/core/state/reader_stater.go @@ -16,6 +16,25 @@ 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. type ContractCodeReaderStats struct { CacheHit int64 // Number of cache hits diff --git a/core/state/state_object.go b/core/state/state_object.go index b83d66d3d6..a4a7ec6679 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -21,7 +21,6 @@ import ( "fmt" "maps" "slices" - "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -226,13 +225,11 @@ func (s *stateObject) GetCommittedState(key common.Hash) common.Hash { } s.db.StorageLoaded++ - start := time.Now() value, err := s.db.reader.Storage(s.address, key) if err != nil { s.db.setError(err) return common.Hash{} } - s.db.StorageReads += time.Since(start) // Schedule the resolved storage slots for prefetching if it's enabled. 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()) { 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())) if len(code) == 0 { 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()) { 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())) if err != nil { s.db.setError(err) diff --git a/core/state/statedb.go b/core/state/statedb.go index b8081c149a..68da74d148 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -156,33 +156,24 @@ type StateDB struct { // State witness if cross validation is needed witness *stateless.Witness - // Measurements gathered during execution for debugging purposes - AccountReads time.Duration + // Per-block counters surfaced in ExecuteStats; read durations and code-loads + // are tracked on the reader (see ReadTimer, CodeLoadTracker). AccountHashes time.Duration AccountUpdates time.Duration AccountCommits time.Duration - StorageReads time.Duration StorageUpdates time.Duration StorageCommits time.Duration DatabaseCommits time.Duration - CodeReads time.Duration - AccountLoaded int // Number of accounts retrieved from the database during the state transition - AccountUpdated int // Number of accounts updated during the state transition - AccountDeleted int // Number of accounts deleted during the state transition - StorageLoaded int // Number of storage slots retrieved from the database during the state transition - StorageUpdated atomic.Int64 // Number of storage slots updated during the state transition - StorageDeleted atomic.Int64 // Number of storage slots deleted during the state transition - - // CodeLoadBytes is the total number of bytes read from contract code. - // 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 + AccountLoaded int + AccountUpdated int + AccountDeleted int + StorageLoaded int + StorageUpdated atomic.Int64 + StorageDeleted atomic.Int64 + CodeUpdated int + CodeUpdateBytes int } // New creates a new state from a given trie. @@ -635,13 +626,11 @@ func (s *StateDB) getStateObject(addr common.Address) *stateObject { s.AccountLoaded++ - start := time.Now() acct, err := s.reader.Account(addr) if err != nil { s.setError(fmt.Errorf("getStateObject (%x) error: %w", addr.Bytes(), err)) return nil } - s.AccountReads += time.Since(start) // Short circuit if the account is not found if acct == nil { diff --git a/core/types/bal/bal_encoding.go b/core/types/bal/bal_encoding.go index fc1d36007e..f0a3ee28f7 100644 --- a/core/types/bal/bal_encoding.go +++ b/core/types/bal/bal_encoding.go @@ -44,6 +44,49 @@ 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 +} + +// 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 { w := rlp.NewEncoderBuffer(_w) l := w.List()