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

View as plain text