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

View as plain text