1
0
mirror of https://github.com/golang/go synced 2024-11-06 15:36:24 -07:00
go/dashboard/builder/bench.go
Brad Fitzpatrick cd91e8d096 dashboard/builder: modernize ancient exec wrappers
They were from a time before we had the os/exec package, if
memory serves.

Also, make verbose also mean that the main build's stdout
and stderr go to the real stdout and stderr as well.
I'll want that for the Docker-based builder.

LGTM=adg
R=adg
CC=golang-codereviews
https://golang.org/cl/135000043
2014-08-28 14:58:15 -07:00

255 lines
7.6 KiB
Go

// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
)
// benchHash benchmarks a single commit.
func (b *Builder) benchHash(hash string, benchs []string) error {
if *verbose {
log.Println(b.name, "benchmarking", hash)
}
res := &PerfResult{Hash: hash, Benchmark: "meta-done"}
// Create place in which to do work.
workpath := filepath.Join(*buildroot, b.name+"-"+hash[:12])
// Prepare a workpath if we don't have one we can reuse.
update := false
if b.lastWorkpath != workpath {
if err := os.Mkdir(workpath, mkdirPerm); err != nil {
return err
}
buildLog, _, err := b.buildRepoOnHash(workpath, hash, makeCmd)
if err != nil {
removePath(workpath)
// record failure
res.Artifacts = append(res.Artifacts, PerfArtifact{"log", buildLog})
return b.recordPerfResult(res)
}
b.lastWorkpath = workpath
update = true
}
// Build the benchmark binary.
benchBin, buildLog, err := b.buildBenchmark(workpath, update)
if err != nil {
// record failure
res.Artifacts = append(res.Artifacts, PerfArtifact{"log", buildLog})
return b.recordPerfResult(res)
}
benchmark, procs, affinity, last := chooseBenchmark(benchBin, benchs)
if benchmark != "" {
res.Benchmark = fmt.Sprintf("%v-%v", benchmark, procs)
res.Metrics, res.Artifacts, res.OK = b.executeBenchmark(workpath, hash, benchBin, benchmark, procs, affinity)
if err = b.recordPerfResult(res); err != nil {
return fmt.Errorf("recordResult: %s", err)
}
}
if last {
// All benchmarks have beed executed, don't need workpath anymore.
removePath(b.lastWorkpath)
b.lastWorkpath = ""
// Notify the app.
res = &PerfResult{Hash: hash, Benchmark: "meta-done", OK: true}
if err = b.recordPerfResult(res); err != nil {
return fmt.Errorf("recordResult: %s", err)
}
}
return nil
}
// buildBenchmark builds the benchmark binary.
func (b *Builder) buildBenchmark(workpath string, update bool) (benchBin, log string, err error) {
goroot := filepath.Join(workpath, "go")
gobin := filepath.Join(goroot, "bin", "go") + exeExt
gopath := filepath.Join(*buildroot, "gopath")
env := append([]string{
"GOROOT=" + goroot,
"GOPATH=" + gopath},
b.envv()...)
// First, download without installing.
args := []string{"get", "-d"}
if update {
args = append(args, "-u")
}
args = append(args, *benchPath)
var buildlog bytes.Buffer
runOpts := []runOpt{runTimeout(*buildTimeout), runEnv(env), allOutput(&buildlog), runDir(workpath)}
err = run(exec.Command(gobin, args...), runOpts...)
if err != nil {
fmt.Fprintf(&buildlog, "go get -d %s failed: %s", *benchPath, err)
return "", buildlog.String(), err
}
// Then, build into workpath.
benchBin = filepath.Join(workpath, "benchbin") + exeExt
args = []string{"build", "-o", benchBin, *benchPath}
buildlog.Reset()
err = run(exec.Command(gobin, args...), runOpts...)
if err != nil {
fmt.Fprintf(&buildlog, "go build %s failed: %s", *benchPath, err)
return "", buildlog.String(), err
}
return benchBin, "", nil
}
// chooseBenchmark chooses the next benchmark to run
// based on the list of available benchmarks, already executed benchmarks
// and -benchcpu list.
func chooseBenchmark(benchBin string, doneBenchs []string) (bench string, procs, affinity int, last bool) {
out, err := exec.Command(benchBin).CombinedOutput()
if err != nil {
log.Printf("Failed to query benchmark list: %v\n%s", err, out)
last = true
return
}
outStr := string(out)
nlIdx := strings.Index(outStr, "\n")
if nlIdx < 0 {
log.Printf("Failed to parse benchmark list (no new line): %s", outStr)
last = true
return
}
localBenchs := strings.Split(outStr[:nlIdx], ",")
benchsMap := make(map[string]bool)
for _, b := range doneBenchs {
benchsMap[b] = true
}
cnt := 0
// We want to run all benchmarks with GOMAXPROCS=1 first.
for i, procs1 := range benchCPU {
for _, bench1 := range localBenchs {
if benchsMap[fmt.Sprintf("%v-%v", bench1, procs1)] {
continue
}
cnt++
if cnt == 1 {
bench = bench1
procs = procs1
if i < len(benchAffinity) {
affinity = benchAffinity[i]
}
}
}
}
last = cnt <= 1
return
}
// executeBenchmark runs a single benchmark and parses its output.
func (b *Builder) executeBenchmark(workpath, hash, benchBin, bench string, procs, affinity int) (metrics []PerfMetric, artifacts []PerfArtifact, ok bool) {
// Benchmarks runs mutually exclusive with other activities.
benchMutex.RUnlock()
defer benchMutex.RLock()
benchMutex.Lock()
defer benchMutex.Unlock()
log.Printf("%v executing benchmark %v-%v on %v", b.name, bench, procs, hash)
// The benchmark executes 'go build'/'go tool',
// so we need properly setup env.
env := append([]string{
"GOROOT=" + filepath.Join(workpath, "go"),
"PATH=" + filepath.Join(workpath, "go", "bin") + string(os.PathListSeparator) + os.Getenv("PATH"),
"GODEBUG=gctrace=1", // since Go1.2
"GOGCTRACE=1", // before Go1.2
fmt.Sprintf("GOMAXPROCS=%v", procs)},
b.envv()...)
args := []string{
"-bench", bench,
"-benchmem", strconv.Itoa(*benchMem),
"-benchtime", benchTime.String(),
"-benchnum", strconv.Itoa(*benchNum),
"-tmpdir", workpath}
if affinity != 0 {
args = append(args, "-affinity", strconv.Itoa(affinity))
}
benchlog := new(bytes.Buffer)
err := run(exec.Command(benchBin, args...), runEnv(env), allOutput(benchlog), runDir(workpath))
if strip := benchlog.Len() - 512<<10; strip > 0 {
// Leave the last 512K, that part contains metrics.
benchlog = bytes.NewBuffer(benchlog.Bytes()[strip:])
}
artifacts = []PerfArtifact{{Type: "log", Body: benchlog.String()}}
if err != nil {
if err != nil {
log.Printf("Failed to execute benchmark '%v': %v", bench, err)
ok = false
}
return
}
metrics1, artifacts1, err := parseBenchmarkOutput(benchlog)
if err != nil {
log.Printf("Failed to parse benchmark output: %v", err)
ok = false
return
}
metrics = metrics1
artifacts = append(artifacts, artifacts1...)
return
}
// parseBenchmarkOutput fetches metrics and artifacts from benchmark output.
func parseBenchmarkOutput(out io.Reader) (metrics []PerfMetric, artifacts []PerfArtifact, err error) {
s := bufio.NewScanner(out)
metricRe := regexp.MustCompile("^GOPERF-METRIC:([a-z,0-9,-]+)=([0-9]+)$")
fileRe := regexp.MustCompile("^GOPERF-FILE:([a-z,0-9,-]+)=(.+)$")
for s.Scan() {
ln := s.Text()
if ss := metricRe.FindStringSubmatch(ln); ss != nil {
var v uint64
v, err = strconv.ParseUint(ss[2], 10, 64)
if err != nil {
err = fmt.Errorf("Failed to parse metric '%v=%v': %v", ss[1], ss[2], err)
return
}
metrics = append(metrics, PerfMetric{Type: ss[1], Val: v})
} else if ss := fileRe.FindStringSubmatch(ln); ss != nil {
var buf []byte
buf, err = ioutil.ReadFile(ss[2])
if err != nil {
err = fmt.Errorf("Failed to read file '%v': %v", ss[2], err)
return
}
artifacts = append(artifacts, PerfArtifact{ss[1], string(buf)})
}
}
return
}
// needsBenchmarking determines whether the commit needs benchmarking.
func needsBenchmarking(log *HgLog) bool {
// Do not benchmark branch commits, they are usually not interesting
// and fall out of the trunk succession.
if log.Branch != "" {
return false
}
// Do not benchmark commits that do not touch source files (e.g. CONTRIBUTORS).
for _, f := range strings.Split(log.Files, " ") {
if (strings.HasPrefix(f, "include") || strings.HasPrefix(f, "src")) &&
!strings.HasSuffix(f, "_test.go") && !strings.Contains(f, "testdata") {
return true
}
}
return false
}