print.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. package main
  2. import (
  3. "bytes"
  4. "encoding/json"
  5. "fmt"
  6. "github.com/mattn/go-isatty"
  7. "github.com/mattn/go-runewidth"
  8. "math"
  9. "os"
  10. "regexp"
  11. "sort"
  12. "strconv"
  13. "strings"
  14. "time"
  15. )
  16. var (
  17. maxBarLen = 40
  18. barStart = "|"
  19. barBody = "■"
  20. barEnd = "|"
  21. barSpinner = []string{"|", "/", "-", "\\"}
  22. clearLine = []byte("\r\033[K")
  23. isTerminal = isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd())
  24. )
  25. type Printer struct {
  26. maxNum int64
  27. maxDuration time.Duration
  28. curNum int64
  29. curDuration time.Duration
  30. pbInc int64
  31. pbNumStr string
  32. pbDurStr string
  33. noClean bool
  34. summary bool
  35. }
  36. func NewPrinter(maxNum int64, maxDuration time.Duration, noCleanBar, summary bool) *Printer {
  37. return &Printer{maxNum: maxNum, maxDuration: maxDuration, noClean: noCleanBar, summary: summary}
  38. }
  39. func (p *Printer) updateProgressValue(rs *SnapshotReport) {
  40. p.pbInc++
  41. if p.maxDuration > 0 {
  42. n := rs.Elapsed
  43. if n > p.maxDuration {
  44. n = p.maxDuration
  45. }
  46. p.curDuration = n
  47. barLen := int((p.curDuration*time.Duration(maxBarLen-2) + p.maxDuration/2) / p.maxDuration)
  48. p.pbDurStr = barStart + strings.Repeat(barBody, barLen) + strings.Repeat(" ", maxBarLen-2-barLen) + barEnd
  49. }
  50. if p.maxNum > 0 {
  51. p.curNum = rs.Count
  52. if p.maxNum > 0 {
  53. barLen := int((p.curNum*int64(maxBarLen-2) + p.maxNum/2) / p.maxNum)
  54. p.pbNumStr = barStart + strings.Repeat(barBody, barLen) + strings.Repeat(" ", maxBarLen-2-barLen) + barEnd
  55. } else {
  56. idx := p.pbInc % int64(len(barSpinner))
  57. p.pbNumStr = barSpinner[int(idx)]
  58. }
  59. }
  60. }
  61. func (p *Printer) PrintLoop(snapshot func() *SnapshotReport, interval time.Duration, useSeconds bool, json bool, doneChan <-chan struct{}) {
  62. var buf bytes.Buffer
  63. var backCursor string
  64. cl := clearLine
  65. if p.summary || interval == 0 || !isTerminal {
  66. cl = nil
  67. }
  68. echo := func(isFinal bool) {
  69. report := snapshot()
  70. p.updateProgressValue(report)
  71. os.Stdout.WriteString(backCursor)
  72. buf.Reset()
  73. if json {
  74. p.formatJSONReports(&buf, report, isFinal, useSeconds)
  75. } else {
  76. p.formatTableReports(&buf, report, isFinal, useSeconds)
  77. }
  78. result := buf.Bytes()
  79. n := 0
  80. for {
  81. i := bytes.IndexByte(result, '\n')
  82. if i == -1 {
  83. os.Stdout.Write(cl)
  84. os.Stdout.Write(result)
  85. break
  86. }
  87. n++
  88. os.Stdout.Write(cl)
  89. os.Stdout.Write(result[:i])
  90. os.Stdout.Write([]byte("\n"))
  91. result = result[i+1:]
  92. }
  93. os.Stdout.Sync()
  94. if isTerminal {
  95. backCursor = fmt.Sprintf("\033[%dA", n)
  96. }
  97. }
  98. if interval > 0 {
  99. ticker := time.NewTicker(interval)
  100. loop:
  101. for {
  102. select {
  103. case <-ticker.C:
  104. if !p.summary {
  105. echo(false)
  106. }
  107. case <-doneChan:
  108. ticker.Stop()
  109. break loop
  110. }
  111. }
  112. } else {
  113. <-doneChan
  114. }
  115. echo(true)
  116. }
  117. // nolint
  118. const (
  119. FgBlackColor int = iota + 30
  120. FgRedColor
  121. FgGreenColor
  122. FgYellowColor
  123. FgBlueColor
  124. FgMagentaColor
  125. FgCyanColor
  126. FgWhiteColor
  127. )
  128. func colorize(s string, seq int) string {
  129. if !isTerminal {
  130. return s
  131. }
  132. return fmt.Sprintf("\033[%dm%s\033[0m", seq, s)
  133. }
  134. func durationToString(d time.Duration, useSeconds bool) string {
  135. d = d.Truncate(time.Microsecond)
  136. if useSeconds {
  137. return formatFloat64(d.Seconds())
  138. }
  139. return d.String()
  140. }
  141. func alignBulk(bulk [][]string, aligns ...int) {
  142. maxLen := map[int]int{}
  143. for _, b := range bulk {
  144. for i, bb := range b {
  145. lbb := displayWidth(bb)
  146. if maxLen[i] < lbb {
  147. maxLen[i] = lbb
  148. }
  149. }
  150. }
  151. for _, b := range bulk {
  152. for i, ali := range aligns {
  153. if len(b) >= i+1 {
  154. if i == len(aligns)-1 && ali == AlignLeft {
  155. continue
  156. }
  157. b[i] = padString(b[i], " ", maxLen[i], ali)
  158. }
  159. }
  160. }
  161. }
  162. func writeBulkWith(writer *bytes.Buffer, bulk [][]string, lineStart, sep, lineEnd string) {
  163. for _, b := range bulk {
  164. writer.WriteString(lineStart)
  165. writer.WriteString(b[0])
  166. for _, bb := range b[1:] {
  167. writer.WriteString(sep)
  168. writer.WriteString(bb)
  169. }
  170. writer.WriteString(lineEnd)
  171. }
  172. }
  173. func writeBulk(writer *bytes.Buffer, bulk [][]string) {
  174. writeBulkWith(writer, bulk, " ", " ", "\n")
  175. }
  176. func formatFloat64(f float64) string {
  177. return strconv.FormatFloat(f, 'f', -1, 64)
  178. }
  179. func (p *Printer) formatJSONReports(writer *bytes.Buffer, snapshot *SnapshotReport, isFinal bool, useSeconds bool) {
  180. indent := 0
  181. writer.WriteString("{\n")
  182. indent++
  183. p.buildJSONSummary(writer, snapshot, indent)
  184. if len(snapshot.Errors) != 0 {
  185. writer.WriteString(",\n")
  186. p.buildJSONErrors(writer, snapshot, indent)
  187. }
  188. writer.WriteString(",\n")
  189. p.buildJSONStats(writer, snapshot, useSeconds, indent)
  190. writer.WriteString(",\n")
  191. p.buildJSONPercentile(writer, snapshot, useSeconds, indent)
  192. writer.WriteString(",\n")
  193. p.buildJSONHistogram(writer, snapshot, useSeconds, indent)
  194. writer.WriteString("\n}\n")
  195. }
  196. func (p *Printer) formatTableReports(writer *bytes.Buffer, snapshot *SnapshotReport, isFinal bool, useSeconds bool) {
  197. summaryBulk := p.buildSummary(snapshot, isFinal)
  198. errorsBulks := p.buildErrors(snapshot)
  199. statsBulk := p.buildStats(snapshot, useSeconds)
  200. percBulk := p.buildPercentile(snapshot, useSeconds)
  201. hisBulk := p.buildHistogram(snapshot, useSeconds, isFinal)
  202. writer.WriteString("Summary:\n")
  203. writeBulk(writer, summaryBulk)
  204. writer.WriteString("\n")
  205. if errorsBulks != nil {
  206. writer.WriteString("Error:\n")
  207. writeBulk(writer, errorsBulks)
  208. writer.WriteString("\n")
  209. }
  210. writeBulkWith(writer, statsBulk, "", " ", "\n")
  211. writer.WriteString("\n")
  212. writer.WriteString("Latency Percentile:\n")
  213. writeBulk(writer, percBulk)
  214. writer.WriteString("\n")
  215. writer.WriteString("Latency Histogram:\n")
  216. writeBulk(writer, hisBulk)
  217. }
  218. func (p *Printer) buildJSONHistogram(writer *bytes.Buffer, snapshot *SnapshotReport, useSeconds bool, indent int) {
  219. tab0 := strings.Repeat(" ", indent)
  220. writer.WriteString(tab0 + "\"Histograms\": [\n")
  221. tab1 := strings.Repeat(" ", indent+1)
  222. maxCount := 0
  223. hisSum := 0
  224. for _, bin := range snapshot.Histograms {
  225. if maxCount < bin.Count {
  226. maxCount = bin.Count
  227. }
  228. hisSum += bin.Count
  229. }
  230. for i, bin := range snapshot.Histograms {
  231. writer.WriteString(fmt.Sprintf(`%s[ "%s", %d ]`, tab1,
  232. durationToString(bin.Mean, useSeconds), bin.Count))
  233. if i != len(snapshot.Histograms)-1 {
  234. writer.WriteString(",")
  235. }
  236. writer.WriteString("\n")
  237. }
  238. writer.WriteString(tab0 + "]")
  239. }
  240. func (p *Printer) buildHistogram(snapshot *SnapshotReport, useSeconds bool, isFinal bool) [][]string {
  241. hisBulk := make([][]string, 0, 8)
  242. maxCount := 0
  243. hisSum := 0
  244. for _, bin := range snapshot.Histograms {
  245. if maxCount < bin.Count {
  246. maxCount = bin.Count
  247. }
  248. hisSum += bin.Count
  249. }
  250. for _, bin := range snapshot.Histograms {
  251. row := []string{durationToString(bin.Mean, useSeconds), strconv.Itoa(bin.Count)}
  252. if isFinal {
  253. row = append(row, fmt.Sprintf("%.2f%%", math.Floor(float64(bin.Count)*1e4/float64(hisSum)+0.5)/100.0))
  254. }
  255. if !isFinal || p.noClean {
  256. barLen := 0
  257. if maxCount > 0 {
  258. barLen = (bin.Count*maxBarLen + maxCount/2) / maxCount
  259. }
  260. row = append(row, strings.Repeat(barBody, barLen))
  261. }
  262. hisBulk = append(hisBulk, row)
  263. }
  264. if isFinal {
  265. alignBulk(hisBulk, AlignLeft, AlignRight, AlignRight)
  266. } else {
  267. alignBulk(hisBulk, AlignLeft, AlignRight, AlignLeft)
  268. }
  269. return hisBulk
  270. }
  271. func (p *Printer) buildJSONPercentile(writer *bytes.Buffer, snapshot *SnapshotReport, useSeconds bool, indent int) {
  272. tab0 := strings.Repeat(" ", indent)
  273. writer.WriteString(tab0 + "\"Percentiles\": {\n")
  274. tab1 := strings.Repeat(" ", indent+1)
  275. for i, percentile := range snapshot.Percentiles {
  276. perc := formatFloat64(percentile.Percentile * 100)
  277. writer.WriteString(fmt.Sprintf(`%s"%s": "%s"`, tab1, "P"+perc,
  278. durationToString(percentile.Latency, useSeconds)))
  279. if i != len(snapshot.Percentiles)-1 {
  280. writer.WriteString(",")
  281. }
  282. writer.WriteString("\n")
  283. }
  284. writer.WriteString(tab0 + "}")
  285. }
  286. func (p *Printer) buildPercentile(snapshot *SnapshotReport, useSeconds bool) [][]string {
  287. percBulk := make([][]string, 2)
  288. percAligns := make([]int, 0, len(snapshot.Percentiles))
  289. for _, percentile := range snapshot.Percentiles {
  290. perc := formatFloat64(percentile.Percentile * 100)
  291. percBulk[0] = append(percBulk[0], "P"+perc)
  292. percBulk[1] = append(percBulk[1], durationToString(percentile.Latency, useSeconds))
  293. percAligns = append(percAligns, AlignCenter)
  294. }
  295. percAligns[0] = AlignLeft
  296. alignBulk(percBulk, percAligns...)
  297. return percBulk
  298. }
  299. func (p *Printer) buildJSONStats(writer *bytes.Buffer, snapshot *SnapshotReport, useSeconds bool, indent int) {
  300. tab0 := strings.Repeat(" ", indent)
  301. writer.WriteString(tab0 + "\"Statistics\": {\n")
  302. tab1 := strings.Repeat(" ", indent+1)
  303. writer.WriteString(fmt.Sprintf(`%s"Latency": { "Min": "%s", "Mean": "%s", "StdDev": "%s", "Max": "%s" }`,
  304. tab1,
  305. durationToString(snapshot.Stats.Min, useSeconds),
  306. durationToString(snapshot.Stats.Mean, useSeconds),
  307. durationToString(snapshot.Stats.StdDev, useSeconds),
  308. durationToString(snapshot.Stats.Max, useSeconds),
  309. ))
  310. if snapshot.RpsStats != nil {
  311. writer.WriteString(",\n")
  312. writer.WriteString(fmt.Sprintf(`%s"RPS": { "Min": %s, "Mean": %s, "StdDev": %s, "Max": %s }`,
  313. tab1,
  314. formatFloat64(math.Trunc(snapshot.RpsStats.Min*100)/100.0),
  315. formatFloat64(math.Trunc(snapshot.RpsStats.Mean*100)/100.0),
  316. formatFloat64(math.Trunc(snapshot.RpsStats.StdDev*100)/100.0),
  317. formatFloat64(math.Trunc(snapshot.RpsStats.Max*100)/100.0),
  318. ))
  319. }
  320. writer.WriteString("\n" + tab0 + "}")
  321. }
  322. func (p *Printer) buildStats(snapshot *SnapshotReport, useSeconds bool) [][]string {
  323. var statsBulk [][]string
  324. statsBulk = append(statsBulk,
  325. []string{"Statistics", "Min", "Mean", "StdDev", "Max"},
  326. []string{
  327. " Latency",
  328. durationToString(snapshot.Stats.Min, useSeconds),
  329. durationToString(snapshot.Stats.Mean, useSeconds),
  330. durationToString(snapshot.Stats.StdDev, useSeconds),
  331. durationToString(snapshot.Stats.Max, useSeconds),
  332. },
  333. )
  334. if snapshot.RpsStats != nil {
  335. statsBulk = append(statsBulk,
  336. []string{
  337. " RPS",
  338. formatFloat64(math.Trunc(snapshot.RpsStats.Min*100) / 100.0),
  339. formatFloat64(math.Trunc(snapshot.RpsStats.Mean*100) / 100.0),
  340. formatFloat64(math.Trunc(snapshot.RpsStats.StdDev*100) / 100.0),
  341. formatFloat64(math.Trunc(snapshot.RpsStats.Max*100) / 100.0),
  342. },
  343. )
  344. }
  345. alignBulk(statsBulk, AlignLeft, AlignCenter, AlignCenter, AlignCenter, AlignCenter)
  346. return statsBulk
  347. }
  348. func (p *Printer) buildJSONErrors(writer *bytes.Buffer, snapshot *SnapshotReport, indent int) {
  349. tab0 := strings.Repeat(" ", indent)
  350. writer.WriteString(tab0 + "\"Error\": {\n")
  351. tab1 := strings.Repeat(" ", indent+1)
  352. errors := sortMapStrInt(snapshot.Errors)
  353. for i, v := range errors {
  354. v[1] = colorize(v[1], FgRedColor)
  355. vb, _ := json.Marshal(v[0])
  356. writer.WriteString(fmt.Sprintf(`%s%s: %s`, tab1, vb, v[1]))
  357. if i != len(errors)-1 {
  358. writer.WriteString(",")
  359. }
  360. writer.WriteString("\n")
  361. }
  362. writer.WriteString(tab0 + "}")
  363. }
  364. func (p *Printer) buildErrors(snapshot *SnapshotReport) [][]string {
  365. var errorsBulks [][]string
  366. for k, v := range snapshot.Errors {
  367. vs := colorize(strconv.FormatInt(v, 10), FgRedColor)
  368. errorsBulks = append(errorsBulks, []string{vs, "\"" + k + "\""})
  369. }
  370. if errorsBulks != nil {
  371. sort.Slice(errorsBulks, func(i, j int) bool { return errorsBulks[i][1] < errorsBulks[j][1] })
  372. }
  373. alignBulk(errorsBulks, AlignLeft, AlignLeft)
  374. return errorsBulks
  375. }
  376. func sortMapStrInt(m map[string]int64) (ret [][]string) {
  377. for k, v := range m {
  378. ret = append(ret, []string{k, strconv.FormatInt(v, 10)})
  379. }
  380. sort.Slice(ret, func(i, j int) bool { return ret[i][0] < ret[j][0] })
  381. return
  382. }
  383. func (p *Printer) buildJSONSummary(writer *bytes.Buffer, snapshot *SnapshotReport, indent int) {
  384. tab0 := strings.Repeat(" ", indent)
  385. writer.WriteString(tab0 + "\"Summary\": {\n")
  386. {
  387. tab1 := strings.Repeat(" ", indent+1)
  388. writer.WriteString(fmt.Sprintf("%s\"Elapsed\": \"%s\",\n", tab1, snapshot.Elapsed.Truncate(100*time.Millisecond).String()))
  389. writer.WriteString(fmt.Sprintf("%s\"Count\": %d,\n", tab1, snapshot.Count))
  390. writer.WriteString(fmt.Sprintf("%s\"Counts\": {\n", tab1))
  391. i := 0
  392. tab2 := strings.Repeat(" ", indent+2)
  393. codes := sortMapStrInt(snapshot.Codes)
  394. for _, v := range codes {
  395. i++
  396. if v[0] != "2xx" {
  397. v[1] = colorize(v[1], FgMagentaColor)
  398. }
  399. writer.WriteString(fmt.Sprintf(`%s"%s": %s`, tab2, v[0], v[1]))
  400. if i != len(snapshot.Codes) {
  401. writer.WriteString(",")
  402. }
  403. writer.WriteString("\n")
  404. }
  405. writer.WriteString(tab1 + "},\n")
  406. writer.WriteString(fmt.Sprintf("%s\"RPS\": %.3f,\n", tab1, snapshot.RPS))
  407. writer.WriteString(fmt.Sprintf("%s\"Reads\": \"%.3fMB/s\",\n", tab1, snapshot.ReadThroughput))
  408. writer.WriteString(fmt.Sprintf("%s\"Writes\": \"%.3fMB/s\"\n", tab1, snapshot.WriteThroughput))
  409. }
  410. writer.WriteString(tab0 + "}")
  411. }
  412. func (p *Printer) buildSummary(snapshot *SnapshotReport, isFinal bool) [][]string {
  413. summarybulk := make([][]string, 0, 8)
  414. elapsedLine := []string{"Elapsed", snapshot.Elapsed.Truncate(100 * time.Millisecond).String()}
  415. if p.maxDuration > 0 && !isFinal {
  416. elapsedLine = append(elapsedLine, p.pbDurStr)
  417. }
  418. countLine := []string{"Count", strconv.FormatInt(snapshot.Count, 10)}
  419. if p.maxNum > 0 && !isFinal {
  420. countLine = append(countLine, p.pbNumStr)
  421. }
  422. summarybulk = append(
  423. summarybulk,
  424. elapsedLine,
  425. countLine,
  426. )
  427. codes := sortMapStrInt(snapshot.Codes)
  428. for _, v := range codes {
  429. if v[0] != "2xx" {
  430. v[1] = colorize(v[1], FgMagentaColor)
  431. }
  432. summarybulk = append(summarybulk, []string{" " + v[0], v[1]})
  433. }
  434. summarybulk = append(summarybulk,
  435. []string{"RPS", fmt.Sprintf("%.3f", snapshot.RPS)},
  436. []string{"Reads", fmt.Sprintf("%.3fMB/s", snapshot.ReadThroughput)},
  437. []string{"Writes", fmt.Sprintf("%.3fMB/s", snapshot.WriteThroughput)},
  438. )
  439. alignBulk(summarybulk, AlignLeft, AlignRight)
  440. return summarybulk
  441. }
  442. var ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]")
  443. func displayWidth(str string) int {
  444. return runewidth.StringWidth(ansi.ReplaceAllLiteralString(str, ""))
  445. }
  446. const (
  447. AlignLeft = iota
  448. AlignRight
  449. AlignCenter
  450. )
  451. func padString(s, pad string, width int, align int) string {
  452. gap := width - displayWidth(s)
  453. if gap > 0 {
  454. if align == AlignLeft {
  455. return s + strings.Repeat(pad, gap)
  456. } else if align == AlignRight {
  457. return strings.Repeat(pad, gap) + s
  458. } else if align == AlignCenter {
  459. gapLeft := gap / 2
  460. gapRight := gap - gapLeft
  461. return strings.Repeat(pad, gapLeft) + s + strings.Repeat(pad, gapRight)
  462. }
  463. }
  464. return s
  465. }