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.

307 lines
7.7 KiB

7 years ago
7 years ago
7 years ago
7 years ago
  1. package main
  2. import (
  3. "encoding/json"
  4. "flag"
  5. "fmt"
  6. "math"
  7. "os"
  8. "strings"
  9. "sync"
  10. "text/tabwriter"
  11. "time"
  12. "github.com/go-kit/kit/log/term"
  13. metrics "github.com/rcrowley/go-metrics"
  14. "github.com/tendermint/tendermint/libs/log"
  15. tmrpc "github.com/tendermint/tendermint/rpc/client"
  16. )
  17. var logger = log.NewNopLogger()
  18. type statistics struct {
  19. TxsThroughput metrics.Histogram `json:"txs_per_sec"`
  20. BlocksThroughput metrics.Histogram `json:"blocks_per_sec"`
  21. }
  22. func main() {
  23. var durationInt, txsRate, connections, txSize int
  24. var verbose bool
  25. var outputFormat, broadcastTxMethod string
  26. flagSet := flag.NewFlagSet("tm-bench", flag.ExitOnError)
  27. flagSet.IntVar(&connections, "c", 1, "Connections to keep open per endpoint")
  28. flagSet.IntVar(&durationInt, "T", 10, "Exit after the specified amount of time in seconds")
  29. flagSet.IntVar(&txsRate, "r", 1000, "Txs per second to send in a connection")
  30. flagSet.IntVar(&txSize, "s", 250, "The size of a transaction in bytes.")
  31. flagSet.StringVar(&outputFormat, "output-format", "plain", "Output format: plain or json")
  32. flagSet.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)")
  33. flagSet.BoolVar(&verbose, "v", false, "Verbose output")
  34. flagSet.Usage = func() {
  35. fmt.Println(`Tendermint blockchain benchmarking tool.
  36. Usage:
  37. tm-bench [-c 1] [-T 10] [-r 1000] [-s 250] [endpoints] [-output-format <plain|json> [-broadcast-tx-method <async|sync|commit>]]
  38. Examples:
  39. tm-bench localhost:26657`)
  40. fmt.Println("Flags:")
  41. flagSet.PrintDefaults()
  42. }
  43. flagSet.Parse(os.Args[1:])
  44. if flagSet.NArg() == 0 {
  45. flagSet.Usage()
  46. os.Exit(1)
  47. }
  48. if verbose {
  49. if outputFormat == "json" {
  50. fmt.Fprintln(os.Stderr, "Verbose mode not supported with json output.")
  51. os.Exit(1)
  52. }
  53. // Color errors red
  54. colorFn := func(keyvals ...interface{}) term.FgBgColor {
  55. for i := 1; i < len(keyvals); i += 2 {
  56. if _, ok := keyvals[i].(error); ok {
  57. return term.FgBgColor{Fg: term.White, Bg: term.Red}
  58. }
  59. }
  60. return term.FgBgColor{}
  61. }
  62. logger = log.NewTMLoggerWithColorFn(log.NewSyncWriter(os.Stdout), colorFn)
  63. fmt.Printf("Running %ds test @ %s\n", durationInt, flagSet.Arg(0))
  64. }
  65. if broadcastTxMethod != "async" &&
  66. broadcastTxMethod != "sync" &&
  67. broadcastTxMethod != "commit" {
  68. fmt.Fprintln(
  69. os.Stderr,
  70. "broadcast-tx-method should be either 'sync', 'async' or 'commit'.",
  71. )
  72. os.Exit(1)
  73. }
  74. var (
  75. endpoints = strings.Split(flagSet.Arg(0), ",")
  76. client = tmrpc.NewHTTP(endpoints[0], "/websocket")
  77. initialHeight = latestBlockHeight(client)
  78. )
  79. logger.Info("Latest block height", "h", initialHeight)
  80. transacters := startTransacters(
  81. endpoints,
  82. connections,
  83. txsRate,
  84. txSize,
  85. "broadcast_tx_"+broadcastTxMethod,
  86. )
  87. // Wait until transacters have begun until we get the start time
  88. timeStart := time.Now()
  89. logger.Info("Time last transacter started", "t", timeStart)
  90. duration := time.Duration(durationInt) * time.Second
  91. timeEnd := timeStart.Add(duration)
  92. logger.Info("End time for calculation", "t", timeEnd)
  93. <-time.After(duration)
  94. for i, t := range transacters {
  95. t.Stop()
  96. numCrashes := countCrashes(t.connsBroken)
  97. if numCrashes != 0 {
  98. fmt.Printf("%d connections crashed on transacter #%d\n", numCrashes, i)
  99. }
  100. }
  101. logger.Debug("Time all transacters stopped", "t", time.Now())
  102. stats, err := calculateStatistics(
  103. client,
  104. initialHeight,
  105. timeStart,
  106. timeEnd,
  107. durationInt,
  108. )
  109. if err != nil {
  110. fmt.Fprintln(os.Stderr, err)
  111. os.Exit(1)
  112. }
  113. printStatistics(stats, outputFormat)
  114. }
  115. func latestBlockHeight(client tmrpc.Client) int64 {
  116. status, err := client.Status()
  117. if err != nil {
  118. fmt.Fprintln(os.Stderr, err)
  119. os.Exit(1)
  120. }
  121. return status.SyncInfo.LatestBlockHeight
  122. }
  123. func countCrashes(crashes []bool) int {
  124. count := 0
  125. for i := 0; i < len(crashes); i++ {
  126. if crashes[i] {
  127. count++
  128. }
  129. }
  130. return count
  131. }
  132. // calculateStatistics calculates the tx / second, and blocks / second based
  133. // off of the number the transactions and number of blocks that occurred from
  134. // the start block, and the end time.
  135. func calculateStatistics(
  136. client tmrpc.Client,
  137. minHeight int64,
  138. timeStart, timeStop time.Time,
  139. duration int,
  140. ) (*statistics, error) {
  141. stats := &statistics{
  142. BlocksThroughput: metrics.NewHistogram(metrics.NewUniformSample(1000)),
  143. TxsThroughput: metrics.NewHistogram(metrics.NewUniformSample(1000)),
  144. }
  145. // get blocks between minHeight and last height
  146. // This returns max(minHeight,(last_height - 20)) to last_height
  147. info, err := client.BlockchainInfo(minHeight, 0)
  148. if err != nil {
  149. return nil, err
  150. }
  151. var (
  152. blockMetas = info.BlockMetas
  153. lastHeight = info.LastHeight
  154. diff = lastHeight - minHeight
  155. offset = len(blockMetas)
  156. )
  157. for offset < int(diff) {
  158. // get blocks between minHeight and last height
  159. info, err := client.BlockchainInfo(minHeight, lastHeight-int64(offset))
  160. if err != nil {
  161. return nil, err
  162. }
  163. blockMetas = append(blockMetas, info.BlockMetas...)
  164. offset = len(blockMetas)
  165. }
  166. var (
  167. numBlocksPerSec = make(map[int64]int64)
  168. numTxsPerSec = make(map[int64]int64)
  169. )
  170. // because during some seconds blocks won't be created...
  171. for i := int64(0); i < int64(duration); i++ {
  172. numBlocksPerSec[i] = 0
  173. numTxsPerSec[i] = 0
  174. }
  175. // iterates from max height to min height
  176. for i, blockMeta := range blockMetas {
  177. // check if block was created after timeStart
  178. if blockMeta.Header.Time.Before(timeStart) {
  179. break
  180. }
  181. // check if block was created before timeStop
  182. if blockMeta.Header.Time.After(timeStop) {
  183. continue
  184. }
  185. sec := secondsSinceTimeStart(timeStart, blockMeta.Header.Time)
  186. // increase number of blocks for that second
  187. numBlocksPerSec[sec]++
  188. // increase number of txs for that second
  189. numTxsPerSec[sec] += blockMeta.Header.NumTxs
  190. logger.Debug(fmt.Sprintf("%d txs in block %d, height %d", blockMeta.Header.NumTxs, i, blockMeta.Header.Height))
  191. }
  192. for _, n := range numBlocksPerSec {
  193. stats.BlocksThroughput.Update(n)
  194. }
  195. for _, n := range numTxsPerSec {
  196. stats.TxsThroughput.Update(n)
  197. }
  198. return stats, nil
  199. }
  200. func secondsSinceTimeStart(timeStart, timePassed time.Time) int64 {
  201. return int64(math.Round(timePassed.Sub(timeStart).Seconds()))
  202. }
  203. func startTransacters(
  204. endpoints []string,
  205. connections,
  206. txsRate int,
  207. txSize int,
  208. broadcastTxMethod string,
  209. ) []*transacter {
  210. transacters := make([]*transacter, len(endpoints))
  211. wg := sync.WaitGroup{}
  212. wg.Add(len(endpoints))
  213. for i, e := range endpoints {
  214. t := newTransacter(e, connections, txsRate, txSize, broadcastTxMethod)
  215. t.SetLogger(logger)
  216. go func(i int) {
  217. defer wg.Done()
  218. if err := t.Start(); err != nil {
  219. fmt.Fprintln(os.Stderr, err)
  220. os.Exit(1)
  221. }
  222. transacters[i] = t
  223. }(i)
  224. }
  225. wg.Wait()
  226. return transacters
  227. }
  228. func printStatistics(stats *statistics, outputFormat string) {
  229. if outputFormat == "json" {
  230. result, err := json.Marshal(struct {
  231. TxsThroughput float64 `json:"txs_per_sec_avg"`
  232. BlocksThroughput float64 `json:"blocks_per_sec_avg"`
  233. }{stats.TxsThroughput.Mean(), stats.BlocksThroughput.Mean()})
  234. if err != nil {
  235. fmt.Fprintln(os.Stderr, err)
  236. os.Exit(1)
  237. }
  238. fmt.Println(string(result))
  239. } else {
  240. w := tabwriter.NewWriter(os.Stdout, 0, 0, 5, ' ', 0)
  241. fmt.Fprintln(w, "Stats\tAvg\tStdDev\tMax\tTotal\t")
  242. fmt.Fprintln(
  243. w,
  244. fmt.Sprintf(
  245. "Txs/sec\t%.0f\t%.0f\t%d\t%d\t",
  246. stats.TxsThroughput.Mean(),
  247. stats.TxsThroughput.StdDev(),
  248. stats.TxsThroughput.Max(),
  249. stats.TxsThroughput.Sum(),
  250. ),
  251. )
  252. fmt.Fprintln(
  253. w,
  254. fmt.Sprintf("Blocks/sec\t%.3f\t%.3f\t%d\t%d\t",
  255. stats.BlocksThroughput.Mean(),
  256. stats.BlocksThroughput.StdDev(),
  257. stats.BlocksThroughput.Max(),
  258. stats.BlocksThroughput.Sum(),
  259. ),
  260. )
  261. w.Flush()
  262. }
  263. }