1// Copyright 2014 Google Inc. All Rights Reserved. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package driver 16 17import ( 18 "bytes" 19 "flag" 20 "fmt" 21 "io/ioutil" 22 "net" 23 _ "net/http/pprof" 24 "os" 25 "reflect" 26 "regexp" 27 "runtime" 28 "strconv" 29 "strings" 30 "testing" 31 "time" 32 33 "github.com/google/pprof/internal/plugin" 34 "github.com/google/pprof/internal/proftest" 35 "github.com/google/pprof/internal/symbolz" 36 "github.com/google/pprof/profile" 37) 38 39var updateFlag = flag.Bool("update", false, "Update the golden files") 40 41func TestParse(t *testing.T) { 42 // Override weblist command to collect output in buffer 43 pprofCommands["weblist"].postProcess = nil 44 45 // Our mockObjTool.Open will always return success, causing 46 // driver.locateBinaries to "find" the binaries below in a non-existent 47 // directory. As a workaround, point the search path to the fake 48 // directory containing out fake binaries. 49 savePath := os.Getenv("PPROF_BINARY_PATH") 50 os.Setenv("PPROF_BINARY_PATH", "/path/to") 51 defer os.Setenv("PPROF_BINARY_PATH", savePath) 52 testcase := []struct { 53 flags, source string 54 }{ 55 {"text,functions,flat", "cpu"}, 56 {"text,functions,noinlines,flat", "cpu"}, 57 {"text,filefunctions,noinlines,flat", "cpu"}, 58 {"text,addresses,noinlines,flat", "cpu"}, 59 {"tree,addresses,flat,nodecount=4", "cpusmall"}, 60 {"text,functions,flat,nodecount=5,call_tree", "unknown"}, 61 {"text,alloc_objects,flat", "heap_alloc"}, 62 {"text,files,flat", "heap"}, 63 {"text,files,flat,focus=[12]00,taghide=[X3]00", "heap"}, 64 {"text,inuse_objects,flat", "heap"}, 65 {"text,lines,cum,hide=line[X3]0", "cpu"}, 66 {"text,lines,cum,show=[12]00", "cpu"}, 67 {"text,lines,cum,hide=line[X3]0,focus=[12]00", "cpu"}, 68 {"topproto,lines,cum,hide=mangled[X3]0", "cpu"}, 69 {"topproto,lines", "cpu"}, 70 {"tree,lines,cum,focus=[24]00", "heap"}, 71 {"tree,relative_percentages,cum,focus=[24]00", "heap"}, 72 {"tree,lines,cum,show_from=line2", "cpu"}, 73 {"callgrind", "cpu"}, 74 {"callgrind,call_tree", "cpu"}, 75 {"callgrind", "heap"}, 76 {"dot,functions,flat", "cpu"}, 77 {"dot,functions,flat,call_tree", "cpu"}, 78 {"dot,lines,flat,focus=[12]00", "heap"}, 79 {"dot,unit=minimum", "heap_sizetags"}, 80 {"dot,addresses,flat,ignore=[X3]002,focus=[X1]000", "contention"}, 81 {"dot,files,cum", "contention"}, 82 {"comments,add_comment=some-comment", "cpu"}, 83 {"comments", "heap"}, 84 {"tags", "cpu"}, 85 {"tags,tagignore=tag[13],tagfocus=key[12]", "cpu"}, 86 {"tags", "heap"}, 87 {"tags,unit=bytes", "heap"}, 88 {"traces", "cpu"}, 89 {"traces,addresses", "cpu"}, 90 {"traces", "heap_tags"}, 91 {"dot,alloc_space,flat,focus=[234]00", "heap_alloc"}, 92 {"dot,alloc_space,flat,tagshow=[2]00", "heap_alloc"}, 93 {"dot,alloc_space,flat,hide=line.*1?23?", "heap_alloc"}, 94 {"dot,inuse_space,flat,tagfocus=1mb:2gb", "heap"}, 95 {"dot,inuse_space,flat,tagfocus=30kb:,tagignore=1mb:2mb", "heap"}, 96 {"disasm=line[13],addresses,flat", "cpu"}, 97 {"peek=line.*01", "cpu"}, 98 {"weblist=line(1000|3000)$,addresses,flat", "cpu"}, 99 {"tags,tagfocus=400kb:", "heap_request"}, 100 {"tags,tagfocus=+400kb:", "heap_request"}, 101 {"dot", "long_name_funcs"}, 102 {"text", "long_name_funcs"}, 103 } 104 105 baseConfig := currentConfig() 106 defer setCurrentConfig(baseConfig) 107 for _, tc := range testcase { 108 t.Run(tc.flags+":"+tc.source, func(t *testing.T) { 109 // Reset config before processing 110 setCurrentConfig(baseConfig) 111 112 testUI := &proftest.TestUI{T: t, AllowRx: "Generating report in|Ignoring local file|expression matched no samples|Interpreted .* as range, not regexp"} 113 114 f := baseFlags() 115 f.args = []string{tc.source} 116 117 flags := strings.Split(tc.flags, ",") 118 119 // Encode profile into a protobuf and decode it again. 120 protoTempFile, err := ioutil.TempFile("", "profile_proto") 121 if err != nil { 122 t.Errorf("cannot create tempfile: %v", err) 123 } 124 defer os.Remove(protoTempFile.Name()) 125 defer protoTempFile.Close() 126 f.strings["output"] = protoTempFile.Name() 127 128 if flags[0] == "topproto" { 129 f.bools["proto"] = false 130 f.bools["topproto"] = true 131 f.bools["addresses"] = true 132 } 133 134 // First pprof invocation to save the profile into a profile.proto. 135 // Pass in flag set hen setting defaults, because otherwise default 136 // transport will try to add flags to the default flag set. 137 o1 := setDefaults(&plugin.Options{Flagset: f}) 138 o1.Fetch = testFetcher{} 139 o1.Sym = testSymbolizer{} 140 o1.UI = testUI 141 if err := PProf(o1); err != nil { 142 t.Fatalf("%s %q: %v", tc.source, tc.flags, err) 143 } 144 // Reset config after the proto invocation 145 setCurrentConfig(baseConfig) 146 147 // Read the profile from the encoded protobuf 148 outputTempFile, err := ioutil.TempFile("", "profile_output") 149 if err != nil { 150 t.Errorf("cannot create tempfile: %v", err) 151 } 152 defer os.Remove(outputTempFile.Name()) 153 defer outputTempFile.Close() 154 155 f = baseFlags() 156 f.strings["output"] = outputTempFile.Name() 157 f.args = []string{protoTempFile.Name()} 158 159 delete(f.bools, "proto") 160 addFlags(&f, flags) 161 solution := solutionFilename(tc.source, &f) 162 // Apply the flags for the second pprof run, and identify name of 163 // the file containing expected results 164 if flags[0] == "topproto" { 165 addFlags(&f, flags) 166 solution = solutionFilename(tc.source, &f) 167 delete(f.bools, "topproto") 168 f.bools["text"] = true 169 } 170 171 // Second pprof invocation to read the profile from profile.proto 172 // and generate a report. 173 // Pass in flag set hen setting defaults, because otherwise default 174 // transport will try to add flags to the default flag set. 175 o2 := setDefaults(&plugin.Options{Flagset: f}) 176 o2.Sym = testSymbolizeDemangler{} 177 o2.Obj = new(mockObjTool) 178 o2.UI = testUI 179 180 if err := PProf(o2); err != nil { 181 t.Errorf("%s: %v", tc.source, err) 182 } 183 b, err := ioutil.ReadFile(outputTempFile.Name()) 184 if err != nil { 185 t.Errorf("Failed to read profile %s: %v", outputTempFile.Name(), err) 186 } 187 188 // Read data file with expected solution 189 solution = "testdata/" + solution 190 sbuf, err := ioutil.ReadFile(solution) 191 if err != nil { 192 t.Fatalf("reading solution file %s: %v", solution, err) 193 } 194 if runtime.GOOS == "windows" { 195 if flags[0] == "dot" { 196 // The .dot test has the paths inside strings, so \ must be escaped. 197 sbuf = bytes.Replace(sbuf, []byte("testdata/"), []byte(`testdata\\`), -1) 198 sbuf = bytes.Replace(sbuf, []byte("/path/to/"), []byte(`\\path\\to\\`), -1) 199 } else { 200 sbuf = bytes.Replace(sbuf, []byte("testdata/"), []byte(`testdata\`), -1) 201 sbuf = bytes.Replace(sbuf, []byte("/path/to/"), []byte(`\path\to\`), -1) 202 } 203 } 204 205 if flags[0] == "svg" { 206 b = removeScripts(b) 207 sbuf = removeScripts(sbuf) 208 } 209 210 if string(b) != string(sbuf) { 211 t.Errorf("diff %s %s", solution, tc.source) 212 d, err := proftest.Diff(sbuf, b) 213 if err != nil { 214 t.Fatalf("diff %s %v", solution, err) 215 } 216 t.Errorf("%s\n%s\n", solution, d) 217 if *updateFlag { 218 err := ioutil.WriteFile(solution, b, 0644) 219 if err != nil { 220 t.Errorf("failed to update the solution file %q: %v", solution, err) 221 } 222 } 223 } 224 }) 225 } 226} 227 228// removeScripts removes <script > .. </script> pairs from its input 229func removeScripts(in []byte) []byte { 230 beginMarker := []byte("<script") 231 endMarker := []byte("</script>") 232 233 if begin := bytes.Index(in, beginMarker); begin > 0 { 234 if end := bytes.Index(in[begin:], endMarker); end > 0 { 235 in = append(in[:begin], removeScripts(in[begin+end+len(endMarker):])...) 236 } 237 } 238 return in 239} 240 241// addFlags parses flag descriptions and adds them to the testFlags 242func addFlags(f *testFlags, flags []string) { 243 for _, flag := range flags { 244 fields := strings.SplitN(flag, "=", 2) 245 switch len(fields) { 246 case 1: 247 f.bools[fields[0]] = true 248 case 2: 249 if i, err := strconv.Atoi(fields[1]); err == nil { 250 f.ints[fields[0]] = i 251 } else { 252 f.strings[fields[0]] = fields[1] 253 } 254 } 255 } 256} 257 258func testSourceURL(port int) string { 259 return fmt.Sprintf("http://%s/", net.JoinHostPort(testSourceAddress, strconv.Itoa(port))) 260} 261 262// solutionFilename returns the name of the solution file for the test 263func solutionFilename(source string, f *testFlags) string { 264 name := []string{"pprof", strings.TrimPrefix(source, testSourceURL(8000))} 265 name = addString(name, f, []string{"flat", "cum"}) 266 name = addString(name, f, []string{"functions", "filefunctions", "files", "lines", "addresses"}) 267 name = addString(name, f, []string{"noinlines"}) 268 name = addString(name, f, []string{"inuse_space", "inuse_objects", "alloc_space", "alloc_objects"}) 269 name = addString(name, f, []string{"relative_percentages"}) 270 name = addString(name, f, []string{"seconds"}) 271 name = addString(name, f, []string{"call_tree"}) 272 name = addString(name, f, []string{"text", "tree", "callgrind", "dot", "svg", "tags", "dot", "traces", "disasm", "peek", "weblist", "topproto", "comments"}) 273 if f.strings["focus"] != "" || f.strings["tagfocus"] != "" { 274 name = append(name, "focus") 275 } 276 if f.strings["ignore"] != "" || f.strings["tagignore"] != "" { 277 name = append(name, "ignore") 278 } 279 if f.strings["show_from"] != "" { 280 name = append(name, "show_from") 281 } 282 name = addString(name, f, []string{"hide", "show"}) 283 if f.strings["unit"] != "minimum" { 284 name = addString(name, f, []string{"unit"}) 285 } 286 return strings.Join(name, ".") 287} 288 289func addString(name []string, f *testFlags, components []string) []string { 290 for _, c := range components { 291 if f.bools[c] || f.strings[c] != "" || f.ints[c] != 0 { 292 return append(name, c) 293 } 294 } 295 return name 296} 297 298// testFlags implements the plugin.FlagSet interface. 299type testFlags struct { 300 bools map[string]bool 301 ints map[string]int 302 floats map[string]float64 303 strings map[string]string 304 args []string 305 stringLists map[string][]string 306} 307 308func (testFlags) ExtraUsage() string { return "" } 309 310func (testFlags) AddExtraUsage(eu string) {} 311 312func (f testFlags) Bool(s string, d bool, c string) *bool { 313 if b, ok := f.bools[s]; ok { 314 return &b 315 } 316 return &d 317} 318 319func (f testFlags) Int(s string, d int, c string) *int { 320 if i, ok := f.ints[s]; ok { 321 return &i 322 } 323 return &d 324} 325 326func (f testFlags) Float64(s string, d float64, c string) *float64 { 327 if g, ok := f.floats[s]; ok { 328 return &g 329 } 330 return &d 331} 332 333func (f testFlags) String(s, d, c string) *string { 334 if t, ok := f.strings[s]; ok { 335 return &t 336 } 337 return &d 338} 339 340func (f testFlags) StringList(s, d, c string) *[]*string { 341 if t, ok := f.stringLists[s]; ok { 342 // convert slice of strings to slice of string pointers before returning. 343 tp := make([]*string, len(t)) 344 for i, v := range t { 345 tp[i] = &v 346 } 347 return &tp 348 } 349 return &[]*string{} 350} 351 352func (f testFlags) Parse(func()) []string { 353 return f.args 354} 355 356func baseFlags() testFlags { 357 return testFlags{ 358 bools: map[string]bool{ 359 "proto": true, 360 "trim": true, 361 "compact_labels": true, 362 }, 363 ints: map[string]int{ 364 "nodecount": 20, 365 }, 366 floats: map[string]float64{ 367 "nodefraction": 0.05, 368 "edgefraction": 0.01, 369 "divide_by": 1.0, 370 }, 371 strings: map[string]string{ 372 "unit": "minimum", 373 }, 374 } 375} 376 377const testStart = 0x1000 378const testOffset = 0x5000 379 380type testFetcher struct{} 381 382func (testFetcher) Fetch(s string, d, t time.Duration) (*profile.Profile, string, error) { 383 var p *profile.Profile 384 switch s { 385 case "cpu", "unknown": 386 p = cpuProfile() 387 case "cpusmall": 388 p = cpuProfileSmall() 389 case "heap": 390 p = heapProfile() 391 case "heap_alloc": 392 p = heapProfile() 393 p.SampleType = []*profile.ValueType{ 394 {Type: "alloc_objects", Unit: "count"}, 395 {Type: "alloc_space", Unit: "bytes"}, 396 } 397 case "heap_request": 398 p = heapProfile() 399 for _, s := range p.Sample { 400 s.NumLabel["request"] = s.NumLabel["bytes"] 401 } 402 case "heap_sizetags": 403 p = heapProfile() 404 tags := []int64{2, 4, 8, 16, 32, 64, 128, 256} 405 for _, s := range p.Sample { 406 numValues := append(s.NumLabel["bytes"], tags...) 407 s.NumLabel["bytes"] = numValues 408 } 409 case "heap_tags": 410 p = heapProfile() 411 for i := 0; i < len(p.Sample); i += 2 { 412 s := p.Sample[i] 413 if s.Label == nil { 414 s.Label = make(map[string][]string) 415 } 416 s.NumLabel["request"] = s.NumLabel["bytes"] 417 s.Label["key1"] = []string{"tag"} 418 } 419 case "contention": 420 p = contentionProfile() 421 case "symbolz": 422 p = symzProfile() 423 case "long_name_funcs": 424 p = longNameFuncsProfile() 425 default: 426 return nil, "", fmt.Errorf("unexpected source: %s", s) 427 } 428 return p, testSourceURL(8000) + s, nil 429} 430 431type testSymbolizer struct{} 432 433func (testSymbolizer) Symbolize(_ string, _ plugin.MappingSources, _ *profile.Profile) error { 434 return nil 435} 436 437type testSymbolizeDemangler struct{} 438 439func (testSymbolizeDemangler) Symbolize(_ string, _ plugin.MappingSources, p *profile.Profile) error { 440 for _, fn := range p.Function { 441 if fn.Name == "" || fn.SystemName == fn.Name { 442 fn.Name = fakeDemangler(fn.SystemName) 443 } 444 } 445 return nil 446} 447 448func testFetchSymbols(source, post string) ([]byte, error) { 449 var buf bytes.Buffer 450 451 switch source { 452 case testSourceURL(8000) + "symbolz": 453 for _, address := range strings.Split(post, "+") { 454 a, _ := strconv.ParseInt(address, 0, 64) 455 fmt.Fprintf(&buf, "%v\t", address) 456 if a-testStart > testOffset { 457 fmt.Fprintf(&buf, "wrong_source_%v_", address) 458 continue 459 } 460 fmt.Fprintf(&buf, "%#x\n", a-testStart) 461 } 462 return buf.Bytes(), nil 463 case testSourceURL(8001) + "symbolz": 464 for _, address := range strings.Split(post, "+") { 465 a, _ := strconv.ParseInt(address, 0, 64) 466 fmt.Fprintf(&buf, "%v\t", address) 467 if a-testStart < testOffset { 468 fmt.Fprintf(&buf, "wrong_source_%v_", address) 469 continue 470 } 471 fmt.Fprintf(&buf, "%#x\n", a-testStart-testOffset) 472 } 473 return buf.Bytes(), nil 474 default: 475 return nil, fmt.Errorf("unexpected source: %s", source) 476 } 477} 478 479type testSymbolzSymbolizer struct{} 480 481func (testSymbolzSymbolizer) Symbolize(variables string, sources plugin.MappingSources, p *profile.Profile) error { 482 return symbolz.Symbolize(p, false, sources, testFetchSymbols, nil) 483} 484 485func fakeDemangler(name string) string { 486 switch name { 487 case "mangled1000": 488 return "line1000" 489 case "mangled2000": 490 return "line2000" 491 case "mangled2001": 492 return "line2001" 493 case "mangled3000": 494 return "line3000" 495 case "mangled3001": 496 return "line3001" 497 case "mangled3002": 498 return "line3002" 499 case "mangledNEW": 500 return "operator new" 501 case "mangledMALLOC": 502 return "malloc" 503 default: 504 return name 505 } 506} 507 508// longNameFuncsProfile returns a profile with function names which should be 509// shortened in graph and flame views. 510func longNameFuncsProfile() *profile.Profile { 511 var longNameFuncsM = []*profile.Mapping{ 512 { 513 ID: 1, 514 Start: 0x1000, 515 Limit: 0x4000, 516 File: "/path/to/testbinary", 517 HasFunctions: true, 518 HasFilenames: true, 519 HasLineNumbers: true, 520 HasInlineFrames: true, 521 }, 522 } 523 524 var longNameFuncsF = []*profile.Function{ 525 {ID: 1, Name: "path/to/package1.object.function1", SystemName: "path/to/package1.object.function1", Filename: "path/to/package1.go"}, 526 {ID: 2, Name: "(anonymous namespace)::Bar::Foo", SystemName: "(anonymous namespace)::Bar::Foo", Filename: "a/long/path/to/package2.cc"}, 527 {ID: 3, Name: "java.bar.foo.FooBar.run(java.lang.Runnable)", SystemName: "java.bar.foo.FooBar.run(java.lang.Runnable)", Filename: "FooBar.java"}, 528 } 529 530 var longNameFuncsL = []*profile.Location{ 531 { 532 ID: 1000, 533 Mapping: longNameFuncsM[0], 534 Address: 0x1000, 535 Line: []profile.Line{ 536 {Function: longNameFuncsF[0], Line: 1}, 537 }, 538 }, 539 { 540 ID: 2000, 541 Mapping: longNameFuncsM[0], 542 Address: 0x2000, 543 Line: []profile.Line{ 544 {Function: longNameFuncsF[1], Line: 4}, 545 }, 546 }, 547 { 548 ID: 3000, 549 Mapping: longNameFuncsM[0], 550 Address: 0x3000, 551 Line: []profile.Line{ 552 {Function: longNameFuncsF[2], Line: 9}, 553 }, 554 }, 555 } 556 557 return &profile.Profile{ 558 PeriodType: &profile.ValueType{Type: "cpu", Unit: "milliseconds"}, 559 Period: 1, 560 DurationNanos: 10e9, 561 SampleType: []*profile.ValueType{ 562 {Type: "samples", Unit: "count"}, 563 {Type: "cpu", Unit: "milliseconds"}, 564 }, 565 Sample: []*profile.Sample{ 566 { 567 Location: []*profile.Location{longNameFuncsL[0], longNameFuncsL[1], longNameFuncsL[2]}, 568 Value: []int64{1000, 1000}, 569 }, 570 { 571 Location: []*profile.Location{longNameFuncsL[0], longNameFuncsL[1]}, 572 Value: []int64{100, 100}, 573 }, 574 { 575 Location: []*profile.Location{longNameFuncsL[2]}, 576 Value: []int64{10, 10}, 577 }, 578 }, 579 Location: longNameFuncsL, 580 Function: longNameFuncsF, 581 Mapping: longNameFuncsM, 582 } 583} 584 585func cpuProfile() *profile.Profile { 586 var cpuM = []*profile.Mapping{ 587 { 588 ID: 1, 589 Start: 0x1000, 590 Limit: 0x4000, 591 File: "/path/to/testbinary", 592 HasFunctions: true, 593 HasFilenames: true, 594 HasLineNumbers: true, 595 HasInlineFrames: true, 596 }, 597 } 598 599 var cpuF = []*profile.Function{ 600 {ID: 1, Name: "mangled1000", SystemName: "mangled1000", Filename: "testdata/file1000.src"}, 601 {ID: 2, Name: "mangled2000", SystemName: "mangled2000", Filename: "testdata/file2000.src"}, 602 {ID: 3, Name: "mangled2001", SystemName: "mangled2001", Filename: "testdata/file2000.src"}, 603 {ID: 4, Name: "mangled3000", SystemName: "mangled3000", Filename: "testdata/file3000.src"}, 604 {ID: 5, Name: "mangled3001", SystemName: "mangled3001", Filename: "testdata/file3000.src"}, 605 {ID: 6, Name: "mangled3002", SystemName: "mangled3002", Filename: "testdata/file3000.src"}, 606 } 607 608 var cpuL = []*profile.Location{ 609 { 610 ID: 1000, 611 Mapping: cpuM[0], 612 Address: 0x1000, 613 Line: []profile.Line{ 614 {Function: cpuF[0], Line: 1}, 615 }, 616 }, 617 { 618 ID: 2000, 619 Mapping: cpuM[0], 620 Address: 0x2000, 621 Line: []profile.Line{ 622 {Function: cpuF[2], Line: 9}, 623 {Function: cpuF[1], Line: 4}, 624 }, 625 }, 626 { 627 ID: 3000, 628 Mapping: cpuM[0], 629 Address: 0x3000, 630 Line: []profile.Line{ 631 {Function: cpuF[5], Line: 2}, 632 {Function: cpuF[4], Line: 5}, 633 {Function: cpuF[3], Line: 6}, 634 }, 635 }, 636 { 637 ID: 3001, 638 Mapping: cpuM[0], 639 Address: 0x3001, 640 Line: []profile.Line{ 641 {Function: cpuF[4], Line: 8}, 642 {Function: cpuF[3], Line: 9}, 643 }, 644 }, 645 { 646 ID: 3002, 647 Mapping: cpuM[0], 648 Address: 0x3002, 649 Line: []profile.Line{ 650 {Function: cpuF[5], Line: 5}, 651 {Function: cpuF[3], Line: 9}, 652 }, 653 }, 654 } 655 656 return &profile.Profile{ 657 PeriodType: &profile.ValueType{Type: "cpu", Unit: "milliseconds"}, 658 Period: 1, 659 DurationNanos: 10e9, 660 SampleType: []*profile.ValueType{ 661 {Type: "samples", Unit: "count"}, 662 {Type: "cpu", Unit: "milliseconds"}, 663 }, 664 Sample: []*profile.Sample{ 665 { 666 Location: []*profile.Location{cpuL[0], cpuL[1], cpuL[2]}, 667 Value: []int64{1000, 1000}, 668 Label: map[string][]string{ 669 "key1": {"tag1"}, 670 "key2": {"tag1"}, 671 }, 672 }, 673 { 674 Location: []*profile.Location{cpuL[0], cpuL[3]}, 675 Value: []int64{100, 100}, 676 Label: map[string][]string{ 677 "key1": {"tag2"}, 678 "key3": {"tag2"}, 679 }, 680 }, 681 { 682 Location: []*profile.Location{cpuL[1], cpuL[4]}, 683 Value: []int64{10, 10}, 684 Label: map[string][]string{ 685 "key1": {"tag3"}, 686 "key2": {"tag2"}, 687 }, 688 }, 689 { 690 Location: []*profile.Location{cpuL[2]}, 691 Value: []int64{10, 10}, 692 Label: map[string][]string{ 693 "key1": {"tag4"}, 694 "key2": {"tag1"}, 695 }, 696 }, 697 }, 698 Location: cpuL, 699 Function: cpuF, 700 Mapping: cpuM, 701 } 702} 703 704func cpuProfileSmall() *profile.Profile { 705 var cpuM = []*profile.Mapping{ 706 { 707 ID: 1, 708 Start: 0x1000, 709 Limit: 0x4000, 710 File: "/path/to/testbinary", 711 HasFunctions: true, 712 HasFilenames: true, 713 HasLineNumbers: true, 714 HasInlineFrames: true, 715 }, 716 } 717 718 var cpuL = []*profile.Location{ 719 { 720 ID: 1000, 721 Mapping: cpuM[0], 722 Address: 0x1000, 723 }, 724 { 725 ID: 2000, 726 Mapping: cpuM[0], 727 Address: 0x2000, 728 }, 729 { 730 ID: 3000, 731 Mapping: cpuM[0], 732 Address: 0x3000, 733 }, 734 { 735 ID: 4000, 736 Mapping: cpuM[0], 737 Address: 0x4000, 738 }, 739 { 740 ID: 5000, 741 Mapping: cpuM[0], 742 Address: 0x5000, 743 }, 744 } 745 746 return &profile.Profile{ 747 PeriodType: &profile.ValueType{Type: "cpu", Unit: "milliseconds"}, 748 Period: 1, 749 DurationNanos: 10e9, 750 SampleType: []*profile.ValueType{ 751 {Type: "samples", Unit: "count"}, 752 {Type: "cpu", Unit: "milliseconds"}, 753 }, 754 Sample: []*profile.Sample{ 755 { 756 Location: []*profile.Location{cpuL[0], cpuL[1], cpuL[2]}, 757 Value: []int64{1000, 1000}, 758 }, 759 { 760 Location: []*profile.Location{cpuL[3], cpuL[1], cpuL[4]}, 761 Value: []int64{1000, 1000}, 762 }, 763 { 764 Location: []*profile.Location{cpuL[2]}, 765 Value: []int64{1000, 1000}, 766 }, 767 { 768 Location: []*profile.Location{cpuL[4]}, 769 Value: []int64{1000, 1000}, 770 }, 771 }, 772 Location: cpuL, 773 Function: nil, 774 Mapping: cpuM, 775 } 776} 777 778func heapProfile() *profile.Profile { 779 var heapM = []*profile.Mapping{ 780 { 781 ID: 1, 782 BuildID: "buildid", 783 Start: 0x1000, 784 Limit: 0x4000, 785 HasFunctions: true, 786 HasFilenames: true, 787 HasLineNumbers: true, 788 HasInlineFrames: true, 789 }, 790 } 791 792 var heapF = []*profile.Function{ 793 {ID: 1, Name: "pruneme", SystemName: "pruneme", Filename: "prune.h"}, 794 {ID: 2, Name: "mangled1000", SystemName: "mangled1000", Filename: "testdata/file1000.src"}, 795 {ID: 3, Name: "mangled2000", SystemName: "mangled2000", Filename: "testdata/file2000.src"}, 796 {ID: 4, Name: "mangled2001", SystemName: "mangled2001", Filename: "testdata/file2000.src"}, 797 {ID: 5, Name: "mangled3000", SystemName: "mangled3000", Filename: "testdata/file3000.src"}, 798 {ID: 6, Name: "mangled3001", SystemName: "mangled3001", Filename: "testdata/file3000.src"}, 799 {ID: 7, Name: "mangled3002", SystemName: "mangled3002", Filename: "testdata/file3000.src"}, 800 {ID: 8, Name: "mangledMALLOC", SystemName: "mangledMALLOC", Filename: "malloc.h"}, 801 {ID: 9, Name: "mangledNEW", SystemName: "mangledNEW", Filename: "new.h"}, 802 } 803 804 var heapL = []*profile.Location{ 805 { 806 ID: 1000, 807 Mapping: heapM[0], 808 Address: 0x1000, 809 Line: []profile.Line{ 810 {Function: heapF[0], Line: 100}, 811 {Function: heapF[7], Line: 100}, 812 {Function: heapF[1], Line: 1}, 813 }, 814 }, 815 { 816 ID: 2000, 817 Mapping: heapM[0], 818 Address: 0x2000, 819 Line: []profile.Line{ 820 {Function: heapF[8], Line: 100}, 821 {Function: heapF[3], Line: 2}, 822 {Function: heapF[2], Line: 3}, 823 }, 824 }, 825 { 826 ID: 3000, 827 Mapping: heapM[0], 828 Address: 0x3000, 829 Line: []profile.Line{ 830 {Function: heapF[8], Line: 100}, 831 {Function: heapF[6], Line: 3}, 832 {Function: heapF[5], Line: 2}, 833 {Function: heapF[4], Line: 4}, 834 }, 835 }, 836 { 837 ID: 3001, 838 Mapping: heapM[0], 839 Address: 0x3001, 840 Line: []profile.Line{ 841 {Function: heapF[0], Line: 100}, 842 {Function: heapF[8], Line: 100}, 843 {Function: heapF[5], Line: 2}, 844 {Function: heapF[4], Line: 4}, 845 }, 846 }, 847 { 848 ID: 3002, 849 Mapping: heapM[0], 850 Address: 0x3002, 851 Line: []profile.Line{ 852 {Function: heapF[6], Line: 3}, 853 {Function: heapF[4], Line: 4}, 854 }, 855 }, 856 } 857 858 return &profile.Profile{ 859 Comments: []string{"comment", "#hidden comment"}, 860 PeriodType: &profile.ValueType{Type: "allocations", Unit: "bytes"}, 861 Period: 524288, 862 SampleType: []*profile.ValueType{ 863 {Type: "inuse_objects", Unit: "count"}, 864 {Type: "inuse_space", Unit: "bytes"}, 865 }, 866 Sample: []*profile.Sample{ 867 { 868 Location: []*profile.Location{heapL[0], heapL[1], heapL[2]}, 869 Value: []int64{10, 1024000}, 870 NumLabel: map[string][]int64{"bytes": {102400}}, 871 }, 872 { 873 Location: []*profile.Location{heapL[0], heapL[3]}, 874 Value: []int64{20, 4096000}, 875 NumLabel: map[string][]int64{"bytes": {204800}}, 876 }, 877 { 878 Location: []*profile.Location{heapL[1], heapL[4]}, 879 Value: []int64{40, 65536000}, 880 NumLabel: map[string][]int64{"bytes": {1638400}}, 881 }, 882 { 883 Location: []*profile.Location{heapL[2]}, 884 Value: []int64{80, 32768000}, 885 NumLabel: map[string][]int64{"bytes": {409600}}, 886 }, 887 }, 888 DropFrames: ".*operator new.*|malloc", 889 Location: heapL, 890 Function: heapF, 891 Mapping: heapM, 892 } 893} 894 895func contentionProfile() *profile.Profile { 896 var contentionM = []*profile.Mapping{ 897 { 898 ID: 1, 899 BuildID: "buildid-contention", 900 Start: 0x1000, 901 Limit: 0x4000, 902 HasFunctions: true, 903 HasFilenames: true, 904 HasLineNumbers: true, 905 HasInlineFrames: true, 906 }, 907 } 908 909 var contentionF = []*profile.Function{ 910 {ID: 1, Name: "mangled1000", SystemName: "mangled1000", Filename: "testdata/file1000.src"}, 911 {ID: 2, Name: "mangled2000", SystemName: "mangled2000", Filename: "testdata/file2000.src"}, 912 {ID: 3, Name: "mangled2001", SystemName: "mangled2001", Filename: "testdata/file2000.src"}, 913 {ID: 4, Name: "mangled3000", SystemName: "mangled3000", Filename: "testdata/file3000.src"}, 914 {ID: 5, Name: "mangled3001", SystemName: "mangled3001", Filename: "testdata/file3000.src"}, 915 {ID: 6, Name: "mangled3002", SystemName: "mangled3002", Filename: "testdata/file3000.src"}, 916 } 917 918 var contentionL = []*profile.Location{ 919 { 920 ID: 1000, 921 Mapping: contentionM[0], 922 Address: 0x1000, 923 Line: []profile.Line{ 924 {Function: contentionF[0], Line: 1}, 925 }, 926 }, 927 { 928 ID: 2000, 929 Mapping: contentionM[0], 930 Address: 0x2000, 931 Line: []profile.Line{ 932 {Function: contentionF[2], Line: 2}, 933 {Function: contentionF[1], Line: 3}, 934 }, 935 }, 936 { 937 ID: 3000, 938 Mapping: contentionM[0], 939 Address: 0x3000, 940 Line: []profile.Line{ 941 {Function: contentionF[5], Line: 2}, 942 {Function: contentionF[4], Line: 3}, 943 {Function: contentionF[3], Line: 5}, 944 }, 945 }, 946 { 947 ID: 3001, 948 Mapping: contentionM[0], 949 Address: 0x3001, 950 Line: []profile.Line{ 951 {Function: contentionF[4], Line: 3}, 952 {Function: contentionF[3], Line: 5}, 953 }, 954 }, 955 { 956 ID: 3002, 957 Mapping: contentionM[0], 958 Address: 0x3002, 959 Line: []profile.Line{ 960 {Function: contentionF[5], Line: 4}, 961 {Function: contentionF[3], Line: 3}, 962 }, 963 }, 964 } 965 966 return &profile.Profile{ 967 PeriodType: &profile.ValueType{Type: "contentions", Unit: "count"}, 968 Period: 524288, 969 SampleType: []*profile.ValueType{ 970 {Type: "contentions", Unit: "count"}, 971 {Type: "delay", Unit: "nanoseconds"}, 972 }, 973 Sample: []*profile.Sample{ 974 { 975 Location: []*profile.Location{contentionL[0], contentionL[1], contentionL[2]}, 976 Value: []int64{10, 10240000}, 977 }, 978 { 979 Location: []*profile.Location{contentionL[0], contentionL[3]}, 980 Value: []int64{20, 40960000}, 981 }, 982 { 983 Location: []*profile.Location{contentionL[1], contentionL[4]}, 984 Value: []int64{40, 65536000}, 985 }, 986 { 987 Location: []*profile.Location{contentionL[2]}, 988 Value: []int64{80, 32768000}, 989 }, 990 }, 991 Location: contentionL, 992 Function: contentionF, 993 Mapping: contentionM, 994 Comments: []string{"Comment #1", "Comment #2"}, 995 } 996} 997 998func symzProfile() *profile.Profile { 999 var symzM = []*profile.Mapping{ 1000 { 1001 ID: 1, 1002 Start: testStart, 1003 Limit: 0x4000, 1004 File: "/path/to/testbinary", 1005 }, 1006 } 1007 1008 var symzL = []*profile.Location{ 1009 {ID: 1, Mapping: symzM[0], Address: testStart}, 1010 {ID: 2, Mapping: symzM[0], Address: testStart + 0x1000}, 1011 {ID: 3, Mapping: symzM[0], Address: testStart + 0x2000}, 1012 } 1013 1014 return &profile.Profile{ 1015 PeriodType: &profile.ValueType{Type: "cpu", Unit: "milliseconds"}, 1016 Period: 1, 1017 DurationNanos: 10e9, 1018 SampleType: []*profile.ValueType{ 1019 {Type: "samples", Unit: "count"}, 1020 {Type: "cpu", Unit: "milliseconds"}, 1021 }, 1022 Sample: []*profile.Sample{ 1023 { 1024 Location: []*profile.Location{symzL[0], symzL[1], symzL[2]}, 1025 Value: []int64{1, 1}, 1026 }, 1027 }, 1028 Location: symzL, 1029 Mapping: symzM, 1030 } 1031} 1032 1033var autoCompleteTests = []struct { 1034 in string 1035 out string 1036}{ 1037 {"", ""}, 1038 {"xyz", "xyz"}, // no match 1039 {"dis", "disasm"}, // single match 1040 {"t", "t"}, // many matches 1041 {"top abc", "top abc"}, // no function name match 1042 {"top mangledM", "top mangledMALLOC"}, // single function name match 1043 {"top cmd cmd mangledM", "top cmd cmd mangledMALLOC"}, 1044 {"top mangled", "top mangled"}, // many function name matches 1045 {"cmd mangledM", "cmd mangledM"}, // invalid command 1046 {"top mangledM cmd", "top mangledM cmd"}, // cursor misplaced 1047 {"top edMA", "top mangledMALLOC"}, // single infix function name match 1048 {"top -mangledM", "top -mangledMALLOC"}, // ignore sign handled 1049 {"lin", "lines"}, // single variable match 1050 {"EdGeF", "edgefraction"}, // single capitalized match 1051 {"help dis", "help disasm"}, // help command match 1052 {"help relative_perc", "help relative_percentages"}, // help variable match 1053 {"help coMpa", "help compact_labels"}, // help variable capitalized match 1054} 1055 1056func TestAutoComplete(t *testing.T) { 1057 complete := newCompleter(functionNames(heapProfile())) 1058 1059 for _, test := range autoCompleteTests { 1060 if out := complete(test.in); out != test.out { 1061 t.Errorf("autoComplete(%s) = %s; want %s", test.in, out, test.out) 1062 } 1063 } 1064} 1065 1066func TestTagFilter(t *testing.T) { 1067 var tagFilterTests = []struct { 1068 desc, value string 1069 tags map[string][]string 1070 want bool 1071 }{ 1072 { 1073 "1 key with 1 matching value", 1074 "tag2", 1075 map[string][]string{"value1": {"tag1", "tag2"}}, 1076 true, 1077 }, 1078 { 1079 "1 key with no matching values", 1080 "tag3", 1081 map[string][]string{"value1": {"tag1", "tag2"}}, 1082 false, 1083 }, 1084 { 1085 "two keys, each with value matching different one value in list", 1086 "tag1,tag3", 1087 map[string][]string{"value1": {"tag1", "tag2"}, "value2": {"tag3"}}, 1088 true, 1089 }, 1090 {"two keys, all value matching different regex value in list", 1091 "t..[12],t..3", 1092 map[string][]string{"value1": {"tag1", "tag2"}, "value2": {"tag3"}}, 1093 true, 1094 }, 1095 { 1096 "one key, not all values in list matched", 1097 "tag2,tag3", 1098 map[string][]string{"value1": {"tag1", "tag2"}}, 1099 false, 1100 }, 1101 { 1102 "key specified, list of tags where all tags in list matched", 1103 "key1=tag1,tag2", 1104 map[string][]string{"key1": {"tag1", "tag2"}}, 1105 true, 1106 }, 1107 {"key specified, list of tag values where not all are matched", 1108 "key1=tag1,tag2", 1109 map[string][]string{"key1": {"tag1"}}, 1110 true, 1111 }, 1112 { 1113 "key included for regex matching, list of values where all values in list matched", 1114 "key1:tag1,tag2", 1115 map[string][]string{"key1": {"tag1", "tag2"}}, 1116 true, 1117 }, 1118 { 1119 "key included for regex matching, list of values where not only second value matched", 1120 "key1:tag1,tag2", 1121 map[string][]string{"key1": {"tag2"}}, 1122 false, 1123 }, 1124 { 1125 "key included for regex matching, list of values where not only first value matched", 1126 "key1:tag1,tag2", 1127 map[string][]string{"key1": {"tag1"}}, 1128 false, 1129 }, 1130 } 1131 for _, test := range tagFilterTests { 1132 t.Run(test.desc, func(t *testing.T) { 1133 filter, err := compileTagFilter(test.desc, test.value, nil, &proftest.TestUI{T: t}, nil) 1134 if err != nil { 1135 t.Fatalf("tagFilter %s:%v", test.desc, err) 1136 } 1137 s := profile.Sample{ 1138 Label: test.tags, 1139 } 1140 if got := filter(&s); got != test.want { 1141 t.Errorf("tagFilter %s: got %v, want %v", test.desc, got, test.want) 1142 } 1143 }) 1144 } 1145} 1146 1147func TestIdentifyNumLabelUnits(t *testing.T) { 1148 var tagFilterTests = []struct { 1149 desc string 1150 tagVals []map[string][]int64 1151 tagUnits []map[string][]string 1152 wantUnits map[string]string 1153 allowedRx string 1154 wantIgnoreErrCount int 1155 }{ 1156 { 1157 "Multiple keys, no units for all keys", 1158 []map[string][]int64{{"keyA": {131072}, "keyB": {128}}}, 1159 []map[string][]string{{"keyA": {}, "keyB": {""}}}, 1160 map[string]string{"keyA": "keyA", "keyB": "keyB"}, 1161 "", 1162 0, 1163 }, 1164 { 1165 "Multiple keys, different units for each key", 1166 []map[string][]int64{{"keyA": {131072}, "keyB": {128}}}, 1167 []map[string][]string{{"keyA": {"bytes"}, "keyB": {"kilobytes"}}}, 1168 map[string]string{"keyA": "bytes", "keyB": "kilobytes"}, 1169 "", 1170 0, 1171 }, 1172 { 1173 "Multiple keys with multiple values, different units for each key", 1174 []map[string][]int64{{"keyC": {131072, 1}, "keyD": {128, 252}}}, 1175 []map[string][]string{{"keyC": {"bytes", "bytes"}, "keyD": {"kilobytes", "kilobytes"}}}, 1176 map[string]string{"keyC": "bytes", "keyD": "kilobytes"}, 1177 "", 1178 0, 1179 }, 1180 { 1181 "Multiple keys with multiple values, some units missing", 1182 []map[string][]int64{{"key1": {131072, 1}, "A": {128, 252}, "key3": {128}, "key4": {1}}, {"key3": {128}, "key4": {1}}}, 1183 []map[string][]string{{"key1": {"", "bytes"}, "A": {"kilobytes", ""}, "key3": {""}, "key4": {"hour"}}, {"key3": {"seconds"}, "key4": {""}}}, 1184 map[string]string{"key1": "bytes", "A": "kilobytes", "key3": "seconds", "key4": "hour"}, 1185 "", 1186 0, 1187 }, 1188 { 1189 "One key with three units in same sample", 1190 []map[string][]int64{{"key": {8, 8, 16}}}, 1191 []map[string][]string{{"key": {"bytes", "megabytes", "kilobytes"}}}, 1192 map[string]string{"key": "bytes"}, 1193 `(For tag key used unit bytes, also encountered unit\(s\) kilobytes, megabytes)`, 1194 1, 1195 }, 1196 { 1197 "One key with four units in same sample", 1198 []map[string][]int64{{"key": {8, 8, 16, 32}}}, 1199 []map[string][]string{{"key": {"bytes", "kilobytes", "a", "megabytes"}}}, 1200 map[string]string{"key": "bytes"}, 1201 `(For tag key used unit bytes, also encountered unit\(s\) a, kilobytes, megabytes)`, 1202 1, 1203 }, 1204 { 1205 "One key with two units in same sample", 1206 []map[string][]int64{{"key": {8, 8}}}, 1207 []map[string][]string{{"key": {"bytes", "seconds"}}}, 1208 map[string]string{"key": "bytes"}, 1209 `(For tag key used unit bytes, also encountered unit\(s\) seconds)`, 1210 1, 1211 }, 1212 { 1213 "One key with different units in different samples", 1214 []map[string][]int64{{"key1": {8}}, {"key1": {8}}, {"key1": {8}}}, 1215 []map[string][]string{{"key1": {"bytes"}}, {"key1": {"kilobytes"}}, {"key1": {"megabytes"}}}, 1216 map[string]string{"key1": "bytes"}, 1217 `(For tag key1 used unit bytes, also encountered unit\(s\) kilobytes, megabytes)`, 1218 1, 1219 }, 1220 { 1221 "Key alignment, unit not specified", 1222 []map[string][]int64{{"alignment": {8}}}, 1223 []map[string][]string{nil}, 1224 map[string]string{"alignment": "bytes"}, 1225 "", 1226 0, 1227 }, 1228 { 1229 "Key request, unit not specified", 1230 []map[string][]int64{{"request": {8}}, {"request": {8, 8}}}, 1231 []map[string][]string{nil, nil}, 1232 map[string]string{"request": "bytes"}, 1233 "", 1234 0, 1235 }, 1236 { 1237 "Check units not over-written for keys with default units", 1238 []map[string][]int64{{ 1239 "alignment": {8}, 1240 "request": {8}, 1241 "bytes": {8}, 1242 }}, 1243 []map[string][]string{{ 1244 "alignment": {"seconds"}, 1245 "request": {"minutes"}, 1246 "bytes": {"hours"}, 1247 }}, 1248 map[string]string{ 1249 "alignment": "seconds", 1250 "request": "minutes", 1251 "bytes": "hours", 1252 }, 1253 "", 1254 0, 1255 }, 1256 } 1257 for _, test := range tagFilterTests { 1258 t.Run(test.desc, func(t *testing.T) { 1259 p := profile.Profile{Sample: make([]*profile.Sample, len(test.tagVals))} 1260 for i, numLabel := range test.tagVals { 1261 s := profile.Sample{ 1262 NumLabel: numLabel, 1263 NumUnit: test.tagUnits[i], 1264 } 1265 p.Sample[i] = &s 1266 } 1267 testUI := &proftest.TestUI{T: t, AllowRx: test.allowedRx} 1268 units := identifyNumLabelUnits(&p, testUI) 1269 if !reflect.DeepEqual(test.wantUnits, units) { 1270 t.Errorf("got %v units, want %v", units, test.wantUnits) 1271 } 1272 if got, want := testUI.NumAllowRxMatches, test.wantIgnoreErrCount; want != got { 1273 t.Errorf("got %d errors logged, want %d errors logged", got, want) 1274 } 1275 }) 1276 } 1277} 1278 1279func TestNumericTagFilter(t *testing.T) { 1280 var tagFilterTests = []struct { 1281 desc, value string 1282 tags map[string][]int64 1283 identifiedUnits map[string]string 1284 want bool 1285 }{ 1286 { 1287 "Match when unit conversion required", 1288 "128kb", 1289 map[string][]int64{"key1": {131072}, "key2": {128}}, 1290 map[string]string{"key1": "bytes", "key2": "kilobytes"}, 1291 true, 1292 }, 1293 { 1294 "Match only when values equal after unit conversion", 1295 "512kb", 1296 map[string][]int64{"key1": {512}, "key2": {128}}, 1297 map[string]string{"key1": "bytes", "key2": "kilobytes"}, 1298 false, 1299 }, 1300 { 1301 "Match when values and units initially equal", 1302 "10bytes", 1303 map[string][]int64{"key1": {10}, "key2": {128}}, 1304 map[string]string{"key1": "bytes", "key2": "kilobytes"}, 1305 true, 1306 }, 1307 { 1308 "Match range without lower bound, no unit conversion required", 1309 ":10bytes", 1310 map[string][]int64{"key1": {8}}, 1311 map[string]string{"key1": "bytes"}, 1312 true, 1313 }, 1314 { 1315 "Match range without lower bound, unit conversion required", 1316 ":10kb", 1317 map[string][]int64{"key1": {8}}, 1318 map[string]string{"key1": "bytes"}, 1319 true, 1320 }, 1321 { 1322 "Match range without upper bound, unit conversion required", 1323 "10b:", 1324 map[string][]int64{"key1": {8}}, 1325 map[string]string{"key1": "kilobytes"}, 1326 true, 1327 }, 1328 { 1329 "Match range without upper bound, no unit conversion required", 1330 "10b:", 1331 map[string][]int64{"key1": {12}}, 1332 map[string]string{"key1": "bytes"}, 1333 true, 1334 }, 1335 { 1336 "Don't match range without upper bound, no unit conversion required", 1337 "10b:", 1338 map[string][]int64{"key1": {8}}, 1339 map[string]string{"key1": "bytes"}, 1340 false, 1341 }, 1342 { 1343 "Multiple keys with different units, don't match range without upper bound", 1344 "10kb:", 1345 map[string][]int64{"key1": {8}}, 1346 map[string]string{"key1": "bytes", "key2": "kilobytes"}, 1347 false, 1348 }, 1349 { 1350 "Match range without upper bound, unit conversion required", 1351 "10b:", 1352 map[string][]int64{"key1": {8}}, 1353 map[string]string{"key1": "kilobytes"}, 1354 true, 1355 }, 1356 { 1357 "Don't match range without lower bound, no unit conversion required", 1358 ":10b", 1359 map[string][]int64{"key1": {12}}, 1360 map[string]string{"key1": "bytes"}, 1361 false, 1362 }, 1363 { 1364 "Match specific key, key present, one of two values match", 1365 "bytes=5b", 1366 map[string][]int64{"bytes": {10, 5}}, 1367 map[string]string{"bytes": "bytes"}, 1368 true, 1369 }, 1370 { 1371 "Match specific key, key present and value matches", 1372 "bytes=1024b", 1373 map[string][]int64{"bytes": {1024}}, 1374 map[string]string{"bytes": "kilobytes"}, 1375 false, 1376 }, 1377 { 1378 "Match specific key, matching key present and value matches, also non-matching key", 1379 "bytes=1024b", 1380 map[string][]int64{"bytes": {1024}, "key2": {5}}, 1381 map[string]string{"bytes": "bytes", "key2": "bytes"}, 1382 true, 1383 }, 1384 { 1385 "Match specific key and range of values, value matches", 1386 "bytes=512b:1024b", 1387 map[string][]int64{"bytes": {780}}, 1388 map[string]string{"bytes": "bytes"}, 1389 true, 1390 }, 1391 { 1392 "Match specific key and range of values, value too large", 1393 "key1=1kb:2kb", 1394 map[string][]int64{"key1": {4096}}, 1395 map[string]string{"key1": "bytes"}, 1396 false, 1397 }, 1398 { 1399 "Match specific key and range of values, value too small", 1400 "key1=1kb:2kb", 1401 map[string][]int64{"key1": {256}}, 1402 map[string]string{"key1": "bytes"}, 1403 false, 1404 }, 1405 { 1406 "Match specific key and value, unit conversion required", 1407 "bytes=1024b", 1408 map[string][]int64{"bytes": {1}}, 1409 map[string]string{"bytes": "kilobytes"}, 1410 true, 1411 }, 1412 { 1413 "Match specific key and value, key does not appear", 1414 "key2=256bytes", 1415 map[string][]int64{"key1": {256}}, 1416 map[string]string{"key1": "bytes"}, 1417 false, 1418 }, 1419 { 1420 "Match negative key and range of values, value matches", 1421 "bytes=-512b:-128b", 1422 map[string][]int64{"bytes": {-256}}, 1423 map[string]string{"bytes": "bytes"}, 1424 true, 1425 }, 1426 { 1427 "Match negative key and range of values, value outside range", 1428 "bytes=-512b:-128b", 1429 map[string][]int64{"bytes": {-2048}}, 1430 map[string]string{"bytes": "bytes"}, 1431 false, 1432 }, 1433 { 1434 "Match exact value, unitless tag", 1435 "pid=123", 1436 map[string][]int64{"pid": {123}}, 1437 nil, 1438 true, 1439 }, 1440 { 1441 "Match range, unitless tag", 1442 "pid=123:123", 1443 map[string][]int64{"pid": {123}}, 1444 nil, 1445 true, 1446 }, 1447 { 1448 "Don't match range, unitless tag", 1449 "pid=124:124", 1450 map[string][]int64{"pid": {123}}, 1451 nil, 1452 false, 1453 }, 1454 { 1455 "Match range without upper bound, unitless tag", 1456 "pid=100:", 1457 map[string][]int64{"pid": {123}}, 1458 nil, 1459 true, 1460 }, 1461 { 1462 "Don't match range without upper bound, unitless tag", 1463 "pid=200:", 1464 map[string][]int64{"pid": {123}}, 1465 nil, 1466 false, 1467 }, 1468 { 1469 "Match range without lower bound, unitless tag", 1470 "pid=:200", 1471 map[string][]int64{"pid": {123}}, 1472 nil, 1473 true, 1474 }, 1475 { 1476 "Don't match range without lower bound, unitless tag", 1477 "pid=:100", 1478 map[string][]int64{"pid": {123}}, 1479 nil, 1480 false, 1481 }, 1482 } 1483 for _, test := range tagFilterTests { 1484 t.Run(test.desc, func(t *testing.T) { 1485 wantErrMsg := strings.Join([]string{"(", test.desc, ":Interpreted '", test.value[strings.Index(test.value, "=")+1:], "' as range, not regexp", ")"}, "") 1486 filter, err := compileTagFilter(test.desc, test.value, test.identifiedUnits, &proftest.TestUI{T: t, 1487 AllowRx: wantErrMsg}, nil) 1488 if err != nil { 1489 t.Fatalf("%v", err) 1490 } 1491 s := profile.Sample{ 1492 NumLabel: test.tags, 1493 } 1494 if got := filter(&s); got != test.want { 1495 t.Fatalf("got %v, want %v", got, test.want) 1496 } 1497 }) 1498 } 1499} 1500 1501// TestOptionsHaveHelp tests that a help message is supplied for every 1502// selectable option. 1503func TestOptionsHaveHelp(t *testing.T) { 1504 for _, f := range configFields { 1505 // Check all choices if this is a group, else check f.name. 1506 names := f.choices 1507 if len(names) == 0 { 1508 names = []string{f.name} 1509 } 1510 for _, name := range names { 1511 if _, ok := configHelp[name]; !ok { 1512 t.Errorf("missing help message for %q", name) 1513 } 1514 } 1515 } 1516} 1517 1518type testSymbolzMergeFetcher struct{} 1519 1520func (testSymbolzMergeFetcher) Fetch(s string, d, t time.Duration) (*profile.Profile, string, error) { 1521 var p *profile.Profile 1522 switch s { 1523 case testSourceURL(8000) + "symbolz": 1524 p = symzProfile() 1525 case testSourceURL(8001) + "symbolz": 1526 p = symzProfile() 1527 p.Mapping[0].Start += testOffset 1528 p.Mapping[0].Limit += testOffset 1529 for i := range p.Location { 1530 p.Location[i].Address += testOffset 1531 } 1532 default: 1533 return nil, "", fmt.Errorf("unexpected source: %s", s) 1534 } 1535 return p, s, nil 1536} 1537 1538func TestSymbolzAfterMerge(t *testing.T) { 1539 baseConfig := currentConfig() 1540 defer setCurrentConfig(baseConfig) 1541 1542 f := baseFlags() 1543 f.args = []string{ 1544 testSourceURL(8000) + "symbolz", 1545 testSourceURL(8001) + "symbolz", 1546 } 1547 1548 o := setDefaults(nil) 1549 o.Flagset = f 1550 o.Obj = new(mockObjTool) 1551 src, cmd, err := parseFlags(o) 1552 if err != nil { 1553 t.Fatalf("parseFlags: %v", err) 1554 } 1555 1556 if len(cmd) != 1 || cmd[0] != "proto" { 1557 t.Fatalf("parseFlags returned command %v, want [proto]", cmd) 1558 } 1559 1560 o.Fetch = testSymbolzMergeFetcher{} 1561 o.Sym = testSymbolzSymbolizer{} 1562 p, err := fetchProfiles(src, o) 1563 if err != nil { 1564 t.Fatalf("fetchProfiles: %v", err) 1565 } 1566 if len(p.Location) != 3 { 1567 t.Errorf("Got %d locations after merge, want %d", len(p.Location), 3) 1568 } 1569 for i, l := range p.Location { 1570 if len(l.Line) != 1 { 1571 t.Errorf("Number of lines for symbolz %#x in iteration %d, got %d, want %d", l.Address, i, len(l.Line), 1) 1572 continue 1573 } 1574 address := l.Address - l.Mapping.Start 1575 if got, want := l.Line[0].Function.Name, fmt.Sprintf("%#x", address); got != want { 1576 t.Errorf("symbolz %#x, got %s, want %s", address, got, want) 1577 } 1578 } 1579} 1580 1581type mockObjTool struct{} 1582 1583func (*mockObjTool) Open(file string, start, limit, offset uint64) (plugin.ObjFile, error) { 1584 return &mockFile{file, "abcdef", 0}, nil 1585} 1586 1587func (m *mockObjTool) Disasm(file string, start, end uint64, intelSyntax bool) ([]plugin.Inst, error) { 1588 const fn1 = "line1000" 1589 const fn3 = "line3000" 1590 const file1 = "testdata/file1000.src" 1591 const file3 = "testdata/file3000.src" 1592 data := []plugin.Inst{ 1593 {Addr: 0x1000, Text: "instruction one", Function: fn1, File: file1, Line: 1}, 1594 {Addr: 0x1001, Text: "instruction two", Function: fn1, File: file1, Line: 1}, 1595 {Addr: 0x1002, Text: "instruction three", Function: fn1, File: file1, Line: 2}, 1596 {Addr: 0x1003, Text: "instruction four", Function: fn1, File: file1, Line: 1}, 1597 {Addr: 0x3000, Text: "instruction one", Function: fn3, File: file3}, 1598 {Addr: 0x3001, Text: "instruction two", Function: fn3, File: file3}, 1599 {Addr: 0x3002, Text: "instruction three", Function: fn3, File: file3}, 1600 {Addr: 0x3003, Text: "instruction four", Function: fn3, File: file3}, 1601 {Addr: 0x3004, Text: "instruction five", Function: fn3, File: file3}, 1602 } 1603 var result []plugin.Inst 1604 for _, inst := range data { 1605 if inst.Addr >= start && inst.Addr <= end { 1606 result = append(result, inst) 1607 } 1608 } 1609 return result, nil 1610} 1611 1612type mockFile struct { 1613 name, buildID string 1614 base uint64 1615} 1616 1617// Name returns the underlyinf file name, if available 1618func (m *mockFile) Name() string { 1619 return m.name 1620} 1621 1622// ObjAddr returns the objdump address corresponding to a runtime address. 1623func (m *mockFile) ObjAddr(addr uint64) (uint64, error) { 1624 return addr - m.base, nil 1625} 1626 1627// BuildID returns the GNU build ID of the file, or an empty string. 1628func (m *mockFile) BuildID() string { 1629 return m.buildID 1630} 1631 1632// SourceLine reports the source line information for a given 1633// address in the file. Due to inlining, the source line information 1634// is in general a list of positions representing a call stack, 1635// with the leaf function first. 1636func (*mockFile) SourceLine(addr uint64) ([]plugin.Frame, error) { 1637 // Return enough data to support the SourceLine() calls needed for 1638 // weblist on cpuProfile() contents. 1639 frame := func(fn, file string, line int) plugin.Frame { 1640 return plugin.Frame{Func: fn, File: file, Line: line} 1641 } 1642 switch addr { 1643 case 0x1000: 1644 return []plugin.Frame{ 1645 frame("mangled1000", "testdata/file1000.src", 1), 1646 }, nil 1647 case 0x1001: 1648 return []plugin.Frame{ 1649 frame("mangled1000", "testdata/file1000.src", 1), 1650 }, nil 1651 case 0x1002: 1652 return []plugin.Frame{ 1653 frame("mangled1000", "testdata/file1000.src", 2), 1654 }, nil 1655 case 0x1003: 1656 return []plugin.Frame{ 1657 frame("mangled1000", "testdata/file1000.src", 1), 1658 }, nil 1659 case 0x2000: 1660 return []plugin.Frame{ 1661 frame("mangled2001", "testdata/file2000.src", 9), 1662 frame("mangled2000", "testdata/file2000.src", 4), 1663 }, nil 1664 case 0x3000: 1665 return []plugin.Frame{ 1666 frame("mangled3002", "testdata/file3000.src", 2), 1667 frame("mangled3001", "testdata/file3000.src", 5), 1668 frame("mangled3000", "testdata/file3000.src", 6), 1669 }, nil 1670 case 0x3001: 1671 return []plugin.Frame{ 1672 frame("mangled3001", "testdata/file3000.src", 8), 1673 frame("mangled3000", "testdata/file3000.src", 9), 1674 }, nil 1675 case 0x3002: 1676 return []plugin.Frame{ 1677 frame("mangled3002", "testdata/file3000.src", 5), 1678 frame("mangled3000", "testdata/file3000.src", 9), 1679 }, nil 1680 } 1681 1682 return nil, nil 1683} 1684 1685// Symbols returns a list of symbols in the object file. 1686// If r is not nil, Symbols restricts the list to symbols 1687// with names matching the regular expression. 1688// If addr is not zero, Symbols restricts the list to symbols 1689// containing that address. 1690func (m *mockFile) Symbols(r *regexp.Regexp, addr uint64) ([]*plugin.Sym, error) { 1691 switch r.String() { 1692 case "line[13]": 1693 return []*plugin.Sym{ 1694 { 1695 Name: []string{"line1000"}, File: m.name, 1696 Start: 0x1000, End: 0x1003, 1697 }, 1698 { 1699 Name: []string{"line3000"}, File: m.name, 1700 Start: 0x3000, End: 0x3004, 1701 }, 1702 }, nil 1703 } 1704 return nil, fmt.Errorf("unimplemented") 1705} 1706 1707// Close closes the file, releasing associated resources. 1708func (*mockFile) Close() error { 1709 return nil 1710} 1711