Skip to content

Commit 74502e9

Browse files
committed
cmd/go: support multiple main packages with -pgo=auto
In -pgo=auto mode, the go command finds a profile named default.pgo in the main package's directly, and if found, use it as the profile for the build. Currently we only support a single main package when -pgo=auto is used. When multiple main packages are included in a build, they may have different default profiles (or some have profiles whereas some don't), so a common dependent package would need to be built multiple times, with different profiles (or lack of). This CL handles this. To do so, we need to split (unshare) the dependency graph so they can attach different profiles. Fixes golang#58099. Change-Id: I1ad21361967aafbf5089d8d5e89229f95fe31276 Reviewed-on: https://go-review.googlesource.com/c/go/+/472358 Reviewed-by: Michael Pratt <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Run-TryBot: Cherry Mui <[email protected]> Reviewed-by: Bryan Mills <[email protected]>
1 parent 5987f3c commit 74502e9

File tree

5 files changed

+150
-42
lines changed

5 files changed

+150
-42
lines changed

src/cmd/go/alldocs.go

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/cmd/go/internal/load/pkg.go

Lines changed: 55 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"path/filepath"
2424
"runtime"
2525
"runtime/debug"
26+
"slices"
2627
"sort"
2728
"strconv"
2829
"strings"
@@ -2905,34 +2906,55 @@ func setPGOProfilePath(pkgs []*Package) {
29052906
return
29062907

29072908
case "auto":
2908-
// Locate PGO profile from the main package.
2909-
2910-
setError := func(p *Package) {
2911-
if p.Error == nil {
2912-
p.Error = &PackageError{Err: errors.New("-pgo=auto requires exactly one main package")}
2909+
// Locate PGO profiles from the main packages, and
2910+
// attach the profile to the main package and its
2911+
// dependencies.
2912+
// If we're builing multiple main packages, they may
2913+
// have different profiles. We may need to split (unshare)
2914+
// the dependency graph so they can attach different
2915+
// profiles.
2916+
for _, p := range pkgs {
2917+
if p.Name != "main" {
2918+
continue
2919+
}
2920+
pmain := p
2921+
file := filepath.Join(pmain.Dir, "default.pgo")
2922+
if _, err := os.Stat(file); err != nil {
2923+
continue // no profile
29132924
}
2914-
}
29152925

2916-
var mainpkg *Package
2917-
for _, p := range pkgs {
2918-
if p.Name == "main" {
2919-
if mainpkg != nil {
2920-
setError(p)
2921-
setError(mainpkg)
2922-
continue
2926+
copied := make(map[*Package]*Package)
2927+
var split func(p *Package) *Package
2928+
split = func(p *Package) *Package {
2929+
if len(pkgs) > 1 && p != pmain {
2930+
// Make a copy, then attach profile.
2931+
// No need to copy if there is only one root package (we can
2932+
// attach profile directly in-place).
2933+
// Also no need to copy the main package.
2934+
if p1 := copied[p]; p1 != nil {
2935+
return p1
2936+
}
2937+
if p.Internal.PGOProfile != "" {
2938+
panic("setPGOProfilePath: already have profile")
2939+
}
2940+
p1 := new(Package)
2941+
*p1 = *p
2942+
// Unalias the Internal.Imports slice, which is we're going to
2943+
// modify. We don't copy other slices as we don't change them.
2944+
p1.Internal.Imports = slices.Clone(p.Internal.Imports)
2945+
copied[p] = p1
2946+
p = p1
29232947
}
2924-
mainpkg = p
2925-
}
2926-
}
2927-
if mainpkg == nil {
2928-
// No main package, no default.pgo to look for.
2929-
return
2930-
}
2931-
file := filepath.Join(mainpkg.Dir, "default.pgo")
2932-
if fi, err := os.Stat(file); err == nil && !fi.IsDir() {
2933-
for _, p := range PackageList(pkgs) {
29342948
p.Internal.PGOProfile = file
2949+
// Recurse to dependencies.
2950+
for i, pp := range p.Internal.Imports {
2951+
p.Internal.Imports[i] = split(pp)
2952+
}
2953+
return p
29352954
}
2955+
2956+
// Replace the package and imports with the PGO version.
2957+
split(pmain)
29362958
}
29372959

29382960
default:
@@ -2979,11 +3001,18 @@ func CheckPackageErrors(pkgs []*Package) {
29793001
seen := map[string]bool{}
29803002
reported := map[string]bool{}
29813003
for _, pkg := range PackageList(pkgs) {
2982-
if seen[pkg.ImportPath] && !reported[pkg.ImportPath] {
2983-
reported[pkg.ImportPath] = true
3004+
// -pgo=auto with multiple main packages can cause a package being
3005+
// built multiple times (with different profiles).
3006+
// We check that package import path + profile path is unique.
3007+
key := pkg.ImportPath
3008+
if pkg.Internal.PGOProfile != "" {
3009+
key += " pgo:" + pkg.Internal.PGOProfile
3010+
}
3011+
if seen[key] && !reported[key] {
3012+
reported[key] = true
29843013
base.Errorf("internal error: duplicate loads of %s", pkg.ImportPath)
29853014
}
2986-
seen[pkg.ImportPath] = true
3015+
seen[key] = true
29873016
}
29883017
base.ExitIfErrors()
29893018
}

src/cmd/go/internal/work/build.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,8 +159,10 @@ and test commands:
159159
run through go run and go test respectively.
160160
-pgo file
161161
specify the file path of a profile for profile-guided optimization (PGO).
162-
Special name "auto" lets the go command select a file named
163-
"default.pgo" in the main package's directory if that file exists.
162+
When the special name "auto" is specified, for each main package in the
163+
build, the go command selects a file named "default.pgo" in the package's
164+
directory if that file exists, and applies it to the (transitive)
165+
dependencies of the main package (other packages are not affected).
164166
Special name "off" turns off PGO.
165167
-pkgdir dir
166168
install and load all packages from dir instead of the usual locations.

src/cmd/go/testdata/script/build_pgo_auto.txt

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@ stderr 'compile.*-p test/dep.*-pgoprofile=.*default\.pgo'
1111
go build -n -pgo=auto ./a/...
1212
stderr 'compile.*-pgoprofile=.*default\.pgo.*a1.go'
1313

14-
# error with multiple packages
15-
! go build -n -pgo=auto ./b/...
16-
stderr '-pgo=auto requires exactly one main package'
17-
1814
# build succeeds without PGO when default.pgo file is absent
1915
go build -n -pgo=auto -o nopgo.exe ./nopgo
2016
stderr 'compile.*nopgo.go'
@@ -54,14 +50,6 @@ package main_test
5450
import "testing"
5551
func TestExternal(*testing.T) {}
5652
-- a/a1/default.pgo --
57-
-- b/b1/b1.go --
58-
package main
59-
func main() {}
60-
-- b/b1/default.pgo --
61-
-- b/b2/b2.go --
62-
package main
63-
func main() {}
64-
-- b/b2/default.pgo --
6553
-- nopgo/nopgo.go --
6654
package main
6755
func main() {}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Test go build -pgo=auto flag with multiple main packages.
2+
3+
go build -n -pgo=auto ./a ./b ./nopgo
4+
5+
# a/default.pgo applies to package a and (transitive)
6+
# dependencies.
7+
stderr 'compile.*-pgoprofile=.*a(/|\\\\)default\.pgo.*a(/|\\\\)a\.go'
8+
stderr 'compile.*-pgoprofile=.*a(/|\\\\)default\.pgo.*dep(/|\\\\)dep\.go'
9+
stderr 'compile.*-pgoprofile=.*a(/|\\\\)default\.pgo.*dep2(/|\\\\)dep2\.go'
10+
stderr -count=1 'compile.*-pgoprofile=.*a(/|\\\\)default\.pgo.*dep3(/|\\\\)dep3\.go'
11+
12+
# b/default.pgo applies to package b and (transitive)
13+
# dependencies.
14+
stderr 'compile.*-pgoprofile=.*b(/|\\\\)default\.pgo.*b(/|\\\\)b\.go'
15+
stderr 'compile.*-pgoprofile=.*b(/|\\\\)default\.pgo.*dep(/|\\\\)dep\.go'
16+
stderr 'compile.*-pgoprofile=.*b(/|\\\\)default\.pgo.*dep2(/|\\\\)dep2\.go'
17+
stderr -count=1 'compile.*-pgoprofile=.*b(/|\\\\)default\.pgo.*dep3(/|\\\\)dep3\.go'
18+
19+
# nopgo should be built without PGO.
20+
! stderr 'compile.*-pgoprofile=.*nopgo(/|\\\\)nopgo\.go'
21+
22+
# Dependencies should also be built without PGO.
23+
# Here we want to match a compile action without -pgoprofile,
24+
# by matching 3 occurrences of "compile dep.go", among which
25+
# 2 of them have -pgoprofile (therefore one without).
26+
stderr -count=3 'compile.*dep(/|\\\\)dep.go'
27+
stderr -count=2 'compile.*-pgoprofile=.*dep(/|\\\\)dep\.go'
28+
29+
stderr -count=3 'compile.*dep2(/|\\\\)dep2.go'
30+
stderr -count=2 'compile.*-pgoprofile=.*dep2(/|\\\\)dep2\.go'
31+
32+
stderr -count=3 'compile.*dep3(/|\\\\)dep3.go'
33+
stderr -count=2 'compile.*-pgoprofile=.*dep3(/|\\\\)dep3\.go'
34+
35+
# go test works the same way
36+
go test -n -pgo=auto ./a ./b ./nopgo
37+
stderr 'compile.*-pgoprofile=.*a(/|\\\\)default\.pgo.*a(/|\\\\)a_test\.go'
38+
stderr 'compile.*-pgoprofile=.*a(/|\\\\)default\.pgo.*dep(/|\\\\)dep\.go'
39+
stderr 'compile.*-pgoprofile=.*b(/|\\\\)default\.pgo.*b(/|\\\\)b_test\.go'
40+
stderr 'compile.*-pgoprofile=.*b(/|\\\\)default\.pgo.*dep(/|\\\\)dep\.go'
41+
! stderr 'compile.*-pgoprofile=.*nopgo(/|\\\\)nopgo_test\.go'
42+
43+
# Here we have 3 main packages, a, b, and nopgo, where a and b each has
44+
# its own default.pgo profile, and nopgo has none.
45+
# All 3 main packages import dep and dep2, both of which then import dep3
46+
# (a diamond-shape import graph).
47+
-- go.mod --
48+
module test
49+
go 1.20
50+
-- a/a.go --
51+
package main
52+
import _ "test/dep"
53+
import _ "test/dep2"
54+
func main() {}
55+
-- a/a_test.go --
56+
package main
57+
import "testing"
58+
func TestA(*testing.T) {}
59+
-- a/default.pgo --
60+
dummy profile a
61+
-- b/b.go --
62+
package main
63+
import _ "test/dep"
64+
import _ "test/dep2"
65+
func main() {}
66+
-- b/b_test.go --
67+
package main
68+
import "testing"
69+
func TestB(*testing.T) {}
70+
-- b/default.pgo --
71+
dummy profile b
72+
-- nopgo/nopgo.go --
73+
package main
74+
import _ "test/dep"
75+
import _ "test/dep2"
76+
func main() {}
77+
-- nopgo/nopgo_test.go --
78+
package main
79+
import "testing"
80+
func TestNopgo(*testing.T) {}
81+
-- dep/dep.go --
82+
package dep
83+
import _ "test/dep3"
84+
-- dep2/dep2.go --
85+
package dep2
86+
-- dep3/dep3.go --
87+
package dep3

0 commit comments

Comments
 (0)