// Copyright 2016 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. // perfrun interacts with the buildlet coordinator to run the go1 // benchmarks on a buildlet slave for the most recent successful // commits according to the build dashboard. package main import ( "bytes" "encoding/json" "flag" "fmt" "io" "log" "net/http" "os" "path" "strings" "time" "golang.org/x/build/buildenv" "golang.org/x/build/buildlet" "golang.org/x/build/types" ) var ( buildletBench = flag.String("buildlet", "", "name of buildlet to use for benchmarks") buildletSrc = flag.String("buildlet_src", "", "name of builder to get binaries from (defaults to the same as buildlet)") buildEnv *buildenv.Environment ) // runBench runs the benchmarks from each of the revisions in // commits. It uses the tarballs built by the "src" buildlet and runs // the benchmarks on the "bench" buildlet. It writes a log to out in // the standard benchmark format // (https://github.com/golang/proposal/blob/master/design/14313-benchmark-format.md). func runBench(out io.Writer, bench, src string, commits []string) error { bc, err := namedClient(*buildletBench) if err != nil { return err } log.Printf("Using buildlet %s", bc.RemoteName()) workDir, err := bc.WorkDir() if err != nil { log.Printf("Getting WorkDir: %v", err) return err } for _, rev := range commits { log.Printf("Installing prebuilt rev %s", rev) dir := fmt.Sprintf("go-%s", rev) // Copy pre-built trees if err := bc.PutTarFromURL(buildEnv.SnapshotURL(src, rev), dir); err != nil { log.Printf("failed to extract snapshot for %s: %v", rev, err) return err } // Build binaries log.Printf("Building bench binary for rev %s", rev) var buf bytes.Buffer remoteErr, err := bc.Exec(path.Join(dir, "bin", "go"), buildlet.ExecOpts{ Output: &buf, ExtraEnv: []string{"GOROOT=" + path.Join(workDir, dir)}, Args: []string{"test", "-c"}, Dir: path.Join(dir, "test/bench/go1"), }) if remoteErr != nil { log.Printf("failed to compile bench for %s: %v", rev, remoteErr) log.Printf("output: %s", buf.Bytes()) return remoteErr } if err != nil { log.Printf("Exec error: %v", err) log.Printf("output: %s", buf.Bytes()) return err } } // Loop over commits and run N times interleaved, grabbing output // TODO: Overhead of multiple Exec calls might be significant; should we ship over a shell script to do this in one go? for i := 0; i < 10; i++ { log.Printf("Starting bench run %d", i) for _, rev := range commits { var buf bytes.Buffer remoteErr, err := bc.Exec(path.Join("go-"+rev, "test/bench/go1/go1.test"), buildlet.ExecOpts{ Output: &buf, Args: []string{"-test.bench", ".", "-test.benchmem"}, }) if remoteErr != nil { log.Printf("failed to run %d-%s: %v", i, rev, remoteErr) log.Printf("output: %s", buf.Bytes()) return remoteErr } if err != nil { log.Printf("Exec error: %v", err) log.Printf("output: %s", buf.Bytes()) return err } log.Printf("%d-%s: %s", i, rev, buf.Bytes()) // XXX fmt.Fprintf(out, "commit: %s\niteration: %d\nstart: %s", rev, i, time.Now().UTC().Format(time.RFC3339)) out.Write(buf.Bytes()) out.Write([]byte{'\n'}) } } // Destroy client // TODO: defer this so we don't leak clients? if err := bc.Close(); err != nil { return err } return nil } func namedClient(name string) (*buildlet.Client, error) { if strings.Contains(name, ":") { return buildlet.NewClient(name, buildlet.NoKeyPair), nil } cc, err := buildlet.NewCoordinatorClientFromFlags() if err != nil { return nil, err } return cc.CreateBuildlet(name) // TODO(quentin): Figure out a way to detect if there's an already running builder with this name. //return cc.NamedBuildlet(name) } // findCommits finds all the recent successful commits for the given builder func findCommits(name string) ([]string, error) { var bs types.BuildStatus res, err := http.Get(buildEnv.DashBase() + "?mode=json") if err != nil { return nil, err } defer res.Body.Close() if err := json.NewDecoder(res.Body).Decode(&bs); err != nil { return nil, err } if res.StatusCode != 200 { return nil, fmt.Errorf("unexpected http status %v", res.Status) } var commits []string for builderIdx := 0; builderIdx < len(bs.Builders); builderIdx++ { if bs.Builders[builderIdx] == name { for _, br := range bs.Revisions { if br.Repo != "go" { // Only process go repo for now continue } if br.Results[builderIdx] == "ok" { commits = append(commits, br.Revision) } } return commits, nil } } return nil, fmt.Errorf("builder %q not found", name) } func usage() { fmt.Fprintf(os.Stderr, `Usage of perfrun: perfrun [flags] Flags: `) flag.PrintDefaults() os.Exit(1) } func main() { buildlet.RegisterFlags() flag.Usage = usage flag.Parse() buildEnv = buildenv.FromFlags() args := flag.Args() if *buildletBench == "" { usage() } if *buildletSrc == "" { *buildletSrc = *buildletBench } if len(args) == 0 { res, err := findCommits(*buildletSrc) args = res if err != nil { fmt.Fprintf(os.Stderr, "Failed finding commits to build: %v", err) os.Exit(1) } } log.Printf("Running bench on %v", args) out, err := os.Create(fmt.Sprintf("perfrun-%s-%s-%s.log", *buildletSrc, *buildletBench, time.Now().Format("20060102150405"))) if err != nil { fmt.Fprintf(os.Stderr, "Creating log failed: %v", err) } defer func() { if err := out.Close(); err != nil { fmt.Fprintf(os.Stderr, "Failed writing log: %v", err) os.Exit(1) } }() if err := runBench(out, *buildletBench, *buildletSrc, args); err != nil { fmt.Fprintf(os.Stderr, "Failed running bench: %v", err) os.Exit(1) } }