mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-22 20:56:42 +00:00
remove statesize tracer
This commit is contained in:
parent
5f5420dfa1
commit
b9d45acf90
2 changed files with 0 additions and 955 deletions
|
|
@ -1,711 +0,0 @@
|
||||||
// Copyright 2025 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package live
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"encoding/csv"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
|
||||||
"github.com/ethereum/go-ethereum/core/tracing"
|
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
|
||||||
"github.com/ethereum/go-ethereum/eth/tracers"
|
|
||||||
"github.com/ethereum/go-ethereum/log"
|
|
||||||
"github.com/ethereum/go-ethereum/rlp"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
tracers.LiveDirectory.Register("statesize", newStateSizeTracer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Database key size constants matching core/state/state_sizer.go
|
|
||||||
var (
|
|
||||||
accountKeySize = int64(len(rawdb.SnapshotAccountPrefix) + common.HashLength)
|
|
||||||
storageKeySize = int64(len(rawdb.SnapshotStoragePrefix) + common.HashLength*2)
|
|
||||||
accountTrienodePrefixSize = int64(len(rawdb.TrieNodeAccountPrefix))
|
|
||||||
storageTrienodePrefixSize = int64(len(rawdb.TrieNodeStoragePrefix) + common.HashLength)
|
|
||||||
codeKeySize = int64(len(rawdb.CodePrefix) + common.HashLength)
|
|
||||||
)
|
|
||||||
|
|
||||||
// CSV column headers (cumulative only)
|
|
||||||
var csvHeaders = []string{
|
|
||||||
"block_number",
|
|
||||||
"root",
|
|
||||||
"parent_root",
|
|
||||||
"accounts",
|
|
||||||
"account_bytes",
|
|
||||||
"storages",
|
|
||||||
"storage_bytes",
|
|
||||||
"account_trienodes",
|
|
||||||
"account_trienode_bytes",
|
|
||||||
"storage_trienodes",
|
|
||||||
"storage_trienode_bytes",
|
|
||||||
"codes",
|
|
||||||
"code_bytes",
|
|
||||||
}
|
|
||||||
|
|
||||||
// depthCSVHeaders generates headers for the depth CSV files.
|
|
||||||
// Format: block_number, root, parent_root, total_nodes, account_depth_0..64, storage_depth_0..64
|
|
||||||
func depthCSVHeaders() []string {
|
|
||||||
headers := make([]string, 0, 4+65+65)
|
|
||||||
headers = append(headers, "block_number", "root", "parent_root", "total_nodes")
|
|
||||||
for i := 0; i <= 64; i++ {
|
|
||||||
headers = append(headers, fmt.Sprintf("account_depth_%d", i))
|
|
||||||
}
|
|
||||||
for i := 0; i <= 64; i++ {
|
|
||||||
headers = append(headers, fmt.Sprintf("storage_depth_%d", i))
|
|
||||||
}
|
|
||||||
return headers
|
|
||||||
}
|
|
||||||
|
|
||||||
// stateSizeStats represents cumulative state size statistics.
|
|
||||||
type stateSizeStats struct {
|
|
||||||
Accounts int64
|
|
||||||
AccountBytes int64
|
|
||||||
Storages int64
|
|
||||||
StorageBytes int64
|
|
||||||
AccountTrienodes int64
|
|
||||||
AccountTrienodeBytes int64
|
|
||||||
StorageTrienodes int64
|
|
||||||
StorageTrienodeBytes int64
|
|
||||||
Codes int64
|
|
||||||
CodeBytes int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// stateSizeRecord represents a single CSV record with cumulative stats.
|
|
||||||
type stateSizeRecord struct {
|
|
||||||
BlockNumber uint64
|
|
||||||
Root common.Hash
|
|
||||||
ParentRoot common.Hash
|
|
||||||
Stats stateSizeStats
|
|
||||||
}
|
|
||||||
|
|
||||||
type stateSizeTracer struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
file *os.File
|
|
||||||
writer *csv.Writer
|
|
||||||
filePath string
|
|
||||||
|
|
||||||
// Depth tracking - separate files for created and deleted nodes
|
|
||||||
depthCreatedFile *os.File
|
|
||||||
depthCreatedWriter *csv.Writer
|
|
||||||
depthCreatedFilePath string
|
|
||||||
|
|
||||||
depthDeletedFile *os.File
|
|
||||||
depthDeletedWriter *csv.Writer
|
|
||||||
depthDeletedFilePath string
|
|
||||||
|
|
||||||
// Map from state root to cumulative stats (for handling forks)
|
|
||||||
stats map[common.Hash]stateSizeStats
|
|
||||||
|
|
||||||
// initialized is set to true after the first state update processes
|
|
||||||
// and loads the parent stats from the CSV file
|
|
||||||
initialized bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type stateSizeTracerConfig struct {
|
|
||||||
Path string `json:"path"` // Path to the directory where the tracer logs will be stored
|
|
||||||
}
|
|
||||||
|
|
||||||
func newStateSizeTracer(cfg json.RawMessage) (*tracing.Hooks, error) {
|
|
||||||
var config stateSizeTracerConfig
|
|
||||||
if err := json.Unmarshal(cfg, &config); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse config: %v", err)
|
|
||||||
}
|
|
||||||
if config.Path == "" {
|
|
||||||
return nil, errors.New("statesize tracer output path is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
filePath := filepath.Join(config.Path, "statesize.csv")
|
|
||||||
depthCreatedFilePath := filepath.Join(config.Path, "statesize_depth_created.csv")
|
|
||||||
depthDeletedFilePath := filepath.Join(config.Path, "statesize_depth_deleted.csv")
|
|
||||||
|
|
||||||
// Ensure the directory exists
|
|
||||||
if err := os.MkdirAll(config.Path, 0o755); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create statesize directory: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t := &stateSizeTracer{
|
|
||||||
filePath: filePath,
|
|
||||||
depthCreatedFilePath: depthCreatedFilePath,
|
|
||||||
depthDeletedFilePath: depthDeletedFilePath,
|
|
||||||
stats: make(map[common.Hash]stateSizeStats),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open statesize file for appending
|
|
||||||
file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to open statesize file: %v", err)
|
|
||||||
}
|
|
||||||
t.file = file
|
|
||||||
t.writer = csv.NewWriter(file)
|
|
||||||
|
|
||||||
// Write header if file is new (empty)
|
|
||||||
info, err := file.Stat()
|
|
||||||
if err != nil {
|
|
||||||
file.Close()
|
|
||||||
return nil, fmt.Errorf("failed to stat statesize file: %v", err)
|
|
||||||
}
|
|
||||||
if info.Size() == 0 {
|
|
||||||
if err := t.writer.Write(csvHeaders); err != nil {
|
|
||||||
file.Close()
|
|
||||||
return nil, fmt.Errorf("failed to write CSV headers: %v", err)
|
|
||||||
}
|
|
||||||
t.writer.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open depth created file for appending
|
|
||||||
depthCreatedFile, err := os.OpenFile(depthCreatedFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
|
||||||
if err != nil {
|
|
||||||
file.Close()
|
|
||||||
return nil, fmt.Errorf("failed to open depth created file: %v", err)
|
|
||||||
}
|
|
||||||
t.depthCreatedFile = depthCreatedFile
|
|
||||||
t.depthCreatedWriter = csv.NewWriter(depthCreatedFile)
|
|
||||||
|
|
||||||
// Write header if depth created file is new (empty)
|
|
||||||
depthCreatedInfo, err := depthCreatedFile.Stat()
|
|
||||||
if err != nil {
|
|
||||||
file.Close()
|
|
||||||
depthCreatedFile.Close()
|
|
||||||
return nil, fmt.Errorf("failed to stat depth created file: %v", err)
|
|
||||||
}
|
|
||||||
if depthCreatedInfo.Size() == 0 {
|
|
||||||
if err := t.depthCreatedWriter.Write(depthCSVHeaders()); err != nil {
|
|
||||||
file.Close()
|
|
||||||
depthCreatedFile.Close()
|
|
||||||
return nil, fmt.Errorf("failed to write depth created CSV headers: %v", err)
|
|
||||||
}
|
|
||||||
t.depthCreatedWriter.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open depth deleted file for appending
|
|
||||||
depthDeletedFile, err := os.OpenFile(depthDeletedFilePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
|
||||||
if err != nil {
|
|
||||||
file.Close()
|
|
||||||
depthCreatedFile.Close()
|
|
||||||
return nil, fmt.Errorf("failed to open depth deleted file: %v", err)
|
|
||||||
}
|
|
||||||
t.depthDeletedFile = depthDeletedFile
|
|
||||||
t.depthDeletedWriter = csv.NewWriter(depthDeletedFile)
|
|
||||||
|
|
||||||
// Write header if depth deleted file is new (empty)
|
|
||||||
depthDeletedInfo, err := depthDeletedFile.Stat()
|
|
||||||
if err != nil {
|
|
||||||
file.Close()
|
|
||||||
depthCreatedFile.Close()
|
|
||||||
depthDeletedFile.Close()
|
|
||||||
return nil, fmt.Errorf("failed to stat depth deleted file: %v", err)
|
|
||||||
}
|
|
||||||
if depthDeletedInfo.Size() == 0 {
|
|
||||||
if err := t.depthDeletedWriter.Write(depthCSVHeaders()); err != nil {
|
|
||||||
file.Close()
|
|
||||||
depthCreatedFile.Close()
|
|
||||||
depthDeletedFile.Close()
|
|
||||||
return nil, fmt.Errorf("failed to write depth deleted CSV headers: %v", err)
|
|
||||||
}
|
|
||||||
t.depthDeletedWriter.Flush()
|
|
||||||
}
|
|
||||||
|
|
||||||
return &tracing.Hooks{
|
|
||||||
OnStateUpdate: t.onStateUpdate,
|
|
||||||
OnClose: t.onClose,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadStatsForRoot searches the CSV file for a record with the given root hash
|
|
||||||
// and returns its stats. Returns zero stats if not found.
|
|
||||||
func (s *stateSizeTracer) loadStatsForRoot(root common.Hash) (stateSizeStats, bool) {
|
|
||||||
file, err := os.Open(s.filePath)
|
|
||||||
if err != nil {
|
|
||||||
return stateSizeStats{}, false
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
reader := csv.NewReader(bufio.NewReader(file))
|
|
||||||
|
|
||||||
// Read and skip header
|
|
||||||
if _, err := reader.Read(); err != nil {
|
|
||||||
return stateSizeStats{}, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search for the root in the CSV file (read backwards would be more efficient,
|
|
||||||
// but CSV doesn't support that easily, so we just remember the last match)
|
|
||||||
var (
|
|
||||||
found bool
|
|
||||||
lastStats stateSizeStats
|
|
||||||
)
|
|
||||||
|
|
||||||
for {
|
|
||||||
record, err := reader.Read()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return stateSizeStats{}, false
|
|
||||||
}
|
|
||||||
if len(record) < len(csvHeaders) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
recordRoot := common.HexToHash(record[1])
|
|
||||||
if recordRoot == root {
|
|
||||||
stats, err := parseStats(record)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lastStats = stats
|
|
||||||
found = true
|
|
||||||
// Don't break - keep searching for the latest occurrence
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if found {
|
|
||||||
log.Info("Loaded initial stats from CSV", "root", root.Hex())
|
|
||||||
}
|
|
||||||
return lastStats, found
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseStats extracts cumulative statistics from a CSV record.
|
|
||||||
func parseStats(record []string) (stateSizeStats, error) {
|
|
||||||
if len(record) < len(csvHeaders) {
|
|
||||||
return stateSizeStats{}, errors.New("record too short")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Cumulative columns start at index 3
|
|
||||||
stats := stateSizeStats{}
|
|
||||||
var err error
|
|
||||||
|
|
||||||
stats.Accounts, err = strconv.ParseInt(record[3], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return stats, err
|
|
||||||
}
|
|
||||||
stats.AccountBytes, err = strconv.ParseInt(record[4], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return stats, err
|
|
||||||
}
|
|
||||||
stats.Storages, err = strconv.ParseInt(record[5], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return stats, err
|
|
||||||
}
|
|
||||||
stats.StorageBytes, err = strconv.ParseInt(record[6], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return stats, err
|
|
||||||
}
|
|
||||||
stats.AccountTrienodes, err = strconv.ParseInt(record[7], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return stats, err
|
|
||||||
}
|
|
||||||
stats.AccountTrienodeBytes, err = strconv.ParseInt(record[8], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return stats, err
|
|
||||||
}
|
|
||||||
stats.StorageTrienodes, err = strconv.ParseInt(record[9], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return stats, err
|
|
||||||
}
|
|
||||||
stats.StorageTrienodeBytes, err = strconv.ParseInt(record[10], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return stats, err
|
|
||||||
}
|
|
||||||
stats.Codes, err = strconv.ParseInt(record[11], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return stats, err
|
|
||||||
}
|
|
||||||
stats.CodeBytes, err = strconv.ParseInt(record[12], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return stats, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stateSizeTracer) onStateUpdate(update *tracing.StateUpdate) {
|
|
||||||
if update == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// On first update, try to load parent stats from the CSV file
|
|
||||||
s.mu.Lock()
|
|
||||||
if !s.initialized && update.OriginRoot != (types.EmptyRootHash) {
|
|
||||||
s.initialized = true
|
|
||||||
stats, found := s.loadStatsForRoot(update.OriginRoot)
|
|
||||||
if !found {
|
|
||||||
log.Crit("Failed to load parent stats from CSV", "root", update.OriginRoot.Hex())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.stats[update.OriginRoot] = stats
|
|
||||||
}
|
|
||||||
s.mu.Unlock()
|
|
||||||
|
|
||||||
// Calculate deltas
|
|
||||||
var (
|
|
||||||
accountsDelta int64
|
|
||||||
accountBytesDelta int64
|
|
||||||
storagesDelta int64
|
|
||||||
storageBytesDelta int64
|
|
||||||
accountTrienodesDelta int64
|
|
||||||
accountTrienodeBytes int64
|
|
||||||
storageTrienodesDelta int64
|
|
||||||
storageTrienodeBytes int64
|
|
||||||
codesDelta int64
|
|
||||||
codeBytesDelta int64
|
|
||||||
)
|
|
||||||
|
|
||||||
// Calculate account size changes
|
|
||||||
for _, change := range update.AccountChanges {
|
|
||||||
prevLen := slimAccountSize(change.Prev)
|
|
||||||
newLen := slimAccountSize(change.New)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case prevLen > 0 && newLen == 0:
|
|
||||||
accountsDelta--
|
|
||||||
accountBytesDelta -= accountKeySize + int64(prevLen)
|
|
||||||
case prevLen == 0 && newLen > 0:
|
|
||||||
accountsDelta++
|
|
||||||
accountBytesDelta += accountKeySize + int64(newLen)
|
|
||||||
default:
|
|
||||||
accountBytesDelta += int64(newLen - prevLen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
encode := func(val common.Hash) []byte {
|
|
||||||
if val == (common.Hash{}) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
blob, _ := rlp.EncodeToBytes(common.TrimLeftZeroes(val[:]))
|
|
||||||
return blob
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate storage size changes
|
|
||||||
for _, slots := range update.StorageChanges {
|
|
||||||
for _, change := range slots {
|
|
||||||
prevLen := len(encode(change.Prev))
|
|
||||||
newLen := len(encode(change.New))
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case prevLen > 0 && newLen == 0:
|
|
||||||
storagesDelta--
|
|
||||||
storageBytesDelta -= storageKeySize + int64(prevLen)
|
|
||||||
case prevLen == 0 && newLen > 0:
|
|
||||||
storagesDelta++
|
|
||||||
storageBytesDelta += storageKeySize + int64(newLen)
|
|
||||||
default:
|
|
||||||
storageBytesDelta += int64(newLen - prevLen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate trie node size changes and depth counts
|
|
||||||
var (
|
|
||||||
accountDepthCreated [65]int64
|
|
||||||
storageDepthCreated [65]int64
|
|
||||||
accountDepthDeleted [65]int64
|
|
||||||
storageDepthDeleted [65]int64
|
|
||||||
)
|
|
||||||
|
|
||||||
for owner, nodes := range update.TrieChanges {
|
|
||||||
var (
|
|
||||||
keyPrefix int64
|
|
||||||
isAccount = owner == (common.Hash{})
|
|
||||||
)
|
|
||||||
if isAccount {
|
|
||||||
keyPrefix = accountTrienodePrefixSize
|
|
||||||
} else {
|
|
||||||
keyPrefix = storageTrienodePrefixSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate depth counts for created/modified and deleted nodes
|
|
||||||
createdCounts, deletedCounts := calculateDepthCountsByType(nodes)
|
|
||||||
|
|
||||||
for path, change := range nodes {
|
|
||||||
var prevLen, newLen int
|
|
||||||
if change.Prev != nil {
|
|
||||||
prevLen = len(change.Prev.Blob)
|
|
||||||
}
|
|
||||||
if change.New != nil {
|
|
||||||
newLen = len(change.New.Blob)
|
|
||||||
}
|
|
||||||
keySize := keyPrefix + int64(len(path))
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case prevLen > 0 && newLen == 0:
|
|
||||||
if isAccount {
|
|
||||||
accountTrienodesDelta--
|
|
||||||
accountTrienodeBytes -= keySize + int64(prevLen)
|
|
||||||
} else {
|
|
||||||
storageTrienodesDelta--
|
|
||||||
storageTrienodeBytes -= keySize + int64(prevLen)
|
|
||||||
}
|
|
||||||
case prevLen == 0 && newLen > 0:
|
|
||||||
if isAccount {
|
|
||||||
accountTrienodesDelta++
|
|
||||||
accountTrienodeBytes += keySize + int64(newLen)
|
|
||||||
} else {
|
|
||||||
storageTrienodesDelta++
|
|
||||||
storageTrienodeBytes += keySize + int64(newLen)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if isAccount {
|
|
||||||
accountTrienodeBytes += int64(newLen - prevLen)
|
|
||||||
} else {
|
|
||||||
storageTrienodeBytes += int64(newLen - prevLen)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Accumulate depth counts
|
|
||||||
if isAccount {
|
|
||||||
for i := range 65 {
|
|
||||||
accountDepthCreated[i] += createdCounts[i]
|
|
||||||
accountDepthDeleted[i] += deletedCounts[i]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for i := range 65 {
|
|
||||||
storageDepthCreated[i] += createdCounts[i]
|
|
||||||
storageDepthDeleted[i] += deletedCounts[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate contract code size changes
|
|
||||||
codeExists := make(map[common.Hash]struct{})
|
|
||||||
for _, code := range update.CodeChanges {
|
|
||||||
if _, ok := codeExists[code.Hash]; ok || code.Exists {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
codesDelta++
|
|
||||||
codeBytesDelta += codeKeySize + int64(len(code.Code))
|
|
||||||
codeExists[code.Hash] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate cumulative statistics
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
// Look up parent stats
|
|
||||||
parentStats := s.stats[update.OriginRoot] // zero value if not found
|
|
||||||
|
|
||||||
// Apply deltas to get new cumulative stats
|
|
||||||
newStats := stateSizeStats{
|
|
||||||
Accounts: parentStats.Accounts + accountsDelta,
|
|
||||||
AccountBytes: parentStats.AccountBytes + accountBytesDelta,
|
|
||||||
Storages: parentStats.Storages + storagesDelta,
|
|
||||||
StorageBytes: parentStats.StorageBytes + storageBytesDelta,
|
|
||||||
AccountTrienodes: parentStats.AccountTrienodes + accountTrienodesDelta,
|
|
||||||
AccountTrienodeBytes: parentStats.AccountTrienodeBytes + accountTrienodeBytes,
|
|
||||||
StorageTrienodes: parentStats.StorageTrienodes + storageTrienodesDelta,
|
|
||||||
StorageTrienodeBytes: parentStats.StorageTrienodeBytes + storageTrienodeBytes,
|
|
||||||
Codes: parentStats.Codes + codesDelta,
|
|
||||||
CodeBytes: parentStats.CodeBytes + codeBytesDelta,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the new stats for this root
|
|
||||||
s.stats[update.Root] = newStats
|
|
||||||
|
|
||||||
// Calculate total nodes for created and deleted
|
|
||||||
var totalCreated, totalDeleted int64
|
|
||||||
for _, c := range accountDepthCreated {
|
|
||||||
totalCreated += c
|
|
||||||
}
|
|
||||||
for _, c := range storageDepthCreated {
|
|
||||||
totalCreated += c
|
|
||||||
}
|
|
||||||
for _, c := range accountDepthDeleted {
|
|
||||||
totalDeleted += c
|
|
||||||
}
|
|
||||||
for _, c := range storageDepthDeleted {
|
|
||||||
totalDeleted += c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to statesize CSV
|
|
||||||
s.writeRecord(stateSizeRecord{
|
|
||||||
BlockNumber: update.BlockNumber,
|
|
||||||
Root: update.Root,
|
|
||||||
ParentRoot: update.OriginRoot,
|
|
||||||
Stats: newStats,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Write to depth CSV files
|
|
||||||
s.writeDepthRecord(s.depthCreatedWriter, update.BlockNumber, update.Root, update.OriginRoot, totalCreated, accountDepthCreated, storageDepthCreated)
|
|
||||||
s.writeDepthRecord(s.depthDeletedWriter, update.BlockNumber, update.Root, update.OriginRoot, totalDeleted, accountDepthDeleted, storageDepthDeleted)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stateSizeTracer) writeRecord(r stateSizeRecord) {
|
|
||||||
row := []string{
|
|
||||||
strconv.FormatUint(r.BlockNumber, 10),
|
|
||||||
r.Root.Hex(),
|
|
||||||
r.ParentRoot.Hex(),
|
|
||||||
strconv.FormatInt(r.Stats.Accounts, 10),
|
|
||||||
strconv.FormatInt(r.Stats.AccountBytes, 10),
|
|
||||||
strconv.FormatInt(r.Stats.Storages, 10),
|
|
||||||
strconv.FormatInt(r.Stats.StorageBytes, 10),
|
|
||||||
strconv.FormatInt(r.Stats.AccountTrienodes, 10),
|
|
||||||
strconv.FormatInt(r.Stats.AccountTrienodeBytes, 10),
|
|
||||||
strconv.FormatInt(r.Stats.StorageTrienodes, 10),
|
|
||||||
strconv.FormatInt(r.Stats.StorageTrienodeBytes, 10),
|
|
||||||
strconv.FormatInt(r.Stats.Codes, 10),
|
|
||||||
strconv.FormatInt(r.Stats.CodeBytes, 10),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.writer.Write(row); err != nil {
|
|
||||||
log.Warn("Failed to write statesize record", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.writer.Flush()
|
|
||||||
if err := s.writer.Error(); err != nil {
|
|
||||||
log.Warn("Failed to flush statesize record", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stateSizeTracer) writeDepthRecord(writer *csv.Writer, blockNumber uint64, root, parentRoot common.Hash, totalNodes int64, accountDepths, storageDepths [65]int64) {
|
|
||||||
// Build row: block_number, root, parent_root, total_nodes, account_depth_0..64, storage_depth_0..64
|
|
||||||
row := make([]string, 0, 4+65+65)
|
|
||||||
row = append(row, strconv.FormatUint(blockNumber, 10))
|
|
||||||
row = append(row, root.Hex())
|
|
||||||
row = append(row, parentRoot.Hex())
|
|
||||||
row = append(row, strconv.FormatInt(totalNodes, 10))
|
|
||||||
|
|
||||||
for i := 0; i < 65; i++ {
|
|
||||||
row = append(row, strconv.FormatInt(accountDepths[i], 10))
|
|
||||||
}
|
|
||||||
for i := 0; i < 65; i++ {
|
|
||||||
row = append(row, strconv.FormatInt(storageDepths[i], 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := writer.Write(row); err != nil {
|
|
||||||
log.Warn("Failed to write depth record", "error", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
writer.Flush()
|
|
||||||
if err := writer.Error(); err != nil {
|
|
||||||
log.Warn("Failed to flush depth record", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *stateSizeTracer) onClose() {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
if s.writer != nil {
|
|
||||||
s.writer.Flush()
|
|
||||||
}
|
|
||||||
if s.file != nil {
|
|
||||||
if err := s.file.Close(); err != nil {
|
|
||||||
log.Warn("Failed to close statesize tracer file", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if s.depthCreatedWriter != nil {
|
|
||||||
s.depthCreatedWriter.Flush()
|
|
||||||
}
|
|
||||||
if s.depthCreatedFile != nil {
|
|
||||||
if err := s.depthCreatedFile.Close(); err != nil {
|
|
||||||
log.Warn("Failed to close depth created file", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if s.depthDeletedWriter != nil {
|
|
||||||
s.depthDeletedWriter.Flush()
|
|
||||||
}
|
|
||||||
if s.depthDeletedFile != nil {
|
|
||||||
if err := s.depthDeletedFile.Close(); err != nil {
|
|
||||||
log.Warn("Failed to close depth deleted file", "error", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// slimAccountSize calculates the RLP-encoded size of an account in slim format.
|
|
||||||
func slimAccountSize(acct *types.StateAccount) int {
|
|
||||||
if acct == nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
data := types.SlimAccountRLP(*acct)
|
|
||||||
return len(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// calculateDepthCountsByType calculates the depth of each node and separates counts
|
|
||||||
// into created/modified nodes and deleted nodes.
|
|
||||||
// - Created/Modified: nodes that exist after the update (New has data)
|
|
||||||
// - Deleted: nodes that existed before but don't exist after (Prev has data, New is empty)
|
|
||||||
func calculateDepthCountsByType(pathMap map[string]*tracing.TrieNodeChange) (created, deleted [65]int64) {
|
|
||||||
n := len(pathMap)
|
|
||||||
if n == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// First, calculate depth for all nodes using the tree structure
|
|
||||||
paths := make([]string, 0, n)
|
|
||||||
for path := range pathMap {
|
|
||||||
paths = append(paths, path)
|
|
||||||
}
|
|
||||||
slices.Sort(paths)
|
|
||||||
|
|
||||||
// Map from path to its depth
|
|
||||||
depthMap := make(map[string]int, n)
|
|
||||||
|
|
||||||
// Stack stores paths of ancestors
|
|
||||||
stack := make([]string, 0, 65)
|
|
||||||
|
|
||||||
for _, path := range paths {
|
|
||||||
// Pop until stack top is a strict prefix of path
|
|
||||||
for len(stack) > 0 {
|
|
||||||
top := stack[len(stack)-1]
|
|
||||||
if len(top) < len(path) && path[:len(top)] == top {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
stack = stack[:len(stack)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
depth := len(stack)
|
|
||||||
depthMap[path] = depth
|
|
||||||
|
|
||||||
stack = append(stack, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now classify each node based on Prev/New status
|
|
||||||
for path, change := range pathMap {
|
|
||||||
depth := depthMap[path]
|
|
||||||
|
|
||||||
var prevLen, newLen int
|
|
||||||
if change.Prev != nil {
|
|
||||||
prevLen = len(change.Prev.Blob)
|
|
||||||
}
|
|
||||||
if change.New != nil {
|
|
||||||
newLen = len(change.New.Blob)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Created/Modified: New has data (node exists after update)
|
|
||||||
if newLen > 0 {
|
|
||||||
created[depth]++
|
|
||||||
}
|
|
||||||
// Deleted: Prev has data but New is empty (node removed)
|
|
||||||
if prevLen > 0 && newLen == 0 {
|
|
||||||
deleted[depth]++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
@ -1,244 +0,0 @@
|
||||||
// Copyright 2025 The go-ethereum Authors
|
|
||||||
// This file is part of the go-ethereum library.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
|
||||||
// it under the terms of the GNU Lesser General Public License as published by
|
|
||||||
// the Free Software Foundation, either version 3 of the License, or
|
|
||||||
// (at your option) any later version.
|
|
||||||
//
|
|
||||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
|
||||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
// GNU Lesser General Public License for more details.
|
|
||||||
//
|
|
||||||
// You should have received a copy of the GNU Lesser General Public License
|
|
||||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
|
|
||||||
package live
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/core/tracing"
|
|
||||||
"github.com/ethereum/go-ethereum/trie/trienode"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCalculateDepthCountsByType(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
paths []string
|
|
||||||
expectedCreated [65]int64
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty map",
|
|
||||||
paths: []string{},
|
|
||||||
expectedCreated: [65]int64{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "root only",
|
|
||||||
paths: []string{""},
|
|
||||||
expectedCreated: func() [65]int64 {
|
|
||||||
var c [65]int64
|
|
||||||
c[0] = 1
|
|
||||||
return c
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "linear chain - all branch nodes",
|
|
||||||
paths: []string{"", "0", "0a", "0a3"},
|
|
||||||
expectedCreated: func() [65]int64 {
|
|
||||||
var c [65]int64
|
|
||||||
c[0] = 1 // ""
|
|
||||||
c[1] = 1 // "0"
|
|
||||||
c[2] = 1 // "0a"
|
|
||||||
c[3] = 1 // "0a3"
|
|
||||||
return c
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "extension node - path jump",
|
|
||||||
paths: []string{"", "0a3"}, // extension from root to "0a3"
|
|
||||||
expectedCreated: func() [65]int64 {
|
|
||||||
var c [65]int64
|
|
||||||
c[0] = 1 // ""
|
|
||||||
c[1] = 1 // "0a3" (child of root)
|
|
||||||
return c
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "branching at root",
|
|
||||||
paths: []string{"", "0", "1", "2"},
|
|
||||||
expectedCreated: func() [65]int64 {
|
|
||||||
var c [65]int64
|
|
||||||
c[0] = 1 // ""
|
|
||||||
c[1] = 3 // "0", "1", "2"
|
|
||||||
return c
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "two branches from root",
|
|
||||||
paths: []string{"", "0", "0a", "1", "1b"},
|
|
||||||
expectedCreated: func() [65]int64 {
|
|
||||||
var c [65]int64
|
|
||||||
c[0] = 1 // ""
|
|
||||||
c[1] = 2 // "0", "1"
|
|
||||||
c[2] = 2 // "0a", "1b"
|
|
||||||
return c
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mixed extension and branch",
|
|
||||||
paths: []string{"", "0", "0a", "0a3", "0b"},
|
|
||||||
expectedCreated: func() [65]int64 {
|
|
||||||
var c [65]int64
|
|
||||||
c[0] = 1 // ""
|
|
||||||
c[1] = 1 // "0"
|
|
||||||
c[2] = 2 // "0a", "0b"
|
|
||||||
c[3] = 1 // "0a3"
|
|
||||||
return c
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "deep path with extensions",
|
|
||||||
paths: []string{"", "abc", "abcdef"},
|
|
||||||
expectedCreated: func() [65]int64 {
|
|
||||||
var c [65]int64
|
|
||||||
c[0] = 1 // ""
|
|
||||||
c[1] = 1 // "abc"
|
|
||||||
c[2] = 1 // "abcdef"
|
|
||||||
return c
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "siblings at various depths",
|
|
||||||
paths: []string{"", "0", "00", "01", "1", "10", "11"},
|
|
||||||
expectedCreated: func() [65]int64 {
|
|
||||||
var c [65]int64
|
|
||||||
c[0] = 1 // ""
|
|
||||||
c[1] = 2 // "0", "1"
|
|
||||||
c[2] = 4 // "00", "01", "10", "11"
|
|
||||||
return c
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "complex tree",
|
|
||||||
paths: []string{"", "a", "ab", "abc", "abd", "b", "bc"},
|
|
||||||
expectedCreated: func() [65]int64 {
|
|
||||||
var c [65]int64
|
|
||||||
c[0] = 1 // ""
|
|
||||||
c[1] = 2 // "a", "b"
|
|
||||||
c[2] = 2 // "ab", "bc"
|
|
||||||
c[3] = 2 // "abc", "abd"
|
|
||||||
return c
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "max depth path",
|
|
||||||
paths: []string{"", "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"},
|
|
||||||
expectedCreated: func() [65]int64 {
|
|
||||||
var c [65]int64
|
|
||||||
c[0] = 1 // ""
|
|
||||||
c[1] = 1 // 64-nibble path (child of root via extension)
|
|
||||||
return c
|
|
||||||
}(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// Build pathMap with New blob data (simulates created nodes)
|
|
||||||
pathMap := make(map[string]*tracing.TrieNodeChange, len(tt.paths))
|
|
||||||
for _, p := range tt.paths {
|
|
||||||
pathMap[p] = &tracing.TrieNodeChange{
|
|
||||||
New: &trienode.Node{Blob: []byte{0x01}}, // Non-empty blob marks as created
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
created, deleted := calculateDepthCountsByType(pathMap)
|
|
||||||
|
|
||||||
if created != tt.expectedCreated {
|
|
||||||
t.Errorf("calculateDepthCountsByType() created mismatch")
|
|
||||||
for i := 0; i < 65; i++ {
|
|
||||||
if created[i] != tt.expectedCreated[i] {
|
|
||||||
t.Errorf(" depth %d: got %d, want %d", i, created[i], tt.expectedCreated[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// All nodes have New data, so deleted should be all zeros
|
|
||||||
var expectedDeleted [65]int64
|
|
||||||
if deleted != expectedDeleted {
|
|
||||||
t.Errorf("calculateDepthCountsByType() deleted should be all zeros for created nodes")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCalculateDepthCountsByType_Deletion(t *testing.T) {
|
|
||||||
// Test that deleted nodes are counted correctly
|
|
||||||
pathMap := map[string]*tracing.TrieNodeChange{
|
|
||||||
"": {Prev: &trienode.Node{Blob: []byte{0x01}}}, // deleted at depth 0
|
|
||||||
"0": {Prev: &trienode.Node{Blob: []byte{0x01}}, New: &trienode.Node{Blob: []byte{}}}, // deleted at depth 1
|
|
||||||
"0a": {Prev: &trienode.Node{Blob: []byte{0x01}}}, // deleted at depth 2
|
|
||||||
"1": {Prev: &trienode.Node{Blob: []byte{0x01}}, New: &trienode.Node{Blob: []byte{0x02}}}, // modified at depth 1 (not deleted)
|
|
||||||
}
|
|
||||||
|
|
||||||
created, deleted := calculateDepthCountsByType(pathMap)
|
|
||||||
|
|
||||||
// Expected deleted: depth 0 = 1, depth 1 = 1, depth 2 = 1
|
|
||||||
expectedDeleted := [65]int64{1, 1, 1}
|
|
||||||
for i := 0; i < 65; i++ {
|
|
||||||
if deleted[i] != expectedDeleted[i] {
|
|
||||||
t.Errorf("deleted depth %d: got %d, want %d", i, deleted[i], expectedDeleted[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expected created: only "1" is modified (has New data), depth 1 = 1
|
|
||||||
expectedCreated := [65]int64{0, 1}
|
|
||||||
for i := 0; i < 65; i++ {
|
|
||||||
if created[i] != expectedCreated[i] {
|
|
||||||
t.Errorf("created depth %d: got %d, want %d", i, created[i], expectedCreated[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCalculateDepthCountsByType_Mixed(t *testing.T) {
|
|
||||||
// Test mixed scenario: some nodes created, some modified, some deleted
|
|
||||||
pathMap := map[string]*tracing.TrieNodeChange{
|
|
||||||
"": {
|
|
||||||
Prev: &trienode.Node{Blob: []byte{0x01}},
|
|
||||||
New: &trienode.Node{Blob: []byte{0x02}},
|
|
||||||
}, // modified at depth 0
|
|
||||||
"0": {
|
|
||||||
New: &trienode.Node{Blob: []byte{0x01}},
|
|
||||||
}, // created at depth 1
|
|
||||||
"1": {
|
|
||||||
Prev: &trienode.Node{Blob: []byte{0x01}},
|
|
||||||
}, // deleted at depth 1
|
|
||||||
"0a": {
|
|
||||||
Prev: &trienode.Node{Blob: []byte{0x01}},
|
|
||||||
New: &trienode.Node{Blob: []byte{0x02}},
|
|
||||||
}, // modified at depth 2
|
|
||||||
"1b": {
|
|
||||||
Prev: &trienode.Node{Blob: []byte{0x01}},
|
|
||||||
}, // deleted at depth 2
|
|
||||||
}
|
|
||||||
|
|
||||||
created, deleted := calculateDepthCountsByType(pathMap)
|
|
||||||
|
|
||||||
// Created/Modified: "" (depth 0), "0" (depth 1), "0a" (depth 2)
|
|
||||||
expectedCreated := [65]int64{1, 1, 1}
|
|
||||||
for i := 0; i < 65; i++ {
|
|
||||||
if created[i] != expectedCreated[i] {
|
|
||||||
t.Errorf("created depth %d: got %d, want %d", i, created[i], expectedCreated[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deleted: "1" (depth 1), "1b" (depth 2)
|
|
||||||
expectedDeleted := [65]int64{0, 1, 1}
|
|
||||||
for i := 0; i < 65; i++ {
|
|
||||||
if deleted[i] != expectedDeleted[i] {
|
|
||||||
t.Errorf("deleted depth %d: got %d, want %d", i, deleted[i], expectedDeleted[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue