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