github.com/aclements/go-misc@v0.0.0-20240129233631-2f6ede80790c/benchmany/benchmany.go (about)

     1  // Copyright 2015 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  // Benchmany runs Go benchmarks across many git commits.
     6  //
     7  // Usage:
     8  //
     9  //      benchmany [-C git-dir] [-n iterations] <commit or range>...
    10  //
    11  // benchmany runs the benchmarks in the current directory <iterations>
    12  // times for each commit in <commit or range> and writes the benchmark
    13  // results to bench.log. Benchmarks may be Go testing framework
    14  // benchmarks or benchmarks from golang.org/x/benchmarks.
    15  //
    16  // <commit or range>... can be either a list of individual commits or
    17  // a revision range. For the spelling of a revision range, see
    18  // "SPECIFYING RANGES" in gitrevisions(7). For exact details, see the
    19  // --no-walk option to git-rev-list(1).
    20  //
    21  // Benchmany will check out each revision in git-dir. The current
    22  // directory may or may not be in the same git repository as git-dir.
    23  // If git-dir refers to a Go installation, benchmany will run
    24  // make.bash at each revision; otherwise, it assumes go test can
    25  // rebuild the necessary dependencies. Benchmany also supports using
    26  // gover (https://godoc.org/github.com/aclements/go-misc/gover) to
    27  // save and reuse Go build trees. This is useful for saving time
    28  // across multiple benchmark runs and for benchmarks that depend on
    29  // the Go tree itself (such as compiler benchmarks).
    30  //
    31  // Benchmany supports multiple ways of prioritizing the order in which
    32  // individual iterations are run. By default, it runs in "sequential"
    33  // mode: it runs the first iteration of all benchmarks, then the
    34  // second, and so forth. It also supports a "spread" mode designed to
    35  // quickly get coverage for large sets of revisions. This mode
    36  // randomizes the order to run iterations in, but biases this order
    37  // toward covering an evenly distributed set of revisions early and
    38  // finishing all of the iterations of the revisions it has started on
    39  // before moving on to new revisions. This way, if benchmany is
    40  // interrupted, the revisions benchmarked cover the space more-or-less
    41  // evenly. Finally, it supports a "metric" mode, which zeroes in on
    42  // changes in a benchmark metric by selecting the commit half way
    43  // between the pair of commits with the biggest difference in the
    44  // metric. This is like "git bisect", but for performance.
    45  //
    46  // Benchmany is safe to interrupt. If it is restarted, it will parse
    47  // the benchmark log files to recover its state.
    48  package main
    49  
    50  import (
    51  	"flag"
    52  	"fmt"
    53  	"os"
    54  	"os/exec"
    55  	"strings"
    56  )
    57  
    58  var gitDir string
    59  var dryRun bool
    60  
    61  // maxFails is the maximum number of benchmark run failures to
    62  // tolerate for a commit before giving up on trying to benchmark that
    63  // commit. Build failures always disqualify a commit.
    64  const maxFails = 5
    65  
    66  func main() {
    67  	flag.Parse()
    68  	doRun()
    69  }
    70  
    71  // git runs git subcommand subcmd and returns its stdout. If git
    72  // fails, it prints the failure and exits.
    73  func git(subcmd string, args ...string) string {
    74  	gitargs := []string{}
    75  	if gitDir != "" {
    76  		gitargs = append(gitargs, "-C", gitDir)
    77  	}
    78  	gitargs = append(gitargs, subcmd)
    79  	gitargs = append(gitargs, args...)
    80  	cmd := exec.Command("git", gitargs...)
    81  	cmd.Stderr = os.Stderr
    82  	if dryRun {
    83  		dryPrint(cmd)
    84  		if !(subcmd == "rev-parse" || subcmd == "rev-list" || subcmd == "show") {
    85  			return ""
    86  		}
    87  	}
    88  	out, err := cmd.Output()
    89  	if err != nil {
    90  		fmt.Fprintf(os.Stderr, "git %s failed: %s\n", shellEscapeList(gitargs), err)
    91  		os.Exit(1)
    92  	}
    93  	return string(out)
    94  }
    95  
    96  func dryPrint(cmd *exec.Cmd) {
    97  	out := shellEscape(cmd.Path)
    98  	for _, a := range cmd.Args[1:] {
    99  		out += " " + shellEscape(a)
   100  	}
   101  	if cmd.Dir != "" {
   102  		out = fmt.Sprintf("(cd %s && %s)", shellEscape(cmd.Dir), out)
   103  	}
   104  	fmt.Fprintln(os.Stderr, out)
   105  }
   106  
   107  func shellEscape(x string) string {
   108  	if len(x) == 0 {
   109  		return "''"
   110  	}
   111  	for _, r := range x {
   112  		if 'a' <= r && r <= 'z' || 'A' <= r && r <= 'Z' || '0' <= r && r <= '9' || strings.ContainsRune("@%_-+:,./", r) {
   113  			continue
   114  		}
   115  		// Unsafe character.
   116  		return "'" + strings.Replace(x, "'", "'\"'\"'", -1) + "'"
   117  	}
   118  	return x
   119  }
   120  
   121  func shellEscapeList(xs []string) string {
   122  	out := make([]string, len(xs))
   123  	for i, x := range xs {
   124  		out[i] = shellEscape(x)
   125  	}
   126  	return strings.Join(out, " ")
   127  }
   128  
   129  func exists(path string) bool {
   130  	_, err := os.Stat(path)
   131  	return !os.IsNotExist(err)
   132  }
   133  
   134  func trimNL(s string) string {
   135  	return strings.TrimRight(s, "\n")
   136  }
   137  
   138  // indent returns s with each line indented by four spaces. If s is
   139  // non-empty, the returned string is guaranteed to end in a "\n".
   140  func indent(s string) string {
   141  	if len(s) == 0 {
   142  		return s
   143  	}
   144  	if strings.HasSuffix(s, "\n") {
   145  		s = s[:len(s)-1]
   146  	}
   147  	return "    " + strings.Replace(s, "\n", "\n    ", -1) + "\n"
   148  }
   149  
   150  // lines splits s in to lines. It omits a final blank line, if any.
   151  func lines(s string) []string {
   152  	l := strings.Split(s, "\n")
   153  	if len(l) > 0 && l[len(l)-1] == "" {
   154  		l = l[:len(l)-1]
   155  	}
   156  	return l
   157  }