Skip to content

Commit 247f8eb

Browse files
committed
sweet: add mode for PGO benchmarking
With -pgo, for each configuration sweet automatically runs an initial profiling configuration. The merged profiles from these runs are used as the PGO input to a ".pgo" variant of the configuration. Comparing the base configuration to the ".pgo" variant indicates the effect of PGO. At a lower level, the config format adds a "pgofiles" map, which can be used to specify PGO profiles to use for each benchmark. Ultimately this sets GOFLAGS=-pgo=/path in BuildEnv. Some benchmarks may not currently properly plumb this into their build (e.g., the GoBuild benchmarks don't build the compiler at all). Existing benchmarks need to be double-checked that they actually get PGO enabled. For golang/go#55022. Change-Id: I81c0cb085ef3b5196a05d2565dd0e2f83057b9fa
1 parent 5a591f8 commit 247f8eb

File tree

5 files changed

+216
-18
lines changed

5 files changed

+216
-18
lines changed

sweet/benchmarks/internal/cgroups/cgroups.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,3 @@ func (c *Cmd) RSSFunc() func() (uint64, error) {
110110
return strconv.ParseUint(strings.TrimSpace(string(data)), 10, 64)
111111
}
112112
}
113-

sweet/cmd/sweet/benchmark.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,20 +202,27 @@ func (b *benchmark) execute(cfgs []*common.Config, r *runCfg) error {
202202
return err
203203
}
204204

