sweet: add support for execution traces and refactor profile plumbing
This change does a lot at once, but it's mostly refactoring. First, it
moves most of the profile abstraction out of benchmarks/internal/driver
and into a new shared package called diagnostics. It also renames
profiles to diagnostics to better capture the breadth of what this
mechanism collects. Then, it adds support for turning on diagnostics
from configuration files. Next, it adds support for generating
additional configurations to capture the overhead of collecting
diagnostics, starting with CPU profiling. Lastly, it adds support for
the new Trace diagnostic.
(This change also fixes a bug in go-build where Linux perf flags weren't
being propagated.)
In the future, core dumps could easily be folded into this new
diagnostics abstraction.
For golang/go#57175.
Change-Id: I999773e8be28c46fb5d4f6a79a94d542491e3754
Reviewed-on: https://go-review.googlesource.com/c/benchmarks/+/459095
Run-TryBot: Michael Knyszek <mknyszek@google.com>
Reviewed-by: Michael Pratt <mpratt@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/sweet/benchmarks/go-build/main.go b/sweet/benchmarks/go-build/main.go
index c49ad0b..3d805ad 100644
--- a/sweet/benchmarks/go-build/main.go
+++ b/sweet/benchmarks/go-build/main.go
@@ -17,6 +17,7 @@
"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"
sprofile "golang.org/x/benchmarks/sweet/common/profile"
)
@@ -67,7 +68,7 @@
"-bench-name", name,
}
flag.CommandLine.Visit(func(f *flag.Flag) {
- if f.Name == "go" || f.Name == "bench-name" {
+ if f.Name == "go" || f.Name == "bench-name" || strings.HasPrefix(f.Name, "perf") {
// No need to pass this along.
return
}
@@ -76,8 +77,12 @@
cmdArgs = append(cmdArgs, "-toolexec", strings.Join(selfCmd, " "))
var baseCmd *exec.Cmd
- if driver.ProfilingEnabled(driver.ProfilePerf) {
- baseCmd = exec.Command("perf", append([]string{"record", "-o", filepath.Join(tmpDir, "perf.data"), goTool}, cmdArgs...)...)
+ if driver.DiagnosticEnabled(diagnostics.Perf) {
+ perfArgs := []string{"record", "-o", filepath.Join(tmpDir, "perf.data")}
+ perfArgs = append(perfArgs, driver.PerfFlags()...)
+ perfArgs = append(perfArgs, goTool)
+ perfArgs = append(perfArgs, cmdArgs...)
+ baseCmd = exec.Command("perf", perfArgs...)
} else {
baseCmd = exec.Command(goTool, cmdArgs...)
}
@@ -98,41 +103,55 @@
// Handle any CPU profiles produced, and merge them.
// Then, write them out to the canonical profiles above.
- if driver.ProfilingEnabled(driver.ProfileCPU) {
- compileProfile, err := mergeProfiles(tmpDir, profilePrefix("compile", driver.ProfileCPU))
+ if driver.DiagnosticEnabled(diagnostics.CPUProfile) {
+ compileProfile, err := mergePprofProfiles(tmpDir, profilePrefix("compile", diagnostics.CPUProfile))
if err != nil {
return err
}
- if err := driver.WriteProfile(compileProfile, driver.ProfileCPU, name+"Compile"); err != nil {
+ if err := driver.WritePprofProfile(compileProfile, diagnostics.CPUProfile, name+"Compile"); err != nil {
return err
}
- linkProfile, err := mergeProfiles(tmpDir, profilePrefix("link", driver.ProfileCPU))
+ linkProfile, err := mergePprofProfiles(tmpDir, profilePrefix("link", diagnostics.CPUProfile))
if err != nil {
return err
}
- if err := driver.WriteProfile(linkProfile, driver.ProfileCPU, name+"Link"); err != nil {
+ if err := driver.WritePprofProfile(linkProfile, diagnostics.CPUProfile, name+"Link"); err != nil {
return err
}
}
- if driver.ProfilingEnabled(driver.ProfileMem) {
- if err := copyProfiles(tmpDir, "compile", driver.ProfileMem, name+"Compile"); err != nil {
+ if driver.DiagnosticEnabled(diagnostics.MemProfile) {
+ if err := copyPprofProfiles(tmpDir, "compile", diagnostics.MemProfile, name+"Compile"); err != nil {
return err
}
- if err := copyProfiles(tmpDir, "link", driver.ProfileMem, name+"Link"); err != nil {
+ if err := copyPprofProfiles(tmpDir, "link", diagnostics.MemProfile, name+"Link"); err != nil {
return err
}
}
- if driver.ProfilingEnabled(driver.ProfilePerf) {
- if err := driver.CopyProfile(filepath.Join(tmpDir, "perf.data"), driver.ProfilePerf, name); err != nil {
+ if driver.DiagnosticEnabled(diagnostics.Perf) {
+ if err := driver.CopyDiagnosticData(filepath.Join(tmpDir, "perf.data"), diagnostics.Perf, name); err != nil {
return err
}
}
+ if driver.DiagnosticEnabled(diagnostics.Trace) {
+ entries, err := os.ReadDir(tmpDir)
+ if err != nil {
+ return err
+ }
+ for _, entry := range entries {
+ if !strings.HasPrefix(entry.Name(), profilePrefix("compile", diagnostics.Trace)) {
+ continue
+ }
+ if err := driver.CopyDiagnosticData(filepath.Join(tmpDir, entry.Name()), diagnostics.Trace, name+"Compile"); err != nil {
+ return err
+ }
+ }
+ }
return printOtherResults(tmpResultsDir())
}
-func mergeProfiles(dir, prefix string) (*profile.Profile, error) {
- profiles, err := sprofile.ReadDir(dir, func(name string) bool {
+func mergePprofProfiles(dir, prefix string) (*profile.Profile, error) {
+ profiles, err := sprofile.ReadDirPprof(dir, func(name string) bool {
return strings.HasPrefix(name, prefix)
})
if err != nil {
@@ -141,23 +160,23 @@
return profile.Merge(profiles)
}
-func copyProfiles(dir, bin string, typ driver.ProfileType, finalPrefix string) error {
+func copyPprofProfiles(dir, bin string, typ diagnostics.Type, finalPrefix string) error {
prefix := profilePrefix(bin, typ)
- profiles, err := sprofile.ReadDir(dir, func(name string) bool {
+ profiles, err := sprofile.ReadDirPprof(dir, func(name string) bool {
return strings.HasPrefix(name, prefix)
})
if err != nil {
return err
}
for _, profile := range profiles {
- if err := driver.WriteProfile(profile, typ, finalPrefix); err != nil {
+ if err := driver.WritePprofProfile(profile, typ, finalPrefix); err != nil {
return err
}
}
return nil
}
-func profilePrefix(bin string, typ driver.ProfileType) string {
+func profilePrefix(bin string, typ diagnostics.Type) string {
return bin + "-prof." + string(typ)
}
@@ -200,15 +219,23 @@
return cmd.Run()
}
var extraFlags []string
- for _, typ := range []driver.ProfileType{driver.ProfileCPU, driver.ProfileMem} {
- if driver.ProfilingEnabled(typ) {
+ for _, typ := range []diagnostics.Type{diagnostics.CPUProfile, diagnostics.MemProfile, diagnostics.Trace} {
+ if driver.DiagnosticEnabled(typ) {
+ if bin == "link" && typ == diagnostics.Trace {
+ // TODO(mknyszek): Traces are not supported for the linker.
+ continue
+ }
// Stake a claim for a filename.
f, err := os.CreateTemp(tmpDir, profilePrefix(bin, typ))
if err != nil {
return err
}
f.Close()
- extraFlags = append(extraFlags, "-"+string(typ)+"profile", f.Name())
+ flag := "-" + string(typ)
+ if typ == diagnostics.Trace {
+ flag += "profile" // The compiler flag is -traceprofile.
+ }
+ extraFlags = append(extraFlags, flag, f.Name())
}
}
cmd := exec.Command(flag.Args()[0], append(extraFlags, flag.Args()[1:]...)...)
diff --git a/sweet/benchmarks/gvisor/common.go b/sweet/benchmarks/gvisor/common.go
index aeb2879..7f18e16 100644
--- a/sweet/benchmarks/gvisor/common.go
+++ b/sweet/benchmarks/gvisor/common.go
@@ -15,6 +15,7 @@
"golang.org/x/benchmarks/sweet/benchmarks/internal/driver"
"golang.org/x/benchmarks/sweet/common"
+ "golang.org/x/benchmarks/sweet/common/diagnostics"
)
func workloadsPath(assetsDir, subBenchmark string) string {
@@ -23,15 +24,15 @@
return filepath.Join(assetsDir, subBenchmark, "bin", platformDir, "workload")
}
-func (c *config) profilePath(typ driver.ProfileType) string {
+func (c *config) profilePath(typ diagnostics.Type) string {
return filepath.Join(c.tmpDir, string(typ)+".prof")
}
func (cfg *config) runscCmd(arg ...string) *exec.Cmd {
var cmd *exec.Cmd
goProfiling := false
- for _, typ := range []driver.ProfileType{driver.ProfileCPU, driver.ProfileMem} {
- if driver.ProfilingEnabled(typ) {
+ for _, typ := range []diagnostics.Type{diagnostics.CPUProfile, diagnostics.MemProfile, diagnostics.Trace} {
+ if driver.DiagnosticEnabled(typ) {
goProfiling = true
break
}
@@ -39,14 +40,21 @@
if goProfiling {
arg = append([]string{"-profile"}, arg...)
}
- if driver.ProfilingEnabled(driver.ProfileCPU) {
- arg = append([]string{"-profile-cpu", cfg.profilePath(driver.ProfileCPU)}, arg...)
+ if driver.DiagnosticEnabled(diagnostics.CPUProfile) {
+ arg = append([]string{"-profile-cpu", cfg.profilePath(diagnostics.CPUProfile)}, arg...)
}
- if driver.ProfilingEnabled(driver.ProfileMem) {
- arg = append([]string{"-profile-heap", cfg.profilePath(driver.ProfileMem)}, arg...)
+ if driver.DiagnosticEnabled(diagnostics.MemProfile) {
+ arg = append([]string{"-profile-heap", cfg.profilePath(diagnostics.MemProfile)}, arg...)
}
- if driver.ProfilingEnabled(driver.ProfilePerf) {
- cmd = exec.Command("perf", append([]string{"record", "-o", cfg.profilePath(driver.ProfilePerf), cfg.runscPath}, arg...)...)
+ if driver.DiagnosticEnabled(diagnostics.Trace) {
+ arg = append([]string{"-trace", cfg.profilePath(diagnostics.Trace)}, arg...)
+ }
+ if driver.DiagnosticEnabled(diagnostics.Perf) {
+ perfArgs := []string{"record", "-o", cfg.profilePath(diagnostics.Perf)}
+ perfArgs = append(perfArgs, driver.PerfFlags()...)
+ perfArgs = append(perfArgs, cfg.runscPath)
+ perfArgs = append(perfArgs, arg...)
+ cmd = exec.Command("perf", perfArgs...)
} else {
cmd = exec.Command(cfg.runscPath, arg...)
}
diff --git a/sweet/benchmarks/gvisor/main.go b/sweet/benchmarks/gvisor/main.go
index 704fec9..b288e28 100644
--- a/sweet/benchmarks/gvisor/main.go
+++ b/sweet/benchmarks/gvisor/main.go
@@ -17,6 +17,7 @@
"time"
"golang.org/x/benchmarks/sweet/benchmarks/internal/driver"
+ "golang.org/x/benchmarks/sweet/common/diagnostics"
)
type config struct {
@@ -66,12 +67,12 @@
}
return err
}
- for _, typ := range driver.ProfileTypes {
- if !driver.ProfilingEnabled(typ) {
+ for _, typ := range diagnostics.Types() {
+ if !driver.DiagnosticEnabled(typ) {
continue
}
// runscCmd ensures these are created if necessary.
- if err := driver.CopyProfile(cliCfg.profilePath(typ), typ, bench.name()); err != nil {
+ if err := driver.CopyDiagnosticData(cliCfg.profilePath(typ), typ, bench.name()); err != nil {
return err
}
}
diff --git a/sweet/benchmarks/internal/driver/driver.go b/sweet/benchmarks/internal/driver/driver.go
index bb6e3eb..36d3ed7 100644
--- a/sweet/benchmarks/internal/driver/driver.go
+++ b/sweet/benchmarks/internal/driver/driver.go
@@ -13,6 +13,7 @@
"os/exec"
"path/filepath"
"runtime/pprof"
+ "runtime/trace"
"sort"
"strconv"
"strings"
@@ -20,23 +21,17 @@
"time"
"github.com/google/pprof/profile"
+ "golang.org/x/benchmarks/sweet/common/diagnostics"
)
var (
- coreDumpDir string
- cpuProfileDir string
- memProfileDir string
- perfDir string
- perfFlags string
- short bool
+ coreDumpDir string
+ diag map[diagnostics.Type]*diagnostics.DriverConfig
)
func SetFlags(f *flag.FlagSet) {
f.StringVar(&coreDumpDir, "dump-cores", "", "dump a core file to the given directory after every benchmark run")
- f.StringVar(&cpuProfileDir, "cpuprofile", "", "write a CPU profile to the given directory after every benchmark run")
- f.StringVar(&memProfileDir, "memprofile", "", "write a memory profile to the given directory after every benchmark run")
- f.StringVar(&perfDir, "perf", "", "write a Linux perf data file to the given directory after every benchmark run")
- f.StringVar(&perfFlags, "perf-flags", "", "pass the following additional flags to Linux perf")
+ diag = diagnostics.SetFlagsForDriver(f)
}
const (
@@ -88,19 +83,25 @@
func DoCPUProfile(v bool) RunOption {
return func(b *B) {
- b.doProfile[ProfileCPU] = v
+ b.collectDiag[diagnostics.CPUProfile] = v
}
}
func DoMemProfile(v bool) RunOption {
return func(b *B) {
- b.doProfile[ProfileMem] = v
+ b.collectDiag[diagnostics.MemProfile] = v
}
}
func DoPerf(v bool) RunOption {
return func(b *B) {
- b.doProfile[ProfilePerf] = v
+ b.collectDiag[diagnostics.Perf] = v
+ }
+}
+
+func DoTrace(v bool) RunOption {
+ return func(b *B) {
+ b.collectDiag[diagnostics.Trace] = v
}
}
@@ -108,9 +109,10 @@
return func(b *B) {
b.pid = pid
if pid != os.Getpid() {
- b.doProfile[ProfileCPU] = false
- b.doProfile[ProfileMem] = false
- b.doProfile[ProfilePerf] = false
+ b.collectDiag[diagnostics.CPUProfile] = false
+ b.collectDiag[diagnostics.MemProfile] = false
+ b.collectDiag[diagnostics.Perf] = false
+ b.collectDiag[diagnostics.Trace] = false
}
}
}
@@ -136,6 +138,7 @@
DoCPUProfile(true),
DoMemProfile(true),
DoPerf(true),
+ DoTrace(true),
}
type B struct {
@@ -148,13 +151,13 @@
doPeakRSS bool
doPeakVM bool
doCoreDump bool
- doProfile map[ProfileType]bool
+ collectDiag map[diagnostics.Type]bool
rssFunc func() (uint64, error)
statsMu sync.Mutex
stats map[string]uint64
ops int
wg sync.WaitGroup
- profiles map[ProfileType]*os.File
+ diagnostics map[diagnostics.Type]*os.File
resultsWriter io.Writer
perfProcess *os.Process
}
@@ -163,13 +166,13 @@
b := &B{
pid: os.Getpid(),
name: name,
- doProfile: map[ProfileType]bool{
- ProfileCPU: false,
- ProfileMem: false,
+ collectDiag: map[diagnostics.Type]bool{
+ diagnostics.CPUProfile: false,
+ diagnostics.MemProfile: false,
},
- stats: make(map[string]uint64),
- ops: 1,
- profiles: make(map[ProfileType]*os.File),
+ stats: make(map[string]uint64),
+ ops: 1,
+ diagnostics: make(map[diagnostics.Type]*os.File),
}
return b
}
@@ -180,15 +183,19 @@
b.stats[name] = value
}
-func (b *B) shouldProfile(typ ProfileType) bool {
- return b.doProfile[typ] && ProfilingEnabled(typ)
+func (b *B) shouldCollectDiag(typ diagnostics.Type) bool {
+ return b.collectDiag[typ] && DiagnosticEnabled(typ)
+}
+
+func (b *B) Name() string {
+ return b.name
}
func (b *B) StartTimer() {
- if b.shouldProfile(ProfileCPU) {
- pprof.StartCPUProfile(b.profiles[ProfileCPU])
+ if b.shouldCollectDiag(diagnostics.CPUProfile) {
+ pprof.StartCPUProfile(b.diagnostics[diagnostics.CPUProfile])
}
- if b.shouldProfile(ProfilePerf) {
+ if b.shouldCollectDiag(diagnostics.Perf) {
if err := b.startPerf(); err != nil {
warningf("failed to start perf: %v", err)
}
@@ -197,18 +204,18 @@
}
func (b *B) ResetTimer() {
- if b.shouldProfile(ProfileCPU) {
+ if b.shouldCollectDiag(diagnostics.CPUProfile) {
pprof.StopCPUProfile()
- if err := b.truncateProfile(ProfileCPU); err != nil {
+ if err := b.truncateDiagnosticData(diagnostics.CPUProfile); err != nil {
warningf("failed to truncate CPU profile: %v", err)
}
- pprof.StartCPUProfile(b.profiles[ProfileCPU])
+ pprof.StartCPUProfile(b.diagnostics[diagnostics.CPUProfile])
}
- if b.shouldProfile(ProfilePerf) {
+ if b.shouldCollectDiag(diagnostics.Perf) {
if err := b.stopPerf(); err != nil {
warningf("failed to stop perf: %v", err)
}
- if err := b.truncateProfile(ProfilePerf); err != nil {
+ if err := b.truncateDiagnosticData(diagnostics.Perf); err != nil {
warningf("failed to truncate perf data file: %v", err)
}
if err := b.startPerf(); err != nil {
@@ -221,8 +228,8 @@
b.dur = 0
}
-func (b *B) truncateProfile(typ ProfileType) error {
- f := b.profiles[typ]
+func (b *B) truncateDiagnosticData(typ diagnostics.Type) error {
+ f := b.diagnostics[typ]
_, err := f.Seek(0, 0)
if err != nil {
return err
@@ -238,10 +245,10 @@
b.dur += end.Sub(b.start)
b.start = time.Time{}
- if b.shouldProfile(ProfileCPU) {
+ if b.shouldCollectDiag(diagnostics.CPUProfile) {
pprof.StopCPUProfile()
}
- if b.shouldProfile(ProfilePerf) {
+ if b.shouldCollectDiag(diagnostics.Perf) {
if err := b.stopPerf(); err != nil {
warningf("failed to stop perf: %v", err)
}
@@ -402,8 +409,8 @@
if b.perfProcess != nil {
panic("perf process already started")
}
- args := []string{"record", "-o", b.profiles[ProfilePerf].Name(), "-p", strconv.Itoa(b.pid)}
- if perfFlags != "" {
+ args := []string{"record", "-o", b.diagnostics[diagnostics.Perf].Name(), "-p", strconv.Itoa(b.pid)}
+ if perfFlags := diag[diagnostics.Perf].Flags; perfFlags != "" {
args = append(args, strings.Split(perfFlags, " ")...)
}
cmd := exec.Command("perf", args...)
@@ -439,13 +446,19 @@
stop := b.startRSSSampler()
// Make sure profile file(s) are created if necessary.
- for _, typ := range ProfileTypes {
- if b.shouldProfile(typ) {
- f, err := newProfileFile(typ, b.name)
+ for _, typ := range diagnostics.Types() {
+ if b.shouldCollectDiag(typ) {
+ f, err := newDiagnosticDataFile(typ, b.name)
if err != nil {
return err
}
- b.profiles[typ] = f
+ b.diagnostics[typ] = f
+ }
+ }
+
+ if b.shouldCollectDiag(diagnostics.Trace) {
+ if err := trace.Start(b.diagnostics[diagnostics.Trace]); err != nil {
+ return err
}
}
@@ -507,12 +520,15 @@
b.wg.Wait()
// Finalize all the profile files we're handling ourselves.
- for typ, f := range b.profiles {
- if typ == ProfileMem {
+ for typ, f := range b.diagnostics {
+ if typ == diagnostics.MemProfile {
if err := pprof.Lookup("heap").WriteTo(f, 0); err != nil {
return err
}
}
+ if typ == diagnostics.Trace {
+ trace.Stop()
+ }
f.Close()
}
@@ -521,37 +537,22 @@
return nil
}
-type ProfileType string
-
-const (
- ProfileCPU ProfileType = "cpu"
- ProfileMem ProfileType = "mem"
- ProfilePerf ProfileType = "perf"
-)
-
-var ProfileTypes = []ProfileType{
- ProfileCPU,
- ProfileMem,
- ProfilePerf,
+func DiagnosticEnabled(typ diagnostics.Type) bool {
+ cfg, ok := diag[typ]
+ if !ok {
+ panic("bad profile type")
+ }
+ return cfg.Dir != ""
}
-func ProfilingEnabled(typ ProfileType) bool {
- switch typ {
- case ProfileCPU:
- return cpuProfileDir != ""
- case ProfileMem:
- return memProfileDir != ""
- case ProfilePerf:
- return perfDir != ""
+func WritePprofProfile(prof *profile.Profile, typ diagnostics.Type, pattern string) error {
+ if !typ.IsPprof() {
+ return fmt.Errorf("this type of diagnostic doesn't use the pprof format")
}
- panic("bad profile type")
-}
-
-func WriteProfile(prof *profile.Profile, typ ProfileType, pattern string) error {
- if !ProfilingEnabled(typ) {
- return fmt.Errorf("this type of profile is not currently enabled")
+ if !DiagnosticEnabled(typ) {
+ return fmt.Errorf("this type of diagnostic is not currently enabled")
}
- f, err := newProfileFile(typ, pattern)
+ f, err := newDiagnosticDataFile(typ, pattern)
if err != nil {
return err
}
@@ -559,13 +560,13 @@
return prof.Write(f)
}
-func CopyProfile(profilePath string, typ ProfileType, pattern string) error {
- inF, err := os.Open(profilePath)
+func CopyDiagnosticData(diagPath string, typ diagnostics.Type, pattern string) error {
+ inF, err := os.Open(diagPath)
if err != nil {
return err
}
defer inF.Close()
- outF, err := newProfileFile(typ, pattern)
+ outF, err := newDiagnosticDataFile(typ, pattern)
if err != nil {
return err
}
@@ -574,21 +575,17 @@
return err
}
-func newProfileFile(typ ProfileType, pattern string) (*os.File, error) {
- if !ProfilingEnabled(typ) {
+func PerfFlags() []string {
+ if !DiagnosticEnabled(diagnostics.Perf) {
+ panic("perf not enabled")
+ }
+ return strings.Split(diag[diagnostics.Perf].Flags, " ")
+}
+
+func newDiagnosticDataFile(typ diagnostics.Type, pattern string) (*os.File, error) {
+ cfg, ok := diag[typ]
+ if !ok || cfg.Dir == "" {
return nil, fmt.Errorf("this type of profile is not currently enabled")
}
- var outDir, patternSuffix string
- switch typ {
- case ProfileCPU:
- outDir = cpuProfileDir
- patternSuffix = ".cpu"
- case ProfileMem:
- outDir = memProfileDir
- patternSuffix = ".mem"
- case ProfilePerf:
- outDir = perfDir
- patternSuffix = ".perf"
- }
- return os.CreateTemp(outDir, pattern+patternSuffix)
+ return os.CreateTemp(cfg.Dir, pattern+"."+string(typ))
}
diff --git a/sweet/benchmarks/tile38/main.go b/sweet/benchmarks/tile38/main.go
index ea3027a..8de4af2 100644
--- a/sweet/benchmarks/tile38/main.go
+++ b/sweet/benchmarks/tile38/main.go
@@ -11,17 +11,20 @@
"fmt"
"io"
"math/rand"
+ "net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strconv"
+ "sync"
"sync/atomic"
"time"
"golang.org/x/benchmarks/sweet/benchmarks/internal/driver"
"golang.org/x/benchmarks/sweet/benchmarks/internal/pool"
+ "golang.org/x/benchmarks/sweet/common/diagnostics"
"golang.org/x/benchmarks/sweet/common/profile"
"github.com/gomodule/redigo/redis"
@@ -39,15 +42,17 @@
short bool
}
-func (c *config) profilePath(typ driver.ProfileType) string {
+func (c *config) diagnosticDataPath(typ diagnostics.Type) string {
var fname string
switch typ {
- case driver.ProfileCPU:
+ case diagnostics.CPUProfile:
fname = "cpu.prof"
- case driver.ProfileMem:
+ case diagnostics.MemProfile:
fname = "mem.prof"
- case driver.ProfilePerf:
+ case diagnostics.Perf:
fname = "perf.data"
+ case diagnostics.Trace:
+ fname = "runtime.trace"
default:
panic("unsupported profile type " + string(typ))
}
@@ -58,7 +63,7 @@
func init() {
driver.SetFlags(flag.CommandLine)
- flag.StringVar(&cliCfg.host, "host", "", "hostname of tile38 server")
+ flag.StringVar(&cliCfg.host, "host", "127.0.0.1", "hostname of tile38 server")
flag.IntVar(&cliCfg.port, "port", 9851, "port for tile38 server")
flag.Int64Var(&cliCfg.seed, "seed", 0, "seed for PRNG")
flag.StringVar(&cliCfg.serverBin, "server", "", "path to tile38 server binary")
@@ -219,13 +224,14 @@
// Set up arguments.
srvArgs := []string{
"-d", cfg.dataPath,
- "-h", "127.0.0.1",
- "-p", "9851",
+ "-h", cfg.host,
+ "-p", strconv.Itoa(cfg.port),
"-threads", strconv.Itoa(cfg.serverProcs),
+ "-pprofport", strconv.Itoa(pprofPort),
}
- for _, typ := range []driver.ProfileType{driver.ProfileCPU, driver.ProfileMem} {
- if driver.ProfilingEnabled(typ) {
- srvArgs = append(srvArgs, "-"+string(typ)+"profile", cfg.profilePath(typ))
+ for _, typ := range []diagnostics.Type{diagnostics.CPUProfile, diagnostics.MemProfile} {
+ if driver.DiagnosticEnabled(typ) {
+ srvArgs = append(srvArgs, "-"+string(typ), cfg.diagnosticDataPath(typ))
}
}
@@ -271,6 +277,26 @@
return nil, fmt.Errorf("timeout trying to connect to server: %v", err)
}
+const pprofPort = 12345
+
+func (cfg *config) readTrace(benchName string) (int64, error) {
+ f, err := os.Create(cfg.diagnosticDataPath(diagnostics.Trace))
+ if err != nil {
+ return 0, err
+ }
+ defer f.Close()
+ resp, err := http.Get(fmt.Sprintf("http://%s:%d/debug/pprof/trace", cfg.host, pprofPort))
+ if err != nil {
+ return 0, err
+ }
+ defer resp.Body.Close()
+ n, err := io.Copy(f, resp.Body)
+ if err != nil {
+ return 0, err
+ }
+ return n, driver.CopyDiagnosticData(cfg.diagnosticDataPath(diagnostics.Trace), diagnostics.Trace, benchName)
+}
+
func runOne(bench benchmark, cfg *config) (err error) {
var buf bytes.Buffer
@@ -306,14 +332,14 @@
// Now that the server is done, the profile should be complete and flushed.
// Copy it over.
- for _, typ := range []driver.ProfileType{driver.ProfileCPU, driver.ProfileMem} {
- if driver.ProfilingEnabled(typ) {
- p, r := profile.Read(cfg.profilePath(typ))
+ for _, typ := range []diagnostics.Type{diagnostics.CPUProfile, diagnostics.MemProfile} {
+ if driver.DiagnosticEnabled(typ) {
+ p, r := profile.ReadPprof(cfg.diagnosticDataPath(typ))
if r != nil {
err = r
return
}
- if r := driver.WriteProfile(p, typ, bench.name()); r != nil {
+ if r := driver.WritePprofProfile(p, typ, bench.name()); r != nil {
err = r
return
}
@@ -329,12 +355,46 @@
driver.DoCoreDump(true),
driver.BenchmarkPID(srvCmd.Process.Pid),
driver.DoPerf(true),
+ driver.DoTrace(true),
}
iters := 20 * 50000
if cfg.short {
iters = 1000
}
return driver.RunBenchmark(bench.name(), func(d *driver.B) error {
+ if driver.DiagnosticEnabled(diagnostics.Trace) {
+ // Handle execution tracing.
+ //
+ // TODO(mknyszek): This is kind of a hack. We really should find a way to just
+ // enable tracing at a lower level for the entire server run.
+ var traceStop chan struct{}
+ var traceWg sync.WaitGroup
+ var traceBytes uint64
+ traceWg.Add(1)
+ traceStop = make(chan struct{})
+ go func() {
+ defer traceWg.Done()
+ for {
+ select {
+ case <-traceStop:
+ return
+ default:
+ }
+ n, err := cfg.readTrace(bench.name())
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "failed to read trace: %v", err)
+ return
+ }
+ traceBytes += uint64(n)
+ }
+ }()
+ defer func() {
+ // Stop the trace loop.
+ close(traceStop)
+ traceWg.Wait()
+ d.Report("trace-bytes", traceBytes)
+ }()
+ }
return bench.run(d, cfg.host, cfg.port, cfg.serverProcs, iters)
}, opts...)
}
@@ -345,8 +405,8 @@
fmt.Fprintf(os.Stderr, "error: unexpected args\n")
os.Exit(1)
}
- for _, typ := range driver.ProfileTypes {
- cliCfg.isProfiling = cliCfg.isProfiling || driver.ProfilingEnabled(typ)
+ for _, typ := range diagnostics.Types() {
+ cliCfg.isProfiling = cliCfg.isProfiling || driver.DiagnosticEnabled(typ)
}
benchmarks := benchmarks
if cliCfg.short {
diff --git a/sweet/cmd/sweet/benchmark.go b/sweet/cmd/sweet/benchmark.go
index 8ad98a0..b8402fd 100644
--- a/sweet/cmd/sweet/benchmark.go
+++ b/sweet/cmd/sweet/benchmark.go
@@ -279,24 +279,15 @@
mkdirAll(resultsBinDir)
copyDirContents(resultsBinDir, binDir)
}
- if r.cpuProfile || r.memProfile || r.perf {
+ if !cfg.Diagnostics.Empty() {
// Create a directory for any profile files to live in.
resultsProfilesDir := r.runProfilesDir(b, cfg)
mkdirAll(resultsProfilesDir)
// We need to pass arguments to the benchmark binary to generate
// profiles. See benchmarks/internal/driver for details.
- if r.cpuProfile {
- args = append(args, "-cpuprofile", resultsProfilesDir)
- }
- if r.memProfile {
- args = append(args, "-memprofile", resultsProfilesDir)
- }
- if r.perf {
- args = append(args, "-perf", resultsProfilesDir)
- if r.perfFlags != "" {
- args = append(args, "-perf-flags", r.perfFlags)
- }
+ for _, d := range cfg.Diagnostics.ToSlice() {
+ args = append(args, d.DriverArgs(resultsProfilesDir)...)
}
}
diff --git a/sweet/cmd/sweet/run.go b/sweet/cmd/sweet/run.go
index 0e69f4a..d9752da 100644
--- a/sweet/cmd/sweet/run.go
+++ b/sweet/cmd/sweet/run.go
@@ -20,6 +20,7 @@
"golang.org/x/benchmarks/sweet/cli/bootstrap"
"golang.org/x/benchmarks/sweet/common"
+ "golang.org/x/benchmarks/sweet/common/diagnostics"
"golang.org/x/benchmarks/sweet/common/log"
sprofile "golang.org/x/benchmarks/sweet/common/profile"
@@ -58,10 +59,6 @@
workDir string
assetsCache string
dumpCore bool
- cpuProfile bool
- memProfile bool
- perf bool
- perfFlags string
pgo bool
pgoCount int
short bool
@@ -150,10 +147,6 @@
f.StringVar(&c.runCfg.workDir, "work-dir", "", "work directory for benchmarks (default: temporary directory)")
f.StringVar(&c.runCfg.assetsCache, "cache", bootstrap.CacheDefault(), "cache location for assets")
f.BoolVar(&c.runCfg.dumpCore, "dump-core", false, "whether to dump core files for each benchmark process when it completes a benchmark")
- f.BoolVar(&c.runCfg.cpuProfile, "cpuprofile", false, "whether to dump a CPU profile for each benchmark (ensures all benchmarks do the same amount of work)")
- f.BoolVar(&c.runCfg.memProfile, "memprofile", false, "whether to dump a memory profile for each benchmark (ensures all executions do the same amount of work")
- f.BoolVar(&c.runCfg.perf, "perf", false, "whether to run each benchmark under Linux perf and dump the results")
- f.StringVar(&c.runCfg.perfFlags, "perf-flags", "", "the flags to pass to Linux perf if -perf is set")
f.BoolVar(&c.pgo, "pgo", false, "perform PGO testing; for each config, collect profiles from a baseline run which are used to feed into a generated PGO config")
f.IntVar(&c.runCfg.pgoCount, "pgo-count", 0, "the number of times to run profiling runs for -pgo; defaults to the value of -count if <=5, or 5 if higher")
f.IntVar(&c.runCfg.count, "count", 0, fmt.Sprintf("the number of times to run each benchmark (default %d)", countDefault))
@@ -362,11 +355,12 @@
if len(unknown) != 0 {
return fmt.Errorf("unknown benchmarks: %s", strings.Join(unknown, ", "))
}
- countString := fmt.Sprintf("%d runs", c.runCfg.count)
+
+ // Print an indication of how many runs will be done.
+ countString := fmt.Sprintf("%d runs", c.runCfg.count*len(configs))
if c.pgo {
- countString += fmt.Sprintf(", %d pgo runs", c.runCfg.pgoCount)
+ countString += fmt.Sprintf(", %d pgo runs", c.runCfg.pgoCount*len(configs))
}
- countString += fmt.Sprintf(" per config (%d)", len(configs))
log.Printf("Benchmarks: %s (%s)", strings.Join(benchmarkNames(benchmarks), " "), countString)
// Check prerequisites for each benchmark.
@@ -406,11 +400,11 @@
for _, c := range configs {
cc := c.Copy()
cc.Name += ".profile"
+ cc.Diagnostics.Set(diagnostics.Config{Type: diagnostics.CPUProfile})
profileConfigs = append(profileConfigs, cc)
}
profileRunCfg := c.runCfg
- profileRunCfg.cpuProfile = true
profileRunCfg.count = profileRunCfg.pgoCount
log.Printf("Running profile collection runs")
@@ -453,10 +447,10 @@
return newConfigs, nil
}
-var cpuProfileRe = regexp.MustCompile(`^.*\.cpu[0-9]+$`)
+var cpuProfileRe = regexp.MustCompile(`^.*\.cpuprofile[0-9]+$`)
func mergeCPUProfiles(dir string) (string, error) {
- profiles, err := sprofile.ReadDir(dir, func(name string) bool {
+ profiles, err := sprofile.ReadDirPprof(dir, func(name string) bool {
return cpuProfileRe.FindString(name) != ""
})
if err != nil {
diff --git a/sweet/common/config.go b/sweet/common/config.go
index a1c3231..5754d24 100644
--- a/sweet/common/config.go
+++ b/sweet/common/config.go
@@ -10,32 +10,44 @@
"path/filepath"
"github.com/BurntSushi/toml"
+ "golang.org/x/benchmarks/sweet/common/diagnostics"
)
const ConfigHelp = `
The input configuration format is TOML consisting of a single array field
called 'config'. Each element of the array consists of the following fields:
- name: a unique name for the configuration (required)
- goroot: path to a GOROOT representing the toolchain to run (required)
- envbuild: additional environment variables that should be used for compilation
- each variable should take the form "X=Y" (optional)
- envexec: additional environment variables that should be used for execution
- each variable should take the form "X=Y" (optional)
+ name: a unique name for the configuration (required)
+ goroot: path to a GOROOT representing the toolchain to run (required)
+ envbuild: additional environment variables that should be used for
+ compilation each variable should take the form "X=Y" (optional)
+ envexec: additional environment variables that should be used for execution
+ each variable should take the form "X=Y" (optional)
+ pgofiles: a map of benchmark names (see 'sweet help run') to profile files
+ to be passed to the Go compiler for optimization (optional)
+ diagnostics: profile types to collect for each benchmark run of this
+ configuration, which may be one of: cpuprofile, memprofile,
+ perf[=flags], trace (optional)
A simple example configuration might look like:
[[config]]
name = "original"
goroot = "/path/to/go"
- envexec = ["GODEBUG=gctrace=1"]
[[config]]
name = "improved"
goroot = "/path/to/go-but-better"
- envexec = ["GODEBUG=gctrace=1"]
Note that because 'config' is an array field, one may have multiple
configurations present in a single file.
+
+An example of using some of the other fields to diagnose performance differences:
+
+[[config]]
+ name = "improved-but-why"
+ goroot = "/path/to/go-but-better"
+ envexec = ["GODEBUG=gctrace=1"]
+ diagnostics = ["cpuprofile", "perf=-e page-faults"]
`
type ConfigFile struct {
@@ -43,11 +55,12 @@
}
type Config struct {
- Name string `toml:"name"`
- GoRoot string `toml:"goroot"`
- BuildEnv ConfigEnv `toml:"envbuild"`
- ExecEnv ConfigEnv `toml:"envexec"`
- PGOFiles map[string]string `toml:"pgofiles"`
+ Name string `toml:"name"`
+ GoRoot string `toml:"goroot"`
+ BuildEnv ConfigEnv `toml:"envbuild"`
+ ExecEnv ConfigEnv `toml:"envexec"`
+ PGOFiles map[string]string `toml:"pgofiles"`
+ Diagnostics diagnostics.ConfigSet `toml:"diagnostics"`
}
func (c *Config) GoTool() *Go {
@@ -61,9 +74,12 @@
// Copy returns a deep copy of Config.
func (c *Config) Copy() *Config {
- // Currently, all fields in Config are immutable, so a simply copy is
- // sufficient.
cc := *c
+ cc.PGOFiles = make(map[string]string)
+ for k, v := range c.PGOFiles {
+ cc.PGOFiles[k] = v
+ }
+ cc.Diagnostics = c.Diagnostics.Copy()
return &cc
}
@@ -76,11 +92,12 @@
// on Config and use dummy types that have a straightforward
// mapping that *does* work.
type config struct {
- Name string `toml:"name"`
- GoRoot string `toml:"goroot"`
- BuildEnv []string `toml:"envbuild"`
- ExecEnv []string `toml:"envexec"`
- PGOFiles map[string]string `toml:"pgofiles"`
+ Name string `toml:"name"`
+ GoRoot string `toml:"goroot"`
+ BuildEnv []string `toml:"envbuild"`
+ ExecEnv []string `toml:"envexec"`
+ PGOFiles map[string]string `toml:"pgofiles"`
+ Diagnostics []string `toml:"diagnostics"`
}
type configFile struct {
Configs []*config `toml:"config"`
@@ -93,6 +110,7 @@
cfg.BuildEnv = c.BuildEnv.Collapse()
cfg.ExecEnv = c.ExecEnv.Collapse()
cfg.PGOFiles = c.PGOFiles
+ cfg.Diagnostics = c.Diagnostics.Strings()
cfgs.Configs = append(cfgs.Configs, &cfg)
}
diff --git a/sweet/common/diagnostics/config.go b/sweet/common/diagnostics/config.go
new file mode 100644
index 0000000..10f3dea
--- /dev/null
+++ b/sweet/common/diagnostics/config.go
@@ -0,0 +1,169 @@
+// Copyright 2023 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 diagnostics
+
+import (
+ "fmt"
+ "strings"
+)
+
+// ConfigSet is an immutable set of Config, containing at most
+// one Config of each supported type.
+type ConfigSet struct {
+ cfgs map[Type]Config
+}
+
+// Strings returns the set of ConfigSet as strings by calling the String
+// method on each Config.
+func (c ConfigSet) Strings() []string {
+ var diags []string
+ for _, diag := range c.cfgs {
+ diags = append(diags, diag.String())
+ }
+ return diags
+}
+
+// UnmarshalTOML implements TOML unmarshaling for ConfigSet.
+func (c *ConfigSet) UnmarshalTOML(data interface{}) error {
+ ldata, ok := data.([]interface{})
+ if !ok {
+ return fmt.Errorf("expected data for diagnostics to be a list")
+ }
+ cfgs := make(map[Type]Config, len(ldata))
+ for _, li := range ldata {
+ s, ok := li.(string)
+ if !ok {
+ return fmt.Errorf("expected data for env to contain strings")
+ }
+ d, err := ParseConfig(s)
+ if err != nil {
+ return err
+ }
+ cfgs[d.Type] = d
+ }
+ c.cfgs = cfgs
+ return nil
+}
+
+// Copy creates a deep clone of a ConfigSet.
+func (c ConfigSet) Copy() ConfigSet {
+ cfgs := make(map[Type]Config, len(c.cfgs))
+ for k, v := range c.cfgs {
+ cfgs[k] = v
+ }
+ return ConfigSet{cfgs}
+}
+
+// Set adds a Config to ConfigSet, overwriting any Config of the same Type.
+func (c *ConfigSet) Set(d Config) {
+ c.cfgs[d.Type] = d
+}
+
+// Clear removes the Config with the provided Type from the ConfigSet, if applicable.
+func (c *ConfigSet) Clear(typ Type) {
+ delete(c.cfgs, typ)
+}
+
+// Get looks up the Config with the provided Type and returns it if it exists with the
+// second result indicating presence.
+func (c ConfigSet) Get(typ Type) (Config, bool) {
+ cfg, ok := c.cfgs[typ]
+ return cfg, ok
+}
+
+// Empty returns true if the ConfigSet is empty.
+func (c ConfigSet) Empty() bool {
+ return len(c.cfgs) == 0
+}
+
+// ToSlice returns each Config contained in the ConfigSet in a slice.
+func (c ConfigSet) ToSlice() []Config {
+ cfgs := make([]Config, 0, len(c.cfgs))
+ for _, cfg := range c.cfgs {
+ cfgs = append(cfgs, cfg)
+ }
+ return cfgs
+}
+
+// Type is a diagnostic type supported by Sweet.
+type Type string
+
+const (
+ CPUProfile Type = "cpuprofile"
+ MemProfile Type = "memprofile"
+ Perf Type = "perf"
+ Trace Type = "trace"
+)
+
+// IsPprof returns whether the diagnostic's data is stored in the pprof format.
+func (t Type) IsPprof() bool {
+ return t == CPUProfile || t == MemProfile
+}
+
+// AsFlag returns the Type suitable for use as a CLI flag.
+func (t Type) AsFlag() string {
+ return "-" + string(t)
+}
+
+// Types returns a slice of all supported types.
+func Types() []Type {
+ return []Type{
+ CPUProfile,
+ MemProfile,
+ Perf,
+ Trace,
+ }
+}
+
+// Config is an intent to collect data for some diagnostic with some room
+// for additional configuration as to how that data is collected.
+type Config struct {
+ // Type is the diagnostic to collect data for.
+ Type
+
+ // Flags is additional opaque configuration for data collection.
+ //
+ // Currently only used if Type == Perf.
+ Flags string
+}
+
+// String returns the string representation of a Config, as it would appear
+// in a Sweet common.Config.
+func (d Config) String() string {
+ result := string(d.Type)
+ if d.Flags != "" {
+ result += "=" + d.Flags
+ }
+ return result
+}
+
+// ParseConfig derives a Config from a string. The string must take the form
+//
+// <type>[=<flags>]
+//
+// where [=<flags>] is only accepted if <type> is perf.
+func ParseConfig(d string) (Config, error) {
+ comp := strings.SplitN(d, "=", 2)
+ var result Config
+ switch comp[0] {
+ case string(CPUProfile):
+ fallthrough
+ case string(MemProfile):
+ fallthrough
+ case string(Trace):
+ if len(comp) != 1 {
+ return result, fmt.Errorf("diagnostic %q does not take flags", comp[0])
+ }
+ result.Type = Type(comp[0])
+ case string(Perf):
+ if len(comp) == 2 {
+ result.Flags = comp[1]
+ }
+ result.Type = Type(comp[0])
+ default:
+ return result, fmt.Errorf("invalid diagnostic %q", comp[0])
+ }
+ return result, nil
+}
diff --git a/sweet/common/diagnostics/driver.go b/sweet/common/diagnostics/driver.go
new file mode 100644
index 0000000..ff692f0
--- /dev/null
+++ b/sweet/common/diagnostics/driver.go
@@ -0,0 +1,40 @@
+// Copyright 2023 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 diagnostics
+
+import (
+ "flag"
+ "fmt"
+)
+
+// DriverArgs returns the arguments that should be passed to a Sweet benchmark
+// binary to collect data for the Config.
+func (d Config) DriverArgs(resultsDir string) []string {
+ flag := d.Type.AsFlag()
+ args := []string{flag, resultsDir}
+ if d.Flags != "" {
+ args = append(args, flag+"-flags", d.Flags)
+ }
+ return args
+}
+
+type DriverConfig struct {
+ Config
+ Dir string
+}
+
+func SetFlagsForDriver(f *flag.FlagSet) map[Type]*DriverConfig {
+ storage := make(map[Type]*DriverConfig)
+ for _, t := range Types() {
+ dc := new(DriverConfig)
+ dc.Type = t
+ storage[t] = dc
+ f.StringVar(&dc.Dir, string(t), "", fmt.Sprintf("directory to write %s data", t))
+ if t == Perf {
+ f.StringVar(&dc.Flags, string(t)+"-flags", "", "flags for Linux perf")
+ }
+ }
+ return storage
+}
diff --git a/sweet/common/profile/profile.go b/sweet/common/profile/profile.go
index d448997..d23bb98 100644
--- a/sweet/common/profile/profile.go
+++ b/sweet/common/profile/profile.go
@@ -12,7 +12,7 @@
"github.com/google/pprof/profile"
)
-func Read(filename string) (*profile.Profile, error) {
+func ReadPprof(filename string) (*profile.Profile, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
@@ -21,8 +21,8 @@
return profile.Parse(f)
}
-// ReadDir reads all profiles in dir whose name matches match(name).
-func ReadDir(dir string, match func(string) bool) ([]*profile.Profile, error) {
+// ReadDir reads all pprof profiles in dir whose name matches match(name).
+func ReadDirPprof(dir string, match func(string) bool) ([]*profile.Profile, error) {
entries, err := os.ReadDir(dir)
if err != nil {
return nil, err
@@ -39,7 +39,7 @@
continue
}
if match(name) {
- p, err := Read(path)
+ p, err := ReadPprof(path)
if err != nil {
return nil, err
}