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.

234 lines
5.6 KiB

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