prettybench.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. package main
  2. import (
  3. "bufio"
  4. "bytes"
  5. "errors"
  6. "flag"
  7. "fmt"
  8. "os"
  9. "regexp"
  10. "strconv"
  11. "strings"
  12. "time"
  13. bench "golang.org/x/tools/benchmark/parse"
  14. )
  15. var noPassthrough = flag.Bool("no-passthrough", false, "Don't print non-benchmark lines")
  16. type BenchOutputGroup struct {
  17. Lines []*bench.Benchmark
  18. // Columns which are in use
  19. Measured int
  20. }
  21. type Table struct {
  22. MaxLengths []int
  23. Cells [][]string
  24. }
  25. func (g *BenchOutputGroup) String() string {
  26. if len(g.Lines) == 0 {
  27. return ""
  28. }
  29. columnNames := []string{"benchmark", "iter", "time/iter"}
  30. if (g.Measured & bench.MBPerS) > 0 {
  31. columnNames = append(columnNames, "throughput")
  32. }
  33. if (g.Measured & bench.AllocedBytesPerOp) > 0 {
  34. columnNames = append(columnNames, "bytes alloc")
  35. }
  36. if (g.Measured & bench.AllocsPerOp) > 0 {
  37. columnNames = append(columnNames, "allocs")
  38. }
  39. table := &Table{Cells: [][]string{columnNames}}
  40. var underlines []string
  41. for _, name := range columnNames {
  42. underlines = append(underlines, strings.Repeat("-", len(name)))
  43. }
  44. table.Cells = append(table.Cells, underlines)
  45. timeFormatFunc := g.TimeFormatFunc()
  46. for _, line := range g.Lines {
  47. row := []string{line.Name, FormatIterations(line.N), timeFormatFunc(line.NsPerOp)}
  48. if (g.Measured & bench.MBPerS) > 0 {
  49. row = append(row, FormatMegaBytesPerSecond(line))
  50. }
  51. if (g.Measured & bench.AllocedBytesPerOp) > 0 {
  52. row = append(row, FormatBytesAllocPerOp(line))
  53. }
  54. if (g.Measured & bench.AllocsPerOp) > 0 {
  55. row = append(row, FormatAllocsPerOp(line))
  56. }
  57. table.Cells = append(table.Cells, row)
  58. }
  59. for i := range columnNames {
  60. maxLength := 0
  61. for _, row := range table.Cells {
  62. if len(row[i]) > maxLength {
  63. maxLength = len(row[i])
  64. }
  65. }
  66. table.MaxLengths = append(table.MaxLengths, maxLength)
  67. }
  68. var buf bytes.Buffer
  69. for _, row := range table.Cells {
  70. for i, cell := range row {
  71. var format string
  72. switch i {
  73. case 0:
  74. format = "%%-%ds "
  75. case len(row) - 1:
  76. format = "%%%ds"
  77. default:
  78. format = "%%%ds "
  79. }
  80. fmt.Fprintf(&buf, fmt.Sprintf(format, table.MaxLengths[i]), cell)
  81. }
  82. fmt.Fprint(&buf, "\n")
  83. }
  84. return buf.String()
  85. }
  86. func FormatIterations(iter int) string {
  87. return strconv.FormatInt(int64(iter), 10)
  88. }
  89. func (g *BenchOutputGroup) TimeFormatFunc() func(float64) string {
  90. // Find the smallest time
  91. smallest := g.Lines[0].NsPerOp
  92. for _, line := range g.Lines[1:] {
  93. if line.NsPerOp < smallest {
  94. smallest = line.NsPerOp
  95. }
  96. }
  97. switch {
  98. case smallest < float64(10000*time.Nanosecond):
  99. return func(ns float64) string {
  100. return fmt.Sprintf("%.2f ns/op", ns)
  101. }
  102. case smallest < float64(time.Millisecond):
  103. return func(ns float64) string {
  104. return fmt.Sprintf("%.2f μs/op", ns/1000)
  105. }
  106. case smallest < float64(10*time.Second):
  107. return func(ns float64) string {
  108. return fmt.Sprintf("%.2f ms/op", (ns / 1e6))
  109. }
  110. default:
  111. return func(ns float64) string {
  112. return fmt.Sprintf("%.2f s/op", ns/1e9)
  113. }
  114. }
  115. }
  116. func FormatMegaBytesPerSecond(l *bench.Benchmark) string {
  117. if (l.Measured & bench.MBPerS) == 0 {
  118. return ""
  119. }
  120. return fmt.Sprintf("%.2f MB/s", l.MBPerS)
  121. }
  122. func FormatBytesAllocPerOp(l *bench.Benchmark) string {
  123. if (l.Measured & bench.AllocedBytesPerOp) == 0 {
  124. return ""
  125. }
  126. return fmt.Sprintf("%d B/op", l.AllocedBytesPerOp)
  127. }
  128. func FormatAllocsPerOp(l *bench.Benchmark) string {
  129. if (l.Measured & bench.AllocsPerOp) == 0 {
  130. return ""
  131. }
  132. return fmt.Sprintf("%d allocs/op", l.AllocsPerOp)
  133. }
  134. func (g *BenchOutputGroup) AddLine(line *bench.Benchmark) {
  135. g.Lines = append(g.Lines, line)
  136. g.Measured |= line.Measured
  137. }
  138. var (
  139. benchLineMatcher = regexp.MustCompile(`^Benchmark.*\t.*\d+`)
  140. okLineMatcher = regexp.MustCompile(`^ok\s`)
  141. notBenchLineErr = errors.New("Not a bench line")
  142. )
  143. func ParseLine(line string) (*bench.Benchmark, error) {
  144. if !benchLineMatcher.MatchString(line) {
  145. return nil, notBenchLineErr
  146. }
  147. fields := strings.Split(line, "\t")
  148. if len(fields) < 3 {
  149. return nil, notBenchLineErr
  150. }
  151. return bench.ParseLine(line)
  152. }
  153. func main() {
  154. flag.Parse()
  155. currentBenchmark := &BenchOutputGroup{}
  156. scanner := bufio.NewScanner(os.Stdin)
  157. for scanner.Scan() {
  158. text := scanner.Text()
  159. line, err := ParseLine(text)
  160. switch err {
  161. case notBenchLineErr:
  162. if okLineMatcher.MatchString(text) {
  163. fmt.Print(currentBenchmark)
  164. currentBenchmark = &BenchOutputGroup{}
  165. }
  166. if !*noPassthrough {
  167. fmt.Println(text)
  168. }
  169. case nil:
  170. currentBenchmark.AddLine(line)
  171. default:
  172. fmt.Fprintln(os.Stderr, "prettybench unrecognized line:")
  173. fmt.Println(text)
  174. }
  175. }
  176. if err := scanner.Err(); err != nil {
  177. fmt.Println(err)
  178. os.Exit(1)
  179. }
  180. }