Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 19 additions & 10 deletions core/state/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,8 @@ func NewDatabaseForTesting() *CachingDB {
return NewDatabase(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil), nil)
}

// Reader returns a state reader associated with the specified state root.
func (db *CachingDB) Reader(stateRoot common.Hash) (Reader, error) {
// StateReader returns a state reader associated with the specified state root.
func (db *CachingDB) StateReader(stateRoot common.Hash) (StateReader, error) {
var readers []StateReader

// Configure the state reader using the standalone snapshot in hash mode.
Expand Down Expand Up @@ -208,23 +208,32 @@ func (db *CachingDB) Reader(stateRoot common.Hash) (Reader, error) {
}
readers = append(readers, tr)

combined, err := newMultiStateReader(readers...)
return newMultiStateReader(readers...)
}

// Reader implements Database, returning a reader associated with the specified
// state root.
func (db *CachingDB) Reader(stateRoot common.Hash) (Reader, error) {
sr, err := db.StateReader(stateRoot)
if err != nil {
return nil, err
}
return newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), combined), nil
return newReader(newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache), sr), nil
}

// ReadersWithCacheStats creates a pair of state readers sharing the same internal cache and
// same backing Reader, but exposing separate statistics.
// and statistics.
// ReadersWithCacheStats creates a pair of state readers that share the same
// underlying state reader and internal state cache, while maintaining separate
// statistics respectively.
func (db *CachingDB) ReadersWithCacheStats(stateRoot common.Hash) (ReaderWithStats, ReaderWithStats, error) {
reader, err := db.Reader(stateRoot)
r, err := db.StateReader(stateRoot)
if err != nil {
return nil, nil, err
}
shared := newReaderWithCache(reader)
return newReaderWithCacheStats(shared), newReaderWithCacheStats(shared), nil
sr := newStateReaderWithCache(r)

ra := newReaderWithStats(sr, newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache))
rb := newReaderWithStats(sr, newCachingCodeReader(db.disk, db.codeCache, db.codeSizeCache))
return ra, rb, nil
}

// OpenTrie opens the main account trie at a specific root hash.
Expand Down
84 changes: 58 additions & 26 deletions core/state/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ type ContractCodeReader interface {
CodeSize(addr common.Address, codeHash common.Hash) (int, error)
}

// ContractCodeReaderWithStats extends ContractCodeReader by adding GetStats to
// expose statistics of code reader.
type ContractCodeReaderWithStats interface {
ContractCodeReader
GetStats() (int64, int64)
}

// StateReader defines the interface for accessing accounts and storage slots
// associated with a specific state.
//
Expand Down Expand Up @@ -97,23 +104,30 @@ type ReaderStats struct {
AccountCacheMiss int64
StorageCacheHit int64
StorageCacheMiss int64
ContractCodeHit int64
ContractCodeMiss int64
}

// String implements fmt.Stringer, returning string format statistics.
func (s ReaderStats) String() string {
var (
accountCacheHitRate float64
storageCacheHitRate float64
contractCodeHitRate float64
)
if s.AccountCacheHit > 0 {
accountCacheHitRate = float64(s.AccountCacheHit) / float64(s.AccountCacheHit+s.AccountCacheMiss) * 100
}
if s.StorageCacheHit > 0 {
storageCacheHitRate = float64(s.StorageCacheHit) / float64(s.StorageCacheHit+s.StorageCacheMiss) * 100
}
if s.ContractCodeHit > 0 {
contractCodeHitRate = float64(s.ContractCodeHit) / float64(s.ContractCodeHit+s.ContractCodeMiss) * 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.ContractCodeHit, s.ContractCodeMiss, contractCodeHitRate)
return msg
}

Expand All @@ -134,6 +148,10 @@ type cachingCodeReader struct {
// they are natively thread-safe.
codeCache *lru.SizeConstrainedCache[common.Hash, []byte]
codeSizeCache *lru.Cache[common.Hash, int]

// Cache statistics
hit atomic.Int64 // Number of code lookups found in the cache.
miss atomic.Int64 // Number of code lookups not found in the cache.
}

