diff --git a/core/stateless/stats.go b/core/stateless/stats.go index adc898929be5..1a6389284ceb 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 - } - d.totalDepth += n - d.samples++ - - if d.minDepth == -1 || n < d.minDepth { - d.minDepth = n - } - if n > d.maxDepth { - d.maxDepth = n +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) } } -// 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 51c78cc9c9fb..6219e622f5bb 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)