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 }