cmd/bench: add benchmark wrapper The coordinator is getting support for running the benchmarks in this repository. Since the benchmarks and interface are in flux, encoding all of the details of running Go tests, bent arguments, etc into the coordinator will likely cause churn and frustrating migration issues. Instead, add cmd/bench which serves as the simple entrypoint for the coordinator. The coordinator runs cmd/bench with the GOROOT to test (eventually multiple GOROOTs), and this binary takes care of the remaining details. Right now, we just do a basic go test golang.org/x/benchmarks/... and simple invocation of bent. Note that bent does not pass without https://golang.org/cl/354634. For golang/go#49207 Change-Id: I5c9cf89540cab605c0a64e17af85311d37985c25 Reviewed-on: https://go-review.googlesource.com/c/benchmarks/+/359854 Trust: Michael Pratt <mpratt@google.com> Run-TryBot: Michael Pratt <mpratt@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Michael Knyszek <mknyszek@google.com>
diff --git a/cmd/bench/bent.go b/cmd/bench/bent.go new file mode 100644 index 0000000..6e20dda --- /dev/null +++ b/cmd/bench/bent.go
@@ -0,0 +1,120 @@ +// 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 ( + "bytes" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "runtime" + "text/template" +) + +// TODO(prattmic): refactor bent to export Todo so we can directly build this +// in Go. +var configurationTmpl = template.Must(template.New("configuration").Parse(` +[[Configurations]] + Name = "Benchmark" + Root = "{{.}}" +`)) + +func writeConfiguration(filename, goroot string) error { + var buf bytes.Buffer + if err := configurationTmpl.Execute(&buf, goroot); err != nil { + return fmt.Errorf("error generating configuration: %w", err) + } + + log.Printf("bent configuration for GOROOT %s:\n%s", goroot, buf.String()) + + if err := os.WriteFile(filename, buf.Bytes(), 0644); err != nil { + return fmt.Errorf("error creating configurations.toml: %w", err) + } + + return nil +} + +// removeAllIncludingReadonly is like os.RemoveAll except that it'll +// also try to change permissions to work around permission errors +// when deleting. +func removeAllIncludingReadonly(dir string) error { + err := os.RemoveAll(dir) + if err == nil || !os.IsPermission(err) || runtime.GOOS == "windows" /* different fs permission model */ { + return err + } + // Make a best effort (ignoring errors) attempt to make all + // files and directories writable before we try to delete them + // all again. + filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { + const ownerWritable = 0200 + if err != nil || fi.Mode().Perm()&ownerWritable != 0 { + return nil + } + os.Chmod(path, fi.Mode().Perm()|ownerWritable) + return nil + }) + return os.RemoveAll(dir) +} + +func bent(goroot string) (err error) { + dir, err := os.MkdirTemp("", "bent") + if err != nil { + return fmt.Errorf("error creating temporary directory: %w", err) + } + defer func() { + err = removeAllIncludingReadonly(dir) + if err != nil { + err = fmt.Errorf("error removing temporary directory: %w", err) + } + }() + log.Printf("Bent temporary directory: %s", dir) + + bentPath := filepath.Join(dir, "bent") + + log.Printf("Building bent...") + + // Build bent itself. N.B. we don't need to do this with the goroot + // under test since we aren't testing bent itself, but we are sure that + // this toolchain exists. + // + // TODO(prattmic): do this only once on first call? + cmd := goCommand(goroot, "build", "-o", bentPath, "golang.org/x/benchmarks/cmd/bent") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("error building bent: %w", err) + } + + log.Printf("Initializing bent...") + + // Initialize scratch dir for bent. + cmd = exec.Command(bentPath, "-I") + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("error running bent -I: %w", err) + } + + confFile := filepath.Join(dir, "configurations.toml") + if err := writeConfiguration(confFile, goroot); err != nil { + return fmt.Errorf("error writing configuration: %w", err) + } + + log.Printf("Running bent...") + + // Finally we can actually run the benchmarks. + cmd = exec.Command(bentPath, "-C", confFile, "-B", filepath.Join(dir, "benchmarks-50.toml")) + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("error running bent -I: %w", err) + } + + return nil +}
diff --git a/cmd/bench/gotest.go b/cmd/bench/gotest.go new file mode 100644 index 0000000..accf6a0 --- /dev/null +++ b/cmd/bench/gotest.go
@@ -0,0 +1,34 @@ +// 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 ( + "log" + "os" + "strings" +) + +func goTest(goroot string) error { + log.Printf("Running Go test benchmarks for GOROOT %s", goroot) + + cmd := goCommand(goroot, "test", "-v", "-run=none", "-bench=.", "-count=5", "golang.org/x/benchmarks/...") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + env := os.Environ() + needGOROOT := true + for i := range env { + if strings.HasPrefix(env[i], "GOROOT=") { + env[i] = "GOROOT=" + goroot + needGOROOT = false + } + } + if needGOROOT { + env = append(env, "GOROOT=" + goroot) + } + cmd.Env = env + + return cmd.Run() +}
diff --git a/cmd/bench/main.go b/cmd/bench/main.go new file mode 100644 index 0000000..3699331 --- /dev/null +++ b/cmd/bench/main.go
@@ -0,0 +1,61 @@ +// 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. + +// Binary bench provides a unified wrapper around the different types of +// benchmarks in x/benchmarks. +package main + +import ( + "log" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func determineGOROOT() (string, error) { + g, ok := os.LookupEnv("GOROOT") + if ok { + return g, nil + } + + cmd := exec.Command("go", "env", "GOROOT") + b, err := cmd.Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(b)), nil +} + +func goCommand(goroot string, args ...string) *exec.Cmd { + bin := filepath.Join(goroot, "bin/go") + cmd := exec.Command(bin, args...) + return cmd +} + +func main() { + goroot, err := determineGOROOT() + if err != nil { + log.Fatalf("Unable to determine GOROOT: %v", err) + } + + log.Printf("GOROOT under test: %s", goroot) + + pass := true + + if err := goTest(goroot); err != nil { + pass = false + log.Printf("Error running Go tests: %v", err) + } + + if err := bent(goroot); err != nil { + pass = false + log.Printf("Error running bent: %v", err) + } + + if !pass { + log.Printf("FAIL") + os.Exit(1) + } +}