Source file src/cmd/compile/internal/test/pgo_inl_test.go

     1  // Copyright 2017 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package test
     6  
     7  import (
     8  	"bufio"
     9  	"bytes"
    10  	"fmt"
    11  	"internal/profile"
    12  	"internal/testenv"
    13  	"io"
    14  	"os"
    15  	"path/filepath"
    16  	"regexp"
    17  	"strings"
    18  	"testing"
    19  )
    20  
    21  const profFile = "inline_hot.pprof"
    22  const preProfFile = "inline_hot.pprof.node_map"
    23  
    24  func buildPGOInliningTest(t *testing.T, dir string, gcflag string) []byte {
    25  	const pkg = "example.com/pgo/inline"
    26  
    27  	// Add a go.mod so we have a consistent symbol names in this temp dir.
    28  	goMod := fmt.Sprintf(`module %s
    29  go 1.19
    30  `, pkg)
    31  	if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(goMod), 0644); err != nil {
    32  		t.Fatalf("error writing go.mod: %v", err)
    33  	}
    34  
    35  	exe := filepath.Join(dir, "test.exe")
    36  	args := []string{"test", "-c", "-o", exe, "-gcflags=" + gcflag}
    37  	cmd := testenv.Command(t, testenv.GoToolPath(t), args...)
    38  	cmd.Dir = dir
    39  	cmd = testenv.CleanCmdEnv(cmd)
    40  	t.Log(cmd)
    41  	out, err := cmd.CombinedOutput()
    42  	if err != nil {
    43  		t.Fatalf("build failed: %v, output:\n%s", err, out)
    44  	}
    45  	return out
    46  }
    47  
    48  // testPGOIntendedInlining tests that specific functions are inlined.
    49  func testPGOIntendedInlining(t *testing.T, dir string, profFile string) {
    50  	testenv.MustHaveGoRun(t)
    51  	t.Parallel()
    52  
    53  	const pkg = "example.com/pgo/inline"
    54  
    55  	want := []string{
    56  		"(*BS).NS",
    57  	}
    58  
    59  	// The functions which are not expected to be inlined are as follows.
    60  	wantNot := []string{
    61  		// The calling edge main->A is hot and the cost of A is large
    62  		// than inlineHotCalleeMaxBudget.
    63  		"A",
    64  		// The calling edge BenchmarkA" -> benchmarkB is cold and the
    65  		// cost of A is large than inlineMaxBudget.
    66  		"benchmarkB",
    67  	}
    68  
    69  	must := map[string]bool{
    70  		"(*BS).NS": true,
    71  	}
    72  
    73  	notInlinedReason := make(map[string]string)
    74  	for _, fname := range want {
    75  		fullName := pkg + "." + fname
    76  		if _, ok := notInlinedReason[fullName]; ok {
    77  			t.Errorf("duplicate func: %s", fullName)
    78  		}
    79  		notInlinedReason[fullName] = "unknown reason"
    80  	}
    81  
    82  	// If the compiler emit "cannot inline for function A", the entry A
    83  	// in expectedNotInlinedList will be removed.
    84  	expectedNotInlinedList := make(map[string]struct{})
    85  	for _, fname := range wantNot {
    86  		fullName := pkg + "." + fname
    87  		expectedNotInlinedList[fullName] = struct{}{}
    88  	}
    89  
    90  	// Build the test with the profile. Use a smaller threshold to test.
    91  	// TODO: maybe adjust the test to work with default threshold.
    92  	gcflag := fmt.Sprintf("-m -m -pgoprofile=%s -d=pgoinlinebudget=160,pgoinlinecdfthreshold=90", profFile)
    93  	out := buildPGOInliningTest(t, dir, gcflag)
    94  
    95  	scanner := bufio.NewScanner(bytes.NewReader(out))
    96  	curPkg := ""
    97  	canInline := regexp.MustCompile(`: can inline ([^ ]*)`)
    98  	haveInlined := regexp.MustCompile(`: inlining call to ([^ ]*)`)
    99  	cannotInline := regexp.MustCompile(`: cannot inline ([^ ]*): (.*)`)
   100  	for scanner.Scan() {
   101  		line := scanner.Text()
   102  		t.Logf("child: %s", line)
   103  		if strings.HasPrefix(line, "# ") {
   104  			curPkg = line[2:]
   105  			splits := strings.Split(curPkg, " ")
   106  			curPkg = splits[0]
   107  			continue
   108  		}
   109  		if m := haveInlined.FindStringSubmatch(line); m != nil {
   110  			fname := m[1]
   111  			delete(notInlinedReason, curPkg+"."+fname)
   112  			continue
   113  		}
   114  		if m := canInline.FindStringSubmatch(line); m != nil {
   115  			fname := m[1]
   116  			fullname := curPkg + "." + fname
   117  			// If function must be inlined somewhere, being inlinable is not enough
   118  			if _, ok := must[fullname]; !ok {
   119  				delete(notInlinedReason, fullname)
   120  				continue
   121  			}
   122  		}
   123  		if m := cannotInline.FindStringSubmatch(line); m != nil {
   124  			fname, reason := m[1], m[2]
   125  			fullName := curPkg + "." + fname
   126  			if _, ok := notInlinedReason[fullName]; ok {
   127  				// cmd/compile gave us a reason why
   128  				notInlinedReason[fullName] = reason
   129  			}
   130  			delete(expectedNotInlinedList, fullName)
   131  			continue
   132  		}
   133  	}
   134  	if err := scanner.Err(); err != nil {
   135  		t.Fatalf("error reading output: %v", err)
   136  	}
   137  	for fullName, reason := range notInlinedReason {
   138  		t.Errorf("%s was not inlined: %s", fullName, reason)
   139  	}
   140  
   141  	// If the list expectedNotInlinedList is not empty, it indicates
   142  	// the functions in the expectedNotInlinedList are marked with caninline.
   143  	for fullName, _ := range expectedNotInlinedList {
   144  		t.Errorf("%s was expected not inlined", fullName)
   145  	}
   146  }
   147  
   148  // TestPGOIntendedInlining tests that specific functions are inlined when PGO
   149  // is applied to the exact source that was profiled.
   150  func TestPGOIntendedInlining(t *testing.T) {
   151  	wd, err := os.Getwd()
   152  	if err != nil {
   153  		t.Fatalf("error getting wd: %v", err)
   154  	}
   155  	srcDir := filepath.Join(wd, "testdata/pgo/inline")
   156  
   157  	// Copy the module to a scratch location so we can add a go.mod.
   158  	dir := t.TempDir()
   159  
   160  	for _, file := range []string{"inline_hot.go", "inline_hot_test.go", profFile} {
   161  		if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
   162  			t.Fatalf("error copying %s: %v", file, err)
   163  		}
   164  	}
   165  
   166  	testPGOIntendedInlining(t, dir, profFile)
   167  }
   168  
   169  // TestPGOPreprocessInlining tests that specific functions are inlined when PGO
   170  // is applied to the exact source that was profiled.
   171  func TestPGOPreprocessInlining(t *testing.T) {
   172  	wd, err := os.Getwd()
   173  	if err != nil {
   174  		t.Fatalf("error getting wd: %v", err)
   175  	}
   176  	srcDir := filepath.Join(wd, "testdata/pgo/inline")
   177  
   178  	// Copy the module to a scratch location so we can add a go.mod.
   179  	dir := t.TempDir()
   180  
   181  	for _, file := range []string{"inline_hot.go", "inline_hot_test.go", preProfFile} {
   182  		if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
   183  			t.Fatalf("error copying %s: %v", file, err)
   184  		}
   185  	}
   186  
   187  	testPGOIntendedInlining(t, dir, preProfFile)
   188  }
   189  
   190  // TestPGOIntendedInliningShiftedLines tests that specific functions are inlined when PGO
   191  // is applied to the modified source.
   192  func TestPGOIntendedInliningShiftedLines(t *testing.T) {
   193  	wd, err := os.Getwd()
   194  	if err != nil {
   195  		t.Fatalf("error getting wd: %v", err)
   196  	}
   197  	srcDir := filepath.Join(wd, "testdata/pgo/inline")
   198  
   199  	// Copy the module to a scratch location so we can modify the source.
   200  	dir := t.TempDir()
   201  
   202  	// Copy most of the files unmodified.
   203  	for _, file := range []string{"inline_hot_test.go", profFile} {
   204  		if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
   205  			t.Fatalf("error copying %s : %v", file, err)
   206  		}
   207  	}
   208  
   209  	// Add some comments to the top of inline_hot.go. This adjusts the line
   210  	// numbers of all of the functions without changing the semantics.
   211  	src, err := os.Open(filepath.Join(srcDir, "inline_hot.go"))
   212  	if err != nil {
   213  		t.Fatalf("error opening src inline_hot.go: %v", err)
   214  	}
   215  	defer src.Close()
   216  
   217  	dst, err := os.Create(filepath.Join(dir, "inline_hot.go"))
   218  	if err != nil {
   219  		t.Fatalf("error creating dst inline_hot.go: %v", err)
   220  	}
   221  	defer dst.Close()
   222  
   223  	if _, err := io.WriteString(dst, `// Autogenerated
   224  // Lines
   225  `); err != nil {
   226  		t.Fatalf("error writing comments to dst: %v", err)
   227  	}
   228  
   229  	if _, err := io.Copy(dst, src); err != nil {
   230  		t.Fatalf("error copying inline_hot.go: %v", err)
   231  	}
   232  
   233  	dst.Close()
   234  
   235  	testPGOIntendedInlining(t, dir, profFile)
   236  }
   237  
   238  // TestPGOSingleIndex tests that the sample index can not be 1 and compilation
   239  // will not fail. All it should care about is that the sample type is either
   240  // CPU nanoseconds or samples count, whichever it finds first.
   241  func TestPGOSingleIndex(t *testing.T) {
   242  	for _, tc := range []struct {
   243  		originalIndex int
   244  	}{{
   245  		// The `testdata/pgo/inline/inline_hot.pprof` file is a standard CPU
   246  		// profile as the runtime would generate. The 0 index contains the
   247  		// value-type samples and value-unit count. The 1 index contains the
   248  		// value-type cpu and value-unit nanoseconds. These tests ensure that
   249  		// the compiler can work with profiles that only have a single index,
   250  		// but are either samples count or CPU nanoseconds.
   251  		originalIndex: 0,
   252  	}, {
   253  		originalIndex: 1,
   254  	}} {
   255  		t.Run(fmt.Sprintf("originalIndex=%d", tc.originalIndex), func(t *testing.T) {
   256  			wd, err := os.Getwd()
   257  			if err != nil {
   258  				t.Fatalf("error getting wd: %v", err)
   259  			}
   260  			srcDir := filepath.Join(wd, "testdata/pgo/inline")
   261  
   262  			// Copy the module to a scratch location so we can add a go.mod.
   263  			dir := t.TempDir()
   264  
   265  			originalPprofFile, err := os.Open(filepath.Join(srcDir, profFile))
   266  			if err != nil {
   267  				t.Fatalf("error opening %v: %v", profFile, err)
   268  			}
   269  			defer originalPprofFile.Close()
   270  
   271  			p, err := profile.Parse(originalPprofFile)
   272  			if err != nil {
   273  				t.Fatalf("error parsing %v: %v", profFile, err)
   274  			}
   275  
   276  			// Move the samples count value-type to the 0 index.
   277  			p.SampleType = []*profile.ValueType{p.SampleType[tc.originalIndex]}
   278  
   279  			// Ensure we only have a single set of sample values.
   280  			for _, s := range p.Sample {
   281  				s.Value = []int64{s.Value[tc.originalIndex]}
   282  			}
   283  
   284  			modifiedPprofFile, err := os.Create(filepath.Join(dir, profFile))
   285  			if err != nil {
   286  				t.Fatalf("error creating %v: %v", profFile, err)
   287  			}
   288  			defer modifiedPprofFile.Close()
   289  
   290  			if err := p.Write(modifiedPprofFile); err != nil {
   291  				t.Fatalf("error writing %v: %v", profFile, err)
   292  			}
   293  
   294  			for _, file := range []string{"inline_hot.go", "inline_hot_test.go"} {
   295  				if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
   296  					t.Fatalf("error copying %s: %v", file, err)
   297  				}
   298  			}
   299  
   300  			testPGOIntendedInlining(t, dir, profFile)
   301  		})
   302  	}
   303  }
   304  
   305  func copyFile(dst, src string) error {
   306  	s, err := os.Open(src)
   307  	if err != nil {
   308  		return err
   309  	}
   310  	defer s.Close()
   311  
   312  	d, err := os.Create(dst)
   313  	if err != nil {
   314  		return err
   315  	}
   316  	defer d.Close()
   317  
   318  	_, err = io.Copy(d, s)
   319  	return err
   320  }
   321  
   322  // TestPGOHash tests that PGO optimization decisions can be selected by pgohash.
   323  func TestPGOHash(t *testing.T) {
   324  	testenv.MustHaveGoRun(t)
   325  	t.Parallel()
   326  
   327  	const pkg = "example.com/pgo/inline"
   328  
   329  	wd, err := os.Getwd()
   330  	if err != nil {
   331  		t.Fatalf("error getting wd: %v", err)
   332  	}
   333  	srcDir := filepath.Join(wd, "testdata/pgo/inline")
   334  
   335  	// Copy the module to a scratch location so we can add a go.mod.
   336  	dir := t.TempDir()
   337  
   338  	for _, file := range []string{"inline_hot.go", "inline_hot_test.go", profFile} {
   339  		if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
   340  			t.Fatalf("error copying %s: %v", file, err)
   341  		}
   342  	}
   343  
   344  	pprof := filepath.Join(dir, profFile)
   345  	// build with -trimpath so the source location (thus the hash)
   346  	// does not depend on the temporary directory path.
   347  	gcflag0 := fmt.Sprintf("-pgoprofile=%s -trimpath %s=>%s -d=pgoinlinebudget=160,pgoinlinecdfthreshold=90,pgodebug=1", pprof, dir, pkg)
   348  
   349  	// Check that a hash match allows PGO inlining.
   350  	const srcPos = "example.com/pgo/inline/inline_hot.go:81:19"
   351  	const hashMatch = "pgohash triggered " + srcPos + " (inline)"
   352  	pgoDebugRE := regexp.MustCompile(`hot-budget check allows inlining for call .* at ` + strings.ReplaceAll(srcPos, ".", "\\."))
   353  	hash := "v1" // 1 matches srcPos, v for verbose (print source location)
   354  	gcflag := gcflag0 + ",pgohash=" + hash
   355  	out := buildPGOInliningTest(t, dir, gcflag)
   356  	if !bytes.Contains(out, []byte(hashMatch)) || !pgoDebugRE.Match(out) {
   357  		t.Errorf("output does not contain expected source line, out:\n%s", out)
   358  	}
   359  
   360  	// Check that a hash mismatch turns off PGO inlining.
   361  	hash = "v0" // 0 should not match srcPos
   362  	gcflag = gcflag0 + ",pgohash=" + hash
   363  	out = buildPGOInliningTest(t, dir, gcflag)
   364  	if bytes.Contains(out, []byte(hashMatch)) || pgoDebugRE.Match(out) {
   365  		t.Errorf("output contains unexpected source line, out:\n%s", out)
   366  	}
   367  }
   368  

View as plain text