github.com/ari-anchor/sei-tendermint@v0.0.0-20230519144642-dc826b7b56bb/test/e2e/runner/benchmark.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"math"
     8  	"path/filepath"
     9  	"time"
    10  
    11  	"github.com/ari-anchor/sei-tendermint/libs/log"
    12  	e2e "github.com/ari-anchor/sei-tendermint/test/e2e/pkg"
    13  	"github.com/ari-anchor/sei-tendermint/types"
    14  )
    15  
    16  // Benchmark is a simple function for fetching, calculating and printing
    17  // the following metrics:
    18  // 1. Average block production time
    19  // 2. Block interval standard deviation
    20  // 3. Max block interval (slowest block)
    21  // 4. Min block interval (fastest block)
    22  //
    23  // Metrics are based of the `benchmarkLength`, the amount of consecutive blocks
    24  // sampled from in the testnet
    25  func Benchmark(ctx context.Context, logger log.Logger, testnet *e2e.Testnet, benchmarkLength int64) error {
    26  	block, err := getLatestBlock(ctx, testnet)
    27  	if err != nil {
    28  		return err
    29  	}
    30  
    31  	logger.Info("Beginning benchmark period...", "height", block.Height)
    32  	startAt := time.Now()
    33  	// wait for the length of the benchmark period in blocks to pass. We allow 5 seconds for each block
    34  	// which should be sufficient.
    35  	waitingTime := time.Duration(benchmarkLength*5) * time.Second
    36  	ctx, cancel := context.WithTimeout(ctx, waitingTime)
    37  	defer cancel()
    38  	block, _, err = waitForHeight(ctx, testnet, block.Height+benchmarkLength)
    39  	if err != nil {
    40  		return err
    41  	}
    42  	dur := time.Since(startAt)
    43  
    44  	logger.Info("Ending benchmark period", "height", block.Height)
    45  
    46  	// fetch a sample of blocks
    47  	blocks, err := fetchBlockChainSample(ctx, testnet, benchmarkLength)
    48  	if err != nil {
    49  		return err
    50  	}
    51  
    52  	// slice into time intervals and collate data
    53  	timeIntervals := splitIntoBlockIntervals(blocks)
    54  	testnetStats := extractTestnetStats(timeIntervals)
    55  
    56  	// populate data
    57  	testnetStats.populateTxns(blocks)
    58  	testnetStats.totalTime = dur
    59  	testnetStats.benchmarkLength = benchmarkLength
    60  	testnetStats.startHeight = blocks[0].Header.Height
    61  	testnetStats.endHeight = blocks[len(blocks)-1].Header.Height
    62  
    63  	// print and return
    64  	logger.Info(testnetStats.String())
    65  	logger.Info(testnetStats.getReportJSON(testnet))
    66  	return nil
    67  }
    68  
    69  func (t *testnetStats) populateTxns(blocks []*types.BlockMeta) {
    70  	t.numtxns = 0
    71  	for _, b := range blocks {
    72  		t.numtxns += int64(b.NumTxs)
    73  	}
    74  }
    75  
    76  type testnetStats struct {
    77  	startHeight int64
    78  	endHeight   int64
    79  
    80  	benchmarkLength int64
    81  	numtxns         int64
    82  	totalTime       time.Duration
    83  	// average time to produce a block
    84  	mean time.Duration
    85  	// standard deviation of block production
    86  	std float64
    87  	// longest time to produce a block
    88  	max time.Duration
    89  	// shortest time to produce a block
    90  	min time.Duration
    91  }
    92  
    93  func (t *testnetStats) getReportJSON(net *e2e.Testnet) string {
    94  	jsn, err := json.Marshal(map[string]interface{}{
    95  		"case":   filepath.Base(net.File),
    96  		"blocks": t.endHeight - t.startHeight,
    97  		"stddev": t.std,
    98  		"mean":   t.mean.Seconds(),
    99  		"max":    t.max.Seconds(),
   100  		"min":    t.min.Seconds(),
   101  		"size":   len(net.Nodes),
   102  		"txns":   t.numtxns,
   103  		"dur":    t.totalTime.Seconds(),
   104  		"length": t.benchmarkLength,
   105  	})
   106  
   107  	if err != nil {
   108  		return ""
   109  	}
   110  
   111  	return string(jsn)
   112  }
   113  
   114  func (t *testnetStats) String() string {
   115  	return fmt.Sprintf(`Benchmarked from height %v to %v
   116  	Mean Block Interval: %v
   117  	Standard Deviation: %f
   118  	Max Block Interval: %v
   119  	Min Block Interval: %v
   120  	`,
   121  		t.startHeight,
   122  		t.endHeight,
   123  		t.mean,
   124  		t.std,
   125  		t.max,
   126  		t.min,
   127  	)
   128  }
   129  
   130  // fetchBlockChainSample waits for `benchmarkLength` amount of blocks to pass, fetching
   131  // all of the headers for these blocks from an archive node and returning it.
   132  func fetchBlockChainSample(ctx context.Context, testnet *e2e.Testnet, benchmarkLength int64) ([]*types.BlockMeta, error) {
   133  	var blocks []*types.BlockMeta
   134  
   135  	// Find the first archive node
   136  	archiveNode := testnet.ArchiveNodes()[0]
   137  	c, err := archiveNode.Client()
   138  	if err != nil {
   139  		return nil, err
   140  	}
   141  
   142  	// find the latest height
   143  	s, err := c.Status(ctx)
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	to := s.SyncInfo.LatestBlockHeight
   149  	from := to - benchmarkLength + 1
   150  	if from <= testnet.InitialHeight {
   151  		return nil, fmt.Errorf("tesnet was unable to reach required height for benchmarking (latest height %d)", to)
   152  	}
   153  
   154  	// Fetch blocks
   155  	for from < to {
   156  		// fetch the blockchain metas. Currently we can only fetch 20 at a time
   157  		resp, err := c.BlockchainInfo(ctx, from, min(from+19, to))
   158  		if err != nil {
   159  			return nil, err
   160  		}
   161  
   162  		blockMetas := resp.BlockMetas
   163  		// we receive blocks in descending order so we have to add them in reverse
   164  		for i := len(blockMetas) - 1; i >= 0; i-- {
   165  			if blockMetas[i].Header.Height != from {
   166  				return nil, fmt.Errorf("node gave us another header. Wanted %d, got %d",
   167  					from,
   168  					blockMetas[i].Header.Height,
   169  				)
   170  			}
   171  			from++
   172  			blocks = append(blocks, blockMetas[i])
   173  		}
   174  	}
   175  
   176  	return blocks, nil
   177  }
   178  
   179  func splitIntoBlockIntervals(blocks []*types.BlockMeta) []time.Duration {
   180  	intervals := make([]time.Duration, len(blocks)-1)
   181  	lastTime := blocks[0].Header.Time
   182  	for i, block := range blocks {
   183  		// skip the first block
   184  		if i == 0 {
   185  			continue
   186  		}
   187  
   188  		intervals[i-1] = block.Header.Time.Sub(lastTime)
   189  		lastTime = block.Header.Time
   190  	}
   191  	return intervals
   192  }
   193  
   194  func extractTestnetStats(intervals []time.Duration) testnetStats {
   195  	var (
   196  		sum, mean time.Duration
   197  		std       float64
   198  		max       = intervals[0]
   199  		min       = intervals[0]
   200  	)
   201  
   202  	for _, interval := range intervals {
   203  		sum += interval
   204  
   205  		if interval > max {
   206  			max = interval
   207  		}
   208  
   209  		if interval < min {
   210  			min = interval
   211  		}
   212  	}
   213  	mean = sum / time.Duration(len(intervals))
   214  
   215  	for _, interval := range intervals {
   216  		diff := (interval - mean).Seconds()
   217  		std += math.Pow(diff, 2)
   218  	}
   219  	std = math.Sqrt(std / float64(len(intervals)))
   220  
   221  	return testnetStats{
   222  		mean: mean,
   223  		std:  std,
   224  		max:  max,
   225  		min:  min,
   226  	}
   227  }
   228  
   229  func min(a, b int64) int64 {
   230  	if a > b {
   231  		return b
   232  	}
   233  	return a
   234  }