var FmaHash *HashDebug // for debugging fused-multiply-add floating point changes
var LoopVarHash *HashDebug // for debugging shared/private loop variable changes
+var PGOHash *HashDebug // for debugging PGO optimization decisions
// DebugHashMatchPkgFunc reports whether debug variable Gossahash
//
}
func (d *HashDebug) matchPos(ctxt *obj.Link, pos src.XPos, note func() string) bool {
+ return d.matchPosWithInfo(ctxt, pos, nil, note)
+}
+
+func (d *HashDebug) matchPosWithInfo(ctxt *obj.Link, pos src.XPos, info any, note func() string) bool {
hash := d.hashPos(ctxt, pos)
- return d.matchAndLog(hash, func() string { return d.fmtPos(ctxt, pos) }, note)
+ if info != nil {
+ hash = bisect.Hash(hash, info)
+ }
+ return d.matchAndLog(hash,
+ func() string {
+ r := d.fmtPos(ctxt, pos)
+ if info != nil {
+ r += fmt.Sprintf(" (%v)", info)
+ }
+ return r
+ },
+ note)
+}
+
+// MatchPosWithInfo is similar to MatchPos, but with additional information
+// that is included for hash computation, so it can distinguish multiple
+// matches on the same source location.
+// Note that the default answer for no environment variable (d == nil)
+// is "yes", do the thing.
+func (d *HashDebug) MatchPosWithInfo(pos src.XPos, info any, desc func() string) bool {
+ if d == nil {
+ return true
+ }
+ // Written this way to make inlining likely.
+ return d.matchPosWithInfo(Ctxt, pos, info, desc)
}
// matchAndLog is the core matcher. It reports whether the hash matches the pattern.
import (
"bufio"
+ "bytes"
"fmt"
"internal/profile"
"internal/testenv"
"testing"
)
-// testPGOIntendedInlining tests that specific functions are inlined.
-func testPGOIntendedInlining(t *testing.T, dir string) {
- testenv.MustHaveGoRun(t)
- t.Parallel()
-
+func buildPGOInliningTest(t *testing.T, dir string, flags ...string) []byte {
const pkg = "example.com/pgo/inline"
// Add a go.mod so we have a consistent symbol names in this temp dir.
t.Fatalf("error writing go.mod: %v", err)
}
+ exe := filepath.Join(dir, "test.exe")
+ args := []string{"test", "-c", "-o", exe}
+ args = append(args, flags...)
+ cmd := testenv.CleanCmdEnv(testenv.Command(t, testenv.GoToolPath(t), args...))
+ cmd.Dir = dir
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("build failed: %v, output:\n%s", err, out)
+ }
+ return out
+}
+
+// testPGOIntendedInlining tests that specific functions are inlined.
+func testPGOIntendedInlining(t *testing.T, dir string) {
+ testenv.MustHaveGoRun(t)
+ t.Parallel()
+
+ const pkg = "example.com/pgo/inline"
+
want := []string{
"(*BS).NS",
}
// TODO: maybe adjust the test to work with default threshold.
pprof := filepath.Join(dir, "inline_hot.pprof")
gcflag := fmt.Sprintf("-gcflags=-m -m -pgoprofile=%s -d=pgoinlinebudget=160,pgoinlinecdfthreshold=90", pprof)
- out := filepath.Join(dir, "test.exe")
- cmd := testenv.CleanCmdEnv(testenv.Command(t, testenv.GoToolPath(t), "test", "-c", "-o", out, gcflag, "."))
- cmd.Dir = dir
-
- pr, pw, err := os.Pipe()
- if err != nil {
- t.Fatalf("error creating pipe: %v", err)
- }
- defer pr.Close()
- cmd.Stdout = pw
- cmd.Stderr = pw
-
- err = cmd.Start()
- pw.Close()
- if err != nil {
- t.Fatalf("error starting go test: %v", err)
- }
+ out := buildPGOInliningTest(t, dir, gcflag)
- scanner := bufio.NewScanner(pr)
+ scanner := bufio.NewScanner(bytes.NewReader(out))
curPkg := ""
canInline := regexp.MustCompile(`: can inline ([^ ]*)`)
haveInlined := regexp.MustCompile(`: inlining call to ([^ ]*)`)
continue
}
}
- if err := cmd.Wait(); err != nil {
- t.Fatalf("error running go test: %v", err)
- }
if err := scanner.Err(); err != nil {
- t.Fatalf("error reading go test output: %v", err)
+ t.Fatalf("error reading output: %v", err)
}
for fullName, reason := range notInlinedReason {
t.Errorf("%s was not inlined: %s", fullName, reason)
_, err = io.Copy(d, s)
return err
}
+
+// TestPGOHash tests that PGO optimization decisions can be selected by pgohash.
+func TestPGOHash(t *testing.T) {
+ testenv.MustHaveGoRun(t)
+ t.Parallel()
+
+ wd, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("error getting wd: %v", err)
+ }
+ srcDir := filepath.Join(wd, "testdata/pgo/inline")
+
+ // Copy the module to a scratch location so we can add a go.mod.
+ dir := t.TempDir()
+
+ for _, file := range []string{"inline_hot.go", "inline_hot_test.go", "inline_hot.pprof"} {
+ if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
+ t.Fatalf("error copying %s: %v", file, err)
+ }
+ }
+
+ pprof := filepath.Join(dir, "inline_hot.pprof")
+ gcflag0 := fmt.Sprintf("-gcflags=-pgoprofile=%s -d=pgoinlinebudget=160,pgoinlinecdfthreshold=90,pgodebug=1,", pprof)
+
+ // Check that a hash match allows PGO inlining.
+ const srcPos = "example.com/pgo/inline/inline_hot.go:81:19"
+ const hashMatch = "pgohash triggered " + srcPos + " (inline)"
+ pgoDebugRE := regexp.MustCompile(`hot-budget check allows inlining for call .* at ` + strings.ReplaceAll(srcPos, ".", "\\."))
+ hash := "v1" // 1 matches srcPos, v for verbose (print source location)
+ gcflag := gcflag0 + ",pgohash=" + hash
+ // build with -trimpath so the source location (thus the hash)
+ // does not depend on the temporary directory path.
+ out := buildPGOInliningTest(t, dir, gcflag, "-trimpath")
+ if !bytes.Contains(out, []byte(hashMatch)) || !pgoDebugRE.Match(out) {
+ t.Errorf("output does not contain expected source line, out:\n%s", out)
+ }
+
+ // Check that a hash mismatch turns off PGO inlining.
+ hash = "v0" // 0 should not match srcPos
+ gcflag = gcflag0 + ",pgohash=" + hash
+ out = buildPGOInliningTest(t, dir, gcflag, "-trimpath")
+ if bytes.Contains(out, []byte(hashMatch)) || pgoDebugRE.Match(out) {
+ t.Errorf("output contains unexpected source line, out:\n%s", out)
+ }
+}