// newCachingCodeReader constructs the code reader.
Expand All @@ -150,8 +168,11 @@ func newCachingCodeReader(db ethdb.KeyValueReader, codeCache *lru.SizeConstraine
func (r *cachingCodeReader) Code(addr common.Address, codeHash common.Hash) ([]byte, error) {
code, _ := r.codeCache.Get(codeHash)
if len(code) > 0 {
r.hit.Add(1)
return code, nil
}
r.miss.Add(1)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will count empty code as misses

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which I think is okay, since we are filtering empty code one layer earlier


code = rawdb.ReadCode(r.db, codeHash)
if len(code) > 0 {
r.codeCache.Add(codeHash, code)
Expand All @@ -164,6 +185,7 @@ func (r *cachingCodeReader) Code(addr common.Address, codeHash common.Hash) ([]b
// If the contract code doesn't exist, no error will be returned.
func (r *cachingCodeReader) CodeSize(addr common.Address, codeHash common.Hash) (int, error) {
if cached, ok := r.codeSizeCache.Get(codeHash); ok {
r.hit.Add(1)
return cached, nil
}
code, err := r.Code(addr, codeHash)
Expand All @@ -180,6 +202,11 @@ func (r *cachingCodeReader) Has(addr common.Address, codeHash common.Hash) bool
return len(code) > 0
}

// GetStats returns the cache statistics fo the code reader.
func (r *cachingCodeReader) GetStats() (int64, int64) {
return r.hit.Load(), r.miss.Load()
}

// flatReader wraps a database state reader and is safe for concurrent access.
type flatReader struct {
reader database.StateReader
Expand Down Expand Up @@ -462,10 +489,10 @@ func newReader(codeReader ContractCodeReader, stateReader StateReader) *reader {
}
}

// readerWithCache is a wrapper around Reader that maintains additional state caches
// to support concurrent state access.
type readerWithCache struct {
Reader // safe for concurrent read
// stateReaderWithCache is a wrapper around StateReader that maintains additional
// state caches to support concurrent state access.
type stateReaderWithCache struct {
StateReader

// Previously resolved state entries.
accounts map[common.Address]*types.StateAccount
Expand All @@ -481,11 +508,11 @@ type readerWithCache struct {
}
}

// newReaderWithCache constructs the reader with local cache.
func newReaderWithCache(reader Reader) *readerWithCache {
r := &readerWithCache{
Reader: reader,
accounts: make(map[common.Address]*types.StateAccount),
// newStateReaderWithCache constructs the state reader with local cache.
func newStateReaderWithCache(sr StateReader) *stateReaderWithCache {
r := &stateReaderWithCache{
StateReader: sr,
accounts: make(map[common.Address]*types.StateAccount),
}
for i := range r.storageBuckets {
r.storageBuckets[i].storages = make(map[common.Address]map[common.Hash]common.Hash)
Expand All @@ -498,7 +525,7 @@ func newReaderWithCache(reader Reader) *readerWithCache {
// might be nil if it's not existent.
//
// An error will be returned if the state is corrupted in the underlying reader.
func (r *readerWithCache) account(addr common.Address) (*types.StateAccount, bool, error) {
func (r *stateReaderWithCache) account(addr common.Address) (*types.StateAccount, bool, error) {
// Try to resolve the requested account in the local cache
r.accountLock.RLock()
acct, ok := r.accounts[addr]
Expand All @@ -507,7 +534,7 @@ func (r *readerWithCache) account(addr common.Address) (*types.StateAccount, boo
return acct, true, nil
}
// Try to resolve the requested account from the underlying reader
acct, err := r.Reader.Account(addr)
acct, err := r.StateReader.Account(addr)
if err != nil {
return nil, false, err
}
Expand All @@ -521,15 +548,15 @@ func (r *readerWithCache) account(addr common.Address) (*types.StateAccount, boo
// The returned account might be nil if it's not existent.
//
// An error will be returned if the state is corrupted in the underlying reader.
func (r *readerWithCache) Account(addr common.Address) (*types.StateAccount, error) {
func (r *stateReaderWithCache) Account(addr common.Address) (*types.StateAccount, error) {
account, _, err := r.account(addr)
return account, err
}

// storage retrieves the storage slot specified by the address and slot key, along
// with a flag indicating whether it's found in the cache or not. The returned
// storage slot might be empty if it's not existent.
func (r *readerWithCache) storage(addr common.Address, slot common.Hash) (common.Hash, bool, error) {
func (r *stateReaderWithCache) storage(addr common.Address, slot common.Hash) (common.Hash, bool, error) {
var (
value common.Hash
ok bool
Expand All @@ -546,7 +573,7 @@ func (r *readerWithCache) storage(addr common.Address, slot common.Hash) (common
return value, true, nil
}
// Try to resolve the requested storage slot from the underlying reader
value, err := r.Reader.Storage(addr, slot)
value, err := r.StateReader.Storage(addr, slot)
if err != nil {
return common.Hash{}, false, err
}
Expand All @@ -567,33 +594,35 @@ func (r *readerWithCache) storage(addr common.Address, slot common.Hash) (common
// existent.
//
// An error will be returned if the state is corrupted in the underlying reader.
func (r *readerWithCache) Storage(addr common.Address, slot common.Hash) (common.Hash, error) {
func (r *stateReaderWithCache) Storage(addr common.Address, slot common.Hash) (common.Hash, error) {
value, _, err := r.storage(addr, slot)
return value, err
}

type readerWithCacheStats struct {
*readerWithCache
type readerWithStats struct {
*stateReaderWithCache
ContractCodeReaderWithStats

accountCacheHit atomic.Int64
accountCacheMiss atomic.Int64
storageCacheHit atomic.Int64
storageCacheMiss atomic.Int64
}

// newReaderWithCacheStats constructs the reader with additional statistics tracked.
func newReaderWithCacheStats(reader *readerWithCache) *readerWithCacheStats {
return &readerWithCacheStats{
readerWithCache: reader,
// newReaderWithStats constructs the reader with additional statistics tracked.
func newReaderWithStats(sr *stateReaderWithCache, cr ContractCodeReaderWithStats) *readerWithStats {
return &readerWithStats{
stateReaderWithCache: sr,
ContractCodeReaderWithStats: cr,
}
}

// Account implements StateReader, retrieving the account specified by the address.
// The returned account might be nil if it's not existent.
//
// An error will be returned if the state is corrupted in the underlying reader.
func (r *readerWithCacheStats) Account(addr common.Address) (*types.StateAccount, error) {
account, incache, err := r.readerWithCache.account(addr)
func (r *readerWithStats) Account(addr common.Address) (*types.StateAccount, error) {
account, incache, err := r.stateReaderWithCache.account(addr)
if err != nil {
return nil, err
}
Expand All @@ -610,8 +639,8 @@ func (r *readerWithCacheStats) Account(addr common.Address) (*types.StateAccount
// existent.
//
// An error will be returned if the state is corrupted in the underlying reader.
func (r *readerWithCacheStats) Storage(addr common.Address, slot common.Hash) (common.Hash, error) {
value, incache, err := r.readerWithCache.storage(addr, slot)
func (r *readerWithStats) Storage(addr common.Address, slot common.Hash) (common.Hash, error) {
value, incache, err := r.stateReaderWithCache.storage(addr, slot)
if err != nil {
return common.Hash{}, err
}
Expand All @@ -624,11 +653,14 @@ func (r *readerWithCacheStats) Storage(addr common.Address, slot common.Hash) (c
}

// GetStats implements ReaderWithStats, returning the statistics of state reader.
func (r *readerWithCacheStats) GetStats() ReaderStats {
func (r *readerWithStats) GetStats() ReaderStats {
codeHit, codeMiss := r.ContractCodeReaderWithStats.GetStats()
return ReaderStats{
AccountCacheHit: r.accountCacheHit.Load(),
AccountCacheMiss: r.accountCacheMiss.Load(),
StorageCacheHit: r.storageCacheHit.Load(),
StorageCacheMiss: r.storageCacheMiss.Load(),
ContractCodeHit: codeHit,
ContractCodeMiss: codeMiss,
}
}