// 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 . package trie import ( "bufio" "bytes" "cmp" "container/heap" "encoding/binary" "encoding/json" "errors" "fmt" "io" "os" "slices" "sort" "strings" "sync" "sync/atomic" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/internal/tablewriter" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/triedb/database" "golang.org/x/sync/errgroup" "golang.org/x/sync/semaphore" ) const ( inspectDumpRecordSize = 32 + trieStatLevels*(3*4+8) inspectDefaultTopN = 10 inspectParallelism = int64(16) ) // inspector is used by the inner inspect function to coordinate across threads. type inspector struct { triedb database.NodeDatabase root common.Hash config *InspectConfig accountStat *LevelStats sem *semaphore.Weighted // Pass 1: dump file writer. dumpMu sync.Mutex dumpBuf *bufio.Writer dumpFile *os.File storageRecordsWritten atomic.Uint64 errMu sync.Mutex err error } // InspectConfig is a set of options to control inspection and format the output. // TopN determines the maximum number of entries retained for each top-list. // Path controls optional JSON output. DumpPath controls the pass-1 dump location. type InspectConfig struct { NoStorage bool TopN int Path string DumpPath string } // Inspect walks the trie with the given root and records the number and type of // nodes at each depth. Storage trie stats are streamed to disk in fixed-size // records, then summarized in a second pass. func Inspect(triedb database.NodeDatabase, root common.Hash, config *InspectConfig) error { trie, err := New(TrieID(root), triedb) if err != nil { 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, } // Start progress reporter start := time.Now() done := make(chan struct{}) go func() { ticker := time.NewTicker(8 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: accountNodes := in.accountStat.TotalNodes() storageRecords := in.storageRecordsWritten.Load() log.Info("Inspecting trie", "accountNodes", accountNodes, "storageRecords", storageRecords, "elapsed", common.PrettyDuration(time.Since(start))) case <-done: return } } }() in.recordRootSize(trie, root, in.accountStat) in.inspect(trie, trie.root, 0, []byte{}, in.accountStat) // inspect is synchronous: it waits for all spawned goroutines in its // subtree before returning, so no additional wait is needed here. // Persist account trie stats as the sentinel record. in.writeDumpRecord(common.Hash{}, in.accountStat) if err := in.closeDump(); err != nil { in.setError(err) } // Stop progress reporter close(done) if err := in.getError(); err != nil { return err } return Summarize(config.DumpPath, config) } // InspectContract inspects the on-disk footprint of a single contract. // It reports snapshot storage (slot count + size) and storage trie node // statistics (node type breakdown and per-depth distribution). func InspectContract(triedb database.NodeDatabase, db ethdb.Database, stateRoot common.Hash, address common.Address) error { // Resolve account from the state trie. accountHash := crypto.Keccak256Hash(address.Bytes()) accountTrie, err := New(TrieID(stateRoot), triedb) if err != nil { return fmt.Errorf("failed to open account trie: %w", err) } accountRLP, err := accountTrie.Get(crypto.Keccak256(address.Bytes())) if err != nil { return fmt.Errorf("failed to read account: %w", err) } if accountRLP == nil { return fmt.Errorf("account not found: %s", address) } var account types.StateAccount if err := rlp.DecodeBytes(accountRLP, &account); err != nil { return fmt.Errorf("failed to decode account: %w", err) } if account.Root == (common.Hash{}) || account.Root == types.EmptyRootHash { return fmt.Errorf("account %s has no storage", address) } // Look up account snapshot. accountData := rawdb.ReadAccountSnapshot(db, accountHash) // Run trie walk + snap iteration in parallel. var ( snapSlots atomic.Uint64 snapSize atomic.Uint64 g errgroup.Group start = time.Now() ) // Goroutine 1: Snapshot storage iteration. g.Go(func() error { prefix := append(rawdb.SnapshotStoragePrefix, accountHash.Bytes()...) it := db.NewIterator(prefix, nil) defer it.Release() for it.Next() { if !bytes.HasPrefix(it.Key(), prefix) { break } snapSlots.Add(1) snapSize.Add(uint64(len(it.Key()) + len(it.Value()))) } return it.Error() }) // Goroutine 2: Storage trie walk using the existing inspector. storageStat := NewLevelStats() g.Go(func() error { owner := accountHash storage, err := New(StorageTrieID(stateRoot, owner, account.Root), triedb) if err != nil { return fmt.Errorf("failed to open storage trie: %w", err) } in := &inspector{ triedb: triedb, root: stateRoot, config: &InspectConfig{NoStorage: true}, accountStat: NewLevelStats(), // unused, but needed by inspector sem: semaphore.NewWeighted(inspectParallelism), dumpBuf: bufio.NewWriter(io.Discard), } in.recordRootSize(storage, account.Root, storageStat) in.inspect(storage, storage.root, 0, []byte{}, storageStat) if err := in.getError(); err != nil { return err } return nil }) // Progress reporter. done := make(chan struct{}) go func() { ticker := time.NewTicker(8 * time.Second) defer ticker.Stop() for { select { case <-ticker.C: log.Info("Inspecting contract", "snapSlots", snapSlots.Load(), "trieNodes", storageStat.TotalNodes(), "elapsed", common.PrettyDuration(time.Since(start))) case <-done: return } } }() if err := g.Wait(); err != nil { close(done) return err } close(done) // Display results. fmt.Printf("\n=== Contract Inspection: %s ===\n", address) fmt.Printf("Account hash: %s\n\n", accountHash) if len(accountData) == 0 { fmt.Println("Account snapshot: not found") } else { fmt.Printf("Account snapshot: %s\n", common.StorageSize(len(accountData))) } fmt.Printf("Snapshot storage: %d slots (%s)\n", snapSlots.Load(), common.StorageSize(snapSize.Load())) // Compute trie totals from LevelStats. var trieTotal, trieSize uint64 for i := 0; i < trieStatLevels; i++ { short, full, value, size := storageStat.level[i].load() trieTotal += short + full + value trieSize += size } fmt.Printf("Storage trie: %d nodes (%s)\n", trieTotal, common.StorageSize(trieSize)) // Depth distribution table with node type columns. fmt.Println("\nStorage Trie Depth Distribution:") b := new(strings.Builder) table := tablewriter.NewWriter(b) table.SetHeader([]string{"Depth", "Short", "Full", "Value", "Nodes", "Size"}) for i := 0; i < trieStatLevels; i++ { short, full, value, size := storageStat.level[i].load() total := short + full + value if total == 0 && size == 0 { continue } table.AppendBulk([][]string{{ fmt.Sprint(i), fmt.Sprint(short), fmt.Sprint(full), fmt.Sprint(value), fmt.Sprint(total), common.StorageSize(size).String(), }}) } table.Render() fmt.Print(b.String()) return nil } func normalizeInspectConfig(config *InspectConfig) *InspectConfig { if config == nil { config = &InspectConfig{} } if config.TopN <= 0 { config.TopN = inspectDefaultTopN } if config.DumpPath == "" { config.DumpPath = "trie-dump.bin" } return config } func (in *inspector) recordRootSize(trie *Trie, root common.Hash, stat *LevelStats) { if root == (common.Hash{}) || root == types.EmptyRootHash { return } blob := trie.prevalueTracer.Get(nil) if len(blob) == 0 { resolved, err := trie.reader.Node(nil, root) if err != nil { log.Error("Failed to read trie root for size accounting", "trie", trie.Hash(), "root", root, "err", err) return } blob = resolved } stat.addSize(0, uint64(len(blob))) } func (in *inspector) closeDump() error { var ret error if in.dumpBuf != nil { if err := in.dumpBuf.Flush(); err != nil { ret = errors.Join(ret, fmt.Errorf("failed to flush trie dump %s: %w", in.config.DumpPath, err)) } } 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 } // trySpawn attempts to run fn in a new goroutine, bounded by the semaphore. // If a slot is available the goroutine is started and tracked via wg; the // caller must call wg.Wait() before reading any state written by fn. // Returns false (and does not start a goroutine) when no slot is available. func (in *inspector) trySpawn(wg *sync.WaitGroup, fn func()) bool { if !in.sem.TryAcquire(1) { return false } wg.Add(1) go func() { defer in.sem.Release(1) defer 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 binary.LittleEndian.PutUint64(buf[off:], s.level[i].size.Load()) off += 8 } 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)) } // Increment counter for storage tries only (not for account trie) if owner != (common.Hash{}) { in.storageRecordsWritten.Add(1) } } // inspect walks the trie rooted at n and records node statistics into stat. // It may spawn goroutines for subtrees, but always waits for them before // returning — the caller sees a fully-populated stat when inspect returns. func (in *inspector) inspect(trie *Trie, n node, height uint32, path []byte, stat *LevelStats) { if n == nil { return } // wg tracks goroutines spawned by this call so we can wait for them // before returning. This guarantees stat is complete when we return, // which is critical for storage tries that write their dump record // immediately after inspect returns. var wg sync.WaitGroup // Four types of nodes can be encountered: // - short: extend path with key, inspect single value. // - full: inspect all 17 children, spin up new threads when possible. // - hash: need to resolve node from disk, retry inspect on result. // - value: if account, begin inspecting storage trie. switch n := (n).(type) { case *shortNode: nextPath := slices.Concat(path, n.Key) in.inspect(trie, n.Val, height+1, nextPath, stat) case *fullNode: for idx, child := range n.Children { if child == nil { continue } childPath := slices.Concat(path, []byte{byte(idx)}) childNode := child if in.trySpawn(&wg, func() { in.inspect(trie, childNode, height+1, childPath, stat) }) { continue } in.inspect(trie, childNode, height+1, childPath, stat) } case hashNode: blob, err := trie.reader.Node(path, common.BytesToHash(n)) if err != nil { log.Error("Failed to resolve HashNode", "err", err, "trie", trie.Hash(), "height", height+1, "path", path) return } stat.addSize(height, uint64(len(blob))) resolved := mustDecodeNode(n, blob) in.inspect(trie, resolved, height, path, stat) // Return early here so this level isn't recorded twice. return case valueNode: if !hasTerm(path) { break } var account types.StateAccount if err := rlp.Decode(bytes.NewReader(n), &account); err != nil { // Not an account value. break } if account.Root == (common.Hash{}) || account.Root == types.EmptyRootHash { // Account is empty, nothing further to inspect. break } if !in.config.NoStorage { owner := common.BytesToHash(hexToCompact(path)) storage, err := New(StorageTrieID(in.root, owner, account.Root), in.triedb) if err != nil { log.Error("Failed to open account storage trie", "node", n, "error", err, "height", height, "path", common.Bytes2Hex(path)) break } storageStat := NewLevelStats() run := func() { in.recordRootSize(storage, account.Root, storageStat) in.inspect(storage, storage.root, 0, []byte{}, storageStat) in.writeDumpRecord(owner, storageStat) } if in.trySpawn(&wg, run) { break } run() } default: panic(fmt.Sprintf("%T: invalid node: %v", n, n)) } // Wait for all goroutines spawned at this level before recording // the current node. This ensures the entire subtree is counted // before this call returns. wg.Wait() // Record stats for current height. stat.add(n, height) } // Summarize performs pass 2 over a trie dump and reports account stats, // aggregate storage statistics, and top-N rankings. func Summarize(dumpPath string, config *InspectConfig) error { config = normalizeInspectConfig(config) if dumpPath == "" { dumpPath = config.DumpPath } if dumpPath == "" { return errors.New("missing dump path") } file, err := os.Open(dumpPath) if err != nil { return fmt.Errorf("failed to open trie dump %s: %w", dumpPath, err) } defer file.Close() if info, err := file.Stat(); err == nil { if info.Size()%inspectDumpRecordSize != 0 { return fmt.Errorf("invalid trie dump size %d (not a multiple of %d)", info.Size(), inspectDumpRecordSize) } } depthTop := newStorageStatsTopN(config.TopN, compareStorageStatsByDepth) totalTop := newStorageStatsTopN(config.TopN, compareStorageStatsByTotal) valueTop := newStorageStatsTopN(config.TopN, compareStorageStatsByValue) 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 := newStorageStats(record.Owner, record.Levels) if record.Owner == (common.Hash{}) { summary.Account = snapshot continue } summary.StorageCount++ summary.DepthHistogram[snapshot.MaxDepth]++ for i := 0; i < trieStatLevels; i++ { summary.StorageLevels[i].Short += record.Levels[i].Short summary.StorageLevels[i].Full += record.Levels[i].Full summary.StorageLevels[i].Value += record.Levels[i].Value summary.StorageLevels[i].Size += record.Levels[i].Size } 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) } for i := 0; i < trieStatLevels; i++ { summary.StorageTotals.Short += summary.StorageLevels[i].Short summary.StorageTotals.Full += summary.StorageLevels[i].Full summary.StorageTotals.Value += summary.StorageLevels[i].Value summary.StorageTotals.Size += summary.StorageLevels[i].Size } summary.TopByDepth = depthTop.Sorted() summary.TopByTotalNodes = totalTop.Sorted() summary.TopByValueNodes = valueTop.Sorted() if config.Path != "" { return summary.writeJSON(config.Path) } summary.display() return nil } type dumpRecord struct { Owner common.Hash Levels [trieStatLevels]jsonLevel } func decodeDumpRecord(raw []byte) dumpRecord { var ( record dumpRecord off = 32 ) copy(record.Owner[:], raw[:32]) 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:])), Size: binary.LittleEndian.Uint64(raw[off+12:]), } off += 20 } return record } type storageStats struct { Owner common.Hash Levels [trieStatLevels]jsonLevel Summary jsonLevel MaxDepth int TotalNodes uint64 TotalSize uint64 } func newStorageStats(owner common.Hash, levels [trieStatLevels]jsonLevel) *storageStats { snapshot := &storageStats{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 snapshot.Summary.Size += level.Size } snapshot.TotalNodes = snapshot.Summary.Short + snapshot.Summary.Full + snapshot.Summary.Value snapshot.TotalSize = snapshot.Summary.Size return snapshot } func trimLevels(levels [trieStatLevels]jsonLevel) []jsonLevel { n := len(levels) for n > 0 && levels[n-1] == (jsonLevel{}) { n-- } return levels[:n] } func (s *storageStats) MarshalJSON() ([]byte, error) { type jsonStorageSnapshot struct { Owner common.Hash `json:"Owner"` MaxDepth int `json:"MaxDepth"` TotalNodes uint64 `json:"TotalNodes"` TotalSize uint64 `json:"TotalSize"` ValueNodes uint64 `json:"ValueNodes"` Levels []jsonLevel `json:"Levels"` Summary jsonLevel `json:"Summary"` } return json.Marshal(jsonStorageSnapshot{ Owner: s.Owner, MaxDepth: s.MaxDepth, TotalNodes: s.TotalNodes, TotalSize: s.TotalSize, ValueNodes: s.Summary.Value, Levels: trimLevels(s.Levels), Summary: s.Summary, }) } func (s *storageStats) 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) stats.level[i].size.Store(s.Levels[i].Size) } return stats } type storageStatsCompare func(a, b *storageStats) int type storageStatsTopN struct { limit int cmp storageStatsCompare heap storageStatsHeap } type storageStatsHeap struct { items []*storageStats cmp storageStatsCompare } func (h storageStatsHeap) Len() int { return len(h.items) } func (h storageStatsHeap) 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 storageStatsHeap) Swap(i, j int) { h.items[i], h.items[j] = h.items[j], h.items[i] } func (h *storageStatsHeap) Push(x any) { h.items = append(h.items, x.(*storageStats)) } func (h *storageStatsHeap) Pop() any { item := h.items[len(h.items)-1] h.items = h.items[:len(h.items)-1] return item } func newStorageStatsTopN(limit int, cmp storageStatsCompare) *storageStatsTopN { h := storageStatsHeap{cmp: cmp} heap.Init(&h) return &storageStatsTopN{limit: limit, cmp: cmp, heap: h} } func (t *storageStatsTopN) TryInsert(item *storageStats) { 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 *storageStatsTopN) Sorted() []*storageStats { out := append([]*storageStats(nil), t.heap.items...) sort.Slice(out, func(i, j int) bool { return t.cmp(out[i], out[j]) > 0 }) return out } func compareStorageStatsByDepth(a, b *storageStats) int { return cmp.Or( cmp.Compare(a.MaxDepth, b.MaxDepth), cmp.Compare(a.TotalNodes, b.TotalNodes), cmp.Compare(a.Summary.Value, b.Summary.Value), bytes.Compare(a.Owner[:], b.Owner[:]), ) } func compareStorageStatsByTotal(a, b *storageStats) int { return cmp.Or( cmp.Compare(a.TotalNodes, b.TotalNodes), cmp.Compare(a.MaxDepth, b.MaxDepth), cmp.Compare(a.Summary.Value, b.Summary.Value), bytes.Compare(a.Owner[:], b.Owner[:]), ) } func compareStorageStatsByValue(a, b *storageStats) int { return cmp.Or( cmp.Compare(a.Summary.Value, b.Summary.Value), cmp.Compare(a.MaxDepth, b.MaxDepth), cmp.Compare(a.TotalNodes, b.TotalNodes), bytes.Compare(a.Owner[:], b.Owner[:]), ) } type inspectSummary struct { Account *storageStats StorageCount uint64 StorageTotals jsonLevel StorageLevels [trieStatLevels]jsonLevel DepthHistogram [trieStatLevels]uint64 TopByDepth []*storageStats TopByTotalNodes []*storageStats TopByValueNodes []*storageStats } func (s *inspectSummary) display() { s.displayCombinedDepthTable() 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("Total size: %s\n", common.StorageSize(s.StorageTotals.Size)) 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) displayCombinedDepthTable() { accountTotal := s.Account.Summary.Short + s.Account.Summary.Full + s.Account.Summary.Value storageTotal := s.StorageTotals.Short + s.StorageTotals.Full + s.StorageTotals.Value accountTotalSize := s.Account.Summary.Size storageTotalSize := s.StorageTotals.Size fmt.Println("Trie Depth Distribution") fmt.Printf("Account Trie: %d nodes (%s)\n", accountTotal, common.StorageSize(accountTotalSize)) fmt.Printf("Storage Tries: %d nodes (%s) across %d tries\n", storageTotal, common.StorageSize(storageTotalSize), s.StorageCount) b := new(strings.Builder) table := tablewriter.NewWriter(b) table.SetHeader([]string{"Depth", "Account Nodes", "Account Size", "Storage Nodes", "Storage Size"}) for i := 0; i < trieStatLevels; i++ { accountNodes := s.Account.Levels[i].Short + s.Account.Levels[i].Full + s.Account.Levels[i].Value accountSize := s.Account.Levels[i].Size storageNodes := s.StorageLevels[i].Short + s.StorageLevels[i].Full + s.StorageLevels[i].Value storageSize := s.StorageLevels[i].Size if accountNodes == 0 && storageNodes == 0 { continue } table.AppendBulk([][]string{{ fmt.Sprint(i), fmt.Sprint(accountNodes), common.StorageSize(accountSize).String(), fmt.Sprint(storageNodes), common.StorageSize(storageSize).String(), }}) } table.Render() fmt.Print(b.String()) fmt.Println() } func (s *inspectSummary) displayTop(title string, list []*storageStats) { 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) MarshalJSON() ([]byte, error) { type jsonAccountTrie struct { Name string `json:"Name"` Levels []jsonLevel `json:"Levels"` Summary jsonLevel `json:"Summary"` } type jsonStorageSummary struct { TotalStorageTries uint64 `json:"TotalStorageTries"` Totals jsonLevel `json:"Totals"` Levels []jsonLevel `json:"Levels"` DepthHistogram [trieStatLevels]uint64 `json:"DepthHistogram"` } type jsonInspectSummary struct { AccountTrie jsonAccountTrie `json:"AccountTrie"` StorageSummary jsonStorageSummary `json:"StorageSummary"` TopByDepth []*storageStats `json:"TopByDepth"` TopByTotalNodes []*storageStats `json:"TopByTotalNodes"` TopByValueNodes []*storageStats `json:"TopByValueNodes"` } return json.Marshal(jsonInspectSummary{ AccountTrie: jsonAccountTrie{ Name: "account trie", Levels: trimLevels(s.Account.Levels), Summary: s.Account.Summary, }, StorageSummary: jsonStorageSummary{ TotalStorageTries: s.StorageCount, Totals: s.StorageTotals, Levels: trimLevels(s.StorageLevels), DepthHistogram: s.DepthHistogram, }, TopByDepth: s.TopByDepth, TopByTotalNodes: s.TopByTotalNodes, TopByValueNodes: s.TopByValueNodes, }) } 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) } // display will print a table displaying the trie's node statistics. func (s *LevelStats) display(title string) { // Shorten title if too long. if len(title) > 32 { title = title[0:8] + "..." + title[len(title)-8:] } b := new(strings.Builder) table := tablewriter.NewWriter(b) table.SetHeader([]string{title, "Level", "Short Nodes", "Full Node", "Value Node"}) stat := &stat{} for i := range s.level { if s.level[i].empty() { continue } short, full, value, _ := s.level[i].load() table.AppendBulk([][]string{{"-", fmt.Sprint(i), fmt.Sprint(short), fmt.Sprint(full), fmt.Sprint(value)}}) stat.add(&s.level[i]) } short, full, value, _ := stat.load() table.SetFooter([]string{"Total", "", fmt.Sprint(short), fmt.Sprint(full), fmt.Sprint(value)}) table.Render() fmt.Print(b.String()) fmt.Println("Max depth", s.MaxDepth()) fmt.Println() } type jsonLevel struct { Short uint64 Full uint64 Value uint64 Size uint64 }