2017-03-16 20:53:09 +04:00
package main
2017-03-13 19:10:51 +04:00
import (
2018-05-03 05:08:19 -04:00
"encoding/json"
2017-03-13 19:10:51 +04:00
"flag"
"fmt"
2018-06-22 16:16:27 -07:00
"math"
2017-03-13 19:10:51 +04:00
"os"
"strings"
2018-06-23 01:57:50 +02:00
"text/tabwriter"
2017-03-13 19:10:51 +04:00
"time"
2017-03-23 16:13:27 +04:00
"github.com/go-kit/kit/log/term"
2017-03-17 00:56:22 +04:00
metrics "github.com/rcrowley/go-metrics"
2018-06-23 01:57:50 +02:00
2018-07-10 16:42:27 -04:00
"github.com/tendermint/tendermint/libs/log"
tmrpc "github.com/tendermint/tendermint/rpc/client"
2017-03-13 19:10:51 +04:00
)
2017-03-23 16:13:27 +04:00
var logger = log . NewNopLogger ( )
2017-03-17 00:56:22 +04:00
type statistics struct {
2018-05-04 16:13:42 +04:00
TxsThroughput metrics . Histogram ` json:"txs_per_sec" `
BlocksThroughput metrics . Histogram ` json:"blocks_per_sec" `
2017-03-17 00:56:22 +04:00
}
2017-03-13 19:10:51 +04:00
func main ( ) {
2018-06-22 18:22:30 -07:00
var duration , txsRate , connections , txSize int
2017-03-23 16:13:27 +04:00
var verbose bool
2018-05-04 16:35:39 +04:00
var outputFormat , broadcastTxMethod string
2017-03-13 19:10:51 +04:00
2018-07-10 22:54:59 -07:00
flagSet := flag . NewFlagSet ( "tm-bench" , flag . ExitOnError )
flagSet . IntVar ( & connections , "c" , 1 , "Connections to keep open per endpoint" )
flagSet . IntVar ( & duration , "T" , 10 , "Exit after the specified amount of time in seconds" )
flagSet . IntVar ( & txsRate , "r" , 1000 , "Txs per second to send in a connection" )
flagSet . IntVar ( & txSize , "s" , 250 , "The size of a transaction in bytes." )
flagSet . StringVar ( & outputFormat , "output-format" , "plain" , "Output format: plain or json" )
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)" )
flagSet . BoolVar ( & verbose , "v" , false , "Verbose output" )
flagSet . Usage = func ( ) {
2017-03-17 01:12:37 +04:00
fmt . Println ( ` Tendermint blockchain benchmarking tool .
2017-03-13 19:10:51 +04:00
Usage :
2018-07-11 21:54:46 -07:00
tm - bench [ - c 1 ] [ - T 10 ] [ - r 1000 ] [ - s 250 ] [ endpoints ] [ - output - format < plain | json > [ - broadcast - tx - method < async | sync | commit > ] ]
2017-03-13 19:10:51 +04:00
Examples :
2018-06-22 11:55:07 -04:00
tm - bench localhost : 26657 ` )
2017-03-13 19:10:51 +04:00
fmt . Println ( "Flags:" )
2018-07-10 22:54:59 -07:00
flagSet . PrintDefaults ( )
2017-03-13 19:10:51 +04:00
}
2018-07-10 22:54:59 -07:00
flagSet . Parse ( os . Args [ 1 : ] )
2017-03-13 19:10:51 +04:00
2018-07-10 22:54:59 -07:00
if flagSet . NArg ( ) == 0 {
flagSet . Usage ( )
2017-03-13 19:10:51 +04:00
os . Exit ( 1 )
}
2017-03-23 16:13:27 +04:00
if verbose {
2018-05-03 05:08:19 -04:00
if outputFormat == "json" {
2018-05-04 16:13:42 +04:00
fmt . Fprintln ( os . Stderr , "Verbose mode not supported with json output." )
2018-05-03 05:08:19 -04:00
os . Exit ( 1 )
}
2017-03-23 16:13:27 +04:00
// 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 { }
}
2017-07-28 18:13:39 -04:00
logger = log . NewTMLoggerWithColorFn ( log . NewSyncWriter ( os . Stdout ) , colorFn )
2017-03-23 16:13:27 +04:00
2018-07-10 22:54:59 -07:00
fmt . Printf ( "Running %ds test @ %s\n" , duration , flagSet . Arg ( 0 ) )
2018-05-03 05:08:19 -04:00
}
2017-03-13 19:10:51 +04:00
2018-06-23 01:57:50 +02:00
if broadcastTxMethod != "async" &&
broadcastTxMethod != "sync" &&
broadcastTxMethod != "commit" {
fmt . Fprintln (
os . Stderr ,
"broadcast-tx-method should be either 'sync', 'async' or 'commit'." ,
)
2018-05-04 16:35:39 +04:00
os . Exit ( 1 )
}
2018-06-23 01:57:50 +02:00
var (
2018-07-10 22:54:59 -07:00
endpoints = strings . Split ( flagSet . Arg ( 0 ) , "," )
2018-06-23 01:57:50 +02:00
client = tmrpc . NewHTTP ( endpoints [ 0 ] , "/websocket" )
initialHeight = latestBlockHeight ( client )
)
2018-06-22 16:16:27 -07:00
logger . Info ( "Latest block height" , "h" , initialHeight )
2018-05-04 16:13:42 +04:00
2018-06-23 01:57:50 +02:00
transacters := startTransacters (
endpoints ,
connections ,
txsRate ,
2018-06-22 18:22:30 -07:00
txSize ,
2018-06-23 01:57:50 +02:00
"broadcast_tx_" + broadcastTxMethod ,
)
2018-07-11 21:54:46 -07:00
2018-07-11 22:55:20 -07:00
// Wait until transacters have begun until we get the start time
for {
started := true
for _ , t := range transacters {
for i := 0 ; i < t . Connections ; i ++ {
if ! t . connsStarted [ i ] && ! t . connsBroken [ i ] {
started = false
}
}
}
if started {
break
}
}
2018-07-11 21:54:46 -07:00
timeStart := time . Now ( )
logger . Info ( "Time last transacter started" , "t" , timeStart )
2018-06-28 01:11:05 -07:00
endTime := time . Duration ( duration ) * time . Second
2018-05-04 16:13:42 +04:00
2018-07-10 16:42:27 -04:00
<- time . After ( endTime )
for i , t := range transacters {
t . Stop ( )
numCrashes := countCrashes ( t . connsBroken )
if numCrashes != 0 {
fmt . Printf ( "%d connections crashed on transacter #%d\n" , numCrashes , i )
2018-06-23 01:57:50 +02:00
}
2018-07-10 16:42:27 -04:00
}
2018-05-04 16:13:42 +04:00
2018-07-10 16:42:27 -04:00
timeStop := time . Now ( )
logger . Info ( "Time stopped" , "t" , timeStop )
2018-05-04 16:13:42 +04:00
2018-07-10 16:42:27 -04:00
stats , err := calculateStatistics (
client ,
initialHeight ,
timeStart ,
timeStop ,
duration ,
)
if err != nil {
fmt . Fprintln ( os . Stderr , err )
os . Exit ( 1 )
2018-05-04 16:13:42 +04:00
}
2018-07-10 16:42:27 -04:00
printStatistics ( stats , outputFormat )
2018-05-04 16:13:42 +04:00
}
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
}
2018-07-02 03:19:51 -07:00
func countCrashes ( crashes [ ] bool ) int {
count := 0
for i := 0 ; i < len ( crashes ) ; i ++ {
if crashes [ i ] {
count ++
}
}
return count
}
2018-06-23 01:57:50 +02:00
// calculateStatistics calculates the tx / second, and blocks / second based
// off of the number the transactions and number of blocks that occurred from
// the start block, and the end time.
func calculateStatistics (
client tmrpc . Client ,
minHeight int64 ,
timeStart , timeStop time . Time ,
duration int ,
) ( * statistics , error ) {
2017-03-17 00:56:22 +04:00
stats := & statistics {
2018-05-04 16:13:42 +04:00
BlocksThroughput : metrics . NewHistogram ( metrics . NewUniformSample ( 1000 ) ) ,
TxsThroughput : metrics . NewHistogram ( metrics . NewUniformSample ( 1000 ) ) ,
2017-03-13 19:10:51 +04:00
}
2018-05-04 16:13:42 +04:00
// get blocks between minHeight and last height
2018-06-22 16:16:27 -07:00
// This returns max(minHeight,(last_height - 20)) to last_height
2018-05-04 16:13:42 +04:00
info , err := client . BlockchainInfo ( minHeight , 0 )
if err != nil {
2018-06-23 01:57:50 +02:00
return nil , err
2018-05-04 16:13:42 +04:00
}
2018-06-23 01:57:50 +02:00
var (
blockMetas = info . BlockMetas
lastHeight = info . LastHeight
diff = lastHeight - minHeight
offset = len ( blockMetas )
)
2018-06-04 12:37:37 -07:00
2018-06-23 01:57:50 +02:00
for offset < int ( diff ) {
2018-06-04 12:37:37 -07:00
// get blocks between minHeight and last height
2018-06-22 16:16:27 -07:00
info , err := client . BlockchainInfo ( minHeight , lastHeight - int64 ( offset ) )
2018-06-04 12:37:37 -07:00
if err != nil {
2018-06-23 01:57:50 +02:00
return nil , err
2018-06-04 12:37:37 -07:00
}
blockMetas = append ( blockMetas , info . BlockMetas ... )
offset = len ( blockMetas )
}
2018-06-23 01:57:50 +02:00
var (
numBlocksPerSec = make ( map [ int64 ] int64 )
numTxsPerSec = make ( map [ int64 ] int64 )
)
2018-05-10 17:08:49 +04:00
// because during some seconds blocks won't be created...
for i := int64 ( 0 ) ; i < int64 ( duration ) ; i ++ {
numBlocksPerSec [ i ] = 0
numTxsPerSec [ i ] = 0
}
2018-06-22 16:16:27 -07:00
// iterates from max height to min height
2018-07-10 21:57:22 -07:00
for i , blockMeta := range blockMetas {
2018-05-08 16:23:00 +04:00
// check if block was created after timeStart
if blockMeta . Header . Time . Before ( timeStart ) {
2018-06-22 16:16:27 -07:00
break
2018-05-08 16:23:00 +04:00
}
2018-05-04 16:13:42 +04:00
// check if block was created before timeStop
if blockMeta . Header . Time . After ( timeStop ) {
2018-06-22 16:16:27 -07:00
continue
2018-05-04 16:13:42 +04:00
}
sec := secondsSinceTimeStart ( timeStart , blockMeta . Header . Time )
2017-03-17 00:56:22 +04:00
2018-05-04 16:13:42 +04:00
// increase number of blocks for that second
numBlocksPerSec [ sec ] ++
// increase number of txs for that second
numTxsPerSec [ sec ] += blockMeta . Header . NumTxs
2018-07-10 21:57:22 -07:00
logger . Debug ( fmt . Sprintf ( "%d txs in block %d, height %d" , blockMeta . Header . NumTxs , i , blockMeta . Header . Height ) )
2017-03-13 19:10:51 +04:00
}
2018-05-04 16:13:42 +04:00
for _ , n := range numBlocksPerSec {
stats . BlocksThroughput . Update ( n )
}
2017-03-13 19:10:51 +04:00
2018-05-04 16:13:42 +04:00
for _ , n := range numTxsPerSec {
stats . TxsThroughput . Update ( n )
2017-03-13 19:10:51 +04:00
}
2018-06-23 01:57:50 +02:00
return stats , nil
2018-05-04 16:13:42 +04:00
}
func secondsSinceTimeStart ( timeStart , timePassed time . Time ) int64 {
2018-06-22 16:16:27 -07:00
return int64 ( math . Round ( timePassed . Sub ( timeStart ) . Seconds ( ) ) )
2017-03-17 00:56:22 +04:00
}
2018-06-23 01:57:50 +02:00
func startTransacters (
endpoints [ ] string ,
connections ,
txsRate int ,
2018-06-22 18:22:30 -07:00
txSize int ,
2018-06-23 01:57:50 +02:00
broadcastTxMethod string ,
) [ ] * transacter {
2017-03-17 00:56:22 +04:00
transacters := make ( [ ] * transacter , len ( endpoints ) )
for i , e := range endpoints {
2018-06-22 18:22:30 -07:00
t := newTransacter ( e , connections , txsRate , txSize , broadcastTxMethod )
2017-03-23 16:13:27 +04:00
t . SetLogger ( logger )
2017-03-17 00:56:22 +04:00
if err := t . Start ( ) ; err != nil {
2018-05-04 16:13:42 +04:00
fmt . Fprintln ( os . Stderr , err )
2017-03-23 16:13:27 +04:00
os . Exit ( 1 )
2017-03-17 00:56:22 +04:00
}
transacters [ i ] = t
2017-03-13 19:10:51 +04:00
}
2017-03-17 00:56:22 +04:00
return transacters
2017-03-13 19:10:51 +04:00
}
2018-05-03 05:08:19 -04:00
func printStatistics ( stats * statistics , outputFormat string ) {
if outputFormat == "json" {
2018-05-04 16:13:42 +04:00
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 )
}
2018-05-03 05:08:19 -04:00
fmt . Println ( string ( result ) )
} else {
w := tabwriter . NewWriter ( os . Stdout , 0 , 0 , 5 , ' ' , 0 )
2018-06-22 19:10:58 -07:00
fmt . Fprintln ( w , "Stats\tAvg\tStdDev\tMax\tTotal\t" )
fmt . Fprintln (
w ,
fmt . Sprintf (
"Txs/sec\t%.0f\t%.0f\t%d\t%d\t" ,
stats . TxsThroughput . Mean ( ) ,
stats . TxsThroughput . StdDev ( ) ,
stats . TxsThroughput . Max ( ) ,
stats . TxsThroughput . Sum ( ) ,
) ,
)
fmt . Fprintln (
w ,
fmt . Sprintf ( "Blocks/sec\t%.3f\t%.3f\t%d\t%d\t" ,
stats . BlocksThroughput . Mean ( ) ,
stats . BlocksThroughput . StdDev ( ) ,
stats . BlocksThroughput . Max ( ) ,
stats . BlocksThroughput . Sum ( ) ,
) ,
)
2018-05-03 05:08:19 -04:00
w . Flush ( )
}
2017-03-13 19:10:51 +04:00
}