Add pgo support for go 1.20 (#3641)
* Add pgo support for go 1.20
* Pull request feedback from @fmeum
* Add test for building a go_binary with and without a pgoprofile
* Add //go/config:pgoprofile to _common_reset_transition_dict
* Apply suggestions from code review
Co-authored-by: Fabian Meumertzheim <fabian@meumertzhe.im>
* Use `cquery --output=files` to simplify `pgo_test`
---------
Co-authored-by: Fabian Meumertzheim <fabian@meumertzhe.im>
diff --git a/BUILD.bazel b/BUILD.bazel
index 60bf72e..e80f3e8 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -123,6 +123,7 @@
gotags = "//go/config:tags",
linkmode = "//go/config:linkmode",
msan = "//go/config:msan",
+ pgoprofile = "//go/config:pgoprofile",
pure = "//go/config:pure",
race = "//go/config:race",
stamp = select({
diff --git a/docs/go/core/rules.md b/docs/go/core/rules.md
index 22e08a2..99968b2 100644
--- a/docs/go/core/rules.md
+++ b/docs/go/core/rules.md
@@ -126,7 +126,7 @@
<pre>
go_binary(<a href="#go_binary-name">name</a>, <a href="#go_binary-basename">basename</a>, <a href="#go_binary-cdeps">cdeps</a>, <a href="#go_binary-cgo">cgo</a>, <a href="#go_binary-clinkopts">clinkopts</a>, <a href="#go_binary-copts">copts</a>, <a href="#go_binary-cppopts">cppopts</a>, <a href="#go_binary-cxxopts">cxxopts</a>, <a href="#go_binary-data">data</a>, <a href="#go_binary-deps">deps</a>, <a href="#go_binary-embed">embed</a>,
<a href="#go_binary-embedsrcs">embedsrcs</a>, <a href="#go_binary-env">env</a>, <a href="#go_binary-gc_goopts">gc_goopts</a>, <a href="#go_binary-gc_linkopts">gc_linkopts</a>, <a href="#go_binary-goarch">goarch</a>, <a href="#go_binary-goos">goos</a>, <a href="#go_binary-gotags">gotags</a>, <a href="#go_binary-importpath">importpath</a>, <a href="#go_binary-linkmode">linkmode</a>, <a href="#go_binary-msan">msan</a>,
- <a href="#go_binary-out">out</a>, <a href="#go_binary-pure">pure</a>, <a href="#go_binary-race">race</a>, <a href="#go_binary-srcs">srcs</a>, <a href="#go_binary-static">static</a>, <a href="#go_binary-x_defs">x_defs</a>)
+ <a href="#go_binary-out">out</a>, <a href="#go_binary-pgoprofile">pgoprofile</a>, <a href="#go_binary-pure">pure</a>, <a href="#go_binary-race">race</a>, <a href="#go_binary-srcs">srcs</a>, <a href="#go_binary-static">static</a>, <a href="#go_binary-x_defs">x_defs</a>)
</pre>
This builds an executable from a set of source files,
@@ -168,6 +168,7 @@
| <a id="go_binary-linkmode"></a>linkmode | Determines how the binary should be built and linked. This accepts some of the same values as `go build -buildmode` and works the same way. <br><br> <ul> <li>`auto` (default): Controlled by `//go/config:linkmode`, which defaults to `normal`.</li> <li>`normal`: Builds a normal executable with position-dependent code.</li> <li>`pie`: Builds a position-independent executable.</li> <li>`plugin`: Builds a shared library that can be loaded as a Go plugin. Only supported on platforms that support plugins.</li> <li>`c-shared`: Builds a shared library that can be linked into a C program.</li> <li>`c-archive`: Builds an archive that can be linked into a C program.</li> </ul> | String | optional | "auto" |
| <a id="go_binary-msan"></a>msan | Controls whether code is instrumented for memory sanitization. May be one of <code>on</code>, <code>off</code>, or <code>auto</code>. Not available when cgo is disabled. In most cases, it's better to control this on the command line with <code>--@io_bazel_rules_go//go/config:msan</code>. See [mode attributes], specifically [msan]. | String | optional | "auto" |
| <a id="go_binary-out"></a>out | Sets the output filename for the generated executable. When set, <code>go_binary</code> will write this file without mode-specific directory prefixes, without linkmode-specific prefixes like "lib", and without platform-specific suffixes like ".exe". Note that without a mode-specific directory prefix, the output file (but not its dependencies) will be invalidated in Bazel's cache when changing configurations. | String | optional | "" |
+| <a id="go_binary-pgoprofile"></a>pgoprofile | Provides a pprof file to be used for profile guided optimization when compiling go targets. A pprof file can also be provided via <code>--@io_bazel_rules_go//go/config:pgoprofile=<label of a pprof file></code>. Profile guided optimization is only supported on go 1.20+. See https://go.dev/doc/pgo for more information. | <a href="https://bazel.build/concepts/labels">Label</a> | optional | //go/config:empty |
| <a id="go_binary-pure"></a>pure | Controls whether cgo source code and dependencies are compiled and linked, similar to setting <code>CGO_ENABLED</code>. May be one of <code>on</code>, <code>off</code>, or <code>auto</code>. If <code>auto</code>, pure mode is enabled when no C/C++ toolchain is configured or when cross-compiling. It's usually better to control this on the command line with <code>--@io_bazel_rules_go//go/config:pure</code>. See [mode attributes], specifically [pure]. | String | optional | "auto" |
| <a id="go_binary-race"></a>race | Controls whether code is instrumented for race detection. May be one of <code>on</code>, <code>off</code>, or <code>auto</code>. Not available when cgo is disabled. In most cases, it's better to control this on the command line with <code>--@io_bazel_rules_go//go/config:race</code>. See [mode attributes], specifically [race]. | String | optional | "auto" |
| <a id="go_binary-srcs"></a>srcs | The list of Go source files that are compiled to create the package. Only <code>.go</code> and <code>.s</code> files are permitted, unless the <code>cgo</code> attribute is set, in which case, <code>.c .cc .cpp .cxx .h .hh .hpp .hxx .inc .m .mm</code> files are also permitted. Files may be filtered at build time using Go [build constraints]. | <a href="https://bazel.build/concepts/labels">List of labels</a> | optional | [] |
diff --git a/go/config/BUILD.bazel b/go/config/BUILD.bazel
index a292fc9..d33e47e 100644
--- a/go/config/BUILD.bazel
+++ b/go/config/BUILD.bazel
@@ -79,3 +79,14 @@
build_setting_default = [],
visibility = ["//visibility:public"],
)
+
+label_flag(
+ name = "pgoprofile",
+ build_setting_default = ":empty",
+ visibility = ["//visibility:public"],
+)
+
+filegroup(
+ name = "empty",
+ visibility = ["//visibility:public"],
+)
diff --git a/go/private/actions/compilepkg.bzl b/go/private/actions/compilepkg.bzl
index fff807f..4085f0c 100644
--- a/go/private/actions/compilepkg.bzl
+++ b/go/private/actions/compilepkg.bzl
@@ -151,6 +151,10 @@
if clinkopts:
args.add("-ldflags", quote_opts(clinkopts))
+ if go.mode.pgoprofile:
+ args.add("-pgoprofile", go.mode.pgoprofile)
+ inputs.append(go.mode.pgoprofile)
+
go.actions.run(
inputs = inputs,
outputs = outputs,
diff --git a/go/private/actions/stdlib.bzl b/go/private/actions/stdlib.bzl
index abacbc8..3dc6fe7 100644
--- a/go/private/actions/stdlib.bzl
+++ b/go/private/actions/stdlib.bzl
@@ -136,6 +136,11 @@
go.sdk.tools +
[go.sdk.go, go.sdk.package_list, go.sdk.root_file] +
go.crosstool)
+
+ if go.mode.pgoprofile:
+ args.add("-pgoprofile", go.mode.pgoprofile)
+ inputs.append(go.mode.pgoprofile)
+
outputs = [pkg]
go.actions.run(
inputs = inputs,
diff --git a/go/private/context.bzl b/go/private/context.bzl
index db4fe09..fad1de3 100644
--- a/go/private/context.bzl
+++ b/go/private/context.bzl
@@ -269,6 +269,7 @@
"cgo_deps": [],
"cgo_exports": [],
"cc_info": None,
+ "pgoprofile": getattr(attr, "pgoprofile", None),
}
if coverage_instrumented:
source["cover"] = attr_srcs
@@ -530,6 +531,7 @@
stamp = mode.stamp,
label = ctx.label,
cover_format = mode.cover_format,
+ pgoprofile = mode.pgoprofile,
# Action generators
archive = toolchain.actions.archive,
binary = toolchain.actions.binary,
@@ -836,6 +838,7 @@
cover_format = ctx.attr.cover_format[BuildSettingInfo].value,
gc_goopts = ctx.attr.gc_goopts[BuildSettingInfo].value,
amd64 = ctx.attr.amd64,
+ pgoprofile = ctx.attr.pgoprofile,
)]
go_config = rule(
@@ -884,6 +887,10 @@
providers = [BuildSettingInfo],
),
"amd64": attr.string(),
+ "pgoprofile": attr.label(
+ mandatory = True,
+ allow_files = True,
+ ),
},
provides = [GoConfigInfo],
doc = """Collects information about build settings in the current
diff --git a/go/private/mode.bzl b/go/private/mode.bzl
index 83ccd04..e189643 100644
--- a/go/private/mode.bzl
+++ b/go/private/mode.bzl
@@ -93,6 +93,12 @@
goos = go_toolchain.default_goos if getattr(ctx.attr, "goos", "auto") == "auto" else ctx.attr.goos
goarch = go_toolchain.default_goarch if getattr(ctx.attr, "goarch", "auto") == "auto" else ctx.attr.goarch
gc_goopts = go_config_info.gc_goopts if go_config_info else []
+ pgoprofile = None
+ if go_config_info:
+ if len(go_config_info.pgoprofile.files.to_list()) > 2:
+ fail("providing more than one pprof file to pgoprofile is not supported")
+ elif len(go_config_info.pgoprofile.files.to_list()) == 1:
+ pgoprofile = go_config_info.pgoprofile.files.to_list()[0]
# TODO(jayconrod): check for more invalid and contradictory settings.
if pure and race:
@@ -130,6 +136,7 @@
cover_format = cover_format,
amd64 = amd64,
gc_goopts = gc_goopts,
+ pgoprofile = pgoprofile,
)
def installsuffix(mode):
diff --git a/go/private/rules/binary.bzl b/go/private/rules/binary.bzl
index 690eb14..7184e21 100644
--- a/go/private/rules/binary.bzl
+++ b/go/private/rules/binary.bzl
@@ -400,6 +400,15 @@
</ul>
""",
),
+ "pgoprofile": attr.label(
+ allow_files = True,
+ doc = """Provides a pprof file to be used for profile guided optimization when compiling go targets.
+ A pprof file can also be provided via `--@io_bazel_rules_go//go/config:pgoprofile=<label of a pprof file>`.
+ Profile guided optimization is only supported on go 1.20+.
+ See https://go.dev/doc/pgo for more information.
+ """,
+ default = "//go/config:empty",
+ ),
"_go_context_data": attr.label(default = "//:go_context_data", cfg = go_transition),
"_allowlist_function_transition": attr.label(
default = "@bazel_tools//tools/allowlists/function_transition_allowlist",
diff --git a/go/private/rules/transition.bzl b/go/private/rules/transition.bzl
index 4e87e30..89840ee 100644
--- a/go/private/rules/transition.bzl
+++ b/go/private/rules/transition.bzl
@@ -43,6 +43,7 @@
"//go/config:pure",
"//go/config:linkmode",
"//go/config:tags",
+ "//go/config:pgoprofile",
]
def _deduped_and_sorted(strs):
@@ -117,6 +118,10 @@
fail("linkmode: invalid mode {}; want one of {}".format(linkmode, ", ".join(LINKMODES)))
settings["//go/config:linkmode"] = linkmode
+ pgoprofile = getattr(attr, "pgoprofile", "auto")
+ if pgoprofile != "auto":
+ settings["//go/config:pgoprofile"] = pgoprofile
+
for key, original_key in _SETTING_KEY_TO_ORIGINAL_SETTING_KEY.items():
old_value = original_settings[key]
value = settings[key]
@@ -132,6 +137,9 @@
# original setting wasn't set explicitly (empty string) or was set
# explicitly to its default (always a non-empty string with JSON
# encoding, e.g. "\"\"" or "[]").
+ if type(old_value) == "Label":
+ # Label is not JSON serializable, so we need to convert it to a string.
+ old_value = str(old_value)
settings[original_key] = json.encode(old_value)
else:
settings[original_key] = ""
@@ -177,6 +185,7 @@
"//go/config:debug": False,
"//go/config:linkmode": LINKMODE_NORMAL,
"//go/config:tags": [],
+ "//go/config:pgoprofile": Label("//go/config:empty"),
}, **{setting: "" for setting in _SETTING_KEY_TO_ORIGINAL_SETTING_KEY.values()})
_reset_transition_dict = dict(_common_reset_transition_dict, **{
@@ -191,6 +200,7 @@
"//go/config:pure",
"//go/config:linkmode",
"//go/config:tags",
+ "//go/config:pgoprofile",
])
def _go_tool_transition_impl(settings, _attr):
diff --git a/go/tools/builders/compilepkg.go b/go/tools/builders/compilepkg.go
index 6e21ca2..9c6202b 100644
--- a/go/tools/builders/compilepkg.go
+++ b/go/tools/builders/compilepkg.go
@@ -56,6 +56,7 @@
var testFilter string
var gcFlags, asmFlags, cppFlags, cFlags, cxxFlags, objcFlags, objcxxFlags, ldFlags quoteMultiFlag
var coverFormat string
+ var pgoprofile string
fs.Var(&unfilteredSrcs, "src", ".go, .c, .cc, .m, .mm, .s, or .S file to be filtered and compiled")
fs.Var(&coverSrcs, "cover", ".go file that should be instrumented for coverage (must also be a -src)")
fs.Var(&embedSrcs, "embedsrc", "file that may be compiled into the package with a //go:embed directive")
@@ -81,6 +82,7 @@
fs.StringVar(&testFilter, "testfilter", "off", "Controls test package filtering")
fs.StringVar(&coverFormat, "cover_format", "", "Emit source file paths in coverage instrumentation suitable for the specified coverage format")
fs.Var(&recompileInternalDeps, "recompile_internal_deps", "The import path of the direct dependencies that needs to be recompiled.")
+ fs.StringVar(&pgoprofile, "pgoprofile", "", "The pprof profile to consider for profile guided optimization.")
if err := fs.Parse(args); err != nil {
return err
}
@@ -99,6 +101,9 @@
for i := range embedSrcs {
embedSrcs[i] = abs(embedSrcs[i])
}
+ if pgoprofile != "" {
+ pgoprofile = abs(pgoprofile)
+ }
// Filter sources.
srcs, err := filterAndSplitFiles(unfilteredSrcs)
@@ -158,7 +163,8 @@
outFactsPath,
cgoExportHPath,
coverFormat,
- recompileInternalDeps)
+ recompileInternalDeps,
+ pgoprofile)
}
func compileArchive(
@@ -189,6 +195,7 @@
cgoExportHPath string,
coverFormat string,
recompileInternalDeps []string,
+ pgoprofile string,
) error {
workDir, cleanup, err := goenv.workDir()
if err != nil {
@@ -467,7 +474,7 @@
}
// Compile the filtered .go files.
- if err := compileGo(goenv, goSrcs, packagePath, importcfgPath, embedcfgPath, asmHdrPath, symabisPath, gcFlags, outPath); err != nil {
+ if err := compileGo(goenv, goSrcs, packagePath, importcfgPath, embedcfgPath, asmHdrPath, symabisPath, gcFlags, pgoprofile, outPath); err != nil {
return err
}
@@ -537,7 +544,7 @@
return appendFiles(goenv, outXPath, []string{pkgDefPath})
}
-func compileGo(goenv *env, srcs []string, packagePath, importcfgPath, embedcfgPath, asmHdrPath, symabisPath string, gcFlags []string, outPath string) error {
+func compileGo(goenv *env, srcs []string, packagePath, importcfgPath, embedcfgPath, asmHdrPath, symabisPath string, gcFlags []string, pgoprofile string, outPath string) error {
args := goenv.goTool("compile")
args = append(args, "-p", packagePath, "-importcfg", importcfgPath, "-pack")
if embedcfgPath != "" {
@@ -549,6 +556,9 @@
if symabisPath != "" {
args = append(args, "-symabis", symabisPath)
}
+ if pgoprofile != "" {
+ args = append(args, "-pgoprofile", pgoprofile)
+ }
args = append(args, gcFlags...)
args = append(args, "-o", outPath)
args = append(args, "--")
diff --git a/go/tools/builders/stdlib.go b/go/tools/builders/stdlib.go
index d7b2bf0..66ed042 100644
--- a/go/tools/builders/stdlib.go
+++ b/go/tools/builders/stdlib.go
@@ -33,6 +33,7 @@
race := flags.Bool("race", false, "Build in race mode")
shared := flags.Bool("shared", false, "Build in shared mode")
dynlink := flags.Bool("dynlink", false, "Build in dynlink mode")
+ pgoprofile := flags.String("pgoprofile", "", "Build with pgo using the given pprof file")
var packages multiFlag
flags.Var(&packages, "package", "Packages to build")
var gcflags quoteMultiFlag
@@ -130,6 +131,9 @@
if *race {
installArgs = append(installArgs, "-race")
}
+ if *pgoprofile != "" {
+ installArgs = append(installArgs, "-pgo", abs(*pgoprofile))
+ }
if *shared {
gcflags = append(gcflags, "-shared")
ldflags = append(ldflags, "-shared")
diff --git a/tests/core/go_binary/BUILD.bazel b/tests/core/go_binary/BUILD.bazel
index 74d4e04..70901b8 100644
--- a/tests/core/go_binary/BUILD.bazel
+++ b/tests/core/go_binary/BUILD.bazel
@@ -216,3 +216,17 @@
name = "non_executable_test",
srcs = ["non_executable_test.go"],
)
+
+exports_files(["pgo.pprof"])
+
+go_binary(
+ name = "pgo",
+ srcs = ["pgo.go"],
+ pgoprofile = "pgo.pprof",
+)
+
+go_bazel_test(
+ name = "pgo_test",
+ srcs = ["pgo_test.go"],
+ embedsrcs = ["pgo.pprof"],
+)
diff --git a/tests/core/go_binary/pgo.go b/tests/core/go_binary/pgo.go
new file mode 100644
index 0000000..dbcab1a
--- /dev/null
+++ b/tests/core/go_binary/pgo.go
@@ -0,0 +1,42 @@
+package main
+
+import (
+ "bytes"
+ "io"
+ "log"
+ "net/http"
+ _ "net/http/pprof"
+)
+
+func render(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ http.Error(w, "Only POST allowed", http.StatusMethodNotAllowed)
+ return
+ }
+
+ src, err := io.ReadAll(r.Body)
+ if err != nil {
+ log.Printf("error reading body: %v", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ var buf bytes.Buffer
+ if _, err := buf.Write(src); err != nil {
+ log.Printf("error writing response: %v", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+
+ if _, err := io.Copy(w, &buf); err != nil {
+ log.Printf("error writing response: %v", err)
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ return
+ }
+}
+
+func main() {
+ http.HandleFunc("/render", render)
+ log.Printf("Serving on port 8080...")
+ log.Fatal(http.ListenAndServe(":8080", nil))
+}
diff --git a/tests/core/go_binary/pgo.pprof b/tests/core/go_binary/pgo.pprof
new file mode 100644
index 0000000..4ea3b09
--- /dev/null
+++ b/tests/core/go_binary/pgo.pprof
Binary files differ
diff --git a/tests/core/go_binary/pgo_test.go b/tests/core/go_binary/pgo_test.go
new file mode 100644
index 0000000..d4ef405
--- /dev/null
+++ b/tests/core/go_binary/pgo_test.go
@@ -0,0 +1,99 @@
+// Copyright 2023 The Bazel Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package pgo_test
+
+import (
+ _ "embed"
+ "os"
+ "path"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/bazelbuild/rules_go/go/tools/bazel_testing"
+)
+
+//go:embed pgo.pprof
+var pgoProfile []byte
+
+func TestMain(m *testing.M) {
+ bazel_testing.TestMain(m, bazel_testing.Args{
+ Main: `
+-- src/BUILD.bazel --
+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_test")
+
+go_binary(
+ name = "pgo_with_profile",
+ srcs = ["pgo.go"],
+ pgoprofile = ":pgo.pprof",
+)
+
+go_binary(
+ name = "pgo_without_profile",
+ srcs = ["pgo.go"],
+)
+
+-- src/pgo.go --
+package main
+
+import "fmt"
+
+func main() {
+ fmt.Println("Did you know that profile guided optimization was added to the go compiler in go version 1.20?")
+}
+`,
+ })
+}
+
+func TestGoBinaryOutputWithPgoProfileDiffersFromGoBinaryWithoutPgoProfile(t *testing.T) {
+ // Write the pgo.pprof file.
+ // This must be done as txtar changes the content of the pprof file and it could not be parsed.
+ pwd, err := os.Getwd()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(path.Join(pwd, "src", "pgo.pprof"), pgoProfile, 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ // Ensure both targets can be built
+ if err := bazel_testing.RunBazel("build", "//src:all"); err != nil {
+ t.Fatal(err)
+ }
+
+ // Get the paths to the two binaries.
+ var out []byte
+ if out, err = bazel_testing.BazelOutput("cquery", "--output=files", "//src:all"); err != nil {
+ t.Fatal(err)
+ }
+ files := strings.Split(strings.TrimSpace(string(out)), "\n")
+ if len(files) != 2 {
+ t.Fatalf("expected 2 files, got %+v", files)
+ }
+
+ // Verify that the binaries differs.
+ firstBinary, err := os.ReadFile(files[0])
+ if err != nil {
+ t.Fatal(err)
+ }
+ secondBinary, err := os.ReadFile(files[1])
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if reflect.DeepEqual(firstBinary, secondBinary) {
+ t.Fatal("the two binaries are equal when they should be different")
+ }
+}