core/stateless: track number of leaf nodes at each trie depth (#32533)

Switches to using counters so that the gauges don't cause any
information to be lost. Counters can be used to calculate all sorts of
metrics on Grafana. Which is also why min/avg/max logic is removed to
make things simple and small here.
This commit is contained in:
Ömer Faruk Irmak 2025-09-02 13:43:26 +03:00 committed by GitHub
parent 1263f3dfc1
commit c36f7bec7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 57 additions and 142 deletions

View file

@ -20,73 +20,32 @@ import (
"maps"
"slices"
"sort"
"strconv"
"strings"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/metrics"
)
var (
accountTrieDepthAvg = metrics.NewRegisteredGauge("witness/trie/account/depth/avg", nil)
accountTrieDepthMin = metrics.NewRegisteredGauge("witness/trie/account/depth/min", nil)
accountTrieDepthMax = metrics.NewRegisteredGauge("witness/trie/account/depth/max", nil)
var accountTrieLeavesAtDepth [16]*metrics.Counter
var storageTrieLeavesAtDepth [16]*metrics.Counter
storageTrieDepthAvg = metrics.NewRegisteredGauge("witness/trie/storage/depth/avg", nil)
storageTrieDepthMin = metrics.NewRegisteredGauge("witness/trie/storage/depth/min", nil)
storageTrieDepthMax = metrics.NewRegisteredGauge("witness/trie/storage/depth/max", nil)
)
// depthStats tracks min/avg/max statistics for trie access depths.
type depthStats struct {
totalDepth int64
samples int64
minDepth int64
maxDepth int64
}
// newDepthStats creates a new depthStats with default values.
func newDepthStats() *depthStats {
return &depthStats{minDepth: -1}
}
// add records a new depth sample.
func (d *depthStats) add(n int64) {
if n < 0 {
return
func init() {
for i := 0; i < 16; i++ {
accountTrieLeavesAtDepth[i] = metrics.NewRegisteredCounter("witness/trie/account/leaves/depth_"+strconv.Itoa(i), nil)
storageTrieLeavesAtDepth[i] = metrics.NewRegisteredCounter("witness/trie/storage/leaves/depth_"+strconv.Itoa(i), nil)
}
d.totalDepth += n
d.samples++
if d.minDepth == -1 || n < d.minDepth {
d.minDepth = n
}
if n > d.maxDepth {
d.maxDepth = n
}
}
// report uploads the collected statistics into the provided gauges.
func (d *depthStats) report(maxGauge, minGauge, avgGauge *metrics.Gauge) {
if d.samples == 0 {
return
}
maxGauge.Update(d.maxDepth)
minGauge.Update(d.minDepth)
avgGauge.Update(d.totalDepth / d.samples)
}
// WitnessStats aggregates statistics for account and storage trie accesses.
type WitnessStats struct {
accountTrie *depthStats
storageTrie *depthStats
accountTrieLeaves [16]int64
storageTrieLeaves [16]int64
}
// NewWitnessStats creates a new WitnessStats collector.
func NewWitnessStats() *WitnessStats {
return &WitnessStats{
accountTrie: newDepthStats(),
storageTrie: newDepthStats(),
}
return &WitnessStats{}
}
// Add records trie access depths from the given node paths.
@ -102,9 +61,9 @@ func (s *WitnessStats) Add(nodes map[string][]byte, owner common.Hash) {
// The last path is always a leaf.
if i == len(paths)-1 || !strings.HasPrefix(paths[i+1], paths[i]) {
if owner == (common.Hash{}) {
s.accountTrie.add(int64(len(path)))
s.accountTrieLeaves[len(path)] += 1
} else {
s.storageTrie.add(int64(len(path)))
s.storageTrieLeaves[len(path)] += 1
}
}
}
@ -112,6 +71,8 @@ func (s *WitnessStats) Add(nodes map[string][]byte, owner common.Hash) {
// ReportMetrics reports the collected statistics to the global metrics registry.
func (s *WitnessStats) ReportMetrics() {
s.accountTrie.report(accountTrieDepthMax, accountTrieDepthMin, accountTrieDepthAvg)
s.storageTrie.report(storageTrieDepthMax, storageTrieDepthMin, storageTrieDepthAvg)
for i := 0; i < 16; i++ {
accountTrieLeavesAtDepth[i].Inc(s.accountTrieLeaves[i])
storageTrieLeavesAtDepth[i].Inc(s.storageTrieLeaves[i])
}
}

View file

@ -24,27 +24,32 @@ import (
func TestWitnessStatsAdd(t *testing.T) {
tests := []struct {
name string
nodes map[string][]byte
owner common.Hash
expectedAccountDepth int64
expectedStorageDepth int64
name string
nodes map[string][]byte
owner common.Hash
expectedAccountLeaves map[int64]int64
expectedStorageLeaves map[int64]int64
}{
{
name: "empty nodes",
nodes: map[string][]byte{},
owner: common.Hash{},
expectedAccountDepth: 0,
expectedStorageDepth: 0,
name: "empty nodes",
nodes: map[string][]byte{},
owner: common.Hash{},
},
{
name: "single account trie leaf at depth 0",
nodes: map[string][]byte{
"": []byte("data"),
},
owner: common.Hash{},
expectedAccountLeaves: map[int64]int64{0: 1},
},
{
name: "single account trie leaf",
nodes: map[string][]byte{
"abc": []byte("data"),
},
owner: common.Hash{},
expectedAccountDepth: 3,
expectedStorageDepth: 0,
owner: common.Hash{},
expectedAccountLeaves: map[int64]int64{3: 1},
},
{
name: "account trie with internal nodes",
@ -53,9 +58,8 @@ func TestWitnessStatsAdd(t *testing.T) {
"ab": []byte("data2"),
"abc": []byte("data3"),
},
owner: common.Hash{},
expectedAccountDepth: 3, // Only "abc" is a leaf
expectedStorageDepth: 0,
owner: common.Hash{},
expectedAccountLeaves: map[int64]int64{3: 1}, // Only "abc" is a leaf
},
{
name: "multiple account trie branches",
@ -67,9 +71,8 @@ func TestWitnessStatsAdd(t *testing.T) {
"bc": []byte("data5"),
"bcd": []byte("data6"),
},
owner: common.Hash{},
expectedAccountDepth: 6, // "abc" (3) + "bcd" (3) = 6
expectedStorageDepth: 0,
owner: common.Hash{},
expectedAccountLeaves: map[int64]int64{3: 2}, // "abc" (3) + "bcd" (3)
},
{
name: "siblings are all leaves",
@ -78,9 +81,8 @@ func TestWitnessStatsAdd(t *testing.T) {
"ab": []byte("data2"),
"ac": []byte("data3"),
},
owner: common.Hash{},
expectedAccountDepth: 6, // 2 + 2 + 2 = 6
expectedStorageDepth: 0,
owner: common.Hash{},
expectedAccountLeaves: map[int64]int64{2: 3},
},
{
name: "storage trie leaves",
@ -90,9 +92,8 @@ func TestWitnessStatsAdd(t *testing.T) {
"123": []byte("data3"),
"124": []byte("data4"),
},
owner: common.HexToHash("0x1234"),
expectedAccountDepth: 0,
expectedStorageDepth: 6, // "123" (3) + "124" (3) = 6
owner: common.HexToHash("0x1234"),
expectedStorageLeaves: map[int64]int64{3: 2}, // "123" (3) + "124" (3)
},
{
name: "complex trie structure",
@ -107,9 +108,8 @@ func TestWitnessStatsAdd(t *testing.T) {
"235": []byte("data8"),
"3": []byte("data9"),
},
owner: common.Hash{},
expectedAccountDepth: 13, // "123"(3) + "124"(3) + "234"(3) + "235"(3) + "3"(1) = 13
expectedStorageDepth: 0,
owner: common.Hash{},
expectedAccountLeaves: map[int64]int64{1: 1, 3: 4}, // "123"(3) + "124"(3) + "234"(3) + "235"(3) + "3"(1)
},
}
@ -118,74 +118,28 @@ func TestWitnessStatsAdd(t *testing.T) {
stats := NewWitnessStats()
stats.Add(tt.nodes, tt.owner)
var expectedAccountTrieLeaves [16]int64
for depth, count := range tt.expectedAccountLeaves {
expectedAccountTrieLeaves[depth] = count
}
var expectedStorageTrieLeaves [16]int64
for depth, count := range tt.expectedStorageLeaves {
expectedStorageTrieLeaves[depth] = count
}
// Check account trie depth
if stats.accountTrie.totalDepth != tt.expectedAccountDepth {
t.Errorf("Account trie total depth = %d, want %d", stats.accountTrie.totalDepth, tt.expectedAccountDepth)
if stats.accountTrieLeaves != expectedAccountTrieLeaves {
t.Errorf("Account trie total depth = %v, want %v", stats.accountTrieLeaves, expectedAccountTrieLeaves)
}
// Check storage trie depth
if stats.storageTrie.totalDepth != tt.expectedStorageDepth {
t.Errorf("Storage trie total depth = %d, want %d", stats.storageTrie.totalDepth, tt.expectedStorageDepth)
if stats.storageTrieLeaves != expectedStorageTrieLeaves {
t.Errorf("Storage trie total depth = %v, want %v", stats.storageTrieLeaves, expectedStorageTrieLeaves)
}
})
}
}
func TestWitnessStatsMinMax(t *testing.T) {
stats := NewWitnessStats()
// Add some account trie nodes with varying depths
stats.Add(map[string][]byte{
"a": []byte("data1"),
"ab": []byte("data2"),
"abc": []byte("data3"),
"abcd": []byte("data4"),
"abcde": []byte("data5"),
}, common.Hash{})
// Only "abcde" is a leaf (depth 5)
if stats.accountTrie.minDepth != 5 {
t.Errorf("Account trie min depth = %d, want %d", stats.accountTrie.minDepth, 5)
}
if stats.accountTrie.maxDepth != 5 {
t.Errorf("Account trie max depth = %d, want %d", stats.accountTrie.maxDepth, 5)
}
// Add more leaves with different depths
stats.Add(map[string][]byte{
"x": []byte("data6"),
"yz": []byte("data7"),
}, common.Hash{})
// Now we have leaves at depths 1, 2, and 5
if stats.accountTrie.minDepth != 1 {
t.Errorf("Account trie min depth after update = %d, want %d", stats.accountTrie.minDepth, 1)
}
if stats.accountTrie.maxDepth != 5 {
t.Errorf("Account trie max depth after update = %d, want %d", stats.accountTrie.maxDepth, 5)
}
}
func TestWitnessStatsAverage(t *testing.T) {
stats := NewWitnessStats()
// Add nodes that will create leaves at depths 2, 3, and 4
stats.Add(map[string][]byte{
"aa": []byte("data1"),
"bb": []byte("data2"),
"ccc": []byte("data3"),
"dddd": []byte("data4"),
}, common.Hash{})
// All are leaves: 2 + 2 + 3 + 4 = 11 total, 4 samples
expectedAvg := int64(11) / int64(4)
actualAvg := stats.accountTrie.totalDepth / stats.accountTrie.samples
if actualAvg != expectedAvg {
t.Errorf("Account trie average depth = %d, want %d", actualAvg, expectedAvg)
}
}
func BenchmarkWitnessStatsAdd(b *testing.B) {
// Create a realistic trie node structure
nodes := make(map[string][]byte)