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 }