Add --watch option to see progress while benchmark is running
This commit is contained in:
parent
dcc72a43ee
commit
4b0ca30daf
@ -68,6 +68,11 @@ Here are some real examples:
|
|||||||
Usage: `Method to benchmark, you can specify multiple methods by repeating this flag. You can also specify method specific options to set the concurrency and qps for each method (see usage).
|
Usage: `Method to benchmark, you can specify multiple methods by repeating this flag. You can also specify method specific options to set the concurrency and qps for each method (see usage).
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
&cli.DurationFlag{
|
||||||
|
Name: "watch",
|
||||||
|
Value: 0 * time.Second,
|
||||||
|
Usage: "If >0 then generates reports every N seconds (only supports linux/unix)",
|
||||||
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "print-response",
|
Name: "print-response",
|
||||||
Value: false,
|
Value: false,
|
||||||
@ -135,6 +140,7 @@ Here are some real examples:
|
|||||||
}
|
}
|
||||||
|
|
||||||
rpcMethods = append(rpcMethods, &RPCMethod{
|
rpcMethods = append(rpcMethods, &RPCMethod{
|
||||||
|
w: os.Stdout,
|
||||||
uri: cctx.String("endpoint"),
|
uri: cctx.String("endpoint"),
|
||||||
method: entries[0],
|
method: entries[0],
|
||||||
concurrency: concurrency,
|
concurrency: concurrency,
|
||||||
@ -177,8 +183,45 @@ Here are some real examples:
|
|||||||
}(e)
|
}(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if watch is set then print a report every N seconds
|
||||||
|
var progressCh chan bool
|
||||||
|
if cctx.Duration("watch") > 0 {
|
||||||
|
progressCh = make(chan bool, 1)
|
||||||
|
go func(progressCh chan bool) {
|
||||||
|
ticker := time.NewTicker(cctx.Duration("watch"))
|
||||||
|
for {
|
||||||
|
clearAndPrintReport := func() {
|
||||||
|
// clear the screen move the curser to the top left
|
||||||
|
fmt.Print("\033[2J")
|
||||||
|
fmt.Printf("\033[%d;%dH", 1, 1)
|
||||||
|
for i, e := range rpcMethods {
|
||||||
|
e.Report()
|
||||||
|
if i < len(rpcMethods)-1 {
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
clearAndPrintReport()
|
||||||
|
case <-progressCh:
|
||||||
|
clearAndPrintReport()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(progressCh)
|
||||||
|
}
|
||||||
|
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
|
if progressCh != nil {
|
||||||
|
// wait for the watch go routine to return
|
||||||
|
progressCh <- true
|
||||||
|
|
||||||
|
// no need to print the report again
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// print the report for each endpoint
|
// print the report for each endpoint
|
||||||
for i, e := range rpcMethods {
|
for i, e := range rpcMethods {
|
||||||
e.Report()
|
e.Report()
|
||||||
@ -193,6 +236,7 @@ Here are some real examples:
|
|||||||
|
|
||||||
// RPCMethod handles the benchmarking of a single endpoint method.
|
// RPCMethod handles the benchmarking of a single endpoint method.
|
||||||
type RPCMethod struct {
|
type RPCMethod struct {
|
||||||
|
w io.Writer
|
||||||
// the endpoint uri
|
// the endpoint uri
|
||||||
uri string
|
uri string
|
||||||
// the rpc method we want to benchmark
|
// the rpc method we want to benchmark
|
||||||
@ -234,7 +278,7 @@ func (rpc *RPCMethod) Run() error {
|
|||||||
rpc.stopCh = make(chan struct{}, rpc.concurrency)
|
rpc.stopCh = make(chan struct{}, rpc.concurrency)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
rpc.reporter = NewReporter(rpc.results)
|
rpc.reporter = NewReporter(rpc.results, rpc.w)
|
||||||
rpc.reporter.Run()
|
rpc.reporter.Run()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -376,20 +420,25 @@ func (rpc *RPCMethod) Stop() {
|
|||||||
|
|
||||||
func (rpc *RPCMethod) Report() {
|
func (rpc *RPCMethod) Report() {
|
||||||
total := time.Since(rpc.start)
|
total := time.Since(rpc.start)
|
||||||
fmt.Printf("[%s]:\n", rpc.method)
|
fmt.Fprintf(rpc.w, "[%s]:\n", rpc.method)
|
||||||
fmt.Printf("- Options:\n")
|
fmt.Fprintf(rpc.w, "- Options:\n")
|
||||||
fmt.Printf(" - concurrency: %d\n", rpc.concurrency)
|
fmt.Fprintf(rpc.w, " - concurrency: %d\n", rpc.concurrency)
|
||||||
fmt.Printf(" - params: %s\n", rpc.params)
|
fmt.Fprintf(rpc.w, " - params: %s\n", rpc.params)
|
||||||
fmt.Printf(" - qps: %d\n", rpc.qps)
|
fmt.Fprintf(rpc.w, " - qps: %d\n", rpc.qps)
|
||||||
rpc.reporter.Print(total)
|
rpc.reporter.Print(total, rpc.w)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reporter reads the results from the workers through the results channel and aggregates the results.
|
// Reporter reads the results from the workers through the results channel and aggregates the results.
|
||||||
type Reporter struct {
|
type Reporter struct {
|
||||||
|
// write the report to this writer
|
||||||
|
w io.Writer
|
||||||
// the reporter read the results from this channel
|
// the reporter read the results from this channel
|
||||||
results chan *result
|
results chan *result
|
||||||
// doneCh is used to signal that the reporter has finished reading the results (channel has closed)
|
// doneCh is used to signal that the reporter has finished reading the results (channel has closed)
|
||||||
doneCh chan bool
|
doneCh chan bool
|
||||||
|
|
||||||
|
// lock protect the following fields during critical sections (if --watch was specified)
|
||||||
|
lock sync.Mutex
|
||||||
// the latencies of all requests
|
// the latencies of all requests
|
||||||
latencies []int64
|
latencies []int64
|
||||||
// the number of requests that returned each status code
|
// the number of requests that returned each status code
|
||||||
@ -398,8 +447,9 @@ type Reporter struct {
|
|||||||
errors map[string]int
|
errors map[string]int
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReporter(results chan *result) *Reporter {
|
func NewReporter(results chan *result, w io.Writer) *Reporter {
|
||||||
return &Reporter{
|
return &Reporter{
|
||||||
|
w: w,
|
||||||
results: results,
|
results: results,
|
||||||
doneCh: make(chan bool, 1),
|
doneCh: make(chan bool, 1),
|
||||||
statusCodes: make(map[int]int),
|
statusCodes: make(map[int]int),
|
||||||
@ -409,6 +459,8 @@ func NewReporter(results chan *result) *Reporter {
|
|||||||
|
|
||||||
func (r *Reporter) Run() {
|
func (r *Reporter) Run() {
|
||||||
for res := range r.results {
|
for res := range r.results {
|
||||||
|
r.lock.Lock()
|
||||||
|
|
||||||
r.latencies = append(r.latencies, res.duration.Milliseconds())
|
r.latencies = append(r.latencies, res.duration.Milliseconds())
|
||||||
|
|
||||||
if res.statusCode != nil {
|
if res.statusCode != nil {
|
||||||
@ -425,12 +477,17 @@ func (r *Reporter) Run() {
|
|||||||
} else {
|
} else {
|
||||||
r.errors["nil"]++
|
r.errors["nil"]++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.lock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
r.doneCh <- true
|
r.doneCh <- true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Reporter) Print(elapsed time.Duration) {
|
func (r *Reporter) Print(elapsed time.Duration, w io.Writer) {
|
||||||
|
r.lock.Lock()
|
||||||
|
defer r.lock.Unlock()
|
||||||
|
|
||||||
nrReq := int64(len(r.latencies))
|
nrReq := int64(len(r.latencies))
|
||||||
if nrReq == 0 {
|
if nrReq == 0 {
|
||||||
fmt.Println("No requests were made")
|
fmt.Println("No requests were made")
|
||||||
@ -447,16 +504,16 @@ func (r *Reporter) Print(elapsed time.Duration) {
|
|||||||
totalLatency += latency
|
totalLatency += latency
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("- Total Requests: %d\n", nrReq)
|
fmt.Fprintf(w, "- Total Requests: %d\n", nrReq)
|
||||||
fmt.Printf("- Total Duration: %dms\n", elapsed.Milliseconds())
|
fmt.Fprintf(w, "- Total Duration: %dms\n", elapsed.Milliseconds())
|
||||||
fmt.Printf("- Requests/sec: %f\n", float64(nrReq)/elapsed.Seconds())
|
fmt.Fprintf(w, "- Requests/sec: %f\n", float64(nrReq)/elapsed.Seconds())
|
||||||
fmt.Printf("- Avg latency: %dms\n", totalLatency/nrReq)
|
fmt.Fprintf(w, "- Avg latency: %dms\n", totalLatency/nrReq)
|
||||||
fmt.Printf("- Median latency: %dms\n", r.latencies[nrReq/2])
|
fmt.Fprintf(w, "- Median latency: %dms\n", r.latencies[nrReq/2])
|
||||||
fmt.Printf("- Latency distribution:\n")
|
fmt.Fprintf(w, "- Latency distribution:\n")
|
||||||
percentiles := []float64{0.1, 0.5, 0.9, 0.95, 0.99, 0.999}
|
percentiles := []float64{0.1, 0.5, 0.9, 0.95, 0.99, 0.999}
|
||||||
for _, p := range percentiles {
|
for _, p := range percentiles {
|
||||||
idx := int64(p * float64(nrReq))
|
idx := int64(p * float64(nrReq))
|
||||||
fmt.Printf(" %s%% in %dms\n", fmt.Sprintf("%.2f", p*100.0), r.latencies[idx])
|
fmt.Fprintf(w, " %s%% in %dms\n", fmt.Sprintf("%.2f", p*100.0), r.latencies[idx])
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a simple histogram with 10 buckets spanning the range of latency
|
// create a simple histogram with 10 buckets spanning the range of latency
|
||||||
@ -489,19 +546,19 @@ func (r *Reporter) Print(elapsed time.Duration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// print the histogram using a tabwriter which will align the columns nicely
|
// print the histogram using a tabwriter which will align the columns nicely
|
||||||
fmt.Printf("- Histogram:\n")
|
fmt.Fprintf(w, "- Histogram:\n")
|
||||||
const padding = 2
|
const padding = 2
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, padding, ' ', tabwriter.AlignRight|tabwriter.Debug)
|
tabWriter := tabwriter.NewWriter(w, 0, 0, padding, ' ', tabwriter.AlignRight|tabwriter.Debug)
|
||||||
for i := 0; i < nrBucket; i++ {
|
for i := 0; i < nrBucket; i++ {
|
||||||
ratio := float64(buckets[i].cnt) / float64(nrReq)
|
ratio := float64(buckets[i].cnt) / float64(nrReq)
|
||||||
bars := strings.Repeat("#", int(ratio*100))
|
bars := strings.Repeat("#", int(ratio*100))
|
||||||
fmt.Fprintf(w, " %d-%dms\t%d\t%s (%s%%)\n", buckets[i].start, buckets[i].end, buckets[i].cnt, bars, fmt.Sprintf("%.2f", ratio*100))
|
fmt.Fprintf(tabWriter, " %d-%dms\t%d\t%s (%s%%)\n", buckets[i].start, buckets[i].end, buckets[i].cnt, bars, fmt.Sprintf("%.2f", ratio*100))
|
||||||
}
|
}
|
||||||
w.Flush() //nolint:errcheck
|
tabWriter.Flush() //nolint:errcheck
|
||||||
|
|
||||||
fmt.Printf("- Status codes:\n")
|
fmt.Fprintf(w, "- Status codes:\n")
|
||||||
for code, cnt := range r.statusCodes {
|
for code, cnt := range r.statusCodes {
|
||||||
fmt.Printf(" [%d]: %d\n", code, cnt)
|
fmt.Fprintf(w, " [%d]: %d\n", code, cnt)
|
||||||
}
|
}
|
||||||
|
|
||||||
// print the 10 most occurring errors (in case error values are not unique)
|
// print the 10 most occurring errors (in case error values are not unique)
|
||||||
@ -517,12 +574,12 @@ func (r *Reporter) Print(elapsed time.Duration) {
|
|||||||
sort.Slice(sortedErrors, func(i, j int) bool {
|
sort.Slice(sortedErrors, func(i, j int) bool {
|
||||||
return sortedErrors[i].cnt > sortedErrors[j].cnt
|
return sortedErrors[i].cnt > sortedErrors[j].cnt
|
||||||
})
|
})
|
||||||
fmt.Printf("- Errors (top 10):\n")
|
fmt.Fprintf(w, "- Errors (top 10):\n")
|
||||||
for i, se := range sortedErrors {
|
for i, se := range sortedErrors {
|
||||||
if i > 10 {
|
if i > 10 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
fmt.Printf(" [%s]: %d\n", se.err, se.cnt)
|
fmt.Fprintf(w, " [%s]: %d\n", se.err, se.cnt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user