123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505 |
- package main
- import (
- "bytes"
- "encoding/json"
- "fmt"
- "math"
- "os"
- "regexp"
- "sort"
- "strconv"
- "strings"
- "time"
- "github.com/mattn/go-isatty"
- "github.com/mattn/go-runewidth"
- )
- var (
- maxBarLen = 40
- barStart = "|"
- barBody = "■"
- barEnd = "|"
- barSpinner = []string{"|", "/", "-", "\\"}
- clearLine = []byte("\r\033[K")
- isTerminal = isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
- )
- type Printer struct {
- maxNum int64
- maxDuration time.Duration
- curNum int64
- curDuration time.Duration
- pbInc int64
- pbNumStr string
- pbDurStr string
- noClean bool
- summary bool
- }
- func NewPrinter(maxNum int64, maxDuration time.Duration, noCleanBar, summary bool) *Printer {
- return &Printer{maxNum: maxNum, maxDuration: maxDuration, noClean: noCleanBar, summary: summary}
- }
- func (p *Printer) updateProgressValue(rs *SnapshotReport) {
- p.pbInc++
- if p.maxDuration > 0 {
- n := rs.Elapsed
- if n > p.maxDuration {
- n = p.maxDuration
- }
- p.curDuration = n
- barLen := int((p.curDuration*time.Duration(maxBarLen-2) + p.maxDuration/2) / p.maxDuration)
- p.pbDurStr = barStart + strings.Repeat(barBody, barLen) + strings.Repeat(" ", maxBarLen-2-barLen) + barEnd
- }
- if p.maxNum > 0 {
- p.curNum = rs.Count
- if p.maxNum > 0 {
- barLen := int((p.curNum*int64(maxBarLen-2) + p.maxNum/2) / p.maxNum)
- p.pbNumStr = barStart + strings.Repeat(barBody, barLen) + strings.Repeat(" ", maxBarLen-2-barLen) + barEnd
- } else {
- idx := p.pbInc % int64(len(barSpinner))
- p.pbNumStr = barSpinner[int(idx)]
- }
- }
- }
- func (p *Printer) PrintLoop(snapshot func() *SnapshotReport, interval time.Duration, useSeconds bool, json bool, doneChan <-chan struct{}) {
- var buf bytes.Buffer
- var backCursor string
- cl := clearLine
- if p.summary || interval == 0 || !isTerminal {
- cl = nil
- }
- echo := func(isFinal bool) {
- report := snapshot()
- p.updateProgressValue(report)
- os.Stdout.WriteString(backCursor)
- buf.Reset()
- if json {
- p.formatJSONReports(&buf, report, isFinal, useSeconds)
- } else {
- p.formatTableReports(&buf, report, isFinal, useSeconds)
- }
- result := buf.Bytes()
- n := 0
- for {
- i := bytes.IndexByte(result, '\n')
- if i == -1 {
- os.Stdout.Write(cl)
- os.Stdout.Write(result)
- break
- }
- n++
- os.Stdout.Write(cl)
- os.Stdout.Write(result[:i])
- os.Stdout.Write([]byte("\n"))
- result = result[i+1:]
- }
- os.Stdout.Sync()
- if isTerminal {
- backCursor = fmt.Sprintf("\033[%dA", n)
- }
- }
- if interval > 0 {
- ticker := time.NewTicker(interval)
- loop:
- for {
- select {
- case <-ticker.C:
- if !p.summary {
- echo(false)
- }
- case <-doneChan:
- ticker.Stop()
- break loop
- }
- }
- } else {
- <-doneChan
- }
- echo(true)
- }
- // nolint
- const (
- FgBlackColor int = iota + 30
- FgRedColor
- FgGreenColor
- FgYellowColor
- FgBlueColor
- FgMagentaColor
- FgCyanColor
- FgWhiteColor
- )
- func colorize(s string, seq int) string {
- if !isTerminal {
- return s
- }
- return fmt.Sprintf("\033[%dm%s\033[0m", seq, s)
- }
- func durationToString(d time.Duration, useSeconds bool) string {
- d = d.Truncate(time.Microsecond)
- if useSeconds {
- return formatFloat64(d.Seconds())
- }
- return d.String()
- }
- func alignBulk(bulk [][]string, aligns ...int) {
- maxLen := map[int]int{}
- for _, b := range bulk {
- for i, bb := range b {
- lbb := displayWidth(bb)
- if maxLen[i] < lbb {
- maxLen[i] = lbb
- }
- }
- }
- for _, b := range bulk {
- for i, ali := range aligns {
- if len(b) >= i+1 {
- if i == len(aligns)-1 && ali == AlignLeft {
- continue
- }
- b[i] = padString(b[i], " ", maxLen[i], ali)
- }
- }
- }
- }
- func writeBulkWith(writer *bytes.Buffer, bulk [][]string, lineStart, sep, lineEnd string) {
- for _, b := range bulk {
- writer.WriteString(lineStart)
- writer.WriteString(b[0])
- for _, bb := range b[1:] {
- writer.WriteString(sep)
- writer.WriteString(bb)
- }
- writer.WriteString(lineEnd)
- }
- }
- func writeBulk(writer *bytes.Buffer, bulk [][]string) {
- writeBulkWith(writer, bulk, " ", " ", "\n")
- }
- func formatFloat64(f float64) string {
- return strconv.FormatFloat(f, 'f', -1, 64)
- }
- func (p *Printer) formatJSONReports(writer *bytes.Buffer, snapshot *SnapshotReport, _ 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) {
- summaryBulk := p.buildSummary(snapshot, isFinal)
- errorsBulks := p.buildErrors(snapshot)
- statsBulk := p.buildStats(snapshot, useSeconds)
- percBulk := p.buildPercentile(snapshot, useSeconds)
- hisBulk := p.buildHistogram(snapshot, useSeconds, isFinal)
- writer.WriteString("Summary:\n")
- writeBulk(writer, summaryBulk)
- writer.WriteString("\n")
- if errorsBulks != nil {
- writer.WriteString("Error:\n")
- writeBulk(writer, errorsBulks)
- writer.WriteString("\n")
- }
- writeBulkWith(writer, statsBulk, "", " ", "\n")
- writer.WriteString("\n")
- writer.WriteString("Latency Percentile:\n")
- writeBulk(writer, percBulk)
- writer.WriteString("\n")
- writer.WriteString("Latency Histogram:\n")
- 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 {
- hisBulk := make([][]string, 0, 8)
- maxCount := 0
- hisSum := 0
- for _, bin := range snapshot.Histograms {
- if maxCount < bin.Count {
- maxCount = bin.Count
- }
- hisSum += bin.Count
- }
- for _, bin := range snapshot.Histograms {
- row := []string{durationToString(bin.Mean, useSeconds), strconv.Itoa(bin.Count)}
- if isFinal {
- row = append(row, fmt.Sprintf("%.2f%%", math.Floor(float64(bin.Count)*1e4/float64(hisSum)+0.5)/100.0))
- }
- if !isFinal || p.noClean {
- barLen := 0
- if maxCount > 0 {
- barLen = (bin.Count*maxBarLen + maxCount/2) / maxCount
- }
- row = append(row, strings.Repeat(barBody, barLen))
- }
- hisBulk = append(hisBulk, row)
- }
- if isFinal {
- alignBulk(hisBulk, AlignLeft, AlignRight, AlignRight)
- } else {
- alignBulk(hisBulk, AlignLeft, AlignRight, AlignLeft)
- }
- 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 {
- percBulk := make([][]string, 2)
- percAligns := make([]int, 0, len(snapshot.Percentiles))
- for _, percentile := range snapshot.Percentiles {
- perc := formatFloat64(percentile.Percentile * 100)
- percBulk[0] = append(percBulk[0], "P"+perc)
- percBulk[1] = append(percBulk[1], durationToString(percentile.Latency, useSeconds))
- percAligns = append(percAligns, AlignCenter)
- }
- percAligns[0] = AlignLeft
- alignBulk(percBulk, percAligns...)
- 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 {
- var statsBulk [][]string
- statsBulk = append(statsBulk,
- []string{"Statistics", "Min", "Mean", "StdDev", "Max"},
- []string{
- " Latency",
- durationToString(snapshot.Stats.Min, useSeconds),
- durationToString(snapshot.Stats.Mean, useSeconds),
- durationToString(snapshot.Stats.StdDev, useSeconds),
- durationToString(snapshot.Stats.Max, useSeconds),
- },
- )
- if snapshot.RpsStats != nil {
- statsBulk = append(statsBulk,
- []string{
- " RPS",
- 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),
- },
- )
- }
- alignBulk(statsBulk, AlignLeft, AlignCenter, AlignCenter, AlignCenter, AlignCenter)
- 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 {
- var errorsBulks [][]string
- for k, v := range snapshot.Errors {
- vs := colorize(strconv.FormatInt(v, 10), FgRedColor)
- errorsBulks = append(errorsBulks, []string{vs, "\"" + k + "\""})
- }
- if errorsBulks != nil {
- sort.Slice(errorsBulks, func(i, j int) bool { return errorsBulks[i][1] < errorsBulks[j][1] })
- }
- alignBulk(errorsBulks, AlignLeft, AlignLeft)
- 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 {
- summarybulk := make([][]string, 0, 8)
- elapsedLine := []string{"Elapsed", snapshot.Elapsed.Truncate(100 * time.Millisecond).String()}
- if p.maxDuration > 0 && !isFinal {
- elapsedLine = append(elapsedLine, p.pbDurStr)
- }
- countLine := []string{"Count", strconv.FormatInt(snapshot.Count, 10)}
- if p.maxNum > 0 && !isFinal {
- countLine = append(countLine, p.pbNumStr)
- }
- summarybulk = append(
- summarybulk,
- elapsedLine,
- countLine,
- )
- codes := sortMapStrInt(snapshot.Codes)
- for _, v := range codes {
- if v[0] != "2xx" {
- v[1] = colorize(v[1], FgMagentaColor)
- }
- summarybulk = append(summarybulk, []string{" " + v[0], v[1]})
- }
- summarybulk = append(summarybulk,
- []string{"RPS", fmt.Sprintf("%.3f", snapshot.RPS)},
- []string{"Reads", fmt.Sprintf("%.3fMB/s", snapshot.ReadThroughput)},
- []string{"Writes", fmt.Sprintf("%.3fMB/s", snapshot.WriteThroughput)},
- )
- alignBulk(summarybulk, AlignLeft, AlignRight)
- return summarybulk
- }
- var ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]")
- func displayWidth(str string) int {
- return runewidth.StringWidth(ansi.ReplaceAllLiteralString(str, ""))
- }
- const (
- AlignLeft = iota
- AlignRight
- AlignCenter
- )
- func padString(s, pad string, width int, align int) string {
- gap := width - displayWidth(s)
- if gap > 0 {
- if align == AlignLeft {
- return s + strings.Repeat(pad, gap)
- } else if align == AlignRight {
- return strings.Repeat(pad, gap) + s
- } else if align == AlignCenter {
- gapLeft := gap / 2
- gapRight := gap - gapLeft
- return strings.Repeat(pad, gapLeft) + s + strings.Repeat(pad, gapRight)
- }
- }
- return s
- }
|