github.com/willabides/benchdiff@v0.9.1/cmd/benchdiff/benchdiff.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "fmt" 6 "log" 7 "os" 8 "path/filepath" 9 "strconv" 10 "strings" 11 "text/template" 12 "time" 13 14 "github.com/alecthomas/kong" 15 "github.com/willabides/benchdiff/cmd/benchdiff/internal" 16 "github.com/willabides/benchdiff/pkg/benchstatter" 17 "golang.org/x/perf/benchstat" 18 ) 19 20 const defaultBenchArgsTmpl = `test {{ .Packages }} -run '^$' 21 {{- if .Bench }} -bench {{ .Bench }}{{end}} 22 {{- if .Count }} -count {{ .Count }}{{end}} 23 {{- if .Benchtime }} -benchtime {{ .Benchtime }}{{end}} 24 {{- if .CPU }} -cpu {{ .CPU }}{{ end }} 25 {{- if .Tags }} -tags "{{ .Tags }}"{{ end }} 26 {{- if .Benchmem }} -benchmem{{ end }}` 27 28 var benchstatVars = kong.Vars{ 29 "AlphaDefault": "0.05", 30 "AlphaHelp": `consider change significant if p < α`, 31 "DeltaTestHelp": `significance test to apply to delta: utest, ttest, or none`, 32 "DeltaTestDefault": `utest`, 33 "DeltaTestEnum": `utest,ttest,none`, 34 "GeomeanHelp": `print the geometric mean of each file`, 35 "NorangeHelp": `suppress range columns (CSV and markdown only)`, 36 "ReverseSortHelp": `reverse sort order`, 37 "SortHelp": `sort by order: delta, name, none`, 38 "SortEnum": `delta,name,none`, 39 "SplitHelp": `split benchmarks by labels`, 40 "SplitDefault": `pkg,goos,goarch`, 41 "BenchstatOutputHelp": `format for benchstat output (csv,html,markdown or text)`, 42 "BenchstatOutputEnum": `csv, html, markdown, text`, 43 } 44 45 type benchstatOpts struct { 46 Alpha float64 `kong:"default=${AlphaDefault},help=${AlphaHelp},group=benchstat"` 47 BenchstatOutput string `kong:"default=text,enum=${BenchstatOutputEnum},help=${BenchstatOutputHelp},group=benchstat"` 48 DeltaTest string `kong:"help=${DeltaTestHelp},default=${DeltaTestDefault},enum='utest,ttest,none',group=benchstat"` 49 Geomean bool `kong:"help=${GeomeanHelp},group=benchstat"` 50 Norange bool `kong:"help=${NorangeHelp},group=benchstat"` 51 ReverseSort bool `kong:"help=${ReverseSortHelp},group=benchstat"` 52 Sort string `kong:"help=${SortHelp},enum=${SortEnum},default=none,group=benchstat"` 53 Split string `kong:"help=${SplitHelp},default=${SplitDefault},group=benchstat"` 54 } 55 56 var version string 57 58 var benchVars = kong.Vars{ 59 "version": version, 60 "BenchCmdDefault": `go`, 61 "CountHelp": `Run each benchmark n times. If --cpu is set, run n times for each GOMAXPROCS value.'`, 62 "BenchHelp": `Run only those benchmarks matching a regular expression. To run all benchmarks, use '--bench .'.`, 63 "BenchmarkArgsHelp": `Override the default args to the go command. This may be a template. See https://github.com/willabides/benchdiff for details."`, 64 "BenchtimeHelp": `Run enough iterations of each benchmark to take t, specified as a time.Duration (for example, --benchtime 1h30s). The default is 1 second (1s). The special syntax Nx means to run the benchmark N times (for example, -benchtime 100x).`, 65 "PackagesHelp": `Run benchmarks in these packages.`, 66 "BenchCmdHelp": `The command to use for benchmarks.`, 67 "CacheDirHelp": `Override the default directory where benchmark output is kept.`, 68 "BaseRefHelp": `The git ref to be used as a baseline.`, 69 "CooldownHelp": `How long to pause for cooldown between head and base runs.`, 70 "ForceBaseHelp": `Rerun benchmarks on the base reference even if the output already exists.`, 71 "OnDegradeHelp": `Exit code when there is a statistically significant degradation in the results.`, 72 "JSONHelp": `Format output as JSON.`, 73 "GitCmdHelp": `The executable to use for git commands.`, 74 "ToleranceHelp": `The minimum percent change before a result is considered degraded.`, 75 "VersionHelp": `Output the benchdiff version and exit.`, 76 "ShowCacheDirHelp": `Output the cache dir and exit.`, 77 "ClearCacheHelp": `Remove benchdiff files from the cache dir.`, 78 "ShowBenchCmdlineHelp": `Instead of running benchmarks, output the command that would be used and exit.`, 79 "CPUHelp": `Specify a list of GOMAXPROCS values for which the benchmarks should be executed. The default is the current value of GOMAXPROCS.`, 80 "BenchmemHelp": `Memory allocation statistics for benchmarks.`, 81 "WarmupCountHelp": `Run benchmarks with -count=n as a warmup`, 82 "WarmupTimeHelp": `When warmups are run, set -benchtime=n`, 83 "TagsHelp": `Set the -tags flag on the go test command`, 84 } 85 86 var groupHelp = kong.Vars{ 87 "benchstatGroupHelp": "benchstat options:", 88 "gotestGroupHelp": "benchmark command line:", 89 "cacheGroupHelp": "benchmark result cache:", 90 } 91 92 var cli struct { 93 Version kong.VersionFlag `kong:"help=${VersionHelp}"` 94 Debug bool `kong:"help='write verbose output to stderr'"` 95 96 BaseRef string `kong:"default=HEAD,help=${BaseRefHelp},group='x'"` 97 Cooldown time.Duration `kong:"default='100ms',help=${CooldownHelp},group='x'"` 98 ForceBase bool `kong:"help=${ForceBaseHelp},group='x'"` 99 GitCmd string `kong:"default=git,help=${GitCmdHelp},group='x'"` 100 JSON bool `kong:"help=${JSONHelp},group='x'"` 101 OnDegrade int `kong:"name=on-degrade,default=0,help=${OnDegradeHelp},group='x'"` 102 Tolerance float64 `kong:"default='10.0',help=${ToleranceHelp},group='x'"` 103 104 Bench string `kong:"default='.',help=${BenchHelp},group='gotest'"` 105 BenchmarkArgs string `kong:"placeholder='args',help=${BenchmarkArgsHelp},group='gotest'"` 106 BenchmarkCmd string `kong:"default=${BenchCmdDefault},help=${BenchCmdHelp},group='gotest'"` 107 Benchmem bool `kong:"help=${BenchmemHelp},group='gotest'"` 108 Benchtime string `kong:"help=${BenchtimeHelp},group='gotest'"` 109 Count int `kong:"default=10,help=${CountHelp},group='gotest'"` 110 CPU CPUFlag `kong:"help=${CPUHelp},group='gotest',placeholder='GOMAXPROCS,...'"` 111 Packages string `kong:"default='./...',help=${PackagesHelp},group='gotest'"` 112 ShowBenchCmdline ShowBenchCmdlineFlag `kong:"help=${ShowBenchCmdlineHelp},group='gotest'"` 113 Tags string `kong:"help=${TagsHelp},group='gotest'"` 114 WarmupCount int `kong:"help=${WarmupCountHelp},group='gotest'"` 115 WarmupTime string `kong:"help=${WarmupTimeHelp},group='gotest'"` 116 117 BenchstatOpts benchstatOpts `kong:"embed"` 118 119 CacheDir string `kong:"type=dir,help=${CacheDirHelp},group='cache'"` 120 ClearCache ClearCacheFlag `kong:"help=${ClearCacheHelp},group='cache'"` 121 ShowCacheDir ShowCacheDirFlag `kong:"help=${ShowCacheDirHelp},group='cache'"` 122 123 ShowDefaultTemplate showDefaultTemplate `kong:"hidden"` 124 } 125 126 // ShowCacheDirFlag flag for showing the cache directory 127 type ShowCacheDirFlag bool 128 129 // AfterApply outputs cli.CacheDir 130 func (v ShowCacheDirFlag) AfterApply(app *kong.Kong) error { 131 cacheDir, err := getCacheDir() 132 if err != nil { 133 return err 134 } 135 fmt.Fprintln(app.Stdout, cacheDir) 136 app.Exit(0) 137 return nil 138 } 139 140 type showDefaultTemplate bool 141 142 func (v showDefaultTemplate) BeforeApply(app *kong.Kong) error { 143 fmt.Println(defaultBenchArgsTmpl) 144 app.Exit(0) 145 return nil 146 } 147 148 // ClearCacheFlag flag for clearing cache 149 type ClearCacheFlag bool 150 151 // AfterApply clears cache 152 func (v ClearCacheFlag) AfterApply(app *kong.Kong) error { 153 cacheDir, err := getCacheDir() 154 if err != nil { 155 return err 156 } 157 files, err := filepath.Glob(filepath.Join(cacheDir, "benchdiff-*.out")) 158 if err != nil { 159 return fmt.Errorf("error finding files in %s: %v", cacheDir, err) 160 } 161 for _, file := range files { 162 err = os.Remove(file) 163 if err != nil { 164 return fmt.Errorf("error removing %s: %v", file, err) 165 } 166 } 167 app.Exit(0) 168 return nil 169 } 170 171 func getCacheDir() (string, error) { 172 if cli.CacheDir != "" { 173 return cli.CacheDir, nil 174 } 175 return defaultCacheDir() 176 } 177 178 func defaultCacheDir() (string, error) { 179 userCacheDir, err := os.UserCacheDir() 180 if err != nil { 181 return "", fmt.Errorf("error finding user cache dir: %v", err) 182 } 183 return filepath.Join(userCacheDir, "benchdiff"), nil 184 } 185 186 // ShowBenchCmdlineFlag flag for --show-bench-cmdling 187 type ShowBenchCmdlineFlag bool 188 189 // AfterApply shows benchmark command line and exits 190 func (v ShowBenchCmdlineFlag) AfterApply(app *kong.Kong) error { 191 benchArgs, err := getBenchArgs() 192 if err != nil { 193 return err 194 } 195 fmt.Fprintln(app.Stdout, cli.BenchmarkCmd, benchArgs) 196 app.Exit(0) 197 return nil 198 } 199 200 // CPUFlag is the flag for --cpu 201 type CPUFlag []int 202 203 func (c CPUFlag) String() string { 204 s := make([]string, len(c)) 205 for i, cc := range c { 206 s[i] = strconv.Itoa(cc) 207 } 208 return strings.Join(s, ",") 209 } 210 211 func getBenchArgs() (string, error) { 212 argsTmpl := cli.BenchmarkArgs 213 if argsTmpl == "" { 214 argsTmpl = defaultBenchArgsTmpl 215 } 216 tmpl, err := template.New("").Parse(argsTmpl) 217 if err != nil { 218 return "", err 219 } 220 var benchArgs bytes.Buffer 221 err = tmpl.Execute(&benchArgs, cli) 222 if err != nil { 223 return "", err 224 } 225 args := benchArgs.String() 226 return args, nil 227 } 228 229 const description = ` 230 benchdiff runs go benchmarks on your current git worktree and a base ref then 231 uses benchstat to show the delta. 232 233 More documentation at https://github.com/willabides/benchdiff. 234 ` 235 236 func main() { 237 userCacheDir, err := os.UserCacheDir() 238 if err != nil { 239 fmt.Fprintf(os.Stdout, "error finding user cache dir: %v\n", err) 240 os.Exit(1) 241 } 242 benchVars["CacheDirDefault"] = filepath.Join(userCacheDir, "benchdiff") 243 244 kctx := kong.Parse(&cli, benchstatVars, benchVars, groupHelp, 245 kong.Description(strings.TrimSpace(description)), 246 kong.ExplicitGroups([]kong.Group{ 247 {Key: "benchstat", Title: "benchstat options"}, 248 {Key: "cache", Title: "benchmark result cache"}, 249 {Key: "gotest", Title: "benchmark command line"}, 250 {Key: "x"}, 251 }), 252 ) 253 254 benchArgs, err := getBenchArgs() 255 kctx.FatalIfErrorf(err) 256 257 cacheDir, err := getCacheDir() 258 kctx.FatalIfErrorf(err) 259 260 bStat, err := buildBenchstat(&cli.BenchstatOpts) 261 kctx.FatalIfErrorf(err) 262 263 bd := &internal.Benchdiff{ 264 BenchCmd: cli.BenchmarkCmd, 265 BenchArgs: benchArgs, 266 ResultsDir: cacheDir, 267 BaseRef: cli.BaseRef, 268 Path: ".", 269 Writer: os.Stdout, 270 Benchstat: bStat, 271 Force: cli.ForceBase, 272 GitCmd: cli.GitCmd, 273 Cooldown: cli.Cooldown, 274 WarmupTime: cli.WarmupTime, 275 WarmupCount: cli.WarmupCount, 276 } 277 if cli.Debug { 278 bd.Debug = log.New(os.Stderr, "", 0) 279 } 280 result, err := bd.Run() 281 kctx.FatalIfErrorf(err) 282 283 outputFormat := "human" 284 if cli.JSON { 285 outputFormat = "json" 286 } 287 288 err = result.WriteOutput(os.Stdout, &internal.RunResultOutputOptions{ 289 BenchstatFormatter: bStat.OutputFormatter, 290 OutputFormat: outputFormat, 291 Tolerance: cli.Tolerance, 292 }) 293 kctx.FatalIfErrorf(err) 294 if result.HasDegradedResult(cli.Tolerance) { 295 os.Exit(cli.OnDegrade) 296 } 297 } 298 299 var deltaTestOpts = map[string]benchstat.DeltaTest{ 300 "none": benchstat.NoDeltaTest, 301 "utest": benchstat.UTest, 302 "ttest": benchstat.TTest, 303 } 304 305 var sortOpts = map[string]benchstat.Order{ 306 "none": nil, 307 "name": benchstat.ByName, 308 "delta": benchstat.ByDelta, 309 } 310 311 func buildBenchstat(opts *benchstatOpts) (*benchstatter.Benchstat, error) { 312 order := sortOpts[opts.Sort] 313 reverse := opts.ReverseSort 314 if order == nil { 315 reverse = false 316 } 317 var formatter benchstatter.OutputFormatter 318 switch opts.BenchstatOutput { 319 case "text": 320 formatter = benchstatter.TextFormatter(nil) 321 case "csv": 322 formatter = benchstatter.CSVFormatter(&benchstatter.CSVFormatterOptions{ 323 NoRange: opts.Norange, 324 }) 325 case "html": 326 formatter = benchstatter.HTMLFormatter(nil) 327 case "markdown": 328 formatter = benchstatter.MarkdownFormatter(&benchstatter.MarkdownFormatterOptions{ 329 CSVFormatterOptions: benchstatter.CSVFormatterOptions{ 330 NoRange: opts.Norange, 331 }, 332 }) 333 default: 334 return nil, fmt.Errorf("unexpected output format: %s", opts.BenchstatOutput) 335 } 336 337 return &benchstatter.Benchstat{ 338 DeltaTest: deltaTestOpts[opts.DeltaTest], 339 Alpha: opts.Alpha, 340 AddGeoMean: opts.Geomean, 341 SplitBy: strings.Split(opts.Split, ","), 342 Order: order, 343 ReverseOrder: reverse, 344 OutputFormatter: formatter, 345 }, nil 346 }