github.com/MetalBlockchain/subnet-evm@v0.4.9/sync/statesync/trie_sync_stats.go (about)

     1  // (c) 2021-2022, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package statesync
     5  
     6  import (
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	utils_math "github.com/MetalBlockchain/metalgo/utils/math"
    12  	"github.com/MetalBlockchain/subnet-evm/metrics"
    13  	"github.com/ethereum/go-ethereum/common"
    14  	"github.com/ethereum/go-ethereum/log"
    15  )
    16  
    17  const (
    18  	updateFrequency  = 1 * time.Minute
    19  	leafRateHalfLife = 1 * time.Minute
    20  	epsilon          = 1e-6 // added to avoid division by 0
    21  )
    22  
    23  // trieSyncStats keeps track of the total number of leafs and tries
    24  // completed during a sync.
    25  type trieSyncStats struct {
    26  	lock sync.Mutex
    27  
    28  	lastUpdated time.Time
    29  	leafsRate   utils_math.Averager
    30  
    31  	triesRemaining   int
    32  	triesSynced      int
    33  	triesStartTime   time.Time
    34  	leafsSinceUpdate uint64
    35  
    36  	remainingLeafs map[*trieSegment]uint64
    37  
    38  	// metrics
    39  	totalLeafs     metrics.Counter
    40  	triesSegmented metrics.Counter
    41  	leafsRateGauge metrics.Gauge
    42  }
    43  
    44  func newTrieSyncStats() *trieSyncStats {
    45  	now := time.Now()
    46  	return &trieSyncStats{
    47  		remainingLeafs: make(map[*trieSegment]uint64),
    48  		lastUpdated:    now,
    49  
    50  		// metrics
    51  		totalLeafs:     metrics.GetOrRegisterCounter("state_sync_total_leafs", nil),
    52  		leafsRateGauge: metrics.GetOrRegisterGauge("state_sync_leafs_per_second", nil),
    53  		triesSegmented: metrics.GetOrRegisterCounter("state_sync_tries_segmented", nil),
    54  	}
    55  }
    56  
    57  // incTriesSegmented increases the metric for segmented tries.
    58  func (t *trieSyncStats) incTriesSegmented() {
    59  	t.triesSegmented.Inc(1) // safe to be called concurrently
    60  }
    61  
    62  // incLeafs takes a lock and adds [count] to the total number of leafs synced.
    63  // periodically outputs a log message with the number of leafs and tries.
    64  func (t *trieSyncStats) incLeafs(segment *trieSegment, count uint64, remaining uint64) {
    65  	t.lock.Lock()
    66  	defer t.lock.Unlock()
    67  
    68  	t.totalLeafs.Inc(int64(count))
    69  	t.leafsSinceUpdate += count
    70  	t.remainingLeafs[segment] = remaining
    71  
    72  	now := time.Now()
    73  	sinceUpdate := now.Sub(t.lastUpdated)
    74  	if sinceUpdate > updateFrequency {
    75  		t.updateETA(sinceUpdate, now)
    76  		t.lastUpdated = now
    77  		t.leafsSinceUpdate = 0
    78  	}
    79  }
    80  
    81  // estimateSegmentsInProgressTime retrns the ETA for all trie segments
    82  // in progress to finish (uses the one with most remaining leafs to estimate).
    83  func (t *trieSyncStats) estimateSegmentsInProgressTime() time.Duration {
    84  	if len(t.remainingLeafs) == 0 {
    85  		// if there are no tries in progress, return 0
    86  		return 0
    87  	}
    88  
    89  	maxLeafs := uint64(0)
    90  	for _, leafs := range t.remainingLeafs {
    91  		if leafs > maxLeafs {
    92  			maxLeafs = leafs
    93  		}
    94  	}
    95  	perThreadLeafsRate := (t.leafsRate.Read() + epsilon) / float64(len(t.remainingLeafs))
    96  	return time.Duration(float64(maxLeafs)/perThreadLeafsRate) * time.Second
    97  }
    98  
    99  // trieDone takes a lock and adds one to the total number of tries synced.
   100  func (t *trieSyncStats) trieDone(root common.Hash) {
   101  	t.lock.Lock()
   102  	defer t.lock.Unlock()
   103  
   104  	for segment := range t.remainingLeafs {
   105  		if segment.trie.root == root {
   106  			delete(t.remainingLeafs, segment)
   107  		}
   108  	}
   109  
   110  	t.triesSynced++
   111  	t.triesRemaining--
   112  }
   113  
   114  // updateETA calculates and logs and ETA based on the number of leafs
   115  // currently in progress and the number of tries remaining.
   116  // assumes lock is held.
   117  func (t *trieSyncStats) updateETA(sinceUpdate time.Duration, now time.Time) {
   118  	leafsRate := float64(t.leafsSinceUpdate) / sinceUpdate.Seconds()
   119  	if t.leafsRate == nil {
   120  		t.leafsRate = utils_math.NewAverager(leafsRate, leafRateHalfLife, now)
   121  	} else {
   122  		t.leafsRate.Observe(leafsRate, now)
   123  	}
   124  	t.leafsRateGauge.Update(int64(t.leafsRate.Read()))
   125  
   126  	leafsTime := t.estimateSegmentsInProgressTime()
   127  	if t.triesSynced == 0 {
   128  		// provide a separate ETA for the account trie syncing step since we
   129  		// don't know the total number of storage tries yet.
   130  		log.Info("state sync: syncing account trie", "ETA", roundETA(leafsTime))
   131  		return
   132  	}
   133  
   134  	triesTime := now.Sub(t.triesStartTime) * time.Duration(t.triesRemaining) / time.Duration(t.triesSynced)
   135  	log.Info(
   136  		"state sync: syncing storage tries",
   137  		"triesRemaining", t.triesRemaining,
   138  		"ETA", roundETA(leafsTime+triesTime), // TODO: should we use max instead of sum?
   139  	)
   140  }
   141  
   142  func (t *trieSyncStats) setTriesRemaining(triesRemaining int) {
   143  	t.lock.Lock()
   144  	defer t.lock.Unlock()
   145  
   146  	t.triesRemaining = triesRemaining
   147  	t.triesStartTime = time.Now()
   148  }
   149  
   150  // roundETA rounds [d] to a minute and chops off the "0s" suffix
   151  // returns "<1m" if [d] rounds to 0 minutes.
   152  func roundETA(d time.Duration) string {
   153  	str := d.Round(time.Minute).String()
   154  	str = strings.TrimSuffix(str, "0s")
   155  	if len(str) == 0 {
   156  		return "<1m"
   157  	}
   158  	return str
   159  }