Dmitriy Vyukov | 052b639 | 2014-05-13 11:00:11 +0400 | [diff] [blame] | 1 | // Copyright 2013 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 | package main |
| 6 | |
| 7 | import ( |
| 8 | "bufio" |
| 9 | "bytes" |
| 10 | "fmt" |
| 11 | "io" |
| 12 | "io/ioutil" |
| 13 | "log" |
| 14 | "os" |
| 15 | "os/exec" |
| 16 | "path/filepath" |
| 17 | "regexp" |
| 18 | "strconv" |
| 19 | "strings" |
| 20 | ) |
| 21 | |
| 22 | // benchHash benchmarks a single commit. |
| 23 | func (b *Builder) benchHash(hash string, benchs []string) error { |
| 24 | if *verbose { |
| 25 | log.Println(b.name, "benchmarking", hash) |
| 26 | } |
| 27 | |
| 28 | res := &PerfResult{Hash: hash, Benchmark: "meta-done"} |
| 29 | |
| 30 | // Create place in which to do work. |
| 31 | workpath := filepath.Join(*buildroot, b.name+"-"+hash[:12]) |
| 32 | // Prepare a workpath if we don't have one we can reuse. |
| 33 | update := false |
| 34 | if b.lastWorkpath != workpath { |
| 35 | if err := os.Mkdir(workpath, mkdirPerm); err != nil { |
| 36 | return err |
| 37 | } |
| 38 | buildLog, _, err := b.buildRepoOnHash(workpath, hash, makeCmd) |
| 39 | if err != nil { |
| 40 | removePath(workpath) |
| 41 | // record failure |
| 42 | res.Artifacts = append(res.Artifacts, PerfArtifact{"log", buildLog}) |
| 43 | return b.recordPerfResult(res) |
| 44 | } |
| 45 | b.lastWorkpath = workpath |
| 46 | update = true |
| 47 | } |
| 48 | |
| 49 | // Build the benchmark binary. |
| 50 | benchBin, buildLog, err := b.buildBenchmark(workpath, update) |
| 51 | if err != nil { |
| 52 | // record failure |
| 53 | res.Artifacts = append(res.Artifacts, PerfArtifact{"log", buildLog}) |
| 54 | return b.recordPerfResult(res) |
| 55 | } |
| 56 | |
| 57 | benchmark, procs, affinity, last := chooseBenchmark(benchBin, benchs) |
| 58 | if benchmark != "" { |
| 59 | res.Benchmark = fmt.Sprintf("%v-%v", benchmark, procs) |
| 60 | res.Metrics, res.Artifacts, res.OK = b.executeBenchmark(workpath, hash, benchBin, benchmark, procs, affinity) |
| 61 | if err = b.recordPerfResult(res); err != nil { |
| 62 | return fmt.Errorf("recordResult: %s", err) |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | if last { |
| 67 | // All benchmarks have beed executed, don't need workpath anymore. |
| 68 | removePath(b.lastWorkpath) |
| 69 | b.lastWorkpath = "" |
| 70 | // Notify the app. |
| 71 | res = &PerfResult{Hash: hash, Benchmark: "meta-done", OK: true} |
| 72 | if err = b.recordPerfResult(res); err != nil { |
| 73 | return fmt.Errorf("recordResult: %s", err) |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | return nil |
| 78 | } |
| 79 | |
| 80 | // buildBenchmark builds the benchmark binary. |
| 81 | func (b *Builder) buildBenchmark(workpath string, update bool) (benchBin, log string, err error) { |
| 82 | goroot := filepath.Join(workpath, "go") |
| 83 | gobin := filepath.Join(goroot, "bin", "go") + exeExt |
| 84 | gopath := filepath.Join(*buildroot, "gopath") |
| 85 | env := append([]string{ |
| 86 | "GOROOT=" + goroot, |
| 87 | "GOPATH=" + gopath}, |
| 88 | b.envv()...) |
| 89 | // First, download without installing. |
Brad Fitzpatrick | 6baf2f1 | 2014-08-28 14:58:15 -0700 | [diff] [blame] | 90 | args := []string{"get", "-d"} |
Dmitriy Vyukov | 052b639 | 2014-05-13 11:00:11 +0400 | [diff] [blame] | 91 | if update { |
Brad Fitzpatrick | 6baf2f1 | 2014-08-28 14:58:15 -0700 | [diff] [blame] | 92 | args = append(args, "-u") |
Dmitriy Vyukov | 052b639 | 2014-05-13 11:00:11 +0400 | [diff] [blame] | 93 | } |
Brad Fitzpatrick | 6baf2f1 | 2014-08-28 14:58:15 -0700 | [diff] [blame] | 94 | args = append(args, *benchPath) |
Dmitriy Vyukov | 052b639 | 2014-05-13 11:00:11 +0400 | [diff] [blame] | 95 | var buildlog bytes.Buffer |
Brad Fitzpatrick | 6baf2f1 | 2014-08-28 14:58:15 -0700 | [diff] [blame] | 96 | runOpts := []runOpt{runTimeout(*buildTimeout), runEnv(env), allOutput(&buildlog), runDir(workpath)} |
| 97 | err = run(exec.Command(gobin, args...), runOpts...) |
| 98 | if err != nil { |
Dmitriy Vyukov | 052b639 | 2014-05-13 11:00:11 +0400 | [diff] [blame] | 99 | fmt.Fprintf(&buildlog, "go get -d %s failed: %s", *benchPath, err) |
| 100 | return "", buildlog.String(), err |
| 101 | } |
| 102 | // Then, build into workpath. |
| 103 | benchBin = filepath.Join(workpath, "benchbin") + exeExt |
Brad Fitzpatrick | 6baf2f1 | 2014-08-28 14:58:15 -0700 | [diff] [blame] | 104 | args = []string{"build", "-o", benchBin, *benchPath} |
Dmitriy Vyukov | 052b639 | 2014-05-13 11:00:11 +0400 | [diff] [blame] | 105 | buildlog.Reset() |
Brad Fitzpatrick | 6baf2f1 | 2014-08-28 14:58:15 -0700 | [diff] [blame] | 106 | err = run(exec.Command(gobin, args...), runOpts...) |
| 107 | if err != nil { |
Dmitriy Vyukov | 052b639 | 2014-05-13 11:00:11 +0400 | [diff] [blame] | 108 | fmt.Fprintf(&buildlog, "go build %s failed: %s", *benchPath, err) |
| 109 | return "", buildlog.String(), err |
| 110 | } |
| 111 | return benchBin, "", nil |
| 112 | } |
| 113 | |
| 114 | // chooseBenchmark chooses the next benchmark to run |
| 115 | // based on the list of available benchmarks, already executed benchmarks |
| 116 | // and -benchcpu list. |
| 117 | func chooseBenchmark(benchBin string, doneBenchs []string) (bench string, procs, affinity int, last bool) { |
Dmitriy Vyukov | cdce0b5 | 2014-10-24 21:05:24 +0400 | [diff] [blame] | 118 | var out bytes.Buffer |
| 119 | err := run(exec.Command(benchBin), allOutput(&out)) |
Dmitriy Vyukov | 052b639 | 2014-05-13 11:00:11 +0400 | [diff] [blame] | 120 | if err != nil { |
Emil Hessman | b362f17 | 2014-12-29 06:15:53 +0100 | [diff] [blame] | 121 | log.Printf("Failed to query benchmark list: %v\n%s", err, &out) |
Dmitriy Vyukov | 052b639 | 2014-05-13 11:00:11 +0400 | [diff] [blame] | 122 | last = true |
| 123 | return |
| 124 | } |
Dmitriy Vyukov | cdce0b5 | 2014-10-24 21:05:24 +0400 | [diff] [blame] | 125 | outStr := out.String() |
Dmitriy Vyukov | 052b639 | 2014-05-13 11:00:11 +0400 | [diff] [blame] | 126 | nlIdx := strings.Index(outStr, "\n") |
| 127 | if nlIdx < 0 { |
| 128 | log.Printf("Failed to parse benchmark list (no new line): %s", outStr) |
| 129 | last = true |
| 130 | return |
| 131 | } |
| 132 | localBenchs := strings.Split(outStr[:nlIdx], ",") |
| 133 | benchsMap := make(map[string]bool) |
| 134 | for _, b := range doneBenchs { |
| 135 | benchsMap[b] = true |
| 136 | } |
| 137 | cnt := 0 |
| 138 | // We want to run all benchmarks with GOMAXPROCS=1 first. |
| 139 | for i, procs1 := range benchCPU { |
| 140 | for _, bench1 := range localBenchs { |
| 141 | if benchsMap[fmt.Sprintf("%v-%v", bench1, procs1)] { |
| 142 | continue |
| 143 | } |
| 144 | cnt++ |
| 145 | if cnt == 1 { |
| 146 | bench = bench1 |
| 147 | procs = procs1 |
| 148 | if i < len(benchAffinity) { |
| 149 | affinity = benchAffinity[i] |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | } |
| 154 | last = cnt <= 1 |
| 155 | return |
| 156 | } |
| 157 | |
| 158 | // executeBenchmark runs a single benchmark and parses its output. |
| 159 | func (b *Builder) executeBenchmark(workpath, hash, benchBin, bench string, procs, affinity int) (metrics []PerfMetric, artifacts []PerfArtifact, ok bool) { |
| 160 | // Benchmarks runs mutually exclusive with other activities. |
| 161 | benchMutex.RUnlock() |
| 162 | defer benchMutex.RLock() |
| 163 | benchMutex.Lock() |
| 164 | defer benchMutex.Unlock() |
| 165 | |
| 166 | log.Printf("%v executing benchmark %v-%v on %v", b.name, bench, procs, hash) |
| 167 | |
| 168 | // The benchmark executes 'go build'/'go tool', |
| 169 | // so we need properly setup env. |
| 170 | env := append([]string{ |
| 171 | "GOROOT=" + filepath.Join(workpath, "go"), |
| 172 | "PATH=" + filepath.Join(workpath, "go", "bin") + string(os.PathListSeparator) + os.Getenv("PATH"), |
| 173 | "GODEBUG=gctrace=1", // since Go1.2 |
| 174 | "GOGCTRACE=1", // before Go1.2 |
| 175 | fmt.Sprintf("GOMAXPROCS=%v", procs)}, |
| 176 | b.envv()...) |
Brad Fitzpatrick | 6baf2f1 | 2014-08-28 14:58:15 -0700 | [diff] [blame] | 177 | args := []string{ |
Dmitriy Vyukov | 052b639 | 2014-05-13 11:00:11 +0400 | [diff] [blame] | 178 | "-bench", bench, |
| 179 | "-benchmem", strconv.Itoa(*benchMem), |
| 180 | "-benchtime", benchTime.String(), |
| 181 | "-benchnum", strconv.Itoa(*benchNum), |
| 182 | "-tmpdir", workpath} |
| 183 | if affinity != 0 { |
Brad Fitzpatrick | 6baf2f1 | 2014-08-28 14:58:15 -0700 | [diff] [blame] | 184 | args = append(args, "-affinity", strconv.Itoa(affinity)) |
Dmitriy Vyukov | 052b639 | 2014-05-13 11:00:11 +0400 | [diff] [blame] | 185 | } |
| 186 | benchlog := new(bytes.Buffer) |
Brad Fitzpatrick | 6baf2f1 | 2014-08-28 14:58:15 -0700 | [diff] [blame] | 187 | err := run(exec.Command(benchBin, args...), runEnv(env), allOutput(benchlog), runDir(workpath)) |
Dmitriy Vyukov | 052b639 | 2014-05-13 11:00:11 +0400 | [diff] [blame] | 188 | if strip := benchlog.Len() - 512<<10; strip > 0 { |
| 189 | // Leave the last 512K, that part contains metrics. |
| 190 | benchlog = bytes.NewBuffer(benchlog.Bytes()[strip:]) |
| 191 | } |
| 192 | artifacts = []PerfArtifact{{Type: "log", Body: benchlog.String()}} |
Brad Fitzpatrick | 6baf2f1 | 2014-08-28 14:58:15 -0700 | [diff] [blame] | 193 | if err != nil { |
Dmitriy Vyukov | 052b639 | 2014-05-13 11:00:11 +0400 | [diff] [blame] | 194 | if err != nil { |
| 195 | log.Printf("Failed to execute benchmark '%v': %v", bench, err) |
| 196 | ok = false |
| 197 | } |
| 198 | return |
| 199 | } |
| 200 | |
| 201 | metrics1, artifacts1, err := parseBenchmarkOutput(benchlog) |
| 202 | if err != nil { |
| 203 | log.Printf("Failed to parse benchmark output: %v", err) |
| 204 | ok = false |
| 205 | return |
| 206 | } |
| 207 | metrics = metrics1 |
| 208 | artifacts = append(artifacts, artifacts1...) |
Dmitriy Vyukov | 9ddd8b6 | 2014-10-17 13:51:23 +0400 | [diff] [blame] | 209 | ok = true |
Dmitriy Vyukov | 052b639 | 2014-05-13 11:00:11 +0400 | [diff] [blame] | 210 | return |
| 211 | } |
| 212 | |
| 213 | // parseBenchmarkOutput fetches metrics and artifacts from benchmark output. |
| 214 | func parseBenchmarkOutput(out io.Reader) (metrics []PerfMetric, artifacts []PerfArtifact, err error) { |
| 215 | s := bufio.NewScanner(out) |
| 216 | metricRe := regexp.MustCompile("^GOPERF-METRIC:([a-z,0-9,-]+)=([0-9]+)$") |
| 217 | fileRe := regexp.MustCompile("^GOPERF-FILE:([a-z,0-9,-]+)=(.+)$") |
| 218 | for s.Scan() { |
| 219 | ln := s.Text() |
| 220 | if ss := metricRe.FindStringSubmatch(ln); ss != nil { |
| 221 | var v uint64 |
| 222 | v, err = strconv.ParseUint(ss[2], 10, 64) |
| 223 | if err != nil { |
| 224 | err = fmt.Errorf("Failed to parse metric '%v=%v': %v", ss[1], ss[2], err) |
| 225 | return |
| 226 | } |
| 227 | metrics = append(metrics, PerfMetric{Type: ss[1], Val: v}) |
| 228 | } else if ss := fileRe.FindStringSubmatch(ln); ss != nil { |
| 229 | var buf []byte |
| 230 | buf, err = ioutil.ReadFile(ss[2]) |
| 231 | if err != nil { |
| 232 | err = fmt.Errorf("Failed to read file '%v': %v", ss[2], err) |
| 233 | return |
| 234 | } |
| 235 | artifacts = append(artifacts, PerfArtifact{ss[1], string(buf)}) |
| 236 | } |
| 237 | } |
| 238 | return |
| 239 | } |
| 240 | |
| 241 | // needsBenchmarking determines whether the commit needs benchmarking. |
| 242 | func needsBenchmarking(log *HgLog) bool { |
| 243 | // Do not benchmark branch commits, they are usually not interesting |
| 244 | // and fall out of the trunk succession. |
| 245 | if log.Branch != "" { |
| 246 | return false |
| 247 | } |
| 248 | // Do not benchmark commits that do not touch source files (e.g. CONTRIBUTORS). |
| 249 | for _, f := range strings.Split(log.Files, " ") { |
| 250 | if (strings.HasPrefix(f, "include") || strings.HasPrefix(f, "src")) && |
| 251 | !strings.HasSuffix(f, "_test.go") && !strings.Contains(f, "testdata") { |
| 252 | return true |
| 253 | } |
| 254 | } |
| 255 | return false |
| 256 | } |