blob: 3edc700f96fb3a6e8274641a6fca90f518e045fb [file] [log] [blame]
// 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.
package main
import (
"flag"
"fmt"
"io"
"math/rand"
"os"
"os/exec"
"path/filepath"
"strings"
"golang.org/x/benchmarks/sweet/benchmarks/internal/cgroups"
"golang.org/x/benchmarks/sweet/benchmarks/internal/driver"
"golang.org/x/benchmarks/sweet/common"
"golang.org/x/benchmarks/sweet/common/diagnostics"
)
var (
goTool string
tmpDir string
toolexec bool
benchName string
diag *driver.Diagnostics
)
func init() {
driver.SetFlags(flag.CommandLine)
flag.StringVar(&goTool, "go", "", "path to cmd/go binary")
flag.StringVar(&tmpDir, "tmp", "", "work directory (cleared before use)")
flag.BoolVar(&toolexec, "toolexec", false, "run as a toolexec binary")
flag.StringVar(&benchName, "bench-name", "", "for -toolexec")
flag.Func("diagnostics", "for -toolexec", func(s string) error {
diag = new(driver.Diagnostics)
return diag.UnmarshalText([]byte(s))
})
}
func tmpResultsDir() string {
return filepath.Join(tmpDir, "results")
}
var benchOpts = []driver.RunOption{
driver.DoTime(true),
}
func run(pkgPath string) error {
// Clear any stale results from previous runs and recreate
// the directory.
if err := os.RemoveAll(tmpResultsDir()); err != nil {
return err
}
if err := os.MkdirAll(tmpResultsDir(), 0777); err != nil {
return err
}
name := "GoBuild" + strings.Title(filepath.Base(pkgPath))
cmdArgs := []string{goTool, "build", "-a"}
// Build a command comprised of this binary to pass to -toolexec.
selfPath, err := filepath.Abs(os.Args[0])
if err != nil {
return err
}
selfCmd := []string{
selfPath, "-toolexec",
"-bench-name", name,
}
flag.CommandLine.Visit(func(f *flag.Flag) {
if f.Name == "go" || f.Name == "bench-name" || strings.HasPrefix(f.Name, "perf") {
// No need to pass this along.
return
}
selfCmd = append(selfCmd, "-"+f.Name, f.Value.String())
})
diag = driver.NewDiagnostics(name)
diagFlag, err := diag.MarshalText()
if err != nil {
return err
}
selfCmd = append(selfCmd, "-diagnostics", string(diagFlag))
cmdArgs = append(cmdArgs, "-toolexec", strings.Join(selfCmd, " "))
if df, err := diag.Create(diagnostics.Perf); err != nil {
fmt.Fprintf(os.Stderr, "failed to create %s diagnostics: %s\n", diagnostics.Perf, err)
} else if df != nil {
df.Close()
defer df.Commit()
perfArgs := []string{"perf", "record", "-o", df.Name()}
perfArgs = append(perfArgs, driver.PerfFlags()...)
perfArgs = append(perfArgs, cmdArgs...)
cmdArgs = perfArgs
}
baseCmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
baseCmd.Dir = pkgPath
baseCmd.Env = common.NewEnvFromEnviron().MustSet("GOROOT=" + filepath.Dir(filepath.Dir(goTool))).Collapse()
baseCmd.Stdout = os.Stderr // Redirect all tool output to stderr.
baseCmd.Stderr = os.Stderr
cmd, err := cgroups.WrapCommand(baseCmd, "test.scope")
if err != nil {
return err
}
err = driver.RunBenchmark(name, func(d *driver.B) error {
defer diag.Commit(d)
return cmd.Run()
}, append(benchOpts, driver.DoAvgRSS(cmd.RSSFunc()))...)
if err != nil {
return err
}
return printOtherResults(tmpResultsDir())
}
func printOtherResults(dir string) error {
entries, err := os.ReadDir(dir)
if err != nil {
return err
}
for _, entry := range entries {
name := entry.Name()
path := filepath.Join(dir, name)
if strings.HasSuffix(name, ".results") {
f, err := os.Open(path)
if err != nil {
return err
}
// Results canonically go to stdout.
if _, err := io.Copy(os.Stdout, f); err != nil {
f.Close()
return err
}
f.Close()
}
}
return nil
}
func runToolexec() error {
var benchSuffix string
benchmark := false
bin := filepath.Base(flag.Arg(0))
switch bin {
case "compile":
case "link":
benchSuffix = "Link"
benchmark = true
default:
cmd := exec.Command(flag.Args()[0], flag.Args()[1:]...)
// Passing through stdout is necessary here because the go tool needs
// to be able to read stdout from some of these tools. This won't appear
// in the final output.
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
var extraFlags []string
for _, typ := range []diagnostics.Type{diagnostics.CPUProfile, diagnostics.MemProfile, diagnostics.Trace} {
if bin == "link" && typ == diagnostics.Trace {
// TODO(mknyszek): Traces are not supported for the linker.
continue
}
subName := ""
if !typ.CanMerge() {
// Create a unique name for this diagnostic file.
subName = fmt.Sprintf("%08x", rand.Uint32())
}
df, err := diag.CreateNamed(typ, subName)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to create %s diagnostics: %s\n", typ, err)
continue
} else if df != nil {
df.Close()
defer df.Commit()
flag := "-" + string(typ)
if typ == diagnostics.Trace {
flag = "-traceprofile"
}
extraFlags = append(extraFlags, flag, df.Name())
}
}
cmd := exec.Command(flag.Args()[0], append(extraFlags, flag.Args()[1:]...)...)
// Passing through stdout is necessary here because the go tool needs
// to be able to read stdout from some of these tools. This won't appear
// in the final output.
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if benchmark {
name := benchName + benchSuffix
f, err := os.Create(filepath.Join(tmpResultsDir(), name+".results"))
if err != nil {
return err
}
defer f.Close()
return driver.RunBenchmark(name, func(d *driver.B) error {
return cmd.Run()
}, append(benchOpts, driver.WriteResultsTo(f))...)
}
return cmd.Run()
}
func main() {
flag.Parse()
if toolexec {
if err := runToolexec(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
return
}
if flag.NArg() != 1 {
fmt.Fprintf(os.Stderr, "error: expected one argument\n")
os.Exit(1)
}
if err := run(flag.Arg(0)); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}