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, nowant []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  	gotNoHot := make(map[devirtualization]struct{})
    73  
    74  	devirtualizedLine := regexp.MustCompile(`(.*): PGO devirtualizing \w+ call .* to (.*)`)
    75  	noHotLine := regexp.MustCompile(`(.*): call .*: no hot callee`)
    76  
    77  	scanner := bufio.NewScanner(pr)
    78  	for scanner.Scan() {
    79  		line := scanner.Text()
    80  		t.Logf("child: %s", line)
    81  
    82  		m := devirtualizedLine.FindStringSubmatch(line)
    83  		if m != nil {
    84  			d := devirtualization{
    85  				pos:    m[1],
    86  				callee: m[2],
    87  			}
    88  			got[d] = struct{}{}
    89  			continue
    90  		}
    91  		m = noHotLine.FindStringSubmatch(line)
    92  		if m != nil {
    93  			d := devirtualization{
    94  				pos: m[1],
    95  			}
    96  			gotNoHot[d] = struct{}{}
    97  		}
    98  	}
    99  	if err := cmd.Wait(); err != nil {
   100  		t.Fatalf("error running go test: %v", err)
   101  	}
   102  	if err := scanner.Err(); err != nil {
   103  		t.Fatalf("error reading go test output: %v", err)
   104  	}
   105  
   106  	if len(got) != len(want) {
   107  		t.Errorf("mismatched devirtualization count; got %v want %v", got, want)
   108  	}
   109  	for _, w := range want {
   110  		if _, ok := got[w]; ok {
   111  			continue
   112  		}
   113  		t.Errorf("devirtualization %v missing; got %v", w, got)
   114  	}
   115  	for _, nw := range nowant {
   116  		if _, ok := gotNoHot[nw]; !ok {
   117  			t.Errorf("unwanted devirtualization %v; got %v", nw, got)
   118  		}
   119  	}
   120  
   121  	// Run test with PGO to ensure the assertions are still true.
   122  	cmd = testenv.CleanCmdEnv(testenv.Command(t, out))
   123  	cmd.Dir = dir
   124  	b, err = cmd.CombinedOutput()
   125  	t.Logf("Test with PGO:\n%s", b)
   126  	if err != nil {
   127  		t.Fatalf("Test failed without PGO: %v", err)
   128  	}
   129  }
   130  
   131  // TestPGODevirtualize tests that specific functions are devirtualized when PGO
   132  // is applied to the exact source that was profiled.
   133  func TestPGODevirtualize(t *testing.T) {
   134  	wd, err := os.Getwd()
   135  	if err != nil {
   136  		t.Fatalf("error getting wd: %v", err)
   137  	}
   138  	srcDir := filepath.Join(wd, "testdata", "pgo", "devirtualize")
   139  
   140  	// Copy the module to a scratch location so we can add a go.mod.
   141  	dir := t.TempDir()
   142  	if err := os.Mkdir(filepath.Join(dir, "mult.pkg"), 0755); err != nil {
   143  		t.Fatalf("error creating dir: %v", err)
   144  	}
   145  	for _, file := range []string{"devirt.go", "devirt_test.go", profFileName, filepath.Join("mult.pkg", "mult.go")} {
   146  		if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
   147  			t.Fatalf("error copying %s: %v", file, err)
   148  		}
   149  	}
   150  
   151  	want := []devirtualization{
   152  		// ExerciseIface
   153  		{
   154  			pos:    "./devirt.go:101:20",
   155  			callee: "mult.Mult.Multiply",
   156  		},
   157  		{
   158  			pos:    "./devirt.go:101:39",
   159  			callee: "Add.Add",
   160  		},
   161  		// ExerciseFuncConcrete
   162  		{
   163  			pos:    "./devirt.go:173:36",
   164  			callee: "AddFn",
   165  		},
   166  		{
   167  			pos:    "./devirt.go:173:15",
   168  			callee: "mult.MultFn",
   169  		},
   170  		// ExerciseFuncField
   171  		{
   172  			pos:    "./devirt.go:207:35",
   173  			callee: "AddFn",
   174  		},
   175  		{
   176  			pos:    "./devirt.go:207:19",
   177  			callee: "mult.MultFn",
   178  		},
   179  		// ExerciseFuncClosure
   180  		// TODO(prattmic): Closure callees not implemented.
   181  		//{
   182  		//	pos:    "./devirt.go:249:27",
   183  		//	callee: "AddClosure.func1",
   184  		//},
   185  		//{
   186  		//	pos:    "./devirt.go:249:15",
   187  		//	callee: "mult.MultClosure.func1",
   188  		//},
   189  	}
   190  	nowant := []devirtualization{
   191  		// ExerciseIfaceZeroWeight
   192  		{
   193  			pos: "./devirt.go:256:29",
   194  		},
   195  		// ExerciseIndirCallZeroWeight
   196  		{
   197  			pos: "./devirt.go:282:37",
   198  		},
   199  	}
   200  
   201  	testPGODevirtualize(t, dir, want, nowant, profFileName)
   202  }
   203  
   204  // TestPGOPreprocessDevirtualize tests that specific functions are devirtualized when PGO
   205  // is applied to the exact source that was profiled. The input profile is PGO preprocessed file.
   206  func TestPGOPreprocessDevirtualize(t *testing.T) {
   207  	wd, err := os.Getwd()
   208  	if err != nil {
   209  		t.Fatalf("error getting wd: %v", err)
   210  	}
   211  	srcDir := filepath.Join(wd, "testdata", "pgo", "devirtualize")
   212  
   213  	// Copy the module to a scratch location so we can add a go.mod.
   214  	dir := t.TempDir()
   215  	if err := os.Mkdir(filepath.Join(dir, "mult.pkg"), 0755); err != nil {
   216  		t.Fatalf("error creating dir: %v", err)
   217  	}
   218  	for _, file := range []string{"devirt.go", "devirt_test.go", preProfFileName, filepath.Join("mult.pkg", "mult.go")} {
   219  		if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
   220  			t.Fatalf("error copying %s: %v", file, err)
   221  		}
   222  	}
   223  
   224  	want := []devirtualization{
   225  		// ExerciseIface
   226  		{
   227  			pos:    "./devirt.go:101:20",
   228  			callee: "mult.Mult.Multiply",
   229  		},
   230  		{
   231  			pos:    "./devirt.go:101:39",
   232  			callee: "Add.Add",
   233  		},
   234  		// ExerciseFuncConcrete
   235  		{
   236  			pos:    "./devirt.go:173:36",
   237  			callee: "AddFn",
   238  		},
   239  		{
   240  			pos:    "./devirt.go:173:15",
   241  			callee: "mult.MultFn",
   242  		},
   243  		// ExerciseFuncField
   244  		{
   245  			pos:    "./devirt.go:207:35",
   246  			callee: "AddFn",
   247  		},
   248  		{
   249  			pos:    "./devirt.go:207:19",
   250  			callee: "mult.MultFn",
   251  		},
   252  		// ExerciseFuncClosure
   253  		// TODO(prattmic): Closure callees not implemented.
   254  		//{
   255  		//	pos:    "./devirt.go:249:27",
   256  		//	callee: "AddClosure.func1",
   257  		//},
   258  		//{
   259  		//	pos:    "./devirt.go:249:15",
   260  		//	callee: "mult.MultClosure.func1",
   261  		//},
   262  	}
   263  	nowant := []devirtualization{
   264  		// ExerciseIfaceZeroWeight
   265  		{
   266  			pos: "./devirt.go:256:29",
   267  		},
   268  		// ExerciseIndirCallZeroWeight
   269  		{
   270  			pos: "./devirt.go:282:37",
   271  		},
   272  	}
   273  
   274  	testPGODevirtualize(t, dir, want, nowant, preProfFileName)
   275  }
   276  
   277  // Regression test for https://go.dev/issue/65615. If a target function changes
   278  // from non-generic to generic we can't devirtualize it (don't know the type
   279  // parameters), but the compiler should not crash.
   280  func TestLookupFuncGeneric(t *testing.T) {
   281  	wd, err := os.Getwd()
   282  	if err != nil {
   283  		t.Fatalf("error getting wd: %v", err)
   284  	}
   285  	srcDir := filepath.Join(wd, "testdata", "pgo", "devirtualize")
   286  
   287  	// Copy the module to a scratch location so we can add a go.mod.
   288  	dir := t.TempDir()
   289  	if err := os.Mkdir(filepath.Join(dir, "mult.pkg"), 0755); err != nil {
   290  		t.Fatalf("error creating dir: %v", err)
   291  	}
   292  	for _, file := range []string{"devirt.go", "devirt_test.go", profFileName, filepath.Join("mult.pkg", "mult.go")} {
   293  		if err := copyFile(filepath.Join(dir, file), filepath.Join(srcDir, file)); err != nil {
   294  			t.Fatalf("error copying %s: %v", file, err)
   295  		}
   296  	}
   297  
   298  	// Change MultFn from a concrete function to a parameterized function.
   299  	if err := convertMultToGeneric(filepath.Join(dir, "mult.pkg", "mult.go")); err != nil {
   300  		t.Fatalf("error editing mult.go: %v", err)
   301  	}
   302  
   303  	// Same as TestPGODevirtualize except for MultFn, which we cannot
   304  	// devirtualize to because it has become generic.
   305  	//
   306  	// Note that the important part of this test is that the build is
   307  	// successful, not the specific devirtualizations.
   308  	want := []devirtualization{
   309  		// ExerciseIface
   310  		{
   311  			pos:    "./devirt.go:101:20",
   312  			callee: "mult.Mult.Multiply",
   313  		},
   314  		{
   315  			pos:    "./devirt.go:101:39",
   316  			callee: "Add.Add",
   317  		},
   318  		// ExerciseFuncConcrete
   319  		{
   320  			pos:    "./devirt.go:173:36",
   321  			callee: "AddFn",
   322  		},
   323  		// ExerciseFuncField
   324  		{
   325  			pos:    "./devirt.go:207:35",
   326  			callee: "AddFn",
   327  		},
   328  		// ExerciseFuncClosure
   329  		// TODO(prattmic): Closure callees not implemented.
   330  		//{
   331  		//	pos:    "./devirt.go:249:27",
   332  		//	callee: "AddClosure.func1",
   333  		//},
   334  		//{
   335  		//	pos:    "./devirt.go:249:15",
   336  		//	callee: "mult.MultClosure.func1",
   337  		//},
   338  	}
   339  	nowant := []devirtualization{
   340  		// ExerciseIfaceZeroWeight
   341  		{
   342  			pos: "./devirt.go:256:29",
   343  		},
   344  		// ExerciseIndirCallZeroWeight
   345  		{
   346  			pos: "./devirt.go:282:37",
   347  		},
   348  	}
   349  
   350  	testPGODevirtualize(t, dir, want, nowant, profFileName)
   351  }
   352  
   353  var multFnRe = regexp.MustCompile(`func MultFn\(a, b int64\) int64`)
   354  
   355  func convertMultToGeneric(path string) error {
   356  	content, err := os.ReadFile(path)
   357  	if err != nil {
   358  		return fmt.Errorf("error opening: %w", err)
   359  	}
   360  
   361  	if !multFnRe.Match(content) {
   362  		return fmt.Errorf("MultFn not found; update regexp?")
   363  	}
   364  
   365  	// Users of MultFn shouldn't need adjustment, type inference should
   366  	// work OK.
   367  	content = multFnRe.ReplaceAll(content, []byte(`func MultFn[T int32|int64](a, b T) T`))
   368  
   369  	return os.WriteFile(path, content, 0644)
   370  }
   371  

View as plain text