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  }