From c36f7bec7f580a34fc6a55b8ea72734be44ba42f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Irmak?= Date: Tue, 2 Sep 2025 13:43:26 +0300 Subject: [PATCH] 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. --- core/stateless/stats.go | 71 +++++-------------- core/stateless/stats_test.go | 128 +++++++++++------------------------ 2 files changed, 57 insertions(+), 142 deletions(-) diff --git a/core/stateless/stats.go b/core/stateless/stats.go index adc898929b..1a6389284c 100644 --- a/core/stateless/stats.go +++ b/core/stateless/stats.go @@ -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]) + } } diff --git a/core/stateless/stats_test.go b/core/stateless/stats_test.go index 51c78cc9c9..6219e622f5 100644 --- a/core/stateless/stats_test.go +++ b/core/stateless/stats_test.go @@ -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)