205-
// Retrieve the benchmark's source.
206-
if err := b.harness.Get(srcDir); err != nil {
207-
return fmt.Errorf("retrieving source for %s: %v", b.name, err)
205+
// Retrieve the benchmark's source, if needed. If execute is called
206+
// multiple times, this will already be done.
207+
_, err := os.Stat(srcDir)
208+
if os.IsNotExist(err) {
209+
if err := b.harness.Get(srcDir); err != nil {
210+
return fmt.Errorf("retrieving source for %s: %v", b.name, err)
211+
}
208212
}
209213

210214
// Create the results directory for the benchmark.
211-
resultsDir := filepath.Join(r.resultsDir, b.name)
215+
resultsDir := r.benchmarkResultsDir(b)
212216
if err := mkdirAll(resultsDir); err != nil {
213217
return fmt.Errorf("creating results directory for %s: %v", b.name, err)
214218
}
215219

216220
// Perform a setup step for each config for the benchmark.
217221
setups := make([]common.RunConfig, 0, len(cfgs))
218-
for _, cfg := range cfgs {
222+
for _, pcfg := range cfgs {
223+
// Local copy for per-benchmark environment adjustments.
224+
cfg := pcfg.Copy()
225+
219226
// Create directory hierarchy for benchmarks.
220227
workDir := filepath.Join(topDir, cfg.Name)
221228
binDir := filepath.Join(workDir, "bin")
@@ -236,6 +243,16 @@ func (b *benchmark) execute(cfgs []*common.Config, r *runCfg) error {
236243
}
237244
}
238245

246+
// Add PGO if profile specified for this benchmark.
247+
if pgo, ok := cfg.PGOFiles[b.name]; ok {
248+
goflags, ok := cfg.BuildEnv.Lookup("GOFLAGS")
249+
if ok {
250+
goflags += " "
251+
}
252+
goflags += fmt.Sprintf("-pgo=%s", pgo)
253+
cfg.BuildEnv.Env = cfg.BuildEnv.MustSet("GOFLAGS=" + goflags)
254+
}
255+
239256
// Build the benchmark (application and any other necessary components).
240257
bcfg := common.BuildConfig{
241258
BinDir: binDir,
@@ -264,7 +281,7 @@ func (b *benchmark) execute(cfgs []*common.Config, r *runCfg) error {
264281
}
265282
if r.cpuProfile || r.memProfile || r.perf {
266283
// Create a directory for any profile files to live in.
267-
resultsProfilesDir := filepath.Join(resultsDir, fmt.Sprintf("%s.debug", cfg.Name))
284+
resultsProfilesDir := r.runProfilesDir(b, cfg)
268285
mkdirAll(resultsProfilesDir)
269286

270287
// We need to pass arguments to the benchmark binary to generate

sweet/cmd/sweet/integration_test.go

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ import (
2020
)
2121

2222
func TestSweetEndToEnd(t *testing.T) {
23+
t.Run("standard", func(t *testing.T) {
24+
testSweetEndToEnd(t, false)
25+
})
26+
t.Run("pgo", func(t *testing.T) {
27+
testSweetEndToEnd(t, true)
28+
})
29+
}
30+
31+
func testSweetEndToEnd(t *testing.T, pgo bool) {
2332
if runtime.GOOS != "linux" || runtime.GOARCH != "amd64" {
2433
t.Skip("Sweet is currently only fully supported on linux/amd64")
2534
}
@@ -39,6 +48,17 @@ func TestSweetEndToEnd(t *testing.T) {
3948
Env: common.NewEnvFromEnviron(),
4049
}
4150

51+
if pgo {
52+
cmd := exec.Command(goTool.Tool, "help", "build")
53+
out, err := cmd.Output()
54+
if err != nil {
55+
t.Fatalf("error running go help build: %v", err)
56+
}
57+
if !strings.Contains(string(out), "-pgo") {
58+
t.Skip("toolchain missing -pgo support")
59+
}
60+
}
61+
4262
// Build sweet.
4363
wd, err := os.Getwd()
4464
if err != nil {
@@ -103,7 +123,8 @@ func TestSweetEndToEnd(t *testing.T) {
103123

104124
var outputMu sync.Mutex
105125
runShard := func(shard, resultsDir, workDir string) {
106-
runCmd := exec.Command(sweetBin, "run",
126+
args := []string{
127+
"run",
107128
"-run", shard,
108129
"-shell",
109130
"-count", "1",
@@ -112,8 +133,12 @@ func TestSweetEndToEnd(t *testing.T) {
112133
"-results", resultsDir,
113134
"-work-dir", workDir,
114135
"-short",
115-
cfgPath,
116-
)
136+
}
137+
if pgo {
138+
args = append(args, "-pgo")
139+
}
140+
args = append(args, cfgPath)
141+
runCmd := exec.Command(sweetBin, args...)
117142
output, runErr := runCmd.CombinedOutput()
118143

119144
outputMu.Lock()

sweet/cmd/sweet/run.go

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"io/fs"
1313
"os"
1414
"path/filepath"
15+
"regexp"
1516
"sort"
1617
"strings"
1718
"unicode/utf8"
@@ -21,6 +22,7 @@ import (
2122
"golang.org/x/benchmarks/sweet/common/log"
2223

2324
"github.com/BurntSushi/toml"
25+
"github.com/google/pprof/profile"
2426
)
2527

2628
type csvFlag []string
@@ -41,6 +43,8 @@ files.`
4143
`
4244
)
4345

46+
const pgoCountDefaultMax = 5
47+
4448
type runCfg struct {
4549
count int
4650
resultsDir string
@@ -53,6 +57,8 @@ type runCfg struct {
5357
memProfile bool
5458
perf bool
5559
perfFlags string
60+
pgo bool
61+
pgoCount int
5662
short bool
5763

5864
assetsFS fs.FS
@@ -67,6 +73,14 @@ func (r *runCfg) logCopyDirCommand(fromRelDir, toDir string) {
6773
}
6874
}
6975

76+
func (r *runCfg) benchmarkResultsDir(b *benchmark) string {
77+
return filepath.Join(r.resultsDir, b.name)
78+
}
79+
80+
func (r *runCfg) runProfilesDir(b *benchmark, c *common.Config) string {
81+
return filepath.Join(r.benchmarkResultsDir(b), fmt.Sprintf("%s.debug", c.Name))
82+
}
83+
7084
type runCmd struct {
7185
runCfg
7286
quiet bool
@@ -135,6 +149,8 @@ func (c *runCmd) SetFlags(f *flag.FlagSet) {
135149
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")
136150
f.BoolVar(&c.runCfg.perf, "perf", false, "whether to run each benchmark under Linux perf and dump the results")
137151
f.StringVar(&c.runCfg.perfFlags, "perf-flags", "", "the flags to pass to Linux perf if -perf is set")
152+
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")
153+
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")
138154
f.IntVar(&c.runCfg.count, "count", 25, "the number of times to run each benchmark")
139155

140156
f.BoolVar(&c.quiet, "quiet", false, "whether to suppress activity output on stderr (no effect on -shell)")
@@ -153,6 +169,13 @@ func (c *runCmd) Run(args []string) error {
153169
log.SetCommandTrace(c.printCmd)
154170
log.SetActivityLog(!c.quiet)
155171

172+
if c.runCfg.pgoCount == 0 {
173+
c.runCfg.pgoCount = c.runCfg.count
174+
if c.runCfg.pgoCount > pgoCountDefaultMax {
175+
c.runCfg.pgoCount = pgoCountDefaultMax
176+
}
177+
}
178+
156179
var err error
157180
if c.workDir == "" {
158181
// Create a temporary work tree for running the benchmarks.
@@ -267,6 +290,14 @@ func (c *runCmd) Run(args []string) error {
267290
if config.ExecEnv.Env == nil {
268291
config.ExecEnv.Env = common.NewEnvFromEnviron()
269292
}
293+
if config.PGOFiles == nil {
294+
config.PGOFiles = make(map[string]string)
295+
}
296+
for k := range config.PGOFiles {
297+
if _, ok := allBenchmarksMap[k]; !ok {
298+
return fmt.Errorf("config %q in %q pgofiles references unknown benchmark %q", config.Name, configFile, k)
299+
}
300+
}
270301
configs = append(configs, config)
271302
}
272303
}
@@ -304,6 +335,14 @@ func (c *runCmd) Run(args []string) error {
304335
}
305336
}
306337

338+
// Collect profiles from baseline runs and create new PGO'd configs.
339+
if c.pgo {
340+
configs, err = c.preparePGO(configs, benchmarks)
341+
if err != nil {
342+
return fmt.Errorf("error preparing PGO profiles: %w", err)
343+
}
344+
}
345+
307346
// Execute each benchmark for all configs.
308347
var errEncountered bool
309348
for _, b := range benchmarks {
@@ -321,6 +360,113 @@ func (c *runCmd) Run(args []string) error {
321360
return nil
322361
}
323362

363+
func (c *runCmd) preparePGO(configs []*common.Config, benchmarks []*benchmark) ([]*common.Config, error) {
364+
profileConfigs := make([]*common.Config, 0, len(configs))
365+
for _, c := range configs {
366+
cc := c.Copy()
367+
cc.Name += ".profile"
368+
profileConfigs = append(profileConfigs, cc)
369+
}
370+
371+
profileRunCfg := c.runCfg
372+
profileRunCfg.cpuProfile = true
373+
profileRunCfg.count = profileRunCfg.pgoCount
374+
375+
log.Printf("Running profile collection runs")
376+
377+
// Execute benchmarks to collect profiles.
378+
var errEncountered bool
379+
for _, b := range benchmarks {
380+
if err := b.execute(profileConfigs, &profileRunCfg); err != nil {
381+
if c.stopOnError {
382+
return nil, err
383+
}
384+
errEncountered = true
385+
log.Error(err)
386+
}
387+
}
388+
if errEncountered {
389+
return nil, fmt.Errorf("failed to execute profile collection benchmarks, see log for details")
390+
}
391+
392+
// Merge all the profiles and add new PGO configs.
393+
newConfigs := configs
394+
for i := range configs {
395+
origConfig := configs[i]
396+
profileConfig := profileConfigs[i]
397+
pgoConfig := origConfig.Copy()
398+
pgoConfig.Name += ".pgo"
399+
pgoConfig.PGOFiles = make(map[string]string)
400+
401+
for _, b := range benchmarks {
402+
p, err := mergeCPUProfiles(profileRunCfg.runProfilesDir(b, profileConfig))
403+
if err != nil {
404+
return nil, fmt.Errorf("error merging profiles for %s/%s: %w", b.name, profileConfig.Name, err)
405+
}
406+
pgoConfig.PGOFiles[b.name] = p
407+
}
408+
409+
newConfigs = append(newConfigs, pgoConfig)
410+
}
411+
412+
return newConfigs, nil
413+
}
414+
415+
var cpuProfileRe = regexp.MustCompile(`^.*\.cpu[0-9]+$`)
416+
417+
func mergeCPUProfiles(dir string) (string, error) {
418+
entries, err := os.ReadDir(dir)
419+
if err != nil {
420+
return "", fmt.Errorf("error reading dir %q: %w", dir, err)
421+
}
422+
423+
var profiles []*profile.Profile
424+
addProfile := func(name string) error {
425+
f, err := os.Open(name)
426+
if err != nil {
427+
return fmt.Errorf("error opening profile %q: %w", name, err)
428+
}
429+
defer f.Close()
430+
431+
p, err := profile.Parse(f)
432+
if err != nil {
433+
return fmt.Errorf("error parsing profile %q: %w", name, err)
434+
}
435+
profiles = append(profiles, p)
436+
return nil
437+
}
438+
439+
for _, e := range entries {
440+
name := e.Name()
441+
if cpuProfileRe.FindString(name) == "" {
442+
continue
443+
}
444+
445+
if err := addProfile(filepath.Join(dir, name)); err != nil {
446+
return "", err
447+
}
448+
}
449+
450+
if len(profiles) == 0 {
451+
return "", fmt.Errorf("no profiles found in %q", dir)
452+
}
453+
454+
p, err := profile.Merge(profiles)
455+
if err != nil {
456+
return "", fmt.Errorf("error merging profiles: %w", err)
457+
}
458+
459+
out := filepath.Join(dir, "merged.cpu")
460+
f, err := os.Create(out)
461+
defer f.Close()
462+
463+
if err := p.Write(f); err != nil {
464+
return "", fmt.Errorf("error writing merged profile: %w", err)
465+
}
466+
467+
return out, nil
468+
}
469+
324470
func canonicalizePath(path, base string) string {
325471
if filepath.IsAbs(path) {
326472
return path

sweet/common/config.go

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,11 @@ type ConfigFile struct {
4343
}
4444

4545
type Config struct {
46-
Name string `toml:"name"`
47-
GoRoot string `toml:"goroot"`
48-
BuildEnv ConfigEnv `toml:"envbuild"`
49-
ExecEnv ConfigEnv `toml:"envexec"`
46+
Name string `toml:"name"`
47+
GoRoot string `toml:"goroot"`
48+
BuildEnv ConfigEnv `toml:"envbuild"`
49+
ExecEnv ConfigEnv `toml:"envexec"`
50+
PGOFiles map[string]string `toml:"pgofiles"`
5051
}
5152

5253
func (c *Config) GoTool() *Go {
@@ -58,6 +59,14 @@ func (c *Config) GoTool() *Go {
5859
}
5960
}
6061

62+
// Copy returns a deep copy of Config.
63+
func (c *Config) Copy() *Config {
64+
// Currently, all fields in Config are immutable, so a simply copy is
65+
// sufficient.
66+
cc := *c
67+
return &cc
68+
}
69+
6170
func ConfigFileMarshalTOML(c *ConfigFile) ([]byte, error) {
6271
// Unfortunately because the github.com/BurntSushi/toml
6372
// package at v1.0.0 doesn't correctly support Marshaler
@@ -67,10 +76,11 @@ func ConfigFileMarshalTOML(c *ConfigFile) ([]byte, error) {
6776
// on Config and use dummy types that have a straightforward
6877
// mapping that *does* work.
6978
type config struct {
70-
Name string `toml:"name"`
71-
GoRoot string `toml:"goroot"`
72-
BuildEnv []string `toml:"envbuild"`
73-
ExecEnv []string `toml:"envexec"`
79+
Name string `toml:"name"`
80+
GoRoot string `toml:"goroot"`
81+
BuildEnv []string `toml:"envbuild"`
82+
ExecEnv []string `toml:"envexec"`
83+
PGOFiles map[string]string `toml:"pgofiles"`
7484
}
7585
type configFile struct {
7686
Configs []*config `toml:"config"`
@@ -82,6 +92,7 @@ func ConfigFileMarshalTOML(c *ConfigFile) ([]byte, error) {
8292
cfg.GoRoot = c.GoRoot
8393
cfg.BuildEnv = c.BuildEnv.Collapse()
8494
cfg.ExecEnv = c.ExecEnv.Collapse()
95+
cfg.PGOFiles = c.PGOFiles
8596

8697
cfgs.Configs = append(cfgs.Configs, &cfg)
8798
}

0 commit comments

Comments
 (0)