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=&lt;label of a pprof file&gt;</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")
+	}
+}