package main import ( "encoding/json" "flag" "fmt" "os" "strings" "time" "github.com/go-kit/kit/log/term" metrics "github.com/rcrowley/go-metrics" "text/tabwriter" tmrpc "github.com/tendermint/tendermint/rpc/client" "github.com/tendermint/tmlibs/log" ) var version = "0.3.0" var logger = log.NewNopLogger() type statistics struct { TxsThroughput metrics.Histogram `json:"txs_per_sec"` BlocksThroughput metrics.Histogram `json:"blocks_per_sec"` } func main() { var duration, txsRate, connections int var verbose bool var outputFormat, broadcastTxMethod string flag.IntVar(&connections, "c", 1, "Connections to keep open per endpoint") flag.IntVar(&duration, "T", 10, "Exit after the specified amount of time in seconds") flag.IntVar(&txsRate, "r", 1000, "Txs per second to send in a connection") flag.StringVar(&outputFormat, "output-format", "plain", "Output format: plain or json") 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)") flag.BoolVar(&verbose, "v", false, "Verbose output") flag.Usage = func() { fmt.Println(`Tendermint blockchain benchmarking tool. Usage: tm-bench [-c 1] [-T 10] [-r 1000] [endpoints] [-output-format [-broadcast-tx-method ]] Examples: tm-bench localhost:46657`) fmt.Println("Flags:") flag.PrintDefaults() } flag.Parse() if flag.NArg() == 0 { flag.Usage() os.Exit(1) } if verbose { if outputFormat == "json" { fmt.Fprintln(os.Stderr, "Verbose mode not supported with json output.") os.Exit(1) } // Color errors red colorFn := func(keyvals ...interface{}) term.FgBgColor { for i := 1; i < len(keyvals); i += 2 { if _, ok := keyvals[i].(error); ok { return term.FgBgColor{Fg: term.White, Bg: term.Red} } } return term.FgBgColor{} } logger = log.NewTMLoggerWithColorFn(log.NewSyncWriter(os.Stdout), colorFn) fmt.Printf("Running %ds test @ %s\n", duration, flag.Arg(0)) } if broadcastTxMethod != "async" && broadcastTxMethod != "sync" && broadcastTxMethod != "commit" { fmt.Fprintln(os.Stderr, "broadcast-tx-method should be either 'sync', 'async' or 'commit'.") os.Exit(1) } endpoints := strings.Split(flag.Arg(0), ",") client := tmrpc.NewHTTP(endpoints[0], "/websocket") minHeight := latestBlockHeight(client) logger.Info("Latest block height", "h", minHeight) // record time start timeStart := time.Now() logger.Info("Time started", "t", timeStart) transacters := startTransacters(endpoints, connections, txsRate, "broadcast_tx_"+broadcastTxMethod) select { case <-time.After(time.Duration(duration) * time.Second): for _, t := range transacters { t.Stop() } timeStop := time.Now() logger.Info("Time stopped", "t", timeStop) stats := calculateStatistics(client, minHeight, timeStart, timeStop, duration) printStatistics(stats, outputFormat) return } } func latestBlockHeight(client tmrpc.Client) int64 { status, err := client.Status() if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } return status.SyncInfo.LatestBlockHeight } func calculateStatistics(client tmrpc.Client, minHeight int64, timeStart, timeStop time.Time, duration int) *statistics { stats := &statistics{ BlocksThroughput: metrics.NewHistogram(metrics.NewUniformSample(1000)), TxsThroughput: metrics.NewHistogram(metrics.NewUniformSample(1000)), } // get blocks between minHeight and last height info, err := client.BlockchainInfo(minHeight, 0) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } numBlocksPerSec := make(map[int64]int64) numTxsPerSec := make(map[int64]int64) // because during some seconds blocks won't be created... for i := int64(0); i < int64(duration); i++ { numBlocksPerSec[i] = 0 numTxsPerSec[i] = 0 } for _, blockMeta := range info.BlockMetas { // check if block was created after timeStart if blockMeta.Header.Time.Before(timeStart) { continue } // check if block was created before timeStop if blockMeta.Header.Time.After(timeStop) { break } sec := secondsSinceTimeStart(timeStart, blockMeta.Header.Time) // increase number of blocks for that second if _, ok := numBlocksPerSec[sec]; !ok { numBlocksPerSec[sec] = 0 } numBlocksPerSec[sec]++ // increase number of txs for that second if _, ok := numTxsPerSec[sec]; !ok { numTxsPerSec[sec] = 0 } numTxsPerSec[sec] += blockMeta.Header.NumTxs } for _, n := range numBlocksPerSec { stats.BlocksThroughput.Update(n) } for _, n := range numTxsPerSec { stats.TxsThroughput.Update(n) } return stats } func secondsSinceTimeStart(timeStart, timePassed time.Time) int64 { return int64(timePassed.Sub(timeStart).Seconds()) } func startTransacters(endpoints []string, connections, txsRate int, broadcastTxMethod string) []*transacter { transacters := make([]*transacter, len(endpoints)) for i, e := range endpoints { t := newTransacter(e, connections, txsRate, broadcastTxMethod) t.SetLogger(logger) if err := t.Start(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } transacters[i] = t } return transacters } func printStatistics(stats *statistics, outputFormat string) { if outputFormat == "json" { result, err := json.Marshal(struct { TxsThroughput float64 `json:"txs_per_sec_avg"` BlocksThroughput float64 `json:"blocks_per_sec_avg"` }{stats.TxsThroughput.Mean(), stats.BlocksThroughput.Mean()}) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } fmt.Println(string(result)) } else { w := tabwriter.NewWriter(os.Stdout, 0, 0, 5, ' ', 0) fmt.Fprintln(w, "Stats\tAvg\tStdDev\tMax\t") fmt.Fprintln(w, fmt.Sprintf("Txs/sec\t%.0f\t%.0f\t%d\t", stats.TxsThroughput.Mean(), stats.TxsThroughput.StdDev(), stats.TxsThroughput.Max())) fmt.Fprintln(w, fmt.Sprintf("Blocks/sec\t%.3f\t%.3f\t%d\t", stats.BlocksThroughput.Mean(), stats.BlocksThroughput.StdDev(), stats.BlocksThroughput.Max())) w.Flush() } }