#46 support JSON snapshot

This commit is contained in:
duncandu 2022-07-01 23:19:55 +08:00
parent 72b10d10a8
commit cf5e6eef85
4 changed files with 174 additions and 30 deletions

View File

@ -99,7 +99,7 @@ A high-performance HTTP benchmarking tool with real-time web UI and terminal dis
Examples: Examples:
plow http://127.0.0.1:8080/ -c 20 -n 100000 plow http://127.0.0.1:8080/ -c 20 -n 100000
plow https://httpbin.org/post -c 20 -d 5m --body @file.json -T 'application/json' -m POST plow https://httpbin.org/post -c 20 -d 5m --body @file.jsonFormat -T 'application/jsonFormat' -m POST
Flags: Flags:
--help Show context-sensitive help. --help Show context-sensitive help.
@ -109,6 +109,7 @@ Flags:
-d, --duration=DURATION Duration of test, examples: -d 10s -d 3m -d, --duration=DURATION Duration of test, examples: -d 10s -d 3m
-i, --interval=200ms Print snapshot result every interval, use 0 to print once at the end -i, --interval=200ms Print snapshot result every interval, use 0 to print once at the end
--seconds Use seconds as time unit to print --seconds Use seconds as time unit to print
--json Print snapshot result as JSON
-b, --body=BODY HTTP request body, if start the body with @, the rest should be a filename to read -b, --body=BODY HTTP request body, if start the body with @, the rest should be a filename to read
--stream Specify whether to stream file specified by '--body @file' using chunked encoding or to read into memory --stream Specify whether to stream file specified by '--body @file' using chunked encoding or to read into memory
-m, --method="GET" HTTP method -m, --method="GET" HTTP method
@ -126,7 +127,7 @@ Flags:
--socks5=ip:port Socks5 proxy --socks5=ip:port Socks5 proxy
--auto-open-browser Specify whether auto open browser to show Web charts --auto-open-browser Specify whether auto open browser to show Web charts
--[no-]clean Clean the histogram bar once its finished. Default is true --[no-]clean Clean the histogram bar once its finished. Default is true
--[no-]summary Only print the summary without realtime reports --summary Only print the summary without realtime reports
--version Show application version. --version Show application version.
Flags default values also read from env PLOW_SOME_FLAG, such as PLOW_TIMEOUT=5s equals to --timeout=5s Flags default values also read from env PLOW_SOME_FLAG, such as PLOW_TIMEOUT=5s equals to --timeout=5s

View File

