blob: da6c4a53d362288bb3b1b4f7f919d92b5e00b9b3 [file] [log] [blame]
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package test
import (
"bufio"
"bytes"
"fmt"
"internal/profile"
"internal/testenv"
"io"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
func buildPGOInliningTest(t *testing.T, dir string, gcflag string) []byte {
const pkg = "example.com/pgo/inline"
// Add a go.mod so we have a consistent symbol names in this temp dir.
goMod := fmt.Sprintf(`module %s
go 1.19
`, pkg)
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(goMod), 0644); err != nil {
t.Fatalf("error writing go.mod: %v", err)
}
exe := filepath.Join(dir, "test.exe")
args := []string{"test", "-c", "-o", exe, "-gcflags=" + gcflag}
cmd := testenv.Command(t, testenv.GoToolPath(t), args...)
cmd.Dir = dir
cmd = testenv.CleanCmdEnv(cmd)
t.Log(cmd)
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",
}
// The functions which are not expected to be inlined are as follows.
wantNot := []string{
// The calling edge main->A is hot and the cost of A is large
// than inlineHotCalleeMaxBudget.
"A",
// The calling edge BenchmarkA" -> benchmarkB is cold and the
// cost of A is large than inlineMaxBudget.
"benchmarkB",
}
must := map[string]bool{
"(*BS).NS": true,
}
notInlinedReason := make(map[string]string)
for _, fname := range want {
fullName := pkg + "." + fname
if _, ok := notInlinedReason[fullName]; ok {
t.Errorf("duplicate func: %s", fullName)
}
notInlinedReason[fullName] = "unknown reason"
}
// If the compiler emit "cannot inline for function A", the entry A
// in expectedNotInlinedList will be removed.
expectedNotInlinedList := make(map[string]struct{})
for _, fname := range wantNot {
fullName := pkg + "." + fname
expectedNotInlinedList[fullName] = struct{}{}
}
// Build the test with the profile. Use a smaller threshold to test.
// TODO: maybe adjust the test to work with default threshold.
pprof := filepath.Join(dir, "inline_hot.pprof")
gcflag := fmt.Sprintf("-m -m -pgoprofile=%s -d=pgoinlinebudget=160,pgoinlinecdfthreshold=90", pprof)
out := buildPGOInliningTest(t, dir, gcflag)
scanner := bufio.NewScanner(bytes.NewReader(out))
curPkg := ""
canInline := regexp.MustCompile(`: can inline ([^ ]*)`)
haveInlined := regexp.MustCompile(`: inlining call to ([^ ]*)`)
cannotInline := regexp.MustCompile(`: cannot inline ([^ ]*): (.*)`)
for scanner.Scan() {
line := scanner.Text()
t.Logf("child: %s", line)
if strings.HasPrefix(line, "# ") {
curPkg = line[2:]
splits := strings.Split(curPkg, " ")
curPkg = splits[0]
continue
}
if m := haveInlined.FindStringSubmatch(line); m != nil {
fname := m[1]
delete(notInlinedReason, curPkg+"."+fname)
continue
}
if m := canInline.FindStringSubmatch(line); m != nil {
fname := m[1]
fullname := curPkg + "." + fname
// If function must be inlined somewhere, being inlinable is not enough
if _, ok := must[fullname]; !ok {
delete(notInlinedReason, fullname)
continue
}
}
if m := cannotInline.FindStringSubmatch(line); m != nil {
fname, reason := m[1], m[2]
fullName := curPkg + "." + fname
if _, ok := notInlinedReason[fullName]; ok {
// cmd/compile gave us a reason why
notInlinedReason[fullName] = reason
}
delete(expectedNotInlinedList, fullName)
continue
}
}
if err := scanner.Err(); err != nil {
t.Fatalf("error reading output: %v", err)
}
for fullName, reason := range notInlinedReason {
t.Errorf("%s was not inlined: %s", fullName, reason)
}
// If the list expectedNotInlinedList is not empty, it indicates
// the functions in the expectedNotInlinedList are marked with caninline.
for fullName, _ := range expectedNotInlinedList {
t.Errorf("%s was expected not inlined", fullName)
}
}
// TestPGOIntendedInlining tests that specific functions are inlined when PGO
// is applied to the exact source that was profiled.
func TestPGOIntendedInlining(t *testing.T) {
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)
}
}
testPGOIntendedInlining(t, dir)
}
// TestPGOIntendedInlining tests that specific functions are inlined when PGO
// is applied to the modified source.
func TestPGOIntendedInliningShiftedLines(t *testing.T) {
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 modify the source.
dir := t.TempDir()
// Copy most of the files unmodified.
for _, file := range []string{"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)
}
}
// Add some comments to the top of inline_hot.go. This adjusts the line
// numbers of all of the functions without changing the semantics.
src, err := os.Open(filepath.Join(srcDir, "inline_hot.go"))
if err != nil {
t.Fatalf("error opening src inline_hot.go: %v", err)
}
defer src.Close()
dst, err := os.Create(filepath.Join(dir, "inline_hot.go"))
if err != nil {
t.Fatalf("error creating dst inline_hot.go: %v", err)
}
defer dst.Close()
if _, err := io.WriteString(dst, `// Autogenerated
// Lines
`); err != nil {
t.Fatalf("error writing comments to dst: %v", err)
}
if _, err := io.Copy(dst, src); err != nil {
t.Fatalf("error copying inline_hot.go: %v", err)
}
dst.Close()
testPGOIntendedInlining(t, dir)
}
// TestPGOSingleIndex tests that the sample index can not be 1 and compilation
// will not fail. All it should care about is that the sample type is either
// CPU nanoseconds or samples count, whichever it finds first.
func TestPGOSingleIndex(t *testing.T) {
for _, tc := range []struct {
originalIndex int
}{{
// The `testdata/pgo/inline/inline_hot.pprof` file is a standard CPU
// profile as the runtime would generate. The 0 index contains the
// value-type samples and value-unit count. The 1 index contains the
// value-type cpu and value-unit nanoseconds. These tests ensure that
// the compiler can work with profiles that only have a single index,
// but are either samples count or CPU nanoseconds.
originalIndex: 0,
}, {
originalIndex: 1,
}} {
t.Run(fmt.Sprintf("originalIndex=%d", tc.originalIndex), func(t *testing.T) {
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()
originalPprofFile, err := os.Open(filepath.Join(srcDir, "inline_hot.pprof"))
if err != nil {
t.Fatalf("error opening inline_hot.pprof: %v", err)
}
defer originalPprofFile.Close()
p, err := profile.Parse(originalPprofFile)
if err != nil {
t.Fatalf("error parsing inline_hot.pprof: %v", err)
}
// Move the samples count value-type to the 0 index.
p.SampleType = []*profile.ValueType{p.SampleType[tc.originalIndex]}
// Ensure we only have a single set of sample values.
for _, s := range p.Sample {
s.Value = []int64{s.Value[tc.originalIndex]}
}
modifiedPprofFile, err := os.Create(filepath.Join(dir, "inline_hot.pprof"))
if err != nil {
t.Fatalf("error creating inline_hot.pprof: %v", err)
}
defer modifiedPprofFile.Close()
if err := p.Write(modifiedPprofFile); err != nil {
t.Fatalf("error writing inline_hot.pprof: %v", err)
}
for _, file := range []string{"inline_hot.go", "inline_hot_test.go"} {
if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
t.Fatalf("error copying %s: %v", file, err)
}
}
testPGOIntendedInlining(t, dir)
})
}
}
func copyFile(dst, src string) error {
s, err := os.Open(src)
if err != nil {
return err
}
defer s.Close()
d, err := os.Create(dst)
if err != nil {
return err
}
defer d.Close()
_, 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()
const pkg = "example.com/pgo/inline"
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")
// build with -trimpath so the source location (thus the hash)
// does not depend on the temporary directory path.
gcflag0 := fmt.Sprintf("-pgoprofile=%s -trimpath %s=>%s -d=pgoinlinebudget=160,pgoinlinecdfthreshold=90,pgodebug=1", pprof, dir, pkg)
// 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
out := buildPGOInliningTest(t, dir, gcflag)
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)
if bytes.Contains(out, []byte(hashMatch)) || pgoDebugRE.Match(out) {
t.Errorf("output contains unexpected source line, out:\n%s", out)
}
}