core/state: persist state size metrics

This commit is contained in:
weiihann 2025-12-22 11:58:02 +08:00
parent 2e5cd21edf
commit 288588b852
5 changed files with 245 additions and 30 deletions

View file

@ -360,3 +360,22 @@ func WriteTrienodeHistory(db ethdb.AncientWriter, id uint64, header []byte, keyS
}) })
return err return err
} }
// HasStateSizeStats checks if the state size stats for the specified state root is present in the database.
func HasStateSizeStats(db ethdb.KeyValueReader, root common.Hash) bool {
ok, _ := db.Has(stateSizeStatsKey(root))
return ok
}
// ReadStateSizeStats retrieves the state size stats for the specified state root.
func ReadStateSizeStats(db ethdb.KeyValueReader, root common.Hash) []byte {
data, _ := db.Get(stateSizeStatsKey(root))
return data
}
// WriteStateSizeStats stores the state size stats for the specified state root.
func WriteStateSizeStats(db ethdb.KeyValueWriter, root common.Hash, stats []byte) {
if err := db.Put(stateSizeStatsKey(root), stats); err != nil {
log.Crit("Failed to store state size stats", "err", err)
}
}

View file

@ -427,6 +427,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
filterMapRows stat filterMapRows stat
filterMapLastBlock stat filterMapLastBlock stat
filterMapBlockLV stat filterMapBlockLV stat
stateSizeStats stat
// Path-mode archive data // Path-mode archive data
stateIndex stat stateIndex stat
@ -527,6 +528,10 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
case bytes.HasPrefix(key, StateHistoryIndexPrefix) && len(key) >= len(StateHistoryIndexPrefix)+common.HashLength: case bytes.HasPrefix(key, StateHistoryIndexPrefix) && len(key) >= len(StateHistoryIndexPrefix)+common.HashLength:
stateIndex.add(size) stateIndex.add(size)
// State size stats
case bytes.HasPrefix(key, stateSizeStatsPrefix):
stateSizeStats.add(size)
// Verkle trie data is detected, determine the sub-category // Verkle trie data is detected, determine the sub-category
case bytes.HasPrefix(key, VerklePrefix): case bytes.HasPrefix(key, VerklePrefix):
remain := key[len(VerklePrefix):] remain := key[len(VerklePrefix):]
@ -631,6 +636,7 @@ func InspectDatabase(db ethdb.Database, keyPrefix, keyStart []byte) error {
{"Key-Value store", "Beacon sync headers", beaconHeaders.sizeString(), beaconHeaders.countString()}, {"Key-Value store", "Beacon sync headers", beaconHeaders.sizeString(), beaconHeaders.countString()},
{"Key-Value store", "Clique snapshots", cliqueSnaps.sizeString(), cliqueSnaps.countString()}, {"Key-Value store", "Clique snapshots", cliqueSnaps.sizeString(), cliqueSnaps.countString()},
{"Key-Value store", "Singleton metadata", metadata.sizeString(), metadata.countString()}, {"Key-Value store", "Singleton metadata", metadata.sizeString(), metadata.countString()},
{"Key-Value store", "State size stats", stateSizeStats.sizeString(), stateSizeStats.countString()},
} }
// Inspect all registered append-only file store then. // Inspect all registered append-only file store then.

View file

