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.

224 lines
6.0 KiB

  1. package main
  2. import (
  3. "encoding/json"
  4. "flag"
  5. "fmt"
  6. "os"
  7. "strings"
  8. "time"
  9. "github.com/go-kit/kit/log/term"
  10. metrics "github.com/rcrowley/go-metrics"
  11. "text/tabwriter"
  12. tmrpc "github.com/tendermint/tendermint/rpc/client"
  13. "github.com/tendermint/tmlibs/log"
  14. )
  15. var version = "0.3.0"
  16. var logger = log.NewNopLogger()
  17. type statistics struct {
  18. TxsThroughput metrics.Histogram `json:"txs_per_sec"`
  19. BlocksThroughput metrics.Histogram `json:"blocks_per_sec"`
  20. }
  21. func main() {
  22. var duration, txsRate, connections int
  23. var verbose bool
  24. var outputFormat, broadcastTxMethod string
  25. flag.IntVar(&connections, "c", 1, "Connections to keep open per endpoint")
  26. flag.IntVar(&duration, "T", 10, "Exit after the specified amount of time in seconds")
  27. flag.IntVar(&txsRate, "r", 1000, "Txs per second to send in a connection")
  28. flag.StringVar(&outputFormat, "output-format", "plain", "Output format: plain or json")
  29. flag.StringVar(&broadcastTxMethod, "broadcast-tx-method", "async", "Broadcast method: async (no guarantees; fastest), sync (ensures tx is checked) or commit (ensures tx is checked and committed; slowest)")
  30. flag.BoolVar(&verbose, "v", false, "Verbose output")
  31. flag.Usage = func() {
  32. fmt.Println(`Tendermint blockchain benchmarking tool.
  33. Usage:
  34. tm-bench [-c 1] [-T 10] [-r 1000] [endpoints] [-output-format <plain|json> [-broadcast-tx-method <async|sync|commit>]]
  35. Examples:
  36. tm-bench localhost:46657`)
  37. fmt.Println("Flags:")
  38. flag.PrintDefaults()
  39. }
  40. flag.Parse()
  41. if flag.NArg() == 0 {
  42. flag.Usage()
  43. os.Exit(1)
  44. }
  45. if verbose {
  46. if outputFormat == "json" {
  47. fmt.Fprintln(os.Stderr, "Verbose mode not supported with json output.")
  48. os.Exit(1)
  49. }
  50. // Color errors red
  51. colorFn := func(keyvals ...interface{}) term.FgBgColor {
  52. for i := 1; i < len(keyvals); i += 2 {
  53. if _, ok := keyvals[i].(error); ok {
  54. return term.FgBgColor{Fg: term.White, Bg: term.Red}
  55. }
  56. }
  57. return term.FgBgColor{}
  58. }
  59. logger = log.NewTMLoggerWithColorFn(log.NewSyncWriter(os.Stdout), colorFn)
  60. fmt.Printf("Running %ds test @ %s\n", duration, flag.Arg(0))
  61. }
  62. if broadcastTxMethod != "async" && broadcastTxMethod != "sync" && broadcastTxMethod != "commit" {
  63. fmt.Fprintln(os.Stderr, "broadcast-tx-method should be either 'sync', 'async' or 'commit'.")
  64. os.Exit(1)
  65. }
  66. endpoints := strings.Split(flag.Arg(0), ",")
  67. client := tmrpc.NewHTTP(endpoints[0], "/websocket")
  68. minHeight := latestBlockHeight(client)
  69. logger.Info("Latest block height", "h", minHeight)
  70. // record time start
  71. timeStart := time.Now()
  72. logger.Info("Time started", "t", timeStart)
  73. transacters := startTransacters(endpoints, connections, txsRate, "broadcast_tx_"+broadcastTxMethod)
  74. select {
  75. case <-time.After(time.Duration(duration) * time.Second):
  76. for _, t := range transacters {
  77. t.Stop()
  78. }
  79. timeStop := time.Now()
  80. logger.Info("Time stopped", "t", timeStop)
  81. stats := calculateStatistics(client, minHeight, timeStart, timeStop, duration)
  82. printStatistics(stats, outputFormat)
  83. return
  84. }
  85. }
  86. func latestBlockHeight(client tmrpc.Client) int64 {
  87. status, err := client.Status()
  88. if err != nil {
  89. fmt.Fprintln(os.Stderr, err)
  90. os.Exit(1)
  91. }
  92. return status.SyncInfo.LatestBlockHeight
  93. }
  94. func calculateStatistics(client tmrpc.Client, minHeight int64, timeStart, timeStop time.Time, duration int) *statistics {
  95. stats := &statistics{
  96. BlocksThroughput: metrics.NewHistogram(metrics.NewUniformSample(1000)),
  97. TxsThroughput: metrics.NewHistogram(metrics.NewUniformSample(1000)),
  98. }
  99. // get blocks between minHeight and last height
  100. info, err := client.BlockchainInfo(minHeight, 0)
  101. if err != nil {
  102. fmt.Fprintln(os.Stderr, err)
  103. os.Exit(1)
  104. }
  105. numBlocksPerSec := make(map[int64]int64)
  106. numTxsPerSec := make(map[int64]int64)
  107. // because during some seconds blocks won't be created...
  108. for i := int64(0); i < int64(duration); i++ {
  109. numBlocksPerSec[i] = 0
  110. numTxsPerSec[i] = 0
  111. }
  112. for _, blockMeta := range info.BlockMetas {
  113. // check if block was created after timeStart
  114. if blockMeta.Header.Time.Before(timeStart) {
  115. continue
  116. }
  117. // check if block was created before timeStop
  118. if blockMeta.Header.Time.After(timeStop) {
  119. break
  120. }
  121. sec := secondsSinceTimeStart(timeStart, blockMeta.Header.Time)
  122. // increase number of blocks for that second
  123. if _, ok := numBlocksPerSec[sec]; !ok {
  124. numBlocksPerSec[sec] = 0
  125. }
  126. numBlocksPerSec[sec]++
  127. // increase number of txs for that second
  128. if _, ok := numTxsPerSec[sec]; !ok {
  129. numTxsPerSec[sec] = 0
  130. }
  131. numTxsPerSec[sec] += blockMeta.Header.NumTxs
  132. }
  133. for _, n := range numBlocksPerSec {
  134. stats.BlocksThroughput.Update(n)
  135. }
  136. for _, n := range numTxsPerSec {
  137. stats.TxsThroughput.Update(n)
  138. }
  139. return stats
  140. }
  141. func secondsSinceTimeStart(timeStart, timePassed time.Time) int64 {
  142. return int64(timePassed.Sub(timeStart).Seconds())
  143. }
  144. func startTransacters(endpoints []string, connections, txsRate int, broadcastTxMethod string) []*transacter {
  145. transacters := make([]*transacter, len(endpoints))
  146. for i, e := range endpoints {
  147. t := newTransacter(e, connections, txsRate, broadcastTxMethod)
  148. t.SetLogger(logger)
  149. if err := t.Start(); err != nil {
  150. fmt.Fprintln(os.Stderr, err)
  151. os.Exit(1)
  152. }
  153. transacters[i] = t
  154. }
  155. return transacters
  156. }
  157. func printStatistics(stats *statistics, outputFormat string) {
  158. if outputFormat == "json" {
  159. result, err := json.Marshal(struct {
  160. TxsThroughput float64 `json:"txs_per_sec_avg"`
  161. BlocksThroughput float64 `json:"blocks_per_sec_avg"`
  162. }{stats.TxsThroughput.Mean(), stats.BlocksThroughput.Mean()})
  163. if err != nil {
  164. fmt.Fprintln(os.Stderr, err)
  165. os.Exit(1)
  166. }
  167. fmt.Println(string(result))
  168. } else {
  169. w := tabwriter.NewWriter(os.Stdout, 0, 0, 5, ' ', 0)
  170. fmt.Fprintln(w, "Stats\tAvg\tStdDev\tMax\t")
  171. fmt.Fprintln(w, fmt.Sprintf("Txs/sec\t%.0f\t%.0f\t%d\t",
  172. stats.TxsThroughput.Mean(),
  173. stats.TxsThroughput.StdDev(),
  174. stats.TxsThroughput.Max()))
  175. fmt.Fprintln(w, fmt.Sprintf("Blocks/sec\t%.3f\t%.3f\t%d\t",
  176. stats.BlocksThroughput.Mean(),
  177. stats.BlocksThroughput.StdDev(),
  178. stats.BlocksThroughput.Max()))
  179. w.Flush()
  180. }
  181. }