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.5 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(ctx context.Context, testnet *e2e.Testnet, benchmarkLength int64) error {
  22. block, _, err := waitForHeight(ctx, 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. ctx, cancel := context.WithTimeout(ctx, waitingTime)
  32. defer cancel()
  33. block, _, err = waitForHeight(ctx, testnet, block.Height+benchmarkLength)
  34. if err != nil {
  35. return err
  36. }
  37. dur := time.Since(startAt)
  38. logger.Info("Ending benchmark period", "height", block.Height)
  39. // fetch a sample of blocks
  40. blocks, err := fetchBlockChainSample(testnet, benchmarkLength)
  41. if err != nil {
  42. return err
  43. }
  44. // slice into time intervals and collate data
  45. timeIntervals := splitIntoBlockIntervals(blocks)
  46. testnetStats := extractTestnetStats(timeIntervals)
  47. // populate data
  48. testnetStats.populateTxns(blocks)
  49. testnetStats.totalTime = dur
  50. testnetStats.benchmarkLength = benchmarkLength
  51. testnetStats.startHeight = blocks[0].Header.Height
  52. testnetStats.endHeight = blocks[len(blocks)-1].Header.Height
  53. // print and return
  54. logger.Info(testnetStats.String())
  55. logger.Info(testnetStats.getReportJSON(testnet))
  56. return nil
  57. }
  58. func (t *testnetStats) populateTxns(blocks []*types.BlockMeta) {
  59. t.numtxns = 0
  60. for _, b := range blocks {
  61. t.numtxns += int64(b.NumTxs)
  62. }
  63. }
  64. type testnetStats struct {
  65. startHeight int64
  66. endHeight int64
  67. benchmarkLength int64
  68. numtxns int64
  69. totalTime time.Duration
  70. // average time to produce a block
  71. mean time.Duration
  72. // standard deviation of block production
  73. std float64
  74. // longest time to produce a block
  75. max time.Duration
  76. // shortest time to produce a block
  77. min time.Duration
  78. }
  79. func (t *testnetStats) getReportJSON(net *e2e.Testnet) string {
  80. jsn, err := json.Marshal(map[string]interface{}{
  81. "case": filepath.Base(net.File),
  82. "blocks": t.endHeight - t.startHeight,
  83. "stddev": t.std,
  84. "mean": t.mean.Seconds(),
  85. "max": t.max.Seconds(),
  86. "min": t.min.Seconds(),
  87. "size": len(net.Nodes),
  88. "txns": t.numtxns,
  89. "dur": t.totalTime.Seconds(),
  90. "length": t.benchmarkLength,
  91. })
  92. if err != nil {
  93. return ""
  94. }
  95. return string(jsn)
  96. }
  97. func (t *testnetStats) String() string {
  98. return fmt.Sprintf(`Benchmarked from height %v to %v
  99. Mean Block Interval: %v
  100. Standard Deviation: %f
  101. Max Block Interval: %v
  102. Min Block Interval: %v
  103. `,
  104. t.startHeight,
  105. t.endHeight,
  106. t.mean,
  107. t.std,
  108. t.max,
  109. t.min,
  110. )
  111. }
  112. // fetchBlockChainSample waits for `benchmarkLength` amount of blocks to pass, fetching
  113. // all of the headers for these blocks from an archive node and returning it.
  114. func fetchBlockChainSample(testnet *e2e.Testnet, benchmarkLength int64) ([]*types.BlockMeta, error) {
  115. var blocks []*types.BlockMeta
  116. // Find the first archive node
  117. archiveNode := testnet.ArchiveNodes()[0]
  118. c, err := archiveNode.Client()
  119. if err != nil {
  120. return nil, err
  121. }
  122. // find the latest height
  123. ctx := context.Background()
  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. }