You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

232 lines
5.4 KiB

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