@ -167,6 +167,9 @@ var (
// Verkle transition information // Verkle transition information
VerkleTransitionStatePrefix = []byte("verkle-transition-state-") VerkleTransitionStatePrefix = []byte("verkle-transition-state-")
// State size metrics
stateSizeStatsPrefix = []byte("st") // stateSizeStatsPrefix + root -> stat (rlp)
) )
// LegacyTxLookupEntry is the legacy TxLookupEntry definition with some unnecessary // LegacyTxLookupEntry is the legacy TxLookupEntry definition with some unnecessary
@ -459,3 +462,10 @@ func trienodeHistoryIndexBlockKey(addressHash common.Hash, path []byte, blockID
func transitionStateKey(hash common.Hash) []byte { func transitionStateKey(hash common.Hash) []byte {
return append(VerkleTransitionStatePrefix, hash.Bytes()...) return append(VerkleTransitionStatePrefix, hash.Bytes()...)
} }
func stateSizeStatsKey(root common.Hash) []byte {
result := make([]byte, len(stateSizeStatsPrefix)+common.HashLength)
copy(result, stateSizeStatsPrefix)
copy(result[len(stateSizeStatsPrefix):], root.Bytes())
return result
}

View file

@ -32,6 +32,7 @@ import (
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/triedb" "github.com/ethereum/go-ethereum/triedb"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
@ -263,7 +264,7 @@ type stateSizeQuery struct {
// SizeTracker handles the state size initialization and tracks of state size metrics. // SizeTracker handles the state size initialization and tracks of state size metrics.
type SizeTracker struct { type SizeTracker struct {
db ethdb.KeyValueStore db ethdb.Database
triedb *triedb.Database triedb *triedb.Database
abort chan struct{} abort chan struct{}
aborted chan struct{} aborted chan struct{}
@ -272,7 +273,7 @@ type SizeTracker struct {
} }
// NewSizeTracker creates a new state size tracker and starts it automatically // NewSizeTracker creates a new state size tracker and starts it automatically
func NewSizeTracker(db ethdb.KeyValueStore, triedb *triedb.Database) (*SizeTracker, error) { func NewSizeTracker(db ethdb.Database, triedb *triedb.Database) (*SizeTracker, error) {
if triedb.Scheme() != rawdb.PathScheme { if triedb.Scheme() != rawdb.PathScheme {
return nil, errors.New("state size tracker is not compatible with hash mode") return nil, errors.New("state size tracker is not compatible with hash mode")
} }
@ -284,7 +285,26 @@ func NewSizeTracker(db ethdb.KeyValueStore, triedb *triedb.Database) (*SizeTrack
updateCh: make(chan *stateUpdate), updateCh: make(chan *stateUpdate),
queryCh: make(chan *stateSizeQuery), queryCh: make(chan *stateSizeQuery),
} }
go t.run()
// Load the existing stats if they exist
stats := make(map[common.Hash]SizeStats, statEvictThreshold)
header := rawdb.ReadHeadHeader(t.db)
headNumber := header.Number.Uint64()
for i := 0; header != nil && i < statEvictThreshold; i++ {
if enc := rawdb.ReadStateSizeStats(t.db, header.Root); len(enc) > 0 {
stats[header.Root] = decodeSizeStats(enc)
}
header = rawdb.ReadHeader(t.db, header.ParentHash, header.Number.Uint64()-1)
}
var h sizeStatsHeap
if len(stats) > 0 {
h = sizeStatsHeap(slices.Collect(maps.Values(stats)))
heap.Init(&h)
log.Info("Loaded state size stats from db", "headNumber", headNumber, "count", len(stats))
}
go t.run(stats, h)
return t, nil return t, nil
} }
@ -314,17 +334,22 @@ func (h *sizeStatsHeap) Pop() any {
} }
// run performs the state size initialization and handles updates // run performs the state size initialization and handles updates
func (t *SizeTracker) run() { func (t *SizeTracker) run(stats map[common.Hash]SizeStats, h sizeStatsHeap) {
defer close(t.aborted) defer close(t.aborted)
var last common.Hash var err error
stats, err := t.init() // launch background thread for state size init if len(stats) == 0 { // if no stats from db, launch background thread for state size init
stats, err = t.init()
if err != nil { if err != nil {
log.Error("Failed to init state size tracker", "err", err)
return return
} }
h := sizeStatsHeap(slices.Collect(maps.Values(stats)))
heap.Init(&h)
h = sizeStatsHeap(slices.Collect(maps.Values(stats)))
heap.Init(&h)
}
var last common.Hash
for { for {
select { select {
case u := <-t.updateCh: case u := <-t.updateCh:
@ -341,6 +366,8 @@ func (t *SizeTracker) run() {
stats[u.root] = stat stats[u.root] = stat
last = u.root last = u.root
rawdb.WriteStateSizeStats(t.db, u.root, encodeSizeStats(stat))
// Publish statistics to metric system // Publish statistics to metric system
stat.publish() stat.publish()
@ -359,12 +386,23 @@ func (t *SizeTracker) run() {
} else { } else {
root = last root = last
} }
if s, ok := stats[root]; ok {
// Check in-memory stats first
s, ok := stats[root]
if ok {
r.result <- &s r.result <- &s
} else { continue
r.result <- nil
} }
// Load from db if exist
if enc := rawdb.ReadStateSizeStats(t.db, root); len(enc) > 0 {
s := decodeSizeStats(enc)
r.result <- &s
continue
}
// Does not exist, return nil result
r.result <- nil
case <-t.abort: case <-t.abort:
return return
} }
@ -381,9 +419,10 @@ type buildResult struct {
func (t *SizeTracker) init() (map[common.Hash]SizeStats, error) { func (t *SizeTracker) init() (map[common.Hash]SizeStats, error) {
// Wait for snapshot completion and then init // Wait for snapshot completion and then init
ticker := time.NewTicker(10 * time.Second) ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop() defer ticker.Stop()
if !t.triedb.SnapshotCompleted() {
wait: wait:
for { for {
select { select {
@ -400,6 +439,7 @@ wait:
return nil, errors.New("size tracker closed") return nil, errors.New("size tracker closed")
} }
} }
}
var ( var (
updates = make(map[common.Hash]*stateUpdate) updates = make(map[common.Hash]*stateUpdate)
@ -413,7 +453,6 @@ wait:
updates[u.root] = u updates[u.root] = u
children[u.originRoot] = append(children[u.originRoot], u.root) children[u.originRoot] = append(children[u.originRoot], u.root)
log.Debug("Received state update", "root", u.root, "blockNumber", u.blockNumber) log.Debug("Received state update", "root", u.root, "blockNumber", u.blockNumber)
case r := <-t.queryCh: case r := <-t.queryCh:
r.err = errors.New("state size is not initialized yet") r.err = errors.New("state size is not initialized yet")
r.result <- nil r.result <- nil
@ -453,7 +492,11 @@ wait:
if err != nil { if err != nil {
return err return err
} }
stats[child] = base.add(diff) stat := base.add(diff)
stats[child] = stat
rawdb.WriteStateSizeStats(t.db, child, encodeSizeStats(stat))
if err := apply(child, stats[child]); err != nil { if err := apply(child, stats[child]); err != nil {
return err return err
} }
@ -466,6 +509,7 @@ wait:
// Set initial latest stats // Set initial latest stats
stats[result.root] = result.stat stats[result.root] = result.stat
rawdb.WriteStateSizeStats(t.db, result.root, encodeSizeStats(result.stat))
log.Info("Measured persistent state size", "root", result.root, "number", result.blockNumber, "stat", result.stat, "elapsed", common.PrettyDuration(result.elapsed)) log.Info("Measured persistent state size", "root", result.root, "number", result.blockNumber, "stat", result.stat, "elapsed", common.PrettyDuration(result.elapsed))
return stats, nil return stats, nil
@ -672,3 +716,64 @@ func (t *SizeTracker) Query(root *common.Hash) (*SizeStats, error) {
return <-r.result, r.err return <-r.result, r.err
} }
} }
// sizeStatsRLP is the RLP-encodable version of SizeStats.
// RLP only supports unsigned integers, so we use uint64 for persistence.
// Cumulative stats are always non-negative, so this is safe for storage.
type sizeStatsRLP struct {
StateRoot common.Hash
BlockNumber uint64
Accounts uint64
AccountBytes uint64
Storages uint64
StorageBytes uint64
AccountTrienodes uint64
AccountTrienodeBytes uint64
StorageTrienodes uint64
StorageTrienodeBytes uint64
ContractCodes uint64
ContractCodeBytes uint64
}
func encodeSizeStats(stat SizeStats) []byte {
rlpStat := sizeStatsRLP{
StateRoot: stat.StateRoot,
BlockNumber: stat.BlockNumber,
Accounts: uint64(stat.Accounts),
AccountBytes: uint64(stat.AccountBytes),
Storages: uint64(stat.Storages),
StorageBytes: uint64(stat.StorageBytes),
AccountTrienodes: uint64(stat.AccountTrienodes),
AccountTrienodeBytes: uint64(stat.AccountTrienodeBytes),
StorageTrienodes: uint64(stat.StorageTrienodes),
StorageTrienodeBytes: uint64(stat.StorageTrienodeBytes),
ContractCodes: uint64(stat.ContractCodes),
ContractCodeBytes: uint64(stat.ContractCodeBytes),
}
data, err := rlp.EncodeToBytes(&rlpStat)
if err != nil {
log.Crit("Failed to encode state size stats", "err", err)
}
return data
}
func decodeSizeStats(data []byte) SizeStats {
var rlpStat sizeStatsRLP
if err := rlp.DecodeBytes(data, &rlpStat); err != nil {
log.Crit("Failed to decode state size stats", "err", err)
}
return SizeStats{
StateRoot: rlpStat.StateRoot,
BlockNumber: rlpStat.BlockNumber,
Accounts: int64(rlpStat.Accounts),
AccountBytes: int64(rlpStat.AccountBytes),
Storages: int64(rlpStat.Storages),
StorageBytes: int64(rlpStat.StorageBytes),
AccountTrienodes: int64(rlpStat.AccountTrienodes),
AccountTrienodeBytes: int64(rlpStat.AccountTrienodeBytes),
StorageTrienodes: int64(rlpStat.StorageTrienodes),
StorageTrienodeBytes: int64(rlpStat.StorageTrienodeBytes),
ContractCodes: int64(rlpStat.ContractCodes),
ContractCodeBytes: int64(rlpStat.ContractCodeBytes),
}
}

View file

@ -237,3 +237,78 @@ func TestSizeTracker(t *testing.T) {
t.Errorf("Storage trie node bytes mismatch: expected %d, got %d", expectedStats.StorageTrienodeBytes, actualStats.StorageTrienodeBytes) t.Errorf("Storage trie node bytes mismatch: expected %d, got %d", expectedStats.StorageTrienodeBytes, actualStats.StorageTrienodeBytes)
} }
} }
func TestSizeStatsPersistence(t *testing.T) {
db := rawdb.NewMemoryDatabase()
defer db.Close()
root := common.HexToHash("0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef")
originalStats := SizeStats{
StateRoot: root,
BlockNumber: 12345,
Accounts: 1000,
AccountBytes: 50000,
Storages: 5000,
StorageBytes: 200000,
AccountTrienodes: 300,
AccountTrienodeBytes: 15000,
StorageTrienodes: 1500,
StorageTrienodeBytes: 75000,
ContractCodes: 100,
ContractCodeBytes: 500000,
}
// Verify no stats exist initially
if rawdb.HasStateSizeStats(db, root) {
t.Fatal("Stats should not exist initially")
}
rawdb.WriteStateSizeStats(db, root, encodeSizeStats(originalStats))
// Verify stats exist
if !rawdb.HasStateSizeStats(db, root) {
t.Fatal("Stats should exist after write")
}
data := rawdb.ReadStateSizeStats(db, root)
loadedStats := decodeSizeStats(data)
// Verify all fields match
if loadedStats.StateRoot != originalStats.StateRoot {
t.Errorf("StateRoot mismatch: have %x, want %x", loadedStats.StateRoot, originalStats.StateRoot)
}
if loadedStats.BlockNumber != originalStats.BlockNumber {
t.Errorf("BlockNumber mismatch: have %d, want %d", loadedStats.BlockNumber, originalStats.BlockNumber)
}
if loadedStats.Accounts != originalStats.Accounts {
t.Errorf("Accounts mismatch: have %d, want %d", loadedStats.Accounts, originalStats.Accounts)
}
if loadedStats.AccountBytes != originalStats.AccountBytes {
t.Errorf("AccountBytes mismatch: have %d, want %d", loadedStats.AccountBytes, originalStats.AccountBytes)
}
if loadedStats.Storages != originalStats.Storages {
t.Errorf("Storages mismatch: have %d, want %d", loadedStats.Storages, originalStats.Storages)
}
if loadedStats.StorageBytes != originalStats.StorageBytes {
t.Errorf("StorageBytes mismatch: have %d, want %d", loadedStats.StorageBytes, originalStats.StorageBytes)
}
if loadedStats.AccountTrienodes != originalStats.AccountTrienodes {
t.Errorf("AccountTrienodes mismatch: have %d, want %d", loadedStats.AccountTrienodes, originalStats.AccountTrienodes)
}
if loadedStats.AccountTrienodeBytes != originalStats.AccountTrienodeBytes {
t.Errorf("AccountTrienodeBytes mismatch: have %d, want %d", loadedStats.AccountTrienodeBytes, originalStats.AccountTrienodeBytes)
}
if loadedStats.StorageTrienodes != originalStats.StorageTrienodes {
t.Errorf("StorageTrienodes mismatch: have %d, want %d", loadedStats.StorageTrienodes, originalStats.StorageTrienodes)
}
if loadedStats.StorageTrienodeBytes != originalStats.StorageTrienodeBytes {
t.Errorf("StorageTrienodeBytes mismatch: have %d, want %d", loadedStats.StorageTrienodeBytes, originalStats.StorageTrienodeBytes)
}
if loadedStats.ContractCodes != originalStats.ContractCodes {
t.Errorf("ContractCodes mismatch: have %d, want %d", loadedStats.ContractCodes, originalStats.ContractCodes)
}
if loadedStats.ContractCodeBytes != originalStats.ContractCodeBytes {
t.Errorf("ContractCodeBytes mismatch: have %d, want %d", loadedStats.ContractCodeBytes, originalStats.ContractCodeBytes)
}
}