| // Copyright 2014 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package main | |
| import ( | |
| "flag" | |
| "fmt" | |
| "os" | |
| "sort" | |
| "strconv" | |
| "text/tabwriter" | |
| "golang.org/x/tools/benchmark/parse" | |
| ) | |
| var ( | |
| changedOnly = flag.Bool("changed", false, "show only benchmarks that have changed") | |
| magSort = flag.Bool("mag", false, "sort benchmarks by magnitude of change") | |
| best = flag.Bool("best", false, "compare best times from old and new") | |
| ) | |
| const usageFooter = ` | |
| Each input file should be from: | |
| go test -run=NONE -bench=. > [old,new].txt | |
| Benchcmp compares old and new for each benchmark. | |
| If -test.benchmem=true is added to the "go test" command | |
| benchcmp will also compare memory allocations. | |
| ` | |
| func main() { | |
| fmt.Fprintf(os.Stderr, "benchcmp is deprecated in favor of benchstat: https://pkg.go.dev/golang.org/x/perf/cmd/benchstat\n") | |
| flag.Usage = func() { | |
| fmt.Fprintf(os.Stderr, "usage: %s old.txt new.txt\n\n", os.Args[0]) | |
| flag.PrintDefaults() | |
| fmt.Fprint(os.Stderr, usageFooter) | |
| os.Exit(2) | |
| } | |
| flag.Parse() | |
| if flag.NArg() != 2 { | |
| flag.Usage() | |
| } | |
| before := parseFile(flag.Arg(0)) | |
| after := parseFile(flag.Arg(1)) | |
| cmps, warnings := Correlate(before, after) | |
| for _, warn := range warnings { | |
| fmt.Fprintln(os.Stderr, warn) | |
| } | |
| if len(cmps) == 0 { | |
| fatal("benchcmp: no repeated benchmarks") | |
| } | |
| w := new(tabwriter.Writer) | |
| w.Init(os.Stdout, 0, 0, 5, ' ', 0) | |
| defer w.Flush() | |
| var header bool // Has the header has been displayed yet for a given block? | |
| if *magSort { | |
| sort.Sort(ByDeltaNsPerOp(cmps)) | |
| } else { | |
| sort.Sort(ByParseOrder(cmps)) | |
| } | |
| for _, cmp := range cmps { | |
| if !cmp.Measured(parse.NsPerOp) { | |
| continue | |
| } | |
| if delta := cmp.DeltaNsPerOp(); !*changedOnly || delta.Changed() { | |
| if !header { | |
| fmt.Fprint(w, "benchmark\told ns/op\tnew ns/op\tdelta\n") | |
| header = true | |
| } | |
| fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", cmp.Name(), formatNs(cmp.Before.NsPerOp), formatNs(cmp.After.NsPerOp), delta.Percent()) | |
| } | |
| } | |
| header = false | |
| if *magSort { | |
| sort.Sort(ByDeltaMBPerS(cmps)) | |
| } | |
| for _, cmp := range cmps { | |
| if !cmp.Measured(parse.MBPerS) { | |
| continue | |
| } | |
| if delta := cmp.DeltaMBPerS(); !*changedOnly || delta.Changed() { | |
| if !header { | |
| fmt.Fprint(w, "\nbenchmark\told MB/s\tnew MB/s\tspeedup\n") | |
| header = true | |
| } | |
| fmt.Fprintf(w, "%s\t%.2f\t%.2f\t%s\n", cmp.Name(), cmp.Before.MBPerS, cmp.After.MBPerS, delta.Multiple()) | |
| } | |
| } | |
| header = false | |
| if *magSort { | |
| sort.Sort(ByDeltaAllocsPerOp(cmps)) | |
| } | |
| for _, cmp := range cmps { | |
| if !cmp.Measured(parse.AllocsPerOp) { | |
| continue | |
| } | |
| if delta := cmp.DeltaAllocsPerOp(); !*changedOnly || delta.Changed() { | |
| if !header { | |
| fmt.Fprint(w, "\nbenchmark\told allocs\tnew allocs\tdelta\n") | |
| header = true | |
| } | |
| fmt.Fprintf(w, "%s\t%d\t%d\t%s\n", cmp.Name(), cmp.Before.AllocsPerOp, cmp.After.AllocsPerOp, delta.Percent()) | |
| } | |
| } | |
| header = false | |
| if *magSort { | |
| sort.Sort(ByDeltaAllocedBytesPerOp(cmps)) | |
| } | |
| for _, cmp := range cmps { | |
| if !cmp.Measured(parse.AllocedBytesPerOp) { | |
| continue | |
| } | |
| if delta := cmp.DeltaAllocedBytesPerOp(); !*changedOnly || delta.Changed() { | |
| if !header { | |
| fmt.Fprint(w, "\nbenchmark\told bytes\tnew bytes\tdelta\n") | |
| header = true | |
| } | |
| fmt.Fprintf(w, "%s\t%d\t%d\t%s\n", cmp.Name(), cmp.Before.AllocedBytesPerOp, cmp.After.AllocedBytesPerOp, cmp.DeltaAllocedBytesPerOp().Percent()) | |
| } | |
| } | |
| } | |
| func fatal(msg any) { | |
| fmt.Fprintln(os.Stderr, msg) | |
| os.Exit(1) | |
| } | |
| func parseFile(path string) parse.Set { | |
| f, err := os.Open(path) | |
| if err != nil { | |
| fatal(err) | |
| } | |
| defer f.Close() | |
| bb, err := parse.ParseSet(f) | |
| if err != nil { | |
| fatal(err) | |
| } | |
| if *best { | |
| selectBest(bb) | |
| } | |
| return bb | |
| } | |
| func selectBest(bs parse.Set) { | |
| for name, bb := range bs { | |
| if len(bb) < 2 { | |
| continue | |
| } | |
| ord := bb[0].Ord | |
| best := bb[0] | |
| for _, b := range bb { | |
| if b.NsPerOp < best.NsPerOp { | |
| b.Ord = ord | |
| best = b | |
| } | |
| } | |
| bs[name] = []*parse.Benchmark{best} | |
| } | |
| } | |
| // formatNs formats ns measurements to expose a useful amount of | |
| // precision. It mirrors the ns precision logic of testing.B. | |
| func formatNs(ns float64) string { | |
| prec := 0 | |
| switch { | |
| case ns < 10: | |
| prec = 2 | |
| case ns < 100: | |
| prec = 1 | |
| } | |
| return strconv.FormatFloat(ns, 'f', prec, 64) | |
| } |