diff --git a/core/blockchain.go b/core/blockchain.go index 9e4562eb4487..469844b503f1 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -87,6 +87,9 @@ var ( storageCacheHitPrefetchMeter = metrics.NewRegisteredMeter("chain/storage/reads/cache/prefetch/hit", nil) storageCacheMissPrefetchMeter = metrics.NewRegisteredMeter("chain/storage/reads/cache/prefetch/miss", nil) + codeCacheHitMeter = metrics.NewRegisteredMeter("chain/code/reads/cache/hit", nil) + codeCacheMissMeter = metrics.NewRegisteredMeter("chain/code/reads/cache/miss", nil) + accountReadSingleTimer = metrics.NewRegisteredResettingTimer("chain/account/single/reads", nil) storageReadSingleTimer = metrics.NewRegisteredResettingTimer("chain/storage/single/reads", nil) codeReadSingleTimer = metrics.NewRegisteredResettingTimer("chain/code/single/reads", nil) @@ -2060,6 +2063,8 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s accountCacheMissMeter.Mark(rStat.AccountCacheMiss) storageCacheHitMeter.Mark(rStat.StorageCacheHit) storageCacheMissMeter.Mark(rStat.StorageCacheMiss) + codeCacheHitMeter.Mark(rStat.CodeCacheHit) + codeCacheMissMeter.Mark(rStat.CodeCacheMiss) if result != nil { result.stats.StatePrefetchCacheStats = pStat @@ -2182,6 +2187,12 @@ func (bc *BlockChain) ProcessBlock(parentRoot common.Hash, block *types.Block, s stats.StorageUpdated = int(statedb.StorageUpdated.Load()) stats.StorageDeleted = int(statedb.StorageDeleted.Load()) stats.CodeLoaded = statedb.CodeLoaded + stats.CodeBytesRead = statedb.CodeBytesRead + + stats.UniqueAccountsAccessed = statedb.UniqueAccountsAccessed() + stats.UniqueStorageAccessed = statedb.UniqueStorageAccessed() + stats.UniqueCodeExecuted = statedb.UniqueCodeExecuted() + stats.SystemCodeExecuted = statedb.SystemCodeExecuted() 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/blockchain_stats.go b/core/blockchain_stats.go index d52426d57495..33a716da0189 100644 --- a/core/blockchain_stats.go +++ b/core/blockchain_stats.go @@ -39,13 +39,20 @@ 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 + 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 + CodeBytesRead int64 // Total bytes of contract code read + + // Unique access metrics + UniqueAccountsAccessed int // Number of unique accounts accessed + UniqueStorageAccessed int // Number of unique storage slots accessed + UniqueCodeExecuted int // Number of unique contracts executed + SystemCodeExecuted int // Number of unique system contracts executed (subset of UniqueCodeExecuted) Execution time.Duration // Time spent on the EVM execution Validation time.Duration // Time spent on the block validation @@ -100,6 +107,8 @@ func (s *ExecuteStats) reportMetrics() { accountCacheMissMeter.Mark(s.StateReadCacheStats.AccountCacheMiss) storageCacheHitMeter.Mark(s.StateReadCacheStats.StorageCacheHit) storageCacheMissMeter.Mark(s.StateReadCacheStats.StorageCacheMiss) + codeCacheHitMeter.Mark(s.StateReadCacheStats.CodeCacheHit) + codeCacheMissMeter.Mark(s.StateReadCacheStats.CodeCacheMiss) } // logSlow prints the detailed execution statistics if the block is regarded as slow. @@ -119,7 +128,12 @@ Validation: %v State read: %v Account read: %v(%d) Storage read: %v(%d) - Code read: %v(%d) + Code read: %v(%d, %s) + +Unique state access: + Accounts: %d + Storage slots: %d + Contracts executed: %d (%d system) State hash: %v Account hash: %v @@ -140,7 +154,12 @@ DB write: %v common.PrettyDuration(s.AccountReads+s.StorageReads+s.CodeReads), common.PrettyDuration(s.AccountReads), s.AccountLoaded, common.PrettyDuration(s.StorageReads), s.StorageLoaded, - common.PrettyDuration(s.CodeReads), s.CodeLoaded, + common.PrettyDuration(s.CodeReads), s.CodeLoaded, common.StorageSize(s.CodeBytesRead), + + // Unique state access + s.UniqueAccountsAccessed, + s.UniqueStorageAccessed, + s.UniqueCodeExecuted, s.SystemCodeExecuted, // State hash common.PrettyDuration(s.AccountHashes+s.AccountUpdates+s.StorageUpdates+max(s.AccountCommits, s.StorageCommits)), diff --git a/core/state/database.go b/core/state/database.go index ae177d964f9a..8df59d75b1be 100644 --- a/core/state/database.go +++ b/core/state/database.go @@ -226,7 +226,6 @@ func (db *CachingDB) Reader(stateRoot common.Hash) (Reader, error) { // ReadersWithCacheStats creates a pair of state readers sharing the same internal cache and // same backing Reader, but exposing separate statistics. -// and statistics. func (db *CachingDB) ReadersWithCacheStats(stateRoot common.Hash) (ReaderWithStats, ReaderWithStats, error) { reader, err := db.Reader(stateRoot) if err != nil { diff --git a/core/state/reader.go b/core/state/reader.go index c912ca28da8b..409237014612 100644 --- a/core/state/reader.go +++ b/core/state/reader.go @@ -98,6 +98,8 @@ type ReaderStats struct { AccountCacheMiss int64 StorageCacheHit int64 StorageCacheMiss int64 + CodeCacheHit int64 + CodeCacheMiss int64 } // String implements fmt.Stringer, returning string format statistics. @@ -105,6 +107,7 @@ func (s ReaderStats) String() string { var ( accountCacheHitRate float64 storageCacheHitRate float64 + codeCacheHitRate float64 ) if s.AccountCacheHit > 0 { accountCacheHitRate = float64(s.AccountCacheHit) / float64(s.AccountCacheHit+s.AccountCacheMiss) * 100 @@ -112,9 +115,13 @@ func (s ReaderStats) String() string { if s.StorageCacheHit > 0 { storageCacheHitRate = float64(s.StorageCacheHit) / float64(s.StorageCacheHit+s.StorageCacheMiss) * 100 } + if s.CodeCacheHit > 0 { + codeCacheHitRate = float64(s.CodeCacheHit) / float64(s.CodeCacheHit+s.CodeCacheMiss) * 100 + } msg := fmt.Sprintf("Reader statistics\n") msg += fmt.Sprintf("account: hit: %d, miss: %d, rate: %.2f\n", s.AccountCacheHit, s.AccountCacheMiss, accountCacheHitRate) msg += fmt.Sprintf("storage: hit: %d, miss: %d, rate: %.2f\n", s.StorageCacheHit, s.StorageCacheMiss, storageCacheHitRate) + msg += fmt.Sprintf("code: hit: %d, miss: %d, rate: %.2f\n", s.CodeCacheHit, s.CodeCacheMiss, codeCacheHitRate) return msg } @@ -146,32 +153,46 @@ func newCachingCodeReader(db ethdb.KeyValueReader, codeCache *lru.SizeConstraine } } -// Code implements ContractCodeReader, retrieving a particular contract's code. -// If the contract code doesn't exist, no error will be returned. -func (r *cachingCodeReader) Code(addr common.Address, codeHash common.Hash) ([]byte, error) { - code, _ := r.codeCache.Get(codeHash) - if len(code) > 0 { - return code, nil +// code retrieves a particular contract's code along with a flag indicating +// whether it was found in the cache. +func (r *cachingCodeReader) code(addr common.Address, codeHash common.Hash) ([]byte, bool, error) { + code, found := r.codeCache.Get(codeHash) + if found && len(code) > 0 { + return code, true, nil } code = rawdb.ReadCode(r.db, codeHash) if len(code) > 0 { r.codeCache.Add(codeHash, code) r.codeSizeCache.Add(codeHash, len(code)) } - return code, nil + return code, false, nil } -// CodeSize implements ContractCodeReader, retrieving a particular contracts code's size. +// Code implements ContractCodeReader, retrieving a particular contract's code. // If the contract code doesn't exist, no error will be returned. -func (r *cachingCodeReader) CodeSize(addr common.Address, codeHash common.Hash) (int, error) { +func (r *cachingCodeReader) Code(addr common.Address, codeHash common.Hash) ([]byte, error) { + code, _, err := r.code(addr, codeHash) + return code, err +} + +// codeSize retrieves a particular contract's code size along with a flag indicating +// whether it was found in the cache. +func (r *cachingCodeReader) codeSize(addr common.Address, codeHash common.Hash) (int, bool, error) { if cached, ok := r.codeSizeCache.Get(codeHash); ok { - return cached, nil + return cached, true, nil } - code, err := r.Code(addr, codeHash) + code, _, err := r.code(addr, codeHash) if err != nil { - return 0, err + return 0, false, err } - return len(code), nil + return len(code), false, nil +} + +// CodeSize implements ContractCodeReader, retrieving a particular contracts code's size. +// If the contract code doesn't exist, no error will be returned. +func (r *cachingCodeReader) CodeSize(addr common.Address, codeHash common.Hash) (int, error) { + size, _, err := r.codeSize(addr, codeHash) + return size, err } // Has returns the flag indicating whether the contract code with @@ -463,6 +484,28 @@ func newReader(codeReader ContractCodeReader, stateReader StateReader) *reader { } } +// code retrieves contract code with a flag indicating cache hit status. +// Returns (code, incache, error). If the underlying code reader doesn't +// support cache tracking, incache is always false. +func (r *reader) code(addr common.Address, codeHash common.Hash) ([]byte, bool, error) { + if cr, ok := r.ContractCodeReader.(*cachingCodeReader); ok { + return cr.code(addr, codeHash) + } + code, err := r.ContractCodeReader.Code(addr, codeHash) + return code, false, err +} + +// codeSize retrieves contract code size with a flag indicating cache hit status. +// Returns (size, incache, error). If the underlying code reader doesn't +// support cache tracking, incache is always false. +func (r *reader) codeSize(addr common.Address, codeHash common.Hash) (int, bool, error) { + if cr, ok := r.ContractCodeReader.(*cachingCodeReader); ok { + return cr.codeSize(addr, codeHash) + } + size, err := r.ContractCodeReader.CodeSize(addr, codeHash) + return size, false, err +} + // readerWithCache is a wrapper around Reader that maintains additional state caches // to support concurrent state access. type readerWithCache struct { @@ -573,6 +616,24 @@ func (r *readerWithCache) Storage(addr common.Address, slot common.Hash) (common return value, err } +// code retrieves contract code with a flag indicating cache hit status. +func (r *readerWithCache) code(addr common.Address, codeHash common.Hash) ([]byte, bool, error) { + if rr, ok := r.Reader.(*reader); ok { + return rr.code(addr, codeHash) + } + code, err := r.Reader.Code(addr, codeHash) + return code, false, err +} + +// codeSize retrieves contract code size with a flag indicating cache hit status. +func (r *readerWithCache) codeSize(addr common.Address, codeHash common.Hash) (int, bool, error) { + if rr, ok := r.Reader.(*reader); ok { + return rr.codeSize(addr, codeHash) + } + size, err := r.Reader.CodeSize(addr, codeHash) + return size, false, err +} + type readerWithCacheStats struct { *readerWithCache @@ -580,6 +641,8 @@ type readerWithCacheStats struct { accountCacheMiss atomic.Int64 storageCacheHit atomic.Int64 storageCacheMiss atomic.Int64 + codeCacheHit atomic.Int64 + codeCacheMiss atomic.Int64 } // newReaderWithCacheStats constructs the reader with additional statistics tracked. @@ -624,6 +687,34 @@ func (r *readerWithCacheStats) Storage(addr common.Address, slot common.Hash) (c return value, nil } +// Code implements ContractCodeReader, retrieving a particular contract's code. +func (r *readerWithCacheStats) Code(addr common.Address, codeHash common.Hash) ([]byte, error) { + code, incache, err := r.readerWithCache.code(addr, codeHash) + if err != nil { + return nil, err + } + if incache { + r.codeCacheHit.Add(1) + } else { + r.codeCacheMiss.Add(1) + } + return code, nil +} + +// CodeSize implements ContractCodeReader, retrieving a particular contract's code size. +func (r *readerWithCacheStats) CodeSize(addr common.Address, codeHash common.Hash) (int, error) { + size, incache, err := r.readerWithCache.codeSize(addr, codeHash) + if err != nil { + return 0, err + } + if incache { + r.codeCacheHit.Add(1) + } else { + r.codeCacheMiss.Add(1) + } + return size, nil +} + // GetStats implements ReaderWithStats, returning the statistics of state reader. func (r *readerWithCacheStats) GetStats() ReaderStats { return ReaderStats{ @@ -631,5 +722,7 @@ func (r *readerWithCacheStats) GetStats() ReaderStats { AccountCacheMiss: r.accountCacheMiss.Load(), StorageCacheHit: r.storageCacheHit.Load(), StorageCacheMiss: r.storageCacheMiss.Load(), + CodeCacheHit: r.codeCacheHit.Load(), + CodeCacheMiss: r.codeCacheMiss.Load(), } } diff --git a/core/state/state_object.go b/core/state/state_object.go index 91623a838bcb..7b2fcab6d8b0 100644 --- a/core/state/state_object.go +++ b/core/state/state_object.go @@ -543,6 +543,7 @@ func (s *stateObject) Code() []byte { if len(code) == 0 { s.db.setError(fmt.Errorf("code is not found %x", s.CodeHash())) } + s.db.CodeBytesRead += int64(len(code)) s.code = code return code } diff --git a/core/state/statedb.go b/core/state/statedb.go index 7c6b8bbdfca3..d3283d18a25d 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -160,6 +160,11 @@ type StateDB struct { StorageUpdated atomic.Int64 // Number of storage slots updated during the state transition StorageDeleted atomic.Int64 // Number of storage slots deleted during the state transition CodeLoaded int // Number of contract code loaded during the state transition + CodeBytesRead int64 // Total bytes of contract code read during the state transition + + // Unique contracts executed tracking (set of code hashes executed during the state transition) + executedCodes map[common.Hash]struct{} + systemExecutedCodes map[common.Hash]struct{} // Subset of executedCodes that are system calls } // New creates a new state from a given trie. @@ -186,6 +191,8 @@ func NewWithReader(root common.Hash, db Database, reader Reader) (*StateDB, erro journal: newJournal(), accessList: newAccessList(), transientStorage: newTransientStorage(), + executedCodes: make(map[common.Hash]struct{}), + systemExecutedCodes: make(map[common.Hash]struct{}), } if db.TrieDB().IsVerkle() { sdb.accessEvents = NewAccessEvents(db.PointCache()) @@ -379,6 +386,47 @@ func (s *StateDB) GetCodeHash(addr common.Address) common.Hash { return common.Hash{} } +// MarkCodeExecuted records that a contract's code was executed. +// This is used for metrics tracking to count unique contracts executed. +// The isSystem parameter indicates if this is a system call (e.g., EIP-4788 beacon root). +func (s *StateDB) MarkCodeExecuted(codeHash common.Hash, isSystem bool) { + if codeHash == types.EmptyCodeHash || codeHash == (common.Hash{}) { + return + } + if s.executedCodes == nil { + return // Skip tracking for state copies (e.g., prefetcher) + } + s.executedCodes[codeHash] = struct{}{} + if isSystem { + s.systemExecutedCodes[codeHash] = struct{}{} + } +} + +// UniqueCodeExecuted returns the number of unique contract codes executed. +func (s *StateDB) UniqueCodeExecuted() int { + return len(s.executedCodes) +} + +// SystemCodeExecuted returns the number of unique system contract codes executed +// (e.g., EIP-4788 beacon root, EIP-2935 history storage, etc.). +func (s *StateDB) SystemCodeExecuted() int { + return len(s.systemExecutedCodes) +} + +// UniqueAccountsAccessed returns the number of unique accounts accessed during the state transition. +func (s *StateDB) UniqueAccountsAccessed() int { + return len(s.stateObjects) +} + +// UniqueStorageAccessed returns the number of unique storage slots accessed during the state transition. +func (s *StateDB) UniqueStorageAccessed() int { + count := 0 + for _, obj := range s.stateObjects { + count += len(obj.originStorage) + } + return count +} + // GetState retrieves the value associated with the specific key. func (s *StateDB) GetState(addr common.Address, hash common.Hash) common.Hash { stateObject := s.getStateObject(addr) diff --git a/core/state/statedb_hooked.go b/core/state/statedb_hooked.go index 50acc03aa8be..0e3b543322f4 100644 --- a/core/state/statedb_hooked.go +++ b/core/state/statedb_hooked.go @@ -291,3 +291,13 @@ func (s *hookedStateDB) Finalise(deleteEmptyObjects bool) { } } } + +// Inner returns the underlying StateDB. +func (s *hookedStateDB) Inner() *StateDB { + return s.inner +} + +// MarkCodeExecuted records that a contract's code was executed. +func (s *hookedStateDB) MarkCodeExecuted(codeHash common.Hash, isSystem bool) { + s.inner.MarkCodeExecuted(codeHash, isSystem) +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 8975c791c842..0ffe4f709553 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -293,7 +293,10 @@ func (evm *EVM) Call(caller common.Address, addr common.Address, input []byte, g // The contract is a scoped environment for this execution context only. contract := NewContract(caller, addr, value, gas, evm.jumpDests) contract.IsSystemCall = isSystemCall(caller) - contract.SetCallCode(evm.resolveCodeHash(addr), code) + codeHash := evm.resolveCodeHash(addr) + contract.SetCallCode(codeHash, code) + // Track unique contract execution for metrics + evm.StateDB.MarkCodeExecuted(codeHash, contract.IsSystemCall) ret, err = evm.Run(contract, input, false) gas = contract.Gas } @@ -352,7 +355,10 @@ func (evm *EVM) CallCode(caller common.Address, addr common.Address, input []byt // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. contract := NewContract(caller, caller, value, gas, evm.jumpDests) - contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) + codeHash := evm.resolveCodeHash(addr) + contract.SetCallCode(codeHash, evm.resolveCode(addr)) + // Track unique contract execution for metrics + evm.StateDB.MarkCodeExecuted(codeHash, isSystemCall(caller)) ret, err = evm.Run(contract, input, false) gas = contract.Gas } @@ -396,7 +402,10 @@ func (evm *EVM) DelegateCall(originCaller common.Address, caller common.Address, // // Note: The value refers to the original value from the parent call. contract := NewContract(originCaller, caller, value, gas, evm.jumpDests) - contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) + codeHash := evm.resolveCodeHash(addr) + contract.SetCallCode(codeHash, evm.resolveCode(addr)) + // Track unique contract execution for metrics + evm.StateDB.MarkCodeExecuted(codeHash, isSystemCall(originCaller)) ret, err = evm.Run(contract, input, false) gas = contract.Gas } @@ -447,7 +456,10 @@ func (evm *EVM) StaticCall(caller common.Address, addr common.Address, input []b // Initialise a new contract and set the code that is to be used by the EVM. // The contract is a scoped environment for this execution context only. contract := NewContract(caller, addr, new(uint256.Int), gas, evm.jumpDests) - contract.SetCallCode(evm.resolveCodeHash(addr), evm.resolveCode(addr)) + codeHash := evm.resolveCodeHash(addr) + contract.SetCallCode(codeHash, evm.resolveCode(addr)) + // Track unique contract execution for metrics + evm.StateDB.MarkCodeExecuted(codeHash, isSystemCall(caller)) // When an error was returned by the EVM or when setting the creation code // above we revert to the snapshot and consume any gas remaining. Additionally diff --git a/core/vm/interface.go b/core/vm/interface.go index d7f4c10e1f5b..13da4a1ea9c1 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -101,4 +101,9 @@ type StateDB interface { // Finalise must be invoked at the end of a transaction Finalise(bool) + + // MarkCodeExecuted records that a contract's code was executed. + // Used for metrics tracking to count unique contracts executed. + // The isSystem parameter indicates if this is a system call (e.g., EIP-4788). + MarkCodeExecuted(codeHash common.Hash, isSystem bool) }