| // Copyright 2021 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. |
| |
| //go:build go1.16 |
| |
| package main |
| |
| import ( |
| "bufio" |
| "bytes" |
| "fmt" |
| "io" |
| "os" |
| "os/exec" |
| "path" |
| "runtime" |
| "strings" |
| "sync" |
| "time" |
| ) |
| |
| // Configuration is a structure that holds all the variables necessary to |
| // initiate a bent run. These structures are read from a .toml file at |
| // boot-time. |
| type Configuration struct { |
| Name string // Short name used for binary names, mention on command line |
| Root string // Specific Go root to use for this trial |
| PgoGen string // Name of sub-directory to put profiles for later loading |
| PgoUse string // Name of sub-directory to take generated profile files |
| BuildFlags []string // BuildFlags supplied to 'go test -c' for building (e.g., "-p 1") |
| AfterBuild []string // Array of commands to run, output of all commands for a configuration (across binaries) is collected in <runstamp>.<config>.<cmd> |
| GcFlags string // GcFlags supplied to 'go test -c' for building |
| GcEnv []string // Environment variables supplied to 'go test -c' for building |
| RunFlags []string // Extra flags passed to the test binary |
| RunEnv []string // Extra environment variables passed to the test binary |
| RunWrapper []string // (Outermost) Command and args to precede whatever the operation is; may fail in the sandbox. |
| Disabled bool // True if this configuration is temporarily disabled |
| benchWriter *os.File |
| rootCopy string // The contents of GOROOT are copied here to allow benchmarking of just the test compilation. |
| } |
| |
| var dirs *directories // constant across all configurations, useful in other contexts. |
| |
| func (c *Configuration) buildBenchName() string { |
| return c.thingBenchName("build") |
| } |
| |
| func (c *Configuration) thingBenchName(suffix string) string { |
| if len(suffix) != 0 { |
| suffix = path.Base(suffix) |
| } |
| return path.Join(dirs.benchDir, runstamp+"."+c.Name+"."+suffix) |
| } |
| |
| func (c *Configuration) benchName(b *Benchmark) string { |
| return b.Name + "_" + c.Name |
| } |
| |
| func (c *Configuration) goCommandCopy() string { |
| gocmd := "go" |
| if c.rootCopy != "" { |
| gocmd = path.Join(c.rootCopy, "bin", gocmd) |
| } |
| return gocmd |
| } |
| |
| func (config *Configuration) createFilesForLater() { |
| if config.Disabled { |
| return |
| } |
| f, err := os.Create(config.buildBenchName()) |
| if err != nil { |
| fmt.Println("Error creating build benchmark file ", config.buildBenchName(), ", err=", err) |
| config.Disabled = true |
| } else { |
| fmt.Fprintf(f, "goos: %s\n", runtime.GOOS) |
| fmt.Fprintf(f, "goarch: %s\n", runtime.GOARCH) |
| f.Close() // will be appending later |
| } |
| |
| for _, cmd := range config.AfterBuild { |
| tbn := config.thingBenchName(cmd) |
| f, err := os.Create(tbn) |
| if err != nil { |
| fmt.Printf("Error creating %s benchmark file %s, err=%v\n", cmd, config.thingBenchName(cmd), err) |
| continue |
| } else { |
| f.Close() // will be appending later |
| } |
| } |
| } |
| |
| func (config *Configuration) runOtherBenchmarks(b *Benchmark, cwd string, cmdEnv []string) { |
| // Run various other "benchmark" commands on the built binaries, e.g., size, quality of debugging information. |
| if config.Disabled { |
| return |
| } |
| |
| for _, cmd := range config.AfterBuild { |
| tbn := config.thingBenchName(cmd) |
| f, err := os.OpenFile(tbn, os.O_WRONLY|os.O_APPEND, os.ModePerm) |
| if err != nil { |
| fmt.Printf("There was an error opening %s for append, error %v\n", tbn, err) |
| continue |
| } |
| |
| s := fmt.Sprintf("toolchain: %s\n", config.Name) |
| if verbose > 0 { |
| fmt.Print(s) |
| } |
| f.Write([]byte(s)) |
| |
| if !strings.ContainsAny(cmd, "/") { |
| cmd = path.Join(cwd, cmd) |
| } |
| if b.Disabled { |
| continue |
| } |
| testBinaryName := config.benchName(b) |
| c := exec.Command(cmd, path.Join(cwd, dirs.testBinDir, testBinaryName), strings.Title(b.Name)) |
| |
| c.Env = cmdEnv |
| if !b.NotSandboxed { |
| c.Env = replaceEnv(c.Env, "GOOS", "linux") |
| } |
| |
| if verbose > 0 { |
| fmt.Println(asCommandLine(cwd, c)) |
| } |
| output, err := c.CombinedOutput() |
| if verbose > 0 || err != nil { |
| fmt.Println(string(output)) |
| } else { |
| fmt.Print(".") |
| } |
| if err != nil { |
| fmt.Printf("Error running %s\n", cmd) |
| continue |
| } |
| f.Write(output) |
| f.Sync() |
| f.Close() |
| } |
| } |
| |
| func (config *Configuration) compileOne(bench *Benchmark, cwd string, count int) string { |
| root := config.rootCopy |
| gocmd := config.goCommandCopy() |
| gopath := path.Join(cwd, "gopath") |
| |
| cmd := exec.Command(gocmd, "test", "-vet=off", "-c") |
| compileTo := path.Join(dirs.wd, dirs.testBinDir, config.benchName(bench)) |
| |
| cmd.Env = DefaultEnv() |
| |
| if !bench.NotSandboxed { |
| cmd.Env = replaceEnv(cmd.Env, "GOOS", "linux") |
| } |
| if root != "" { |
| cmd.Env = replaceEnv(cmd.Env, "GOROOT", root) |
| } |
| cmd.Env = append(cmd.Env, "BENT_BENCH="+bench.Name) |
| cmd.Env = append(cmd.Env, "BENT_CONFIG="+config.Name) |
| cmd.Env = append(cmd.Env, "BENT_I="+fmt.Sprintf("%d", count)) |
| cmd.Env = replaceEnvs(cmd.Env, sliceExpandEnv(bench.GcEnv, cmd.Env)) |
| cmd.Env = replaceEnvs(cmd.Env, sliceExpandEnv(config.GcEnv, cmd.Env)) |
| configGoArch := getenv(cmd.Env, "GOARCH") |
| |
| cmdEnv := append([]string{}, cmd.Env...) // for after-build |
| if configGoArch == "" { |
| // inject a default, since the after-build may not be a go program |
| cmdEnv = append(cmdEnv, "GOARCH="+runtime.GOARCH) |
| } |
| |
| cmd.Args = append(cmd.Args, "-o", compileTo) |
| cmd.Args = append(cmd.Args, sliceExpandEnv(bench.BuildFlags, cmd.Env)...) |
| // Instead of cleaning the cache, specify -a; cache use changed with 1.20, which made builds take much longer. |
| cmd.Args = append(cmd.Args, "-a") |
| cmd.Args = append(cmd.Args, sliceExpandEnv(config.BuildFlags, cmd.Env)...) |
| |
| if config.PgoUse != "" { |
| // We want to use pprof file for pgo |
| cmd.Args = append(cmd.Args, "-pgo="+path.Join(dirs.wd, config.PgoUse, bench.Name+".prof")) |
| } |
| |
| if config.GcFlags != "" { |
| cmd.Args = append(cmd.Args, "-gcflags="+expandEnv(config.GcFlags, cmd.Env)) |
| } |
| cmd.Args = append(cmd.Args, bench.Repo) |
| cmd.Dir = bench.BuildDir // use module-mode |
| |
| if verbose > 0 { |
| fmt.Println(asCommandLine(cwd, cmd)) |
| } else { |
| fmt.Print(".") |
| } |
| |
| defer cleanup(gopath) |
| |
| start := time.Now() |
| output, err := cmd.CombinedOutput() |
| realTime := time.Since(start) |
| if err != nil { |
| s := "" |
| switch e := err.(type) { |
| case *exec.ExitError: |
| s = fmt.Sprintf("There was an error running 'go test', output = %s", output) |
| default: |
| s = fmt.Sprintf("There was an error running 'go test', output = %s, error = %v", output, e) |
| } |
| fmt.Println(s + "DISABLING benchmark " + bench.Name) |
| bench.Disabled = true // if it won't compile, it won't run, either. |
| return s + "(" + bench.Name + ")\n" |
| } |
| |
| if reportBuildTime { |
| // Report and record build stats to testbin |
| bs := BenchStat{ |
| Name: bench.Name, |
| RealTime: realTime, |
| UserTime: cmd.ProcessState.UserTime(), |
| SysTime: cmd.ProcessState.SystemTime(), |
| } |
| |
| buf := new(bytes.Buffer) |
| var goarchVal string |
| if configGoArch != runtime.GOARCH && configGoArch != "" { |
| goarchVal = fmt.Sprintf("%s-%s", runtime.GOARCH, configGoArch) |
| } else { |
| goarchVal = runtime.GOARCH |
| } |
| var s string |
| s += fmt.Sprintf("goarch: %s\n", goarchVal) |
| s += fmt.Sprintf("toolchain: %s\n", config.Name) |
| if verbose > 0 { |
| fmt.Print(s) |
| } |
| buf.WriteString(s) |
| s = fmt.Sprintf("Benchmark%s 1 %d build-real-ns/op %d build-user-ns/op %d build-sys-ns/op\n", |
| strings.Title(bench.Name), bs.RealTime.Nanoseconds(), bs.UserTime.Nanoseconds(), bs.SysTime.Nanoseconds()) |
| if verbose > 0 { |
| fmt.Print(s) |
| } |
| buf.WriteString(s) |
| f, err := os.OpenFile(config.buildBenchName(), os.O_WRONLY|os.O_APPEND, os.ModePerm) |
| if err != nil { |
| fmt.Printf("There was an error opening %s for append, error %v\n", config.buildBenchName(), err) |
| cleanup(gopath) |
| os.Exit(2) |
| } |
| f.Write(buf.Bytes()) |
| f.Sync() |
| f.Close() |
| } |
| |
| // Trim /usr/bin/time info from soutput, it's ugly |
| if verbose > 0 { |
| soutput := string(output) |
| i := strings.LastIndex(soutput, "real") |
| if i >= 0 { |
| soutput = soutput[:i] |
| } |
| fmt.Print(soutput) |
| } |
| |
| // Do this here before any cleanup. |
| if count == 0 { |
| config.runOtherBenchmarks(bench, cwd, cmdEnv) |
| } |
| |
| return "" |
| } |
| |
| // say writes s to c's benchmark output file |
| func (c *Configuration) say(s string) { |
| b := []byte(s) |
| nw, err := c.benchWriter.Write(b) |
| if err != nil { |
| fmt.Printf("Error writing, err = %v, nwritten = %d, nrequested = %d\n", err, nw, len(b)) |
| } |
| c.benchWriter.Sync() |
| fmt.Print(string(b)) |
| } |
| |
| // runBinary runs cmd and displays the output. |
| // If the command returns an error, returns an error string. |
| func (c *Configuration) runBinary(cwd string, cmd *exec.Cmd, printWorkingDot bool) (string, int) { |
| line := asCommandLine(cwd, cmd) |
| if verbose > 0 { |
| fmt.Println(line) |
| } else { |
| if printWorkingDot { |
| fmt.Print(".") |
| } |
| } |
| |
| rc := 0 |
| |
| stdout, err := cmd.StdoutPipe() |
| if err != nil { |
| return fmt.Sprintf("Error [stdoutpipe] running '%s', %v", line, err), rc |
| } |
| stderr, err := cmd.StderrPipe() |
| if err != nil { |
| return fmt.Sprintf("Error [stderrpipe] running '%s', %v", line, err), rc |
| } |
| err = cmd.Start() |
| if err != nil { |
| return fmt.Sprintf("Error [command start] running '%s', %v", line, err), rc |
| } |
| |
| var mu = &sync.Mutex{} |
| |
| f := func(r *bufio.Reader, done chan error) { |
| for { |
| bytes, err := r.ReadBytes('\n') |
| n := len(bytes) |
| if n > 0 { |
| mu.Lock() |
| nw, err := c.benchWriter.Write(bytes[0:n]) |
| if err != nil { |
| fmt.Printf("Error writing, err = %v, nwritten = %d, nrequested = %d\n", err, nw, n) |
| } |
| c.benchWriter.Sync() |
| fmt.Print(string(bytes[0:n])) |
| mu.Unlock() |
| } |
| if err == io.EOF || n == 0 { |
| break |
| } |
| if err != nil { |
| done <- err |
| return |
| } |
| } |
| done <- nil |
| } |
| |
| doneS := make(chan error) |
| doneE := make(chan error) |
| |
| go f(bufio.NewReader(stdout), doneS) |
| go f(bufio.NewReader(stderr), doneE) |
| |
| errS := <-doneS |
| errE := <-doneE |
| |
| err = cmd.Wait() |
| rc = cmd.ProcessState.ExitCode() |
| |
| if err != nil { |
| switch e := err.(type) { |
| case *exec.ExitError: |
| return fmt.Sprintf("Error running '%s', stderr = %s, rc = %d", line, e.Stderr, rc), rc |
| default: |
| return fmt.Sprintf("Error running '%s', %v, rc = %d", line, e, rc), rc |
| |
| } |
| } |
| if errS != nil { |
| return fmt.Sprintf("Error [read stdout] running '%s', %v, rc = %d", line, errS, rc), rc |
| } |
| if errE != nil { |
| return fmt.Sprintf("Error [read stderr] running '%s', %v, rc = %d", line, errE, rc), rc |
| } |
| return "", rc |
| } |