2022-09-09 11:29:32 -07:00
|
|
|
// 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"
|
|
|
|
|
"fmt"
|
|
|
|
|
"internal/testenv"
|
2022-11-02 11:11:03 -04:00
|
|
|
"io"
|
2022-09-09 11:29:32 -07:00
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
2022-10-28 14:13:59 -04:00
|
|
|
"path/filepath"
|
2022-09-09 11:29:32 -07:00
|
|
|
"regexp"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
|
|
|
|
)
|
|
|
|
|
|
2022-11-02 11:11:03 -04:00
|
|
|
// testPGOIntendedInlining tests that specific functions are inlined.
|
|
|
|
|
func testPGOIntendedInlining(t *testing.T, dir string) {
|
2022-09-09 11:29:32 -07:00
|
|
|
testenv.MustHaveGoRun(t)
|
|
|
|
|
t.Parallel()
|
|
|
|
|
|
2022-11-02 11:11:03 -04:00
|
|
|
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)
|
|
|
|
|
}
|
2022-09-09 11:29:32 -07:00
|
|
|
|
2022-10-28 14:13:59 -04:00
|
|
|
want := []string{
|
|
|
|
|
"(*BS).NS",
|
2022-09-09 11:29:32 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The functions which are not expected to be inlined are as follows.
|
2022-10-28 14:13:59 -04:00
|
|
|
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",
|
2022-09-09 11:29:32 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
must := map[string]bool{
|
|
|
|
|
"(*BS).NS": true,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
notInlinedReason := make(map[string]string)
|
2022-10-28 14:13:59 -04:00
|
|
|
for _, fname := range want {
|
|
|
|
|
fullName := pkg + "." + fname
|
|
|
|
|
if _, ok := notInlinedReason[fullName]; ok {
|
|
|
|
|
t.Errorf("duplicate func: %s", fullName)
|
2022-09-09 11:29:32 -07:00
|
|
|
}
|
2022-10-28 14:13:59 -04:00
|
|
|
notInlinedReason[fullName] = "unknown reason"
|
2022-09-09 11:29:32 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If the compiler emit "cannot inline for function A", the entry A
|
|
|
|
|
// in expectedNotInlinedList will be removed.
|
|
|
|
|
expectedNotInlinedList := make(map[string]struct{})
|
2022-10-28 14:13:59 -04:00
|
|
|
for _, fname := range wantNot {
|
|
|
|
|
fullName := pkg + "." + fname
|
|
|
|
|
expectedNotInlinedList[fullName] = struct{}{}
|
2022-09-09 11:29:32 -07:00
|
|
|
}
|
|
|
|
|
|
2022-11-02 11:11:03 -04:00
|
|
|
// go test -c -o /tmp/test.exe -cpuprofile inline_hot.pprof
|
|
|
|
|
pprof := filepath.Join(dir, "inline_hot.pprof")
|
|
|
|
|
gcflag := fmt.Sprintf("-gcflags=-m -m -pgoprofile %s", pprof)
|
|
|
|
|
out := filepath.Join(dir, "test.exe")
|
|
|
|
|
cmd := testenv.CleanCmdEnv(exec.Command(testenv.GoToolPath(t), "test", "-c", "-o", out, gcflag, "."))
|
|
|
|
|
cmd.Dir = dir
|
2022-09-09 11:29:32 -07:00
|
|
|
|
2022-10-28 14:13:59 -04:00
|
|
|
pr, pw, err := os.Pipe()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("error creating pipe: %v", err)
|
|
|
|
|
}
|
|
|
|
|
defer pr.Close()
|
2022-09-09 11:29:32 -07:00
|
|
|
cmd.Stdout = pw
|
|
|
|
|
cmd.Stderr = pw
|
2022-10-28 14:13:59 -04:00
|
|
|
|
|
|
|
|
err = cmd.Start()
|
|
|
|
|
pw.Close()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("error starting go test: %v", err)
|
|
|
|
|
}
|
|
|
|
|
|
2022-09-09 11:29:32 -07:00
|
|
|
scanner := bufio.NewScanner(pr)
|
|
|
|
|
curPkg := ""
|
|
|
|
|
canInline := regexp.MustCompile(`: can inline ([^ ]*)`)
|
|
|
|
|
haveInlined := regexp.MustCompile(`: inlining call to ([^ ]*)`)
|
|
|
|
|
cannotInline := regexp.MustCompile(`: cannot inline ([^ ]*): (.*)`)
|
|
|
|
|
for scanner.Scan() {
|
|
|
|
|
line := scanner.Text()
|
2022-10-28 14:13:59 -04:00
|
|
|
t.Logf("child: %s", line)
|
2022-09-09 11:29:32 -07:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-10-28 14:13:59 -04:00
|
|
|
if err := cmd.Wait(); err != nil {
|
|
|
|
|
t.Fatalf("error running go test: %v", err)
|
2022-09-09 11:29:32 -07:00
|
|
|
}
|
|
|
|
|
if err := scanner.Err(); err != nil {
|
2022-10-28 14:13:59 -04:00
|
|
|
t.Fatalf("error reading go test output: %v", err)
|
2022-09-09 11:29:32 -07:00
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-11-02 11:11:03 -04:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|