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

     1  // Copyright 2023 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  	"fmt"
    10  	"internal/testenv"
    11  	"os"
    12  	"path/filepath"
    13  	"regexp"
    14  	"testing"
    15  )
    16  
    17  type devirtualization struct {
    18  	pos    string
    19  	callee string
    20  }
    21  
    22  const profFileName = "devirt.pprof"
    23  const preProfFileName = "devirt.pprof.node_map"
    24  
    25  // testPGODevirtualize tests that specific PGO devirtualize rewrites are performed.
    26  func testPGODevirtualize(t *testing.T, dir string, want []devirtualization, pgoProfileName string) {
    27  	testenv.MustHaveGoRun(t)
    28  	t.Parallel()
    29  
    30  	const pkg = "example.com/pgo/devirtualize"
    31  
    32  	// Add a go.mod so we have a consistent symbol names in this temp dir.
    33  	goMod := fmt.Sprintf(`module %s
    34  go 1.21
    35  `, pkg)
    36  	if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(goMod), 0644); err != nil {
    37  		t.Fatalf("error writing go.mod: %v", err)
    38  	}
    39  
    40  	// Run the test without PGO to ensure that the test assertions are
    41  	// correct even in the non-optimized version.
    42  	cmd := testenv.CleanCmdEnv(testenv.Command(t, testenv.GoToolPath(t), "test", "."))
    43  	cmd.Dir = dir
    44  	b, err := cmd.CombinedOutput()
    45  	t.Logf("Test without PGO:\n%s", b)
    46  	if err != nil {
    47  		t.Fatalf("Test failed without PGO: %v", err)
    48  	}
    49  
    50  	// Build the test with the profile.
    51  	pprof := filepath.Join(dir, pgoProfileName)
    52  	gcflag := fmt.Sprintf("-gcflags=-m=2 -pgoprofile=%s -d=pgodebug=3", pprof)
    53  	out := filepath.Join(dir, "test.exe")
    54  	cmd = testenv.CleanCmdEnv(testenv.Command(t, testenv.GoToolPath(t), "test", "-o", out, gcflag, "."))
    55  	cmd.Dir = dir
    56  
    57  	pr, pw, err := os.Pipe()
    58  	if err != nil {
    59  		t.Fatalf("error creating pipe: %v", err)
    60  	}
    61  	defer pr.Close()
    62  	cmd.Stdout = pw
    63  	cmd.Stderr = pw
    64  
    65  	err = cmd.Start()
    66  	pw.Close()
    67  	if err != nil {
    68  		t.Fatalf("error starting go test: %v", err)
    69  	}
    70  
    71  	got := make(map[devirtualization]struct{})
    72  
    73  	devirtualizedLine := regexp.MustCompile(`(.*): PGO devirtualizing \w+ call .* to (.*)`)
    74  
    75  	scanner := bufio.NewScanner(pr)
    76  	for scanner.Scan() {
    77  		line := scanner.Text()
    78  		t.Logf("child: %s", line)
    79  
    80  		m := devirtualizedLine.FindStringSubmatch(line)
    81  		if m == nil {
    82  			continue
    83  		}
    84  
    85  		d := devirtualization{
    86  			pos:    m[1],
    87  			callee: m[2],
    88  		}
    89  		got[d] = struct{}{}
    90  	}
    91  	if err := cmd.Wait(); err != nil {
    92  		t.Fatalf("error running go test: %v", err)
    93  	}
    94  	if err := scanner.Err(); err != nil {
    95  		t.Fatalf("error reading go test output: %v", err)
    96  	}
    97  
    98  	if len(got) != len(want) {
    99  		t.Errorf("mismatched devirtualization count; got %v want %v", got, want)
   100  	}
   101  	for _, w := range want {
   102  		if _, ok := got[w]; ok {
   103  			continue
   104  		}
   105  		t.Errorf("devirtualization %v missing; got %v", w, got)
   106  	}
   107  
   108  	// Run test with PGO to ensure the assertions are still true.
   109  	cmd = testenv.CleanCmdEnv(testenv.Command(t, out))
   110  	cmd.Dir = dir
   111  	b, err = cmd.CombinedOutput()
   112  	t.Logf("Test with PGO:\n%s", b)
   113  	if err != nil {
   114  		t.Fatalf("Test failed without PGO: %v", err)
   115  	}
   116  }
   117  
   118  // TestPGODevirtualize tests that specific functions are devirtualized when PGO
   119  // is applied to the exact source that was profiled.
   120  func TestPGODevirtualize(t *testing.T) {
   121  	wd, err := os.Getwd()
   122  	if err != nil {
   123  		t.Fatalf("error getting wd: %v", err)
   124  	}
   125  	srcDir := filepath.Join(wd, "testdata", "pgo", "devirtualize")
   126  
   127  	// Copy the module to a scratch location so we can add a go.mod.
   128  	dir := t.TempDir()
   129  	if err := os.Mkdir(filepath.Join(dir, "mult.pkg"), 0755); err != nil {
   130  		t.Fatalf("error creating dir: %v", err)
   131  	}
   132  	for _, file := range []string{"devirt.go", "devirt_test.go", profFileName, filepath.Join("mult.pkg", "mult.go")} {
   133  		if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
   134  			t.Fatalf("error copying %s: %v", file, err)
   135  		}
   136  	}
   137  
   138  	want := []devirtualization{
   139  		// ExerciseIface
   140  		{
   141  			pos:    "./devirt.go:101:20",
   142  			callee: "mult.Mult.Multiply",
   143  		},
   144  		{
   145  			pos:    "./devirt.go:101:39",
   146  			callee: "Add.Add",
   147  		},
   148  		// ExerciseFuncConcrete
   149  		{
   150  			pos:    "./devirt.go:173:36",
   151  			callee: "AddFn",
   152  		},
   153  		{
   154  			pos:    "./devirt.go:173:15",
   155  			callee: "mult.MultFn",
   156  		},
   157  		// ExerciseFuncField
   158  		{
   159  			pos:    "./devirt.go:207:35",
   160  			callee: "AddFn",
   161  		},
   162  		{
   163  			pos:    "./devirt.go:207:19",
   164  			callee: "mult.MultFn",
   165  		},
   166  		// ExerciseFuncClosure
   167  		// TODO(prattmic): Closure callees not implemented.
   168  		//{
   169  		//	pos:    "./devirt.go:249:27",
   170  		//	callee: "AddClosure.func1",
   171  		//},
   172  		//{
   173  		//	pos:    "./devirt.go:249:15",
   174  		//	callee: "mult.MultClosure.func1",
   175  		//},
   176  	}
   177  
   178  	testPGODevirtualize(t, dir, want, profFileName)
   179  }
   180  
   181  // TestPGOPreprocessDevirtualize tests that specific functions are devirtualized when PGO
   182  // is applied to the exact source that was profiled. The input profile is PGO preprocessed file.
   183  func TestPGOPreprocessDevirtualize(t *testing.T) {
   184  	wd, err := os.Getwd()
   185  	if err != nil {
   186  		t.Fatalf("error getting wd: %v", err)
   187  	}
   188  	srcDir := filepath.Join(wd, "testdata", "pgo", "devirtualize")
   189  
   190  	// Copy the module to a scratch location so we can add a go.mod.
   191  	dir := t.TempDir()
   192  	if err := os.Mkdir(filepath.Join(dir, "mult.pkg"), 0755); err != nil {
   193  		t.Fatalf("error creating dir: %v", err)
   194  	}
   195  	for _, file := range []string{"devirt.go", "devirt_test.go", preProfFileName, filepath.Join("mult.pkg", "mult.go")} {
   196  		if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
   197  			t.Fatalf("error copying %s: %v", file, err)
   198  		}
   199  	}
   200  
   201  	want := []devirtualization{
   202  		// ExerciseIface
   203  		{
   204  			pos:    "./devirt.go:101:20",
   205  			callee: "mult.Mult.Multiply",
   206  		},
   207  		{
   208  			pos:    "./devirt.go:101:39",
   209  			callee: "Add.Add",
   210  		},
   211  		// ExerciseFuncConcrete
   212  		{
   213  			pos:    "./devirt.go:173:36",
   214  			callee: "AddFn",
   215  		},
   216  		{
   217  			pos:    "./devirt.go:173:15",
   218  			callee: "mult.MultFn",
   219  		},
   220  		// ExerciseFuncField
   221  		{
   222  			pos:    "./devirt.go:207:35",
   223  			callee: "AddFn",
   224  		},
   225  		{
   226  			pos:    "./devirt.go:207:19",
   227  			callee: "mult.MultFn",
   228  		},
   229  		// ExerciseFuncClosure
   230  		// TODO(prattmic): Closure callees not implemented.
   231  		//{
   232  		//	pos:    "./devirt.go:249:27",
   233  		//	callee: "AddClosure.func1",
   234  		//},
   235  		//{
   236  		//	pos:    "./devirt.go:249:15",
   237  		//	callee: "mult.MultClosure.func1",
   238  		//},
   239  	}
   240  
   241  	testPGODevirtualize(t, dir, want, preProfFileName)
   242  }
   243  
   244  // Regression test for https://go.dev/issue/65615. If a target function changes
   245  // from non-generic to generic we can't devirtualize it (don't know the type
   246  // parameters), but the compiler should not crash.
   247  func TestLookupFuncGeneric(t *testing.T) {
   248  	wd, err := os.Getwd()
   249  	if err != nil {
   250  		t.Fatalf("error getting wd: %v", err)
   251  	}
   252  	srcDir := filepath.Join(wd, "testdata", "pgo", "devirtualize")
   253  
   254  	// Copy the module to a scratch location so we can add a go.mod.
   255  	dir := t.TempDir()
   256  	if err := os.Mkdir(filepath.Join(dir, "mult.pkg"), 0755); err != nil {
   257  		t.Fatalf("error creating dir: %v", err)
   258  	}
   259  	for _, file := range []string{"devirt.go", "devirt_test.go", profFileName, filepath.Join("mult.pkg", "mult.go")} {
   260  		if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
   261  			t.Fatalf("error copying %s: %v", file, err)
   262  		}
   263  	}
   264  
   265  	// Change MultFn from a concrete function to a parameterized function.
   266  	if err := convertMultToGeneric(filepath.Join(dir, "mult.pkg", "mult.go")); err != nil {
   267  		t.Fatalf("error editing mult.go: %v", err)
   268  	}
   269  
   270  	// Same as TestPGODevirtualize except for MultFn, which we cannot
   271  	// devirtualize to because it has become generic.
   272  	//
   273  	// Note that the important part of this test is that the build is
   274  	// successful, not the specific devirtualizations.
   275  	want := []devirtualization{
   276  		// ExerciseIface
   277  		{
   278  			pos:    "./devirt.go:101:20",
   279  			callee: "mult.Mult.Multiply",
   280  		},
   281  		{
   282  			pos:    "./devirt.go:101:39",
   283  			callee: "Add.Add",
   284  		},
   285  		// ExerciseFuncConcrete
   286  		{
   287  			pos:    "./devirt.go:173:36",
   288  			callee: "AddFn",
   289  		},
   290  		// ExerciseFuncField
   291  		{
   292  			pos:    "./devirt.go:207:35",
   293  			callee: "AddFn",
   294  		},
   295  		// ExerciseFuncClosure
   296  		// TODO(prattmic): Closure callees not implemented.
   297  		//{
   298  		//	pos:    "./devirt.go:249:27",
   299  		//	callee: "AddClosure.func1",
   300  		//},
   301  		//{
   302  		//	pos:    "./devirt.go:249:15",
   303  		//	callee: "mult.MultClosure.func1",
   304  		//},
   305  	}
   306  
   307  	testPGODevirtualize(t, dir, want, profFileName)
   308  }
   309  
   310  var multFnRe = regexp.MustCompile(`func MultFn\(a, b int64\) int64`)
   311  
   312  func convertMultToGeneric(path string) error {
   313  	content, err := os.ReadFile(path)
   314  	if err != nil {
   315  		return fmt.Errorf("error opening: %w", err)
   316  	}
   317  
   318  	if !multFnRe.Match(content) {
   319  		return fmt.Errorf("MultFn not found; update regexp?")
   320  	}
   321  
   322  	// Users of MultFn shouldn't need adjustment, type inference should
   323  	// work OK.
   324  	content = multFnRe.ReplaceAll(content, []byte(`func MultFn[T int32|int64](a, b T) T`))
   325  
   326  	return os.WriteFile(path, content, 0644)
   327  }
   328  

View as plain text