@ -42,7 +42,7 @@ function {{ .ViewID }}_sync() {
$.ajax({ $.ajax({
type: "GET", type: "GET",
url: "{{ .APIPath }}{{ .Route }}", url: "{{ .APIPath }}{{ .Route }}",
dataType: "json", dataType: "jsonFormat",
success: function (result) { success: function (result) {
let opt = goecharts_{{ .ViewID }}.getOption(); let opt = goecharts_{{ .ViewID }}.getOption();
let x = opt.xAxis[0].data; let x = opt.xAxis[0].data;
@ -141,8 +141,8 @@ func (c *Charts) newRPSView() components.Charter {
} }
type Metrics struct { type Metrics struct {
Values []interface{} `json:"values"` Values []interface{} `jsonFormat:"values"`
Time string `json:"time"` Time string `jsonFormat:"time"`
} }
type Charts struct { type Charts struct {

26
main.go
View File

@ -5,6 +5,8 @@ import (
"golang.org/x/time/rate" "golang.org/x/time/rate"
"io/ioutil" "io/ioutil"
"net" "net"
"net/http"
_ "net/http/pprof"
"os" "os"
"strconv" "strconv"
"strings" "strings"
@ -20,6 +22,7 @@ var (
duration = kingpin.Flag("duration", "Duration of test, examples: -d 10s -d 3m").Short('d').PlaceHolder("DURATION").Duration() duration = kingpin.Flag("duration", "Duration of test, examples: -d 10s -d 3m").Short('d').PlaceHolder("DURATION").Duration()
interval = kingpin.Flag("interval", "Print snapshot result every interval, use 0 to print once at the end").Short('i').Default("200ms").Duration() interval = kingpin.Flag("interval", "Print snapshot result every interval, use 0 to print once at the end").Short('i').Default("200ms").Duration()
seconds = kingpin.Flag("seconds", "Use seconds as time unit to print").Bool() seconds = kingpin.Flag("seconds", "Use seconds as time unit to print").Bool()
jsonFormat = kingpin.Flag("json", "Print snapshot result as JSON").Bool()
body = kingpin.Flag("body", "HTTP request body, if start the body with @, the rest should be a filename to read").Short('b').String() body = kingpin.Flag("body", "HTTP request body, if start the body with @, the rest should be a filename to read").Short('b').String()
stream = kingpin.Flag("stream", "Specify whether to stream file specified by '--body @file' using chunked encoding or to read into memory").Default("false").Bool() stream = kingpin.Flag("stream", "Specify whether to stream file specified by '--body @file' using chunked encoding or to read into memory").Default("false").Bool()
@ -40,7 +43,8 @@ var (
autoOpenBrowser = kingpin.Flag("auto-open-browser", "Specify whether auto open browser to show Web charts").Bool() autoOpenBrowser = kingpin.Flag("auto-open-browser", "Specify whether auto open browser to show Web charts").Bool()
clean = kingpin.Flag("clean", "Clean the histogram bar once its finished. Default is true").Default("true").NegatableBool() clean = kingpin.Flag("clean", "Clean the histogram bar once its finished. Default is true").Default("true").NegatableBool()
summary = kingpin.Flag("summary", "Only print the summary without realtime reports").Default("false").NegatableBool() summary = kingpin.Flag("summary", "Only print the summary without realtime reports").Default("false").Bool()
pprofAddr = kingpin.Flag("pprof", "Enable pprof at special address").Hidden().String()
url = kingpin.Arg("url", "request url").Required().String() url = kingpin.Arg("url", "request url").Required().String()
) )
@ -79,7 +83,7 @@ var CompactUsageTemplate = `{{define "FormatCommand" -}}
Examples: Examples:
plow http://127.0.0.1:8080/ -c 20 -n 100000 plow http://127.0.0.1:8080/ -c 20 -n 100000
plow https://httpbin.org/post -c 20 -d 5m --body @file.json -T 'application/json' -m POST plow https://httpbin.org/post -c 20 -d 5m --body @file.jsonFormat -T 'application/jsonFormat' -m POST
{{if .Context.Flags -}} {{if .Context.Flags -}}
{{T "Flags:"}} {{T "Flags:"}}
@ -182,6 +186,10 @@ func main() {
return return
} }
if *pprofAddr != "" {
go http.ListenAndServe(*pprofAddr, nil)
}
var err error var err error
var bodyBytes []byte var bodyBytes []byte
var bodyFile string var bodyFile string
@ -232,11 +240,6 @@ func main() {
return return
} }
outStream := os.Stdout
if *summary {
outStream = os.Stderr
isTerminal = false
}
// description // description
var desc string var desc string
desc = fmt.Sprintf("Benchmarking %s", *url) desc = fmt.Sprintf("Benchmarking %s", *url)
@ -247,7 +250,7 @@ func main() {
desc += fmt.Sprintf(" for %s", duration.String()) desc += fmt.Sprintf(" for %s", duration.String())
} }
desc += fmt.Sprintf(" using %d connection(s).", *concurrency) desc += fmt.Sprintf(" using %d connection(s).", *concurrency)
fmt.Fprintln(outStream, desc) fmt.Fprintln(os.Stderr, desc)
// charts listener // charts listener
var ln net.Listener var ln net.Listener
@ -257,9 +260,9 @@ func main() {
errAndExit(err.Error()) errAndExit(err.Error())
return return
} }
fmt.Fprintf(outStream, "@ Real-time charts is listening on http://%s\n", ln.Addr().String()) fmt.Fprintf(os.Stderr, "@ Real-time charts is listening on http://%s\n", ln.Addr().String())
} }
fmt.Fprintln(outStream, "") fmt.Fprintln(os.Stderr, "")
// do request // do request
go requester.Run() go requester.Run()
@ -280,6 +283,5 @@ func main() {
// terminal printer // terminal printer
printer := NewPrinter(*requests, *duration, !*clean, *summary) printer := NewPrinter(*requests, *duration, !*clean, *summary)
printer.PrintLoop(report.Snapshot, *interval, *seconds, report.Done()) printer.PrintLoop(report.Snapshot, *interval, *seconds, *jsonFormat, report.Done())
} }

167
print.go
View File

@ -2,6 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"github.com/mattn/go-isatty" "github.com/mattn/go-isatty"
"github.com/mattn/go-runewidth" "github.com/mattn/go-runewidth"
@ -63,12 +64,12 @@ func (p *Printer) updateProgressValue(rs *SnapshotReport) {
} }
} }
func (p *Printer) PrintLoop(snapshot func() *SnapshotReport, interval time.Duration, useSeconds bool, doneChan <-chan struct{}) { func (p *Printer) PrintLoop(snapshot func() *SnapshotReport, interval time.Duration, useSeconds bool, json bool, doneChan <-chan struct{}) {
var buf bytes.Buffer var buf bytes.Buffer
var backCursor string var backCursor string
cl := clearLine cl := clearLine
if p.summary || interval == 0 { if p.summary || interval == 0 || !isTerminal {
cl = nil cl = nil
} }
echo := func(isFinal bool) { echo := func(isFinal bool) {
@ -76,7 +77,11 @@ func (p *Printer) PrintLoop(snapshot func() *SnapshotReport, interval time.Durat
p.updateProgressValue(report) p.updateProgressValue(report)
os.Stdout.WriteString(backCursor) os.Stdout.WriteString(backCursor)
buf.Reset() buf.Reset()
p.formatTableReports(&buf, report, isFinal, useSeconds) if json {
p.formatJSONReports(&buf, report, isFinal, useSeconds)
} else {
p.formatTableReports(&buf, report, isFinal, useSeconds)
}
result := buf.Bytes() result := buf.Bytes()
n := 0 n := 0
for { for {
@ -93,7 +98,9 @@ func (p *Printer) PrintLoop(snapshot func() *SnapshotReport, interval time.Durat
result = result[i+1:] result = result[i+1:]
} }
os.Stdout.Sync() os.Stdout.Sync()
backCursor = fmt.Sprintf("\033[%dA", n) if isTerminal {
backCursor = fmt.Sprintf("\033[%dA", n)
}
} }
if interval > 0 { if interval > 0 {
@ -185,6 +192,24 @@ func formatFloat64(f float64) string {
return strconv.FormatFloat(f, 'f', -1, 64) return strconv.FormatFloat(f, 'f', -1, 64)
} }
func (p *Printer) formatJSONReports(writer *bytes.Buffer, snapshot *SnapshotReport, isFinal bool, useSeconds bool) {
indent := 0
writer.WriteString("{\n")
indent++
p.buildJSONSummary(writer, snapshot, indent)
if len(snapshot.Errors) != 0 {
writer.WriteString(",\n")
p.buildJSONErrors(writer, snapshot, indent)
}
writer.WriteString(",\n")
p.buildJSONStats(writer, snapshot, useSeconds, indent)
writer.WriteString(",\n")
p.buildJSONPercentile(writer, snapshot, useSeconds, indent)
writer.WriteString(",\n")
p.buildJSONHistogram(writer, snapshot, useSeconds, indent)
writer.WriteString("\n}\n")
}
func (p *Printer) formatTableReports(writer *bytes.Buffer, snapshot *SnapshotReport, isFinal bool, useSeconds bool) { func (p *Printer) formatTableReports(writer *bytes.Buffer, snapshot *SnapshotReport, isFinal bool, useSeconds bool) {
summaryBulk := p.buildSummary(snapshot, isFinal) summaryBulk := p.buildSummary(snapshot, isFinal)
errorsBulks := p.buildErrors(snapshot) errorsBulks := p.buildErrors(snapshot)
@ -213,6 +238,30 @@ func (p *Printer) formatTableReports(writer *bytes.Buffer, snapshot *SnapshotRep
writeBulk(writer, hisBulk) writeBulk(writer, hisBulk)
} }
func (p *Printer) buildJSONHistogram(writer *bytes.Buffer, snapshot *SnapshotReport, useSeconds bool, indent int) {
tab0 := strings.Repeat(" ", indent)
writer.WriteString(tab0 + "\"Histograms\": [\n")
tab1 := strings.Repeat(" ", indent+1)
maxCount := 0
hisSum := 0
for _, bin := range snapshot.Histograms {
if maxCount < bin.Count {
maxCount = bin.Count
}
hisSum += bin.Count
}
for i, bin := range snapshot.Histograms {
writer.WriteString(fmt.Sprintf(`%s[ "%s", %d ]`, tab1,
durationToString(bin.Mean, useSeconds), bin.Count))
if i != len(snapshot.Histograms)-1 {
writer.WriteString(",")
}
writer.WriteString("\n")
}
writer.WriteString(tab0 + "]")
}
func (p *Printer) buildHistogram(snapshot *SnapshotReport, useSeconds bool, isFinal bool) [][]string { func (p *Printer) buildHistogram(snapshot *SnapshotReport, useSeconds bool, isFinal bool) [][]string {
hisBulk := make([][]string, 0, 8) hisBulk := make([][]string, 0, 8)
maxCount := 0 maxCount := 0
@ -245,6 +294,22 @@ func (p *Printer) buildHistogram(snapshot *SnapshotReport, useSeconds bool, isFi
return hisBulk return hisBulk
} }
func (p *Printer) buildJSONPercentile(writer *bytes.Buffer, snapshot *SnapshotReport, useSeconds bool, indent int) {
tab0 := strings.Repeat(" ", indent)
writer.WriteString(tab0 + "\"Percentiles\": {\n")
tab1 := strings.Repeat(" ", indent+1)
for i, percentile := range snapshot.Percentiles {
perc := formatFloat64(percentile.Percentile * 100)
writer.WriteString(fmt.Sprintf(`%s"%s": "%s"`, tab1, "P"+perc,
durationToString(percentile.Latency, useSeconds)))
if i != len(snapshot.Percentiles)-1 {
writer.WriteString(",")
}
writer.WriteString("\n")
}
writer.WriteString(tab0 + "}")
}
func (p *Printer) buildPercentile(snapshot *SnapshotReport, useSeconds bool) [][]string { func (p *Printer) buildPercentile(snapshot *SnapshotReport, useSeconds bool) [][]string {
percBulk := make([][]string, 2) percBulk := make([][]string, 2)
percAligns := make([]int, 0, len(snapshot.Percentiles)) percAligns := make([]int, 0, len(snapshot.Percentiles))
@ -259,6 +324,30 @@ func (p *Printer) buildPercentile(snapshot *SnapshotReport, useSeconds bool) [][
return percBulk return percBulk
} }
func (p *Printer) buildJSONStats(writer *bytes.Buffer, snapshot *SnapshotReport, useSeconds bool, indent int) {
tab0 := strings.Repeat(" ", indent)
writer.WriteString(tab0 + "\"Statistics\": {\n")
tab1 := strings.Repeat(" ", indent+1)
writer.WriteString(fmt.Sprintf(`%s"Latency": { "Min": "%s", "Mean": "%s", "StdDev": "%s", "Max": "%s" }`,
tab1,
durationToString(snapshot.Stats.Min, useSeconds),
durationToString(snapshot.Stats.Mean, useSeconds),
durationToString(snapshot.Stats.StdDev, useSeconds),
durationToString(snapshot.Stats.Max, useSeconds),
))
if snapshot.RpsStats != nil {
writer.WriteString(",\n")
writer.WriteString(fmt.Sprintf(`%s"RPS": { "Min": %s, "Mean": %s, "StdDev": %s, "Max": %s }`,
tab1,
formatFloat64(math.Trunc(snapshot.RpsStats.Min*100)/100.0),
formatFloat64(math.Trunc(snapshot.RpsStats.Mean*100)/100.0),
formatFloat64(math.Trunc(snapshot.RpsStats.StdDev*100)/100.0),
formatFloat64(math.Trunc(snapshot.RpsStats.Max*100)/100.0),
))
}
writer.WriteString("\n" + tab0 + "}")
}
func (p *Printer) buildStats(snapshot *SnapshotReport, useSeconds bool) [][]string { func (p *Printer) buildStats(snapshot *SnapshotReport, useSeconds bool) [][]string {
var statsBulk [][]string var statsBulk [][]string
statsBulk = append(statsBulk, statsBulk = append(statsBulk,
@ -286,6 +375,23 @@ func (p *Printer) buildStats(snapshot *SnapshotReport, useSeconds bool) [][]stri
return statsBulk return statsBulk
} }
func (p *Printer) buildJSONErrors(writer *bytes.Buffer, snapshot *SnapshotReport, indent int) {
tab0 := strings.Repeat(" ", indent)
writer.WriteString(tab0 + "\"Error\": {\n")
tab1 := strings.Repeat(" ", indent+1)
errors := sortMapStrInt(snapshot.Errors)
for i, v := range errors {
v[1] = colorize(v[1], FgRedColor)
vb, _ := json.Marshal(v[0])
writer.WriteString(fmt.Sprintf(`%s%s: %s`, tab1, vb, v[1]))
if i != len(errors)-1 {
writer.WriteString(",")
}
writer.WriteString("\n")
}
writer.WriteString(tab0 + "}")
}
func (p *Printer) buildErrors(snapshot *SnapshotReport) [][]string { func (p *Printer) buildErrors(snapshot *SnapshotReport) [][]string {
var errorsBulks [][]string var errorsBulks [][]string
for k, v := range snapshot.Errors { for k, v := range snapshot.Errors {
@ -299,9 +405,47 @@ func (p *Printer) buildErrors(snapshot *SnapshotReport) [][]string {
return errorsBulks return errorsBulks
} }
func sortMapStrInt(m map[string]int64) (ret [][]string) {
for k, v := range m {
ret = append(ret, []string{k, strconv.FormatInt(v, 10)})
}
sort.Slice(ret, func(i, j int) bool { return ret[i][0] < ret[j][0] })
return
}
func (p *Printer) buildJSONSummary(writer *bytes.Buffer, snapshot *SnapshotReport, indent int) {
tab0 := strings.Repeat(" ", indent)
writer.WriteString(tab0 + "\"Summary\": {\n")
{
tab1 := strings.Repeat(" ", indent+1)
writer.WriteString(fmt.Sprintf("%s\"Elapsed\": \"%s\",\n", tab1, snapshot.Elapsed.Truncate(100*time.Millisecond).String()))
writer.WriteString(fmt.Sprintf("%s\"Count\": %d,\n", tab1, snapshot.Count))
writer.WriteString(fmt.Sprintf("%s\"Counts\": {\n", tab1))
i := 0
tab2 := strings.Repeat(" ", indent+2)
codes := sortMapStrInt(snapshot.Codes)
for _, v := range codes {
i++
if v[0] != "2xx" {
v[1] = colorize(v[1], FgMagentaColor)
}
writer.WriteString(fmt.Sprintf(`%s"%s": %s`, tab2, v[0], v[1]))
if i != len(snapshot.Codes) {
writer.WriteString(",")
}
writer.WriteString("\n")
}
writer.WriteString(tab1 + "},\n")
writer.WriteString(fmt.Sprintf("%s\"RPS\": %.3f,\n", tab1, snapshot.RPS))
writer.WriteString(fmt.Sprintf("%s\"Reads\": \"%.3fMB/s\",\n", tab1, snapshot.ReadThroughput))
writer.WriteString(fmt.Sprintf("%s\"Writes\": \"%.3fMB/s\"\n", tab1, snapshot.WriteThroughput))
}
writer.WriteString(tab0 + "}")
}
func (p *Printer) buildSummary(snapshot *SnapshotReport, isFinal bool) [][]string { func (p *Printer) buildSummary(snapshot *SnapshotReport, isFinal bool) [][]string {
summarybulk := make([][]string, 0, 8) summarybulk := make([][]string, 0, 8)
elapsedLine := []string{"Elapsed", snapshot.Elapsed.Truncate(time.Millisecond).String()} elapsedLine := []string{"Elapsed", snapshot.Elapsed.Truncate(100 * time.Millisecond).String()}
if p.maxDuration > 0 && !isFinal { if p.maxDuration > 0 && !isFinal {
elapsedLine = append(elapsedLine, p.pbDurStr) elapsedLine = append(elapsedLine, p.pbDurStr)
} }
@ -315,16 +459,13 @@ func (p *Printer) buildSummary(snapshot *SnapshotReport, isFinal bool) [][]strin
countLine, countLine,
) )
codesBulks := make([][]string, 0, len(snapshot.Codes)) codes := sortMapStrInt(snapshot.Codes)
for k, v := range snapshot.Codes { for _, v := range codes {
vs := strconv.FormatInt(v, 10) if v[0] != "2xx" {
if k != "2xx" { v[1] = colorize(v[1], FgMagentaColor)
vs = colorize(vs, FgMagentaColor)
} }
codesBulks = append(codesBulks, []string{" " + k, vs}) summarybulk = append(summarybulk, []string{" " + v[0], v[1]})
} }
sort.Slice(codesBulks, func(i, j int) bool { return codesBulks[i][0] < codesBulks[j][0] })
summarybulk = append(summarybulk, codesBulks...)
summarybulk = append(summarybulk, summarybulk = append(summarybulk,
[]string{"RPS", fmt.Sprintf("%.3f", snapshot.RPS)}, []string{"RPS", fmt.Sprintf("%.3f", snapshot.RPS)},
[]string{"Reads", fmt.Sprintf("%.3fMB/s", snapshot.ReadThroughput)}, []string{"Reads", fmt.Sprintf("%.3fMB/s", snapshot.ReadThroughput)},