blob: 73008c170874c16dea496ddc02eab980ad9a40cc [file] [log] [blame]
// Copyright 2017 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 buildgo
import (
"bytes"
"context"
"fmt"
"io"
"path"
"strings"
"time"
"golang.org/x/build/buildenv"
"golang.org/x/build/buildlet"
"golang.org/x/build/cmd/coordinator/spanlog"
"golang.org/x/build/dashboard"
"golang.org/x/build/internal/sourcecache"
)
// benchRuns is the number of times to run each benchmark binary
const benchRuns = 5
// BenchmarkItem represents a single package's benchmarks, to be run on one or more commit.
// After Run is called, the output of each commit can be retrieved from the Output field.
type BenchmarkItem struct {
binary string // name of binary relative to goroot
args []string // args to run binary with
preamble string // string to print before benchmark results (e.g. "pkg: test/bench/go1\n")
dir string // optional directory (relative to $WORKDIR) to run benchmarks in
Output []string // benchmark output for each commit
build func(bc *buildlet.Client, goroot string, w io.Writer) (remoteErr, err error) // how to build benchmark binary
}
// Name returns a string that uniquely identifies this benchmark.
func (b *BenchmarkItem) Name() string {
return b.binary + " " + strings.Join(b.args, " ")
}
// buildGo1 builds the Go 1 benchmarks.
func buildGo1(conf dashboard.BuildConfig, bc *buildlet.Client, goroot string, w io.Writer) (remoteErr, err error) {
workDir, err := bc.WorkDir()
if err != nil {
return nil, err
}
var found bool
if err := bc.ListDir(path.Join(goroot, "test/bench/go1"), buildlet.ListDirOpts{}, func(e buildlet.DirEntry) {
switch e.Name() {
case "go1.test", "go1.test.exe":
found = true
}
}); err != nil {
return nil, err
}
if found {
return nil, nil
}
return bc.Exec(path.Join(goroot, "bin", "go"), buildlet.ExecOpts{
Output: w,
ExtraEnv: []string{"GOROOT=" + conf.FilePathJoin(workDir, goroot)},
Args: []string{"test", "-c"},
Dir: path.Join(goroot, "test/bench/go1"),
})
}
// buildPkg builds a package's benchmarks.
func buildPkg(conf dashboard.BuildConfig, bc *buildlet.Client, goroot string, w io.Writer, pkg, name string) (remoteErr, err error) {
workDir, err := bc.WorkDir()
if err != nil {
return nil, err
}
return bc.Exec(path.Join(goroot, "bin", "go"), buildlet.ExecOpts{
Output: w,
ExtraEnv: []string{"GOROOT=" + conf.FilePathJoin(workDir, goroot)},
Args: []string{"test", "-c", "-o", conf.FilePathJoin(workDir, goroot, name), pkg},
})
}
// buildXBenchmark builds a benchmark from x/benchmarks.
func buildXBenchmark(sl spanlog.Logger, conf dashboard.BuildConfig, bc *buildlet.Client, goroot string, w io.Writer, rev, pkg, name string) (remoteErr, err error) {
workDir, err := bc.WorkDir()
if err != nil {
return nil, err
}
if err := bc.ListDir("gopath/src/golang.org/x/benchmarks", buildlet.ListDirOpts{}, func(buildlet.DirEntry) {}); err != nil {
if err := FetchSubrepo(sl, bc, "benchmarks", rev); err != nil {
return nil, err
}
}
return bc.Exec(path.Join(goroot, "bin/go"), buildlet.ExecOpts{
Output: w,
ExtraEnv: []string{
"GOROOT=" + conf.FilePathJoin(workDir, goroot),
"GOPATH=" + conf.FilePathJoin(workDir, "gopath"),
},
Args: []string{"build", "-o", conf.FilePathJoin(workDir, goroot, name), pkg},
})
}
// EnumerateBenchmarks returns a slice of the benchmarks to be run for the built Go distribution found in gb.Goroot.
// If benchmarksRev is non-empty, it is the revision of x/benchmarks to check out for additional benchmarks.
// pkgs contains a list of possibly duplicate packages that will be searched for benchmarks.
func (gb GoBuilder) EnumerateBenchmarks(bc *buildlet.Client, benchmarksRev string, pkgs []string) ([]*BenchmarkItem, error) {
workDir, err := bc.WorkDir()
if err != nil {
err = fmt.Errorf("buildBench, WorkDir: %v", err)
return nil, err
}
// Fetch x/benchmarks
if benchmarksRev != "" {
if err := FetchSubrepo(gb.Logger, bc, "benchmarks", benchmarksRev); err != nil {
return nil, err
}
}
var out []*BenchmarkItem
// These regexes shard the go1 tests so each shard takes about 20s, ensuring no test runs for
for _, re := range []string{`^Benchmark[BF]`, `^Benchmark[HR]`, `^Benchmark[^BFHR]`} {
out = append(out, &BenchmarkItem{
binary: "test/bench/go1/go1.test",
args: []string{"-test.bench", re, "-test.benchmem"},
preamble: "pkg: test/bench/go1\n",
build: func(bc *buildlet.Client, goroot string, w io.Writer) (error, error) {
return buildGo1(gb.Conf, bc, goroot, w)
},
})
}
// Enumerate x/benchmarks
if benchmarksRev != "" {
var buf bytes.Buffer
remoteErr, err := bc.Exec(path.Join(gb.Goroot, "bin/go"), buildlet.ExecOpts{
Output: &buf,
ExtraEnv: []string{
"GOROOT=" + gb.Conf.FilePathJoin(workDir, gb.Goroot),
"GOPATH=" + gb.Conf.FilePathJoin(workDir, "gopath"),
},
Args: []string{"list", "-f", `{{if eq .Name "main"}}{{.ImportPath}}{{end}}`, "golang.org/x/benchmarks/..."},
})
if remoteErr != nil {
return nil, remoteErr
}
if err != nil {
return nil, err
}
for _, pkg := range strings.Fields(buf.String()) {
pkg := pkg
name := "bench-" + path.Base(pkg) + ".exe"
out = append(out, &BenchmarkItem{
binary: name, args: nil, build: func(bc *buildlet.Client, goroot string, w io.Writer) (error, error) {
return buildXBenchmark(gb.Logger, gb.Conf, bc, goroot, w, benchmarksRev, pkg, name)
}})
}
}
// Enumerate package benchmarks that were affected by the CL
if len(pkgs) > 0 {
// Find packages that actually have benchmarks or tests.
var buf bytes.Buffer
remoteErr, err := bc.Exec(path.Join(gb.Goroot, "bin/go"), buildlet.ExecOpts{
Output: &buf,
ExtraEnv: []string{
"GOROOT=" + gb.Conf.FilePathJoin(workDir, gb.Goroot),
},
Args: append([]string{"list", "-e", "-f", "{{if or (len .TestGoFiles) (len .XTestGoFiles)}}{{.ImportPath}}{{end}}"}, pkgs...),
})
if remoteErr != nil {
return nil, remoteErr
}
if err != nil {
return nil, err
}
for _, pkg := range strings.Fields(buf.String()) {
// Some packages have large numbers of benchmarks.
// To avoid running benchmarks for hours and hours, we exclude runtime (which has 350+ benchmarks) and run benchmarks for .1s instead of the default 1s.
// This allows the remaining standard library packages to run a single iteration of a package's benchmarks in <20s, making them have the same scale as go1 benchmark shards.
if pkg == "runtime" {
continue
}
name := "bench-" + strings.Replace(pkg, "/", "-", -1) + ".exe"
out = append(out, &BenchmarkItem{
binary: name,
dir: path.Join(gb.Goroot, "src", pkg),
args: []string{"-test.bench", ".", "-test.benchmem", "-test.run", "^$", "-test.benchtime", "100ms"},
build: func(bc *buildlet.Client, goroot string, w io.Writer) (error, error) {
return buildPkg(gb.Conf, bc, goroot, w, pkg, name)
}})
}
}
return out, nil
}
// runOneBenchBinary runs a binary on the buildlet and writes its output to w with a trailing newline.
//
// TODO: this signature is too big. Make it a method of something?
func runOneBenchBinary(conf dashboard.BuildConfig, bc *buildlet.Client, w io.Writer, goroot, dir, binaryPath string, args []string) (remoteErr, err error) {
defer w.Write([]byte{'\n'})
workDir, err := bc.WorkDir()
if err != nil {
return nil, fmt.Errorf("runOneBenchBinary, WorkDir: %v", err)
}
// Some benchmarks need GOROOT so they can invoke cmd/go.
return bc.Exec(binaryPath, buildlet.ExecOpts{
Output: w,
Dir: dir,
Args: args,
Path: []string{"$WORKDIR/" + goroot + "/bin", "$PATH"},
ExtraEnv: []string{
"GOROOT=" + conf.FilePathJoin(workDir, goroot),
// Some builders run in virtualization
// environments (GCE, GKE, etc.). These
// environments have CPU antagonists - by
// limiting GOMAXPROCS to 2 we can reduce the
// variability of benchmarks by leaving free
// cores available for antagonists. We don't
// want GOMAXPROCS=1 because that invokes
// special runtime behavior. Test data is at
// https://perf.golang.org/search?q=upload%3A20170512.4+cores%3Aall+parallel%3A4+%7C+gomaxprocs%3A2+vs+gomaxprocs%3A16+vs+gomaxprocs%3A32
"GOMAXPROCS=2",
},
})
}
func buildRev(buildEnv *buildenv.Environment, sl spanlog.Logger, conf dashboard.BuildConfig, bc *buildlet.Client, w io.Writer, goroot string, br BuilderRev) error {
if br.SnapshotExists(context.TODO(), buildEnv) {
return bc.PutTarFromURL(br.SnapshotURL(buildEnv), goroot)
}
if err := bc.PutTar(VersionTgz(br.Rev), goroot); err != nil {
return err
}
srcTar, err := sourcecache.GetSourceTgz(sl, "go", br.Rev)
if err != nil {
return err
}
if err := bc.PutTar(srcTar, goroot); err != nil {
return err
}
builder := GoBuilder{
Logger: sl,
BuilderRev: br,
Conf: conf,
Goroot: goroot,
}
remoteErr, err := builder.RunMake(bc, w)
if err != nil {
return err
}
return remoteErr
}
// Run runs all the iterations of this benchmark on bc.
// Build output is sent to w. Benchmark output is stored in b.output.
// revs must contain exactly two revs. The first rev is assumed to be present in "go", and the second will be placed into "go-parent".
// TODO(quentin): Support len(revs) != 2.
func (b *BenchmarkItem) Run(buildEnv *buildenv.Environment, sl spanlog.Logger, conf dashboard.BuildConfig, bc *buildlet.Client, w io.Writer, revs []BuilderRev) (remoteErr, err error) {
// Ensure we have a built parent repo.
if err := bc.ListDir("go-parent", buildlet.ListDirOpts{}, func(buildlet.DirEntry) {}); err != nil {
pbr := revs[1]
sp := sl.CreateSpan("bench_build_parent", bc.Name())
err = buildRev(buildEnv, sl, conf, bc, w, "go-parent", pbr)
sp.Done(err)
if err != nil {
return nil, err
}
}
// Build benchmark.
for _, goroot := range []string{"go", "go-parent"} {
sp := sl.CreateSpan("bench_build", fmt.Sprintf("%s/%s: %s", goroot, b.binary, bc.Name()))
remoteErr, err = b.build(bc, goroot, w)
sp.Done(err)
if remoteErr != nil || err != nil {
return remoteErr, err
}
}
type commit struct {
path string
out bytes.Buffer
}
commits := []*commit{
{path: "go-parent"},
{path: "go"},
}
for _, c := range commits {
c.out.WriteString(b.preamble)
}
// Run bench binaries and capture the results
for i := 0; i < benchRuns; i++ {
for _, c := range commits {
fmt.Fprintf(&c.out, "iteration: %d\nstart-time: %s\n", i, time.Now().UTC().Format(time.RFC3339))
binaryPath := path.Join(c.path, b.binary)
sp := sl.CreateSpan("run_one_bench", binaryPath)
remoteErr, err = runOneBenchBinary(conf, bc, &c.out, c.path, b.dir, binaryPath, b.args)
sp.Done(err)
if err != nil || remoteErr != nil {
c.out.WriteTo(w)
if err != nil {
fmt.Fprintf(w, "execution error: %v\n", err)
} else if remoteErr != nil {
fmt.Fprintf(w, "remote error: %v\n", remoteErr)
}
return
}
}
}
b.Output = []string{
commits[0].out.String(),
commits[1].out.String(),
}
return nil, nil
}