cmd/geth,trie: stream trie inspect to disk then summarize after

This commit is contained in:
lightclient 2026-02-17 19:23:34 +00:00
parent 97cdc16c3b
commit b4f7b076a5
No known key found for this signature in database
GPG key ID: 91289C7FE4ADADBD
3 changed files with 562 additions and 112 deletions

View file

@ -55,6 +55,19 @@ var (
Name: "remove.chain", Name: "remove.chain",
Usage: "If set, selects the state data for removal", Usage: "If set, selects the state data for removal",
} }
inspectTrieTopFlag = &cli.IntFlag{
Name: "top",
Usage: "Print the top N results per ranking category",
Value: 10,
}
inspectTrieDumpPathFlag = &cli.StringFlag{
Name: "dump-path",
Usage: "Path for the trie statistics dump file",
}
inspectTrieSummarizeFlag = &cli.StringFlag{
Name: "summarize",
Usage: "Summarize an existing trie dump file (skip trie traversal)",
}
removedbCommand = &cli.Command{ removedbCommand = &cli.Command{
Action: removeDB, Action: removeDB,
@ -99,8 +112,14 @@ Remove blockchain and state databases`,
Action: inspectTrie, Action: inspectTrie,
Name: "inspect-trie", Name: "inspect-trie",
ArgsUsage: "<blocknum>", ArgsUsage: "<blocknum>",
Flags: slices.Concat([]cli.Flag{utils.ExcludeStorageFlag, utils.TopFlag, utils.OutputFileFlag}, utils.NetworkFlags, utils.DatabaseFlags), Flags: slices.Concat([]cli.Flag{
Usage: "Print detailed trie information about the structure of account trie and storage tries.", utils.ExcludeStorageFlag,
inspectTrieTopFlag,
utils.OutputFileFlag,
inspectTrieDumpPathFlag,
inspectTrieSummarizeFlag,
}, utils.NetworkFlags, utils.DatabaseFlags),
Usage: "Print detailed trie information about the structure of account trie and storage tries.",
Description: `This commands iterates the entrie trie-backed state. If the 'blocknum' is not specified, Description: `This commands iterates the entrie trie-backed state. If the 'blocknum' is not specified,
the latest block number will be used by default.`, the latest block number will be used by default.`,
} }
@ -398,9 +417,28 @@ func checkStateContent(ctx *cli.Context) error {
} }
func inspectTrie(ctx *cli.Context) error { func inspectTrie(ctx *cli.Context) error {
topN := ctx.Int(inspectTrieTopFlag.Name)
if topN <= 0 {
return fmt.Errorf("invalid --%s value %d (must be > 0)", inspectTrieTopFlag.Name, topN)
}
config := &trie.InspectConfig{
NoStorage: ctx.Bool(utils.ExcludeStorageFlag.Name),
TopN: topN,
Path: ctx.String(utils.OutputFileFlag.Name),
}
if summarizePath := ctx.String(inspectTrieSummarizeFlag.Name); summarizePath != "" {
if ctx.NArg() > 0 {
return fmt.Errorf("block number argument is not supported with --%s", inspectTrieSummarizeFlag.Name)
}
config.DumpPath = summarizePath
log.Info("Summarizing trie dump", "path", summarizePath, "top", topN)
return trie.Summarize(summarizePath, config)
}
if ctx.NArg() > 1 { if ctx.NArg() > 1 {
return fmt.Errorf("excessive number of arguments: %v", ctx.Command.ArgsUsage) return fmt.Errorf("excessive number of arguments: %v", ctx.Command.ArgsUsage)
} }
stack, _ := makeConfigNode(ctx) stack, _ := makeConfigNode(ctx)
db := utils.MakeChainDatabase(ctx, stack, false) db := utils.MakeChainDatabase(ctx, stack, false)
defer stack.Close() defer stack.Close()
@ -413,8 +451,8 @@ func inspectTrie(ctx *cli.Context) error {
) )
switch { switch {
case ctx.NArg() == 0 || ctx.Args().Get(0) == "latest": case ctx.NArg() == 0 || ctx.Args().Get(0) == "latest":
hash := rawdb.ReadHeadHeaderHash(db) head := rawdb.ReadHeadHeaderHash(db)
n, ok := rawdb.ReadHeaderNumber(db, hash) n, ok := rawdb.ReadHeaderNumber(db, head)
if !ok { if !ok {
return fmt.Errorf("could not load head block hash") return fmt.Errorf("could not load head block hash")
} }
@ -430,7 +468,6 @@ func inspectTrie(ctx *cli.Context) error {
} }
} }
// Load head block number based on canonical hash, if applicable.
if number != math.MaxUint64 { if number != math.MaxUint64 {
hash = rawdb.ReadCanonicalHash(db, number) hash = rawdb.ReadCanonicalHash(db, number)
if hash == (common.Hash{}) { if hash == (common.Hash{}) {
@ -439,24 +476,20 @@ func inspectTrie(ctx *cli.Context) error {
blockHeader := rawdb.ReadHeader(db, hash, number) blockHeader := rawdb.ReadHeader(db, hash, number)
trieRoot = blockHeader.Root trieRoot = blockHeader.Root
} }
if (trieRoot == common.Hash{}) { if trieRoot == (common.Hash{}) {
log.Error("Empty root hash") log.Error("Empty root hash")
} }
config.DumpPath = ctx.String(inspectTrieDumpPathFlag.Name)
if config.DumpPath == "" {
config.DumpPath = stack.ResolvePath("trie-dump.bin")
}
triedb := utils.MakeTrieDatabase(ctx, stack, db, false, true, false) triedb := utils.MakeTrieDatabase(ctx, stack, db, false, true, false)
defer triedb.Close() defer triedb.Close()
log.Info("Inspecting trie", "root", trieRoot, "block", number) log.Info("Inspecting trie", "root", trieRoot, "block", number, "dump", config.DumpPath, "top", topN)
config := &trie.InspectConfig{ return trie.Inspect(triedb, trieRoot, config)
NoStorage: ctx.Bool(utils.ExcludeStorageFlag.Name),
TopN: ctx.Int(utils.TopFlag.Name),
Path: ctx.String(utils.OutputFileFlag.Name),
}
err := trie.Inspect(triedb, trieRoot, config)
if err != nil {
return err
}
return nil
} }
func showDBStats(db ethdb.KeyValueStater) { func showDBStats(db ethdb.KeyValueStater) {

View file

@ -17,10 +17,16 @@
package trie package trie
import ( import (
"bufio"
"bytes" "bytes"
"container/heap"
"encoding/binary"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"os" "os"
"slices"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@ -34,58 +40,162 @@ import (
"golang.org/x/sync/semaphore" "golang.org/x/sync/semaphore"
) )
const (
inspectDumpRecordSize = 32 + trieStatLevels*3*4
inspectDefaultTopN = 10
inspectParallelism = int64(16)
)
// inspector is used by the inner inspect function to coordinate across threads. // inspector is used by the inner inspect function to coordinate across threads.
type inspector struct { type inspector struct {
triedb database.NodeDatabase triedb database.NodeDatabase
root common.Hash root common.Hash
config *InspectConfig config *InspectConfig
stats map[common.Hash]*LevelStats accountStat *LevelStats
m sync.Mutex // protects stats
sem *semaphore.Weighted sem *semaphore.Weighted
wg sync.WaitGroup wg sync.WaitGroup
// Pass 1: dump file writer.
dumpMu sync.Mutex
dumpBuf *bufio.Writer
dumpFile *os.File
errMu sync.Mutex
err error
} }
// InspectConfig is a set of options to control inspection and format the // InspectConfig is a set of options to control inspection and format the output.
// output. TopN will print the deepest min(len(results), N) storage tries. // TopN determines the maximum number of entries retained for each top-list.
// If path is set, a JSON version of the data will be written to a file at this path. // Path controls optional JSON output. DumpPath controls the pass-1 dump location.
type InspectConfig struct { type InspectConfig struct {
NoStorage bool NoStorage bool
TopN int TopN int
Path string Path string
DumpPath string
} }
// Inspect walks the trie with the given root and records the number and type of // Inspect walks the trie with the given root and records the number and type of
// nodes at each depth. It works by recursively calling the inner inspect // nodes at each depth. Storage trie stats are streamed to disk in fixed-size
// function on each child node. // records, then summarized in a second pass.
func Inspect(triedb database.NodeDatabase, root common.Hash, config *InspectConfig) error { func Inspect(triedb database.NodeDatabase, root common.Hash, config *InspectConfig) error {
trie, err := New(TrieID(root), triedb) trie, err := New(TrieID(root), triedb)
if err != nil { if err != nil {
return fmt.Errorf("fail to open trie %s: %w", root, err) return fmt.Errorf("fail to open trie %s: %w", root, err)
} }
config = normalizeInspectConfig(config)
dumpFile, err := os.OpenFile(config.DumpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
return fmt.Errorf("failed to create trie dump %s: %w", config.DumpPath, err)
}
in := inspector{
triedb: triedb,
root: root,
config: config,
accountStat: NewLevelStats(),
sem: semaphore.NewWeighted(inspectParallelism),
dumpBuf: bufio.NewWriterSize(dumpFile, 1<<20),
dumpFile: dumpFile,
}
in.inspect(trie, trie.root, 0, []byte{}, in.accountStat)
in.wg.Wait()
// Persist account trie stats as the sentinel record.
in.writeDumpRecord(common.Hash{}, in.accountStat)
if err := in.closeDump(); err != nil {
in.setError(err)
}
if err := in.getError(); err != nil {
return err
}
return Summarize(config.DumpPath, config)
}
func normalizeInspectConfig(config *InspectConfig) *InspectConfig {
if config == nil { if config == nil {
config = &InspectConfig{} config = &InspectConfig{}
} }
in := inspector{ if config.TopN <= 0 {
triedb: triedb, config.TopN = inspectDefaultTopN
root: root,
config: config,
stats: make(map[common.Hash]*LevelStats),
sem: semaphore.NewWeighted(int64(128)),
} }
in.stats[root] = NewLevelStats() if config.DumpPath == "" {
config.DumpPath = "trie-dump.bin"
}
return config
}
in.inspect(trie, trie.root, 0, []byte{}, in.stats[root]) func (in *inspector) closeDump() error {
in.wg.Wait() var ret error
if len(config.Path) > 0 { if in.dumpBuf != nil {
if err := in.writeJSON(); err != nil { if err := in.dumpBuf.Flush(); err != nil {
log.Crit("Error during json encodeing", "error", err) ret = errors.Join(ret, fmt.Errorf("failed to flush trie dump %s: %w", in.config.DumpPath, err))
} }
} else {
in.displayResult()
} }
return nil if in.dumpFile != nil {
if err := in.dumpFile.Close(); err != nil {
ret = errors.Join(ret, fmt.Errorf("failed to close trie dump %s: %w", in.config.DumpPath, err))
}
}
return ret
}
func (in *inspector) setError(err error) {
if err == nil {
return
}
in.errMu.Lock()
defer in.errMu.Unlock()
in.err = errors.Join(in.err, err)
}
func (in *inspector) getError() error {
in.errMu.Lock()
defer in.errMu.Unlock()
return in.err
}
func (in *inspector) hasError() bool {
return in.getError() != nil
}
func (in *inspector) spawn(fn func()) bool {
if !in.sem.TryAcquire(1) {
return false
}
in.wg.Add(1)
go func() {
defer in.sem.Release(1)
defer in.wg.Done()
fn()
}()
return true
}
func (in *inspector) writeDumpRecord(owner common.Hash, s *LevelStats) {
if in.hasError() {
return
}
var buf [inspectDumpRecordSize]byte
copy(buf[:32], owner[:])
off := 32
for i := 0; i < trieStatLevels; i++ {
binary.LittleEndian.PutUint32(buf[off:], uint32(s.level[i].short.Load()))
off += 4
binary.LittleEndian.PutUint32(buf[off:], uint32(s.level[i].full.Load()))
off += 4
binary.LittleEndian.PutUint32(buf[off:], uint32(s.level[i].value.Load()))
off += 4
}
in.dumpMu.Lock()
_, err := in.dumpBuf.Write(buf[:])
in.dumpMu.Unlock()
if err != nil {
in.setError(fmt.Errorf("failed writing trie dump record: %w", err))
}
} }
// inspect is called recursively down the trie. At each level it records the // inspect is called recursively down the trie. At each level it records the
@ -102,22 +212,21 @@ func (in *inspector) inspect(trie *Trie, n node, height uint32, path []byte, sta
// - value: if account, begin inspecting storage trie. // - value: if account, begin inspecting storage trie.
switch n := (n).(type) { switch n := (n).(type) {
case *shortNode: case *shortNode:
in.inspect(trie, n.Val, height+1, append(path, n.Key...), stat) nextPath := slices.Concat(path, n.Key)
in.inspect(trie, n.Val, height+1, nextPath, stat)
case *fullNode: case *fullNode:
for idx, child := range n.Children { for idx, child := range n.Children {
if child == nil { if child == nil {
continue continue
} }
childPath := append(path, byte(idx)) childPath := slices.Concat(path, []byte{byte(idx)})
if in.sem.TryAcquire(1) { childNode := child
in.wg.Add(1) if in.spawn(func() {
go func() { in.inspect(trie, childNode, height+1, childPath, stat)
in.inspect(trie, child, height+1, childPath, stat) }) {
in.wg.Done() continue
}()
} else {
in.inspect(trie, child, height+1, childPath, stat)
} }
in.inspect(trie, childNode, height+1, childPath, stat)
} }
case hashNode: case hashNode:
resolved, err := trie.resolveWithoutTrack(n, path) resolved, err := trie.resolveWithoutTrack(n, path)
@ -143,7 +252,6 @@ func (in *inspector) inspect(trie *Trie, n node, height uint32, path []byte, sta
break break
} }
// Start inspecting storage trie.
if !in.config.NoStorage { if !in.config.NoStorage {
owner := common.BytesToHash(hexToCompact(path)) owner := common.BytesToHash(hexToCompact(path))
storage, err := New(StorageTrieID(in.root, owner, account.Root), in.triedb) storage, err := New(StorageTrieID(in.root, owner, account.Root), in.triedb)
@ -151,86 +259,383 @@ func (in *inspector) inspect(trie *Trie, n node, height uint32, path []byte, sta
log.Error("Failed to open account storage trie", "node", n, "error", err, "height", height, "path", common.Bytes2Hex(path)) log.Error("Failed to open account storage trie", "node", n, "error", err, "height", height, "path", common.Bytes2Hex(path))
break break
} }
stat := NewLevelStats() storageStat := NewLevelStats()
run := func() {
in.m.Lock() in.inspect(storage, storage.root, 0, []byte{}, storageStat)
in.stats[owner] = stat in.writeDumpRecord(owner, storageStat)
in.m.Unlock() }
if in.spawn(run) {
in.wg.Add(1) break
go func() { }
in.inspect(storage, storage.root, 0, []byte{}, stat) run()
in.wg.Done()
}()
} }
default: default:
panic(fmt.Sprintf("%T: invalid node: %v", n, n)) panic(fmt.Sprintf("%T: invalid node: %v", n, n))
} }
// Record stats for current height // Record stats for current height.
stat.add(n, height) stat.add(n, height)
} }
// Display results prints out the inspect results. // Summarize performs pass 2 over a trie dump and reports account stats,
func (in *inspector) displayResult() { // aggregate storage statistics, and top-N rankings.
fmt.Println("Results for trie", in.root) func Summarize(dumpPath string, config *InspectConfig) error {
in.stats[in.root].display("Accounts trie") config = normalizeInspectConfig(config)
fmt.Println("===") if dumpPath == "" {
fmt.Println() dumpPath = config.DumpPath
if !in.config.NoStorage {
// Sort stats by max node depth.
keys, stats := sortedLevelStats(in.stats).sort()
fmt.Println("Results for top storage tries")
for i := range keys[0:min(in.config.TopN, len(keys))] {
fmt.Printf("%d: %s\n", i+1, keys[i])
stats[i].display("storage trie")
}
} }
} if dumpPath == "" {
return errors.New("missing dump path")
func (in *inspector) writeJSON() error { }
file, err := os.OpenFile(in.config.Path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777) file, err := os.Open(dumpPath)
if err != nil { if err != nil {
return err return fmt.Errorf("failed to open trie dump %s: %w", dumpPath, err)
} }
enc := json.NewEncoder(file) defer file.Close()
accountTrie := newJsonStat(in.stats[in.root], "account trie") if info, err := file.Stat(); err == nil {
if err := enc.Encode(accountTrie); err != nil { if info.Size()%inspectDumpRecordSize != 0 {
return err return fmt.Errorf("invalid trie dump size %d (not a multiple of %d)", info.Size(), inspectDumpRecordSize)
}
if !in.config.NoStorage {
// Sort stats by max node depth.
keys, stats := sortedLevelStats(in.stats).sort()
for i := range keys[0:min(in.config.TopN, len(keys))] {
storageTrie := newJsonStat(stats[i], fmt.Sprintf("%x", keys[i]))
if err := enc.Encode(storageTrie); err != nil {
return err
}
} }
} }
depthTop := newStorageTopN(config.TopN, compareStorageByDepth)
totalTop := newStorageTopN(config.TopN, compareStorageByTotal)
valueTop := newStorageTopN(config.TopN, compareStorageByValue)
summary := &inspectSummary{}
reader := bufio.NewReaderSize(file, 1<<20)
var buf [inspectDumpRecordSize]byte
for {
_, err := io.ReadFull(reader, buf[:])
if errors.Is(err, io.EOF) {
break
}
if errors.Is(err, io.ErrUnexpectedEOF) {
return fmt.Errorf("truncated trie dump %s", dumpPath)
}
if err != nil {
return fmt.Errorf("failed reading trie dump %s: %w", dumpPath, err)
}
record := decodeDumpRecord(buf[:])
snapshot := newStorageSnapshot(record.Owner, record.Levels)
if record.Owner == (common.Hash{}) {
summary.Account = snapshot
continue
}
summary.StorageCount++
summary.DepthHistogram[snapshot.MaxDepth]++
summary.StorageTotals.Short += snapshot.Summary.Short
summary.StorageTotals.Full += snapshot.Summary.Full
summary.StorageTotals.Value += snapshot.Summary.Value
depthTop.TryInsert(snapshot)
totalTop.TryInsert(snapshot)
valueTop.TryInsert(snapshot)
}
if summary.Account == nil {
return fmt.Errorf("dump file %s does not contain the account trie sentinel record", dumpPath)
}
summary.TopByDepth = depthTop.Sorted()
summary.TopByTotalNodes = totalTop.Sorted()
summary.TopByValueNodes = valueTop.Sorted()
if config.Path != "" {
return summary.writeJSON(config.Path)
}
summary.display()
return nil return nil
} }
// sortedLevelStats implements sort(). type dumpRecord struct {
type sortedLevelStats map[common.Hash]*LevelStats Owner common.Hash
Levels [trieStatLevels]jsonLevel
}
// sort returns the keys and stats in descending order of maximum trie node depth. func decodeDumpRecord(raw []byte) dumpRecord {
func (s sortedLevelStats) sort() ([]common.Hash, []*LevelStats) {
var ( var (
keys = make([]common.Hash, 0, len(s)) record dumpRecord
stats = make([]*LevelStats, 0, len(s)) off = 32
) )
for k := range s { copy(record.Owner[:], raw[:32])
keys = append(keys, k) for i := 0; i < trieStatLevels; i++ {
record.Levels[i] = jsonLevel{
Short: uint64(binary.LittleEndian.Uint32(raw[off:])),
Full: uint64(binary.LittleEndian.Uint32(raw[off+4:])),
Value: uint64(binary.LittleEndian.Uint32(raw[off+8:])),
}
off += 12
} }
sort.Slice(keys, func(i, j int) bool { return s[keys[i]].maxDepth() > s[keys[j]].maxDepth() }) return record
for _, k := range keys { }
stats = append(stats, s[k])
type storageSnapshot struct {
Owner common.Hash
Levels [trieStatLevels]jsonLevel
Summary jsonLevel
MaxDepth int
TotalNodes uint64
}
func newStorageSnapshot(owner common.Hash, levels [trieStatLevels]jsonLevel) *storageSnapshot {
snapshot := &storageSnapshot{Owner: owner, Levels: levels}
for i := 0; i < trieStatLevels; i++ {
level := levels[i]
if level.Short != 0 || level.Full != 0 || level.Value != 0 {
snapshot.MaxDepth = i
}
snapshot.Summary.Short += level.Short
snapshot.Summary.Full += level.Full
snapshot.Summary.Value += level.Value
} }
return keys, stats snapshot.TotalNodes = snapshot.Summary.Short + snapshot.Summary.Full + snapshot.Summary.Value
return snapshot
}
func (s *storageSnapshot) toLevelStats() *LevelStats {
stats := NewLevelStats()
for i := 0; i < trieStatLevels; i++ {
stats.level[i].short.Store(s.Levels[i].Short)
stats.level[i].full.Store(s.Levels[i].Full)
stats.level[i].value.Store(s.Levels[i].Value)
}
return stats
}
type storageCompare func(a, b *storageSnapshot) int
type topStorage struct {
limit int
cmp storageCompare
heap storageHeap
}
type storageHeap struct {
items []*storageSnapshot
cmp storageCompare
}
func (h storageHeap) Len() int { return len(h.items) }
func (h storageHeap) Less(i, j int) bool {
// Keep the weakest entry at the root (min-heap semantics).
return h.cmp(h.items[i], h.items[j]) < 0
}
func (h storageHeap) Swap(i, j int) { h.items[i], h.items[j] = h.items[j], h.items[i] }
func (h *storageHeap) Push(x any) {
h.items = append(h.items, x.(*storageSnapshot))
}
func (h *storageHeap) Pop() any {
item := h.items[len(h.items)-1]
h.items = h.items[:len(h.items)-1]
return item
}
func newStorageTopN(limit int, cmp storageCompare) *topStorage {
h := storageHeap{cmp: cmp}
heap.Init(&h)
return &topStorage{limit: limit, cmp: cmp, heap: h}
}
func (t *topStorage) TryInsert(item *storageSnapshot) {
if t.limit <= 0 {
return
}
if t.heap.Len() < t.limit {
heap.Push(&t.heap, item)
return
}
if t.cmp(item, t.heap.items[0]) <= 0 {
return
}
heap.Pop(&t.heap)
heap.Push(&t.heap, item)
}
func (t *topStorage) Sorted() []*storageSnapshot {
out := append([]*storageSnapshot(nil), t.heap.items...)
sort.Slice(out, func(i, j int) bool { return t.cmp(out[i], out[j]) > 0 })
return out
}
func compareStorageByDepth(a, b *storageSnapshot) int {
if cmp := compareInt(a.MaxDepth, b.MaxDepth); cmp != 0 {
return cmp
}
if cmp := compareUint64(a.TotalNodes, b.TotalNodes); cmp != 0 {
return cmp
}
if cmp := compareUint64(a.Summary.Value, b.Summary.Value); cmp != 0 {
return cmp
}
return bytes.Compare(a.Owner[:], b.Owner[:])
}
func compareStorageByTotal(a, b *storageSnapshot) int {
if cmp := compareUint64(a.TotalNodes, b.TotalNodes); cmp != 0 {
return cmp
}
if cmp := compareInt(a.MaxDepth, b.MaxDepth); cmp != 0 {
return cmp
}
if cmp := compareUint64(a.Summary.Value, b.Summary.Value); cmp != 0 {
return cmp
}
return bytes.Compare(a.Owner[:], b.Owner[:])
}
func compareStorageByValue(a, b *storageSnapshot) int {
if cmp := compareUint64(a.Summary.Value, b.Summary.Value); cmp != 0 {
return cmp
}
if cmp := compareInt(a.MaxDepth, b.MaxDepth); cmp != 0 {
return cmp
}
if cmp := compareUint64(a.TotalNodes, b.TotalNodes); cmp != 0 {
return cmp
}
return bytes.Compare(a.Owner[:], b.Owner[:])
}
func compareInt(a, b int) int {
switch {
case a > b:
return 1
case a < b:
return -1
default:
return 0
}
}
func compareUint64(a, b uint64) int {
switch {
case a > b:
return 1
case a < b:
return -1
default:
return 0
}
}
type inspectSummary struct {
Account *storageSnapshot
StorageCount uint64
StorageTotals jsonLevel
DepthHistogram [trieStatLevels]uint64
TopByDepth []*storageSnapshot
TopByTotalNodes []*storageSnapshot
TopByValueNodes []*storageSnapshot
}
func (s *inspectSummary) display() {
s.Account.toLevelStats().display("Accounts trie")
fmt.Println("Storage trie aggregate summary")
fmt.Printf("Total storage tries: %d\n", s.StorageCount)
totalNodes := s.StorageTotals.Short + s.StorageTotals.Full + s.StorageTotals.Value
fmt.Printf("Total nodes: %d\n", totalNodes)
fmt.Printf(" Short nodes: %d\n", s.StorageTotals.Short)
fmt.Printf(" Full nodes: %d\n", s.StorageTotals.Full)
fmt.Printf(" Value nodes: %d\n", s.StorageTotals.Value)
b := new(strings.Builder)
table := tablewriter.NewWriter(b)
table.SetHeader([]string{"Max Depth", "Storage Tries"})
for i, count := range s.DepthHistogram {
table.AppendBulk([][]string{{fmt.Sprint(i), fmt.Sprint(count)}})
}
table.Render()
fmt.Print(b.String())
fmt.Println()
s.displayTop("Top storage tries by max depth", s.TopByDepth)
s.displayTop("Top storage tries by total node count", s.TopByTotalNodes)
s.displayTop("Top storage tries by value (slot) count", s.TopByValueNodes)
}
func (s *inspectSummary) displayTop(title string, list []*storageSnapshot) {
fmt.Println(title)
if len(list) == 0 {
fmt.Println("No storage tries found")
fmt.Println()
return
}
for i, item := range list {
fmt.Printf("%d: %s\n", i+1, item.Owner)
item.toLevelStats().display("storage trie")
}
}
func (s *inspectSummary) writeJSON(path string) error {
file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
return err
}
defer file.Close()
enc := json.NewEncoder(file)
enc.SetIndent("", " ")
return enc.Encode(s.toJSON())
}
func (s *inspectSummary) toJSON() *jsonSummary {
jsonSummary := &jsonSummary{
AccountTrie: newJsonStat(s.Account.toLevelStats(), "account trie"),
StorageSummary: jsonStorageSummary{
TotalStorageTries: s.StorageCount,
Totals: s.StorageTotals,
DepthHistogram: s.DepthHistogram,
},
TopByDepth: snapshotsToJSON(s.TopByDepth),
TopByTotalNodes: snapshotsToJSON(s.TopByTotalNodes),
TopByValueNodes: snapshotsToJSON(s.TopByValueNodes),
}
return jsonSummary
}
type jsonSummary struct {
AccountTrie *jsonStat
StorageSummary jsonStorageSummary
TopByDepth []jsonStorageStat
TopByTotalNodes []jsonStorageStat
TopByValueNodes []jsonStorageStat
}
type jsonStorageSummary struct {
TotalStorageTries uint64
Totals jsonLevel
DepthHistogram [trieStatLevels]uint64
}
type jsonStorageStat struct {
Owner string
MaxDepth int
TotalNodes uint64
ValueNodes uint64
Levels []jsonLevel
Summary jsonLevel
}
func snapshotsToJSON(list []*storageSnapshot) []jsonStorageStat {
out := make([]jsonStorageStat, 0, len(list))
for _, item := range list {
stat := newJsonStat(item.toLevelStats(), item.Owner.Hex())
out = append(out, jsonStorageStat{
Owner: item.Owner.Hex(),
MaxDepth: item.MaxDepth,
TotalNodes: item.TotalNodes,
ValueNodes: item.Summary.Value,
Levels: stat.Levels,
Summary: stat.Summary,
})
}
return out
} }
// display will print a table displaying the trie's node statistics. // display will print a table displaying the trie's node statistics.
@ -247,7 +652,7 @@ func (s *LevelStats) display(title string) {
stat := &stat{} stat := &stat{}
for i := range s.level { for i := range s.level {
if s.level[i].empty() { if s.level[i].empty() {
break continue
} }
short, full, value := s.level[i].load() short, full, value := s.level[i].load()
table.AppendBulk([][]string{{"-", fmt.Sprint(i), fmt.Sprint(short), fmt.Sprint(full), fmt.Sprint(value)}}) table.AppendBulk([][]string{{"-", fmt.Sprint(i), fmt.Sprint(short), fmt.Sprint(full), fmt.Sprint(value)}})
@ -276,7 +681,6 @@ type jsonStat struct {
func newJsonStat(s *LevelStats, name string) *jsonStat { func newJsonStat(s *LevelStats, name string) *jsonStat {
ret := jsonStat{Name: name, Summary: jsonLevel{}} ret := jsonStat{Name: name, Summary: jsonLevel{}}
for i := 0; i < len(s.level); i++ { for i := 0; i < len(s.level); i++ {
// only count non-empty levels
if s.level[i].empty() { if s.level[i].empty() {
continue continue
} }

View file

@ -18,6 +18,7 @@ package trie
import ( import (
"math/rand" "math/rand"
"path/filepath"
"testing" "testing"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
@ -46,9 +47,21 @@ func TestInspect(t *testing.T) {
db.Update(root, types.EmptyRootHash, trienode.NewWithNodeSet(nodes)) db.Update(root, types.EmptyRootHash, trienode.NewWithNodeSet(nodes))
db.Commit(root) db.Commit(root)
if err := Inspect(db, root, &InspectConfig{TopN: 1}); err != nil { tempDir := t.TempDir()
dumpPath := filepath.Join(tempDir, "trie-dump.bin")
if err := Inspect(db, root, &InspectConfig{
TopN: 1,
DumpPath: dumpPath,
Path: filepath.Join(tempDir, "trie-summary.json"),
}); err != nil {
t.Fatalf("inspect failed: %v", err) t.Fatalf("inspect failed: %v", err)
} }
if err := Summarize(dumpPath, &InspectConfig{
TopN: 1,
Path: filepath.Join(tempDir, "trie-summary-reanalysis.json"),
}); err != nil {
t.Fatalf("summarize failed: %v", err)
}
} }
func makeAccountsWithStorage(db *testDb, size int, storage bool) (addresses [][20]byte, accounts [][]byte) { func makeAccountsWithStorage(db *testDb, size int, storage bool) (addresses [][20]byte, accounts [][]byte) {