...

Source file src/cmd/compile/internal/inline/inlheur/funcprops_test.go

Documentation: cmd/compile/internal/inline/inlheur

     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 inlheur
     6  
     7  import (
     8  	"bufio"
     9  	"encoding/json"
    10  	"flag"
    11  	"fmt"
    12  	"internal/testenv"
    13  	"os"
    14  	"path/filepath"
    15  	"regexp"
    16  	"strconv"
    17  	"strings"
    18  	"testing"
    19  	"time"
    20  )
    21  
    22  var remasterflag = flag.Bool("update-expected", false, "if true, generate updated golden results in testcases for all props tests")
    23  
    24  func TestFuncProperties(t *testing.T) {
    25  	td := t.TempDir()
    26  	// td = "/tmp/qqq"
    27  	// os.RemoveAll(td)
    28  	// os.Mkdir(td, 0777)
    29  	testenv.MustHaveGoBuild(t)
    30  
    31  	// NOTE: this testpoint has the unfortunate characteristic that it
    32  	// relies on the installed compiler, meaning that if you make
    33  	// changes to the inline heuristics code in your working copy and
    34  	// then run the test, it will test the installed compiler and not
    35  	// your local modifications. TODO: decide whether to convert this
    36  	// to building a fresh compiler on the fly, or using some other
    37  	// scheme.
    38  
    39  	testcases := []string{"funcflags", "returns", "params",
    40  		"acrosscall", "calls", "returns2"}
    41  	for _, tc := range testcases {
    42  		dumpfile, err := gatherPropsDumpForFile(t, tc, td)
    43  		if err != nil {
    44  			t.Fatalf("dumping func props for %q: error %v", tc, err)
    45  		}
    46  		// Read in the newly generated dump.
    47  		dentries, dcsites, derr := readDump(t, dumpfile)
    48  		if derr != nil {
    49  			t.Fatalf("reading func prop dump: %v", derr)
    50  		}
    51  		if *remasterflag {
    52  			updateExpected(t, tc, dentries, dcsites)
    53  			continue
    54  		}
    55  		// Generate expected dump.
    56  		epath, egerr := genExpected(td, tc)
    57  		if egerr != nil {
    58  			t.Fatalf("generating expected func prop dump: %v", egerr)
    59  		}
    60  		// Read in the expected result entries.
    61  		eentries, ecsites, eerr := readDump(t, epath)
    62  		if eerr != nil {
    63  			t.Fatalf("reading expected func prop dump: %v", eerr)
    64  		}
    65  		// Compare new vs expected.
    66  		n := len(dentries)
    67  		eidx := 0
    68  		for i := 0; i < n; i++ {
    69  			dentry := dentries[i]
    70  			dcst := dcsites[i]
    71  			if !interestingToCompare(dentry.fname) {
    72  				continue
    73  			}
    74  			if eidx >= len(eentries) {
    75  				t.Errorf("testcase %s missing expected entry for %s, skipping", tc, dentry.fname)
    76  				continue
    77  			}
    78  			eentry := eentries[eidx]
    79  			ecst := ecsites[eidx]
    80  			eidx++
    81  			if dentry.fname != eentry.fname {
    82  				t.Errorf("got fn %q wanted %q, skipping checks",
    83  					dentry.fname, eentry.fname)
    84  				continue
    85  			}
    86  			compareEntries(t, tc, &dentry, dcst, &eentry, ecst)
    87  		}
    88  	}
    89  }
    90  
    91  func propBitsToString[T interface{ String() string }](sl []T) string {
    92  	var sb strings.Builder
    93  	for i, f := range sl {
    94  		fmt.Fprintf(&sb, "%d: %s\n", i, f.String())
    95  	}
    96  	return sb.String()
    97  }
    98  
    99  func compareEntries(t *testing.T, tc string, dentry *fnInlHeur, dcsites encodedCallSiteTab, eentry *fnInlHeur, ecsites encodedCallSiteTab) {
   100  	dfp := dentry.props
   101  	efp := eentry.props
   102  	dfn := dentry.fname
   103  
   104  	// Compare function flags.
   105  	if dfp.Flags != efp.Flags {
   106  		t.Errorf("testcase %q: Flags mismatch for %q: got %s, wanted %s",
   107  			tc, dfn, dfp.Flags.String(), efp.Flags.String())
   108  	}
   109  	// Compare returns
   110  	rgot := propBitsToString[ResultPropBits](dfp.ResultFlags)
   111  	rwant := propBitsToString[ResultPropBits](efp.ResultFlags)
   112  	if rgot != rwant {
   113  		t.Errorf("testcase %q: Results mismatch for %q: got:\n%swant:\n%s",
   114  			tc, dfn, rgot, rwant)
   115  	}
   116  	// Compare receiver + params.
   117  	pgot := propBitsToString[ParamPropBits](dfp.ParamFlags)
   118  	pwant := propBitsToString[ParamPropBits](efp.ParamFlags)
   119  	if pgot != pwant {
   120  		t.Errorf("testcase %q: Params mismatch for %q: got:\n%swant:\n%s",
   121  			tc, dfn, pgot, pwant)
   122  	}
   123  	// Compare call sites.
   124  	for k, ve := range ecsites {
   125  		if vd, ok := dcsites[k]; !ok {
   126  			t.Errorf("testcase %q missing expected callsite %q in func %q", tc, k, dfn)
   127  			continue
   128  		} else {
   129  			if vd != ve {
   130  				t.Errorf("testcase %q callsite %q in func %q: got %+v want %+v",
   131  					tc, k, dfn, vd.String(), ve.String())
   132  			}
   133  		}
   134  	}
   135  	for k := range dcsites {
   136  		if _, ok := ecsites[k]; !ok {
   137  			t.Errorf("testcase %q unexpected extra callsite %q in func %q", tc, k, dfn)
   138  		}
   139  	}
   140  }
   141  
   142  type dumpReader struct {
   143  	s  *bufio.Scanner
   144  	t  *testing.T
   145  	p  string
   146  	ln int
   147  }
   148  
   149  // readDump reads in the contents of a dump file produced
   150  // by the "-d=dumpinlfuncprops=..." command line flag by the Go
   151  // compiler. It breaks the dump down into separate sections
   152  // by function, then deserializes each func section into a
   153  // fnInlHeur object and returns a slice of those objects.
   154  func readDump(t *testing.T, path string) ([]fnInlHeur, []encodedCallSiteTab, error) {
   155  	content, err := os.ReadFile(path)
   156  	if err != nil {
   157  		return nil, nil, err
   158  	}
   159  	dr := &dumpReader{
   160  		s:  bufio.NewScanner(strings.NewReader(string(content))),
   161  		t:  t,
   162  		p:  path,
   163  		ln: 1,
   164  	}
   165  	// consume header comment until preamble delimiter.
   166  	found := false
   167  	for dr.scan() {
   168  		if dr.curLine() == preambleDelimiter {
   169  			found = true
   170  			break
   171  		}
   172  	}
   173  	if !found {
   174  		return nil, nil, fmt.Errorf("malformed testcase file %s, missing preamble delimiter", path)
   175  	}
   176  	res := []fnInlHeur{}
   177  	csres := []encodedCallSiteTab{}
   178  	for {
   179  		dentry, dcst, err := dr.readEntry()
   180  		if err != nil {
   181  			t.Fatalf("reading func prop dump: %v", err)
   182  		}
   183  		if dentry.fname == "" {
   184  			break
   185  		}
   186  		res = append(res, dentry)
   187  		csres = append(csres, dcst)
   188  	}
   189  	return res, csres, nil
   190  }
   191  
   192  func (dr *dumpReader) scan() bool {
   193  	v := dr.s.Scan()
   194  	if v {
   195  		dr.ln++
   196  	}
   197  	return v
   198  }
   199  
   200  func (dr *dumpReader) curLine() string {
   201  	res := strings.TrimSpace(dr.s.Text())
   202  	if !strings.HasPrefix(res, "// ") {
   203  		dr.t.Fatalf("malformed line %s:%d, no comment: %s", dr.p, dr.ln, res)
   204  	}
   205  	return res[3:]
   206  }
   207  
   208  // readObjBlob reads in a series of commented lines until
   209  // it hits a delimiter, then returns the contents of the comments.
   210  func (dr *dumpReader) readObjBlob(delim string) (string, error) {
   211  	var sb strings.Builder
   212  	foundDelim := false
   213  	for dr.scan() {
   214  		line := dr.curLine()
   215  		if delim == line {
   216  			foundDelim = true
   217  			break
   218  		}
   219  		sb.WriteString(line + "\n")
   220  	}
   221  	if err := dr.s.Err(); err != nil {
   222  		return "", err
   223  	}
   224  	if !foundDelim {
   225  		return "", fmt.Errorf("malformed input %s, missing delimiter %q",
   226  			dr.p, delim)
   227  	}
   228  	return sb.String(), nil
   229  }
   230  
   231  // readEntry reads a single function's worth of material from
   232  // a file produced by the "-d=dumpinlfuncprops=..." command line
   233  // flag. It deserializes the json for the func properties and
   234  // returns the resulting properties and function name. EOF is
   235  // signaled by a nil FuncProps return (with no error
   236  func (dr *dumpReader) readEntry() (fnInlHeur, encodedCallSiteTab, error) {
   237  	var funcInlHeur fnInlHeur
   238  	var callsites encodedCallSiteTab
   239  	if !dr.scan() {
   240  		return funcInlHeur, callsites, nil
   241  	}
   242  	// first line contains info about function: file/name/line
   243  	info := dr.curLine()
   244  	chunks := strings.Fields(info)
   245  	funcInlHeur.file = chunks[0]
   246  	funcInlHeur.fname = chunks[1]
   247  	if _, err := fmt.Sscanf(chunks[2], "%d", &funcInlHeur.line); err != nil {
   248  		return funcInlHeur, callsites, fmt.Errorf("scanning line %q: %v", info, err)
   249  	}
   250  	// consume comments until and including delimiter
   251  	for {
   252  		if !dr.scan() {
   253  			break
   254  		}
   255  		if dr.curLine() == comDelimiter {
   256  			break
   257  		}
   258  	}
   259  
   260  	// Consume JSON for encoded props.
   261  	dr.scan()
   262  	line := dr.curLine()
   263  	fp := &FuncProps{}
   264  	if err := json.Unmarshal([]byte(line), fp); err != nil {
   265  		return funcInlHeur, callsites, err
   266  	}
   267  	funcInlHeur.props = fp
   268  
   269  	// Consume callsites.
   270  	callsites = make(encodedCallSiteTab)
   271  	for dr.scan() {
   272  		line := dr.curLine()
   273  		if line == csDelimiter {
   274  			break
   275  		}
   276  		// expected format: "// callsite: <expanded pos> flagstr <desc> flagval <flags> score <score> mask <scoremask> maskstr <scoremaskstring>"
   277  		fields := strings.Fields(line)
   278  		if len(fields) != 12 {
   279  			return funcInlHeur, nil, fmt.Errorf("malformed callsite (nf=%d) %s line %d: %s", len(fields), dr.p, dr.ln, line)
   280  		}
   281  		if fields[2] != "flagstr" || fields[4] != "flagval" || fields[6] != "score" || fields[8] != "mask" || fields[10] != "maskstr" {
   282  			return funcInlHeur, nil, fmt.Errorf("malformed callsite %s line %d: %s",
   283  				dr.p, dr.ln, line)
   284  		}
   285  		tag := fields[1]
   286  		flagstr := fields[5]
   287  		flags, err := strconv.Atoi(flagstr)
   288  		if err != nil {
   289  			return funcInlHeur, nil, fmt.Errorf("bad flags val %s line %d: %q err=%v",
   290  				dr.p, dr.ln, line, err)
   291  		}
   292  		scorestr := fields[7]
   293  		score, err2 := strconv.Atoi(scorestr)
   294  		if err2 != nil {
   295  			return funcInlHeur, nil, fmt.Errorf("bad score val %s line %d: %q err=%v",
   296  				dr.p, dr.ln, line, err2)
   297  		}
   298  		maskstr := fields[9]
   299  		mask, err3 := strconv.Atoi(maskstr)
   300  		if err3 != nil {
   301  			return funcInlHeur, nil, fmt.Errorf("bad mask val %s line %d: %q err=%v",
   302  				dr.p, dr.ln, line, err3)
   303  		}
   304  		callsites[tag] = propsAndScore{
   305  			props: CSPropBits(flags),
   306  			score: score,
   307  			mask:  scoreAdjustTyp(mask),
   308  		}
   309  	}
   310  
   311  	// Consume function delimiter.
   312  	dr.scan()
   313  	line = dr.curLine()
   314  	if line != fnDelimiter {
   315  		return funcInlHeur, nil, fmt.Errorf("malformed testcase file %q, missing delimiter %q", dr.p, fnDelimiter)
   316  	}
   317  
   318  	return funcInlHeur, callsites, nil
   319  }
   320  
   321  // gatherPropsDumpForFile builds the specified testcase 'testcase' from
   322  // testdata/props passing the "-d=dumpinlfuncprops=..." compiler option,
   323  // to produce a properties dump, then returns the path of the newly
   324  // created file. NB: we can't use "go tool compile" here, since
   325  // some of the test cases import stdlib packages (such as "os").
   326  // This means using "go build", which is problematic since the
   327  // Go command can potentially cache the results of the compile step,
   328  // causing the test to fail when being run interactively. E.g.
   329  //
   330  //	$ rm -f dump.txt
   331  //	$ go build -o foo.a -gcflags=-d=dumpinlfuncprops=dump.txt foo.go
   332  //	$ rm -f dump.txt foo.a
   333  //	$ go build -o foo.a -gcflags=-d=dumpinlfuncprops=dump.txt foo.go
   334  //	$ ls foo.a dump.txt > /dev/null
   335  //	ls : cannot access 'dump.txt': No such file or directory
   336  //	$
   337  //
   338  // For this reason, pick a unique filename for the dump, so as to
   339  // defeat the caching.
   340  func gatherPropsDumpForFile(t *testing.T, testcase string, td string) (string, error) {
   341  	t.Helper()
   342  	gopath := "testdata/props/" + testcase + ".go"
   343  	outpath := filepath.Join(td, testcase+".a")
   344  	salt := fmt.Sprintf(".p%dt%d", os.Getpid(), time.Now().UnixNano())
   345  	dumpfile := filepath.Join(td, testcase+salt+".dump.txt")
   346  	run := []string{testenv.GoToolPath(t), "build",
   347  		"-gcflags=-d=dumpinlfuncprops=" + dumpfile, "-o", outpath, gopath}
   348  	out, err := testenv.Command(t, run[0], run[1:]...).CombinedOutput()
   349  	if err != nil {
   350  		t.Logf("compile command: %+v", run)
   351  	}
   352  	if strings.TrimSpace(string(out)) != "" {
   353  		t.Logf("%s", out)
   354  	}
   355  	return dumpfile, err
   356  }
   357  
   358  // genExpected reads in a given Go testcase file, strips out all the
   359  // unindented (column 0) commands, writes them out to a new file, and
   360  // returns the path of that new file. By picking out just the comments
   361  // from the Go file we wind up with something that resembles the
   362  // output from a "-d=dumpinlfuncprops=..." compilation.
   363  func genExpected(td string, testcase string) (string, error) {
   364  	epath := filepath.Join(td, testcase+".expected")
   365  	outf, err := os.OpenFile(epath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
   366  	if err != nil {
   367  		return "", err
   368  	}
   369  	gopath := "testdata/props/" + testcase + ".go"
   370  	content, err := os.ReadFile(gopath)
   371  	if err != nil {
   372  		return "", err
   373  	}
   374  	lines := strings.Split(string(content), "\n")
   375  	for _, line := range lines[3:] {
   376  		if !strings.HasPrefix(line, "// ") {
   377  			continue
   378  		}
   379  		fmt.Fprintf(outf, "%s\n", line)
   380  	}
   381  	if err := outf.Close(); err != nil {
   382  		return "", err
   383  	}
   384  	return epath, nil
   385  }
   386  
   387  type upexState struct {
   388  	dentries   []fnInlHeur
   389  	newgolines []string
   390  	atline     map[uint]uint
   391  }
   392  
   393  func mkUpexState(dentries []fnInlHeur) *upexState {
   394  	atline := make(map[uint]uint)
   395  	for _, e := range dentries {
   396  		atline[e.line] = atline[e.line] + 1
   397  	}
   398  	return &upexState{
   399  		dentries: dentries,
   400  		atline:   atline,
   401  	}
   402  }
   403  
   404  // updateExpected takes a given Go testcase file X.go and writes out a
   405  // new/updated version of the file to X.go.new, where the column-0
   406  // "expected" comments have been updated using fresh data from
   407  // "dentries".
   408  //
   409  // Writing of expected results is complicated by closures and by
   410  // generics, where you can have multiple functions that all share the
   411  // same starting line. Currently we combine up all the dups and
   412  // closures into the single pre-func comment.
   413  func updateExpected(t *testing.T, testcase string, dentries []fnInlHeur, dcsites []encodedCallSiteTab) {
   414  	nd := len(dentries)
   415  
   416  	ues := mkUpexState(dentries)
   417  
   418  	gopath := "testdata/props/" + testcase + ".go"
   419  	newgopath := "testdata/props/" + testcase + ".go.new"
   420  
   421  	// Read the existing Go file.
   422  	content, err := os.ReadFile(gopath)
   423  	if err != nil {
   424  		t.Fatalf("opening %s: %v", gopath, err)
   425  	}
   426  	golines := strings.Split(string(content), "\n")
   427  
   428  	// Preserve copyright.
   429  	ues.newgolines = append(ues.newgolines, golines[:4]...)
   430  	if !strings.HasPrefix(golines[0], "// Copyright") {
   431  		t.Fatalf("missing copyright from existing testcase")
   432  	}
   433  	golines = golines[4:]
   434  
   435  	clore := regexp.MustCompile(`.+\.func\d+[\.\d]*$`)
   436  
   437  	emitFunc := func(e *fnInlHeur, dcsites encodedCallSiteTab,
   438  		instance, atl uint) {
   439  		var sb strings.Builder
   440  		dumpFnPreamble(&sb, e, dcsites, instance, atl)
   441  		ues.newgolines = append(ues.newgolines,
   442  			strings.Split(strings.TrimSpace(sb.String()), "\n")...)
   443  	}
   444  
   445  	// Write file preamble with "DO NOT EDIT" message and such.
   446  	var sb strings.Builder
   447  	dumpFilePreamble(&sb)
   448  	ues.newgolines = append(ues.newgolines,
   449  		strings.Split(strings.TrimSpace(sb.String()), "\n")...)
   450  
   451  	// Helper to add a clump of functions to the output file.
   452  	processClump := func(idx int, emit bool) int {
   453  		// Process func itself, plus anything else defined
   454  		// on the same line
   455  		atl := ues.atline[dentries[idx].line]
   456  		for k := uint(0); k < atl; k++ {
   457  			if emit {
   458  				emitFunc(&dentries[idx], dcsites[idx], k, atl)
   459  			}
   460  			idx++
   461  		}
   462  		// now process any closures it contains
   463  		ncl := 0
   464  		for idx < nd {
   465  			nfn := dentries[idx].fname
   466  			if !clore.MatchString(nfn) {
   467  				break
   468  			}
   469  			ncl++
   470  			if emit {
   471  				emitFunc(&dentries[idx], dcsites[idx], 0, 1)
   472  			}
   473  			idx++
   474  		}
   475  		return idx
   476  	}
   477  
   478  	didx := 0
   479  	for _, line := range golines {
   480  		if strings.HasPrefix(line, "func ") {
   481  
   482  			// We have a function definition.
   483  			// Pick out the corresponding entry or entries in the dump
   484  			// and emit if interesting (or skip if not).
   485  			dentry := dentries[didx]
   486  			emit := interestingToCompare(dentry.fname)
   487  			didx = processClump(didx, emit)
   488  		}
   489  
   490  		// Consume all existing comments.
   491  		if strings.HasPrefix(line, "//") {
   492  			continue
   493  		}
   494  		ues.newgolines = append(ues.newgolines, line)
   495  	}
   496  
   497  	if didx != nd {
   498  		t.Logf("didx=%d wanted %d", didx, nd)
   499  	}
   500  
   501  	// Open new Go file and write contents.
   502  	of, err := os.OpenFile(newgopath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
   503  	if err != nil {
   504  		t.Fatalf("opening %s: %v", newgopath, err)
   505  	}
   506  	fmt.Fprintf(of, "%s", strings.Join(ues.newgolines, "\n"))
   507  	if err := of.Close(); err != nil {
   508  		t.Fatalf("closing %s: %v", newgopath, err)
   509  	}
   510  
   511  	t.Logf("update-expected: emitted updated file %s", newgopath)
   512  	t.Logf("please compare the two files, then overwrite %s with %s\n",
   513  		gopath, newgopath)
   514  }
   515  
   516  // interestingToCompare returns TRUE if we want to compare results
   517  // for function 'fname'.
   518  func interestingToCompare(fname string) bool {
   519  	if strings.HasPrefix(fname, "init.") {
   520  		return true
   521  	}
   522  	if strings.HasPrefix(fname, "T_") {
   523  		return true
   524  	}
   525  	f := strings.Split(fname, ".")
   526  	if len(f) == 2 && strings.HasPrefix(f[1], "T_") {
   527  		return true
   528  	}
   529  	return false
   530  }
   531  

View as plain text