| 1 | // Copyright 2014 The Go Authors. All rights reserved. |
|---|---|
| 2 | // Use of this source code is governed by a BSD-style |
| 3 | // license that can be found in the LICENSE file. |
| 4 | |
| 5 | package main |
| 6 | |
| 7 | import ( |
| 8 | "flag" |
| 9 | "fmt" |
| 10 | "os" |
| 11 | "sort" |
| 12 | "strconv" |
| 13 | "text/tabwriter" |
| 14 | |
| 15 | "golang.org/x/tools/benchmark/parse" |
| 16 | ) |
| 17 | |
| 18 | var ( |
| 19 | changedOnly = flag.Bool("changed", false, "show only benchmarks that have changed") |
| 20 | magSort = flag.Bool("mag", false, "sort benchmarks by magnitude of change") |
| 21 | best = flag.Bool("best", false, "compare best times from old and new") |
| 22 | ) |
| 23 | |
| 24 | const usageFooter = ` |
| 25 | Each input file should be from: |
| 26 | go test -run=NONE -bench=. > [old,new].txt |
| 27 | |
| 28 | Benchcmp compares old and new for each benchmark. |
| 29 | |
| 30 | If -test.benchmem=true is added to the "go test" command |
| 31 | benchcmp will also compare memory allocations. |
| 32 | ` |
| 33 | |
| 34 | func main() { |
| 35 | fmt.Fprintf(os.Stderr, "benchcmp is deprecated in favor of benchstat: https://pkg.go.dev/golang.org/x/perf/cmd/benchstat\n") |
| 36 | flag.Usage = func() { |
| 37 | fmt.Fprintf(os.Stderr, "usage: %s old.txt new.txt\n\n", os.Args[0]) |
| 38 | flag.PrintDefaults() |
| 39 | fmt.Fprint(os.Stderr, usageFooter) |
| 40 | os.Exit(2) |
| 41 | } |
| 42 | flag.Parse() |
| 43 | if flag.NArg() != 2 { |
| 44 | flag.Usage() |
| 45 | } |
| 46 | |
| 47 | before := parseFile(flag.Arg(0)) |
| 48 | after := parseFile(flag.Arg(1)) |
| 49 | |
| 50 | cmps, warnings := Correlate(before, after) |
| 51 | |
| 52 | for _, warn := range warnings { |
| 53 | fmt.Fprintln(os.Stderr, warn) |
| 54 | } |
| 55 | |
| 56 | if len(cmps) == 0 { |
| 57 | fatal("benchcmp: no repeated benchmarks") |
| 58 | } |
| 59 | |
| 60 | w := new(tabwriter.Writer) |
| 61 | w.Init(os.Stdout, 0, 0, 5, ' ', 0) |
| 62 | defer w.Flush() |
| 63 | |
| 64 | var header bool // Has the header has been displayed yet for a given block? |
| 65 | |
| 66 | if *magSort { |
| 67 | sort.Sort(ByDeltaNsPerOp(cmps)) |
| 68 | } else { |
| 69 | sort.Sort(ByParseOrder(cmps)) |
| 70 | } |
| 71 | for _, cmp := range cmps { |
| 72 | if !cmp.Measured(parse.NsPerOp) { |
| 73 | continue |
| 74 | } |
| 75 | if delta := cmp.DeltaNsPerOp(); !*changedOnly || delta.Changed() { |
| 76 | if !header { |
| 77 | fmt.Fprint(w, "benchmark\told ns/op\tnew ns/op\tdelta\n") |
| 78 | header = true |
| 79 | } |
| 80 | fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", cmp.Name(), formatNs(cmp.Before.NsPerOp), formatNs(cmp.After.NsPerOp), delta.Percent()) |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | header = false |
| 85 | if *magSort { |
| 86 | sort.Sort(ByDeltaMBPerS(cmps)) |
| 87 | } |
| 88 | for _, cmp := range cmps { |
| 89 | if !cmp.Measured(parse.MBPerS) { |
| 90 | continue |
| 91 | } |
| 92 | if delta := cmp.DeltaMBPerS(); !*changedOnly || delta.Changed() { |
| 93 | if !header { |
| 94 | fmt.Fprint(w, "\nbenchmark\told MB/s\tnew MB/s\tspeedup\n") |
| 95 | header = true |
| 96 | } |
| 97 | fmt.Fprintf(w, "%s\t%.2f\t%.2f\t%s\n", cmp.Name(), cmp.Before.MBPerS, cmp.After.MBPerS, delta.Multiple()) |
| 98 | } |
| 99 | } |
| 100 | |
| 101 | header = false |
| 102 | if *magSort { |
| 103 | sort.Sort(ByDeltaAllocsPerOp(cmps)) |
| 104 | } |
| 105 | for _, cmp := range cmps { |
| 106 | if !cmp.Measured(parse.AllocsPerOp) { |
| 107 | continue |
| 108 | } |
| 109 | if delta := cmp.DeltaAllocsPerOp(); !*changedOnly || delta.Changed() { |
| 110 | if !header { |
| 111 | fmt.Fprint(w, "\nbenchmark\told allocs\tnew allocs\tdelta\n") |
| 112 | header = true |
| 113 | } |
| 114 | fmt.Fprintf(w, "%s\t%d\t%d\t%s\n", cmp.Name(), cmp.Before.AllocsPerOp, cmp.After.AllocsPerOp, delta.Percent()) |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | header = false |
| 119 | if *magSort { |
| 120 | sort.Sort(ByDeltaAllocedBytesPerOp(cmps)) |
| 121 | } |
| 122 | for _, cmp := range cmps { |
| 123 | if !cmp.Measured(parse.AllocedBytesPerOp) { |
| 124 | continue |
| 125 | } |
| 126 | if delta := cmp.DeltaAllocedBytesPerOp(); !*changedOnly || delta.Changed() { |
| 127 | if !header { |
| 128 | fmt.Fprint(w, "\nbenchmark\told bytes\tnew bytes\tdelta\n") |
| 129 | header = true |
| 130 | } |
| 131 | fmt.Fprintf(w, "%s\t%d\t%d\t%s\n", cmp.Name(), cmp.Before.AllocedBytesPerOp, cmp.After.AllocedBytesPerOp, cmp.DeltaAllocedBytesPerOp().Percent()) |
| 132 | } |
| 133 | } |
| 134 | } |
| 135 | |
| 136 | func fatal(msg interface{}) { |
| 137 | fmt.Fprintln(os.Stderr, msg) |
| 138 | os.Exit(1) |
| 139 | } |
| 140 | |
| 141 | func parseFile(path string) parse.Set { |
| 142 | f, err := os.Open(path) |
| 143 | if err != nil { |
| 144 | fatal(err) |
| 145 | } |
| 146 | defer f.Close() |
| 147 | bb, err := parse.ParseSet(f) |
| 148 | if err != nil { |
| 149 | fatal(err) |
| 150 | } |
| 151 | if *best { |
| 152 | selectBest(bb) |
| 153 | } |
| 154 | return bb |
| 155 | } |
| 156 | |
| 157 | func selectBest(bs parse.Set) { |
| 158 | for name, bb := range bs { |
| 159 | if len(bb) < 2 { |
| 160 | continue |
| 161 | } |
| 162 | ord := bb[0].Ord |
| 163 | best := bb[0] |
| 164 | for _, b := range bb { |
| 165 | if b.NsPerOp < best.NsPerOp { |
| 166 | b.Ord = ord |
| 167 | best = b |
| 168 | } |
| 169 | } |
| 170 | bs[name] = []*parse.Benchmark{best} |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | // formatNs formats ns measurements to expose a useful amount of |
| 175 | // precision. It mirrors the ns precision logic of testing.B. |
| 176 | func formatNs(ns float64) string { |
| 177 | prec := 0 |
| 178 | switch { |
| 179 | case ns < 10: |
| 180 | prec = 2 |
| 181 | case ns < 100: |
| 182 | prec = 1 |
| 183 | } |
| 184 | return strconv.FormatFloat(ns, 'f', prec, 64) |
| 185 | } |
| 186 |
Members