1// Copyright 2018 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 profile 16 17import ( 18 "fmt" 19 "regexp" 20 "strings" 21 "testing" 22 23 "github.com/google/pprof/internal/proftest" 24) 25 26var mappings = []*Mapping{ 27 {ID: 1, Start: 0x10000, Limit: 0x40000, File: "map0", HasFunctions: true, HasFilenames: true, HasLineNumbers: true, HasInlineFrames: true}, 28 {ID: 2, Start: 0x50000, Limit: 0x70000, File: "map1", HasFunctions: true, HasFilenames: true, HasLineNumbers: true, HasInlineFrames: true}, 29} 30 31var functions = []*Function{ 32 {ID: 1, Name: "fun0", SystemName: "fun0", Filename: "file0"}, 33 {ID: 2, Name: "fun1", SystemName: "fun1", Filename: "file1"}, 34 {ID: 3, Name: "fun2", SystemName: "fun2", Filename: "file2"}, 35 {ID: 4, Name: "fun3", SystemName: "fun3", Filename: "file3"}, 36 {ID: 5, Name: "fun4", SystemName: "fun4", Filename: "file4"}, 37 {ID: 6, Name: "fun5", SystemName: "fun5", Filename: "file5"}, 38 {ID: 7, Name: "fun6", SystemName: "fun6", Filename: "file6"}, 39 {ID: 8, Name: "fun7", SystemName: "fun7", Filename: "file7"}, 40 {ID: 9, Name: "fun8", SystemName: "fun8", Filename: "file8"}, 41 {ID: 10, Name: "fun9", SystemName: "fun9", Filename: "file9"}, 42 {ID: 11, Name: "fun10", SystemName: "fun10", Filename: "file10"}, 43} 44 45var noInlinesLocs = []*Location{ 46 {ID: 1, Mapping: mappings[0], Address: 0x1000, Line: []Line{{Function: functions[0], Line: 1}}}, 47 {ID: 2, Mapping: mappings[0], Address: 0x2000, Line: []Line{{Function: functions[1], Line: 1}}}, 48 {ID: 3, Mapping: mappings[0], Address: 0x3000, Line: []Line{{Function: functions[2], Line: 1}}}, 49 {ID: 4, Mapping: mappings[0], Address: 0x4000, Line: []Line{{Function: functions[3], Line: 1}}}, 50 {ID: 5, Mapping: mappings[0], Address: 0x5000, Line: []Line{{Function: functions[4], Line: 1}}}, 51 {ID: 6, Mapping: mappings[0], Address: 0x6000, Line: []Line{{Function: functions[5], Line: 1}}}, 52 {ID: 7, Mapping: mappings[0], Address: 0x7000, Line: []Line{{Function: functions[6], Line: 1}}}, 53 {ID: 8, Mapping: mappings[0], Address: 0x8000, Line: []Line{{Function: functions[7], Line: 1}}}, 54 {ID: 9, Mapping: mappings[0], Address: 0x9000, Line: []Line{{Function: functions[8], Line: 1}}}, 55 {ID: 10, Mapping: mappings[0], Address: 0x10000, Line: []Line{{Function: functions[9], Line: 1}}}, 56 {ID: 11, Mapping: mappings[1], Address: 0x11000, Line: []Line{{Function: functions[10], Line: 1}}}, 57} 58 59var noInlinesProfile = &Profile{ 60 TimeNanos: 10000, 61 PeriodType: &ValueType{Type: "cpu", Unit: "milliseconds"}, 62 Period: 1, 63 DurationNanos: 10e9, 64 SampleType: []*ValueType{{Type: "samples", Unit: "count"}}, 65 Mapping: mappings, 66 Function: functions, 67 Location: noInlinesLocs, 68 Sample: []*Sample{ 69 {Value: []int64{1}, Location: []*Location{noInlinesLocs[0], noInlinesLocs[1], noInlinesLocs[2], noInlinesLocs[3]}}, 70 {Value: []int64{2}, Location: []*Location{noInlinesLocs[4], noInlinesLocs[5], noInlinesLocs[1], noInlinesLocs[6]}}, 71 {Value: []int64{3}, Location: []*Location{noInlinesLocs[7], noInlinesLocs[8]}}, 72 {Value: []int64{4}, Location: []*Location{noInlinesLocs[9], noInlinesLocs[4], noInlinesLocs[10], noInlinesLocs[7]}}, 73 }, 74} 75 76var allNoInlinesSampleFuncs = []string{ 77 "fun0 fun1 fun2 fun3: 1", 78 "fun4 fun5 fun1 fun6: 2", 79 "fun7 fun8: 3", 80 "fun9 fun4 fun10 fun7: 4", 81} 82 83var inlinesLocs = []*Location{ 84 {ID: 1, Mapping: mappings[0], Address: 0x1000, Line: []Line{{Function: functions[0], Line: 1}, {Function: functions[1], Line: 1}}}, 85 {ID: 2, Mapping: mappings[0], Address: 0x2000, Line: []Line{{Function: functions[2], Line: 1}, {Function: functions[3], Line: 1}}}, 86 {ID: 3, Mapping: mappings[0], Address: 0x3000, Line: []Line{{Function: functions[4], Line: 1}, {Function: functions[5], Line: 1}, {Function: functions[6], Line: 1}}}, 87} 88 89var inlinesProfile = &Profile{ 90 TimeNanos: 10000, 91 PeriodType: &ValueType{Type: "cpu", Unit: "milliseconds"}, 92 Period: 1, 93 DurationNanos: 10e9, 94 SampleType: []*ValueType{{Type: "samples", Unit: "count"}}, 95 Mapping: mappings, 96 Function: functions, 97 Location: inlinesLocs, 98 Sample: []*Sample{ 99 {Value: []int64{1}, Location: []*Location{inlinesLocs[0], inlinesLocs[1]}}, 100 {Value: []int64{2}, Location: []*Location{inlinesLocs[2]}}, 101 }, 102} 103 104var emptyLinesLocs = []*Location{ 105 {ID: 1, Mapping: mappings[0], Address: 0x1000, Line: []Line{{Function: functions[0], Line: 1}, {Function: functions[1], Line: 1}}}, 106 {ID: 2, Mapping: mappings[0], Address: 0x2000, Line: []Line{}}, 107 {ID: 3, Mapping: mappings[1], Address: 0x2000, Line: []Line{}}, 108} 109 110var emptyLinesProfile = &Profile{ 111 TimeNanos: 10000, 112 PeriodType: &ValueType{Type: "cpu", Unit: "milliseconds"}, 113 Period: 1, 114 DurationNanos: 10e9, 115 SampleType: []*ValueType{{Type: "samples", Unit: "count"}}, 116 Mapping: mappings, 117 Function: functions, 118 Location: emptyLinesLocs, 119 Sample: []*Sample{ 120 {Value: []int64{1}, Location: []*Location{emptyLinesLocs[0], emptyLinesLocs[1]}}, 121 {Value: []int64{2}, Location: []*Location{emptyLinesLocs[2]}}, 122 {Value: []int64{3}, Location: []*Location{}}, 123 }, 124} 125 126func TestFilterSamplesByName(t *testing.T) { 127 for _, tc := range []struct { 128 // name is the name of the test case. 129 name string 130 // profile is the profile that gets filtered. 131 profile *Profile 132 // These are the inputs to FilterSamplesByName(). 133 focus, ignore, hide, show *regexp.Regexp 134 // want{F,I,S,H}m are expected return values from FilterSamplesByName. 135 wantFm, wantIm, wantSm, wantHm bool 136 // wantSampleFuncs contains expected stack functions and sample value after 137 // filtering, in the same order as in the profile. The format is as 138 // returned by sampleFuncs function below, which is "callee caller: <num>". 139 wantSampleFuncs []string 140 }{ 141 // No Filters 142 { 143 name: "empty filters keep all frames", 144 profile: noInlinesProfile, 145 wantFm: true, 146 wantSampleFuncs: allNoInlinesSampleFuncs, 147 }, 148 // Focus 149 { 150 name: "focus with no matches", 151 profile: noInlinesProfile, 152 focus: regexp.MustCompile("unknown"), 153 }, 154 { 155 name: "focus matches function names", 156 profile: noInlinesProfile, 157 focus: regexp.MustCompile("fun1"), 158 wantFm: true, 159 wantSampleFuncs: []string{ 160 "fun0 fun1 fun2 fun3: 1", 161 "fun4 fun5 fun1 fun6: 2", 162 "fun9 fun4 fun10 fun7: 4", 163 }, 164 }, 165 { 166 name: "focus matches file names", 167 profile: noInlinesProfile, 168 focus: regexp.MustCompile("file1"), 169 wantFm: true, 170 wantSampleFuncs: []string{ 171 "fun0 fun1 fun2 fun3: 1", 172 "fun4 fun5 fun1 fun6: 2", 173 "fun9 fun4 fun10 fun7: 4", 174 }, 175 }, 176 { 177 name: "focus matches mapping names", 178 profile: noInlinesProfile, 179 focus: regexp.MustCompile("map1"), 180 wantFm: true, 181 wantSampleFuncs: []string{ 182 "fun9 fun4 fun10 fun7: 4", 183 }, 184 }, 185 { 186 name: "focus matches inline functions", 187 profile: inlinesProfile, 188 focus: regexp.MustCompile("fun5"), 189 wantFm: true, 190 wantSampleFuncs: []string{ 191 "fun4 fun5 fun6: 2", 192 }, 193 }, 194 // Ignore 195 { 196 name: "ignore with no matches matches all samples", 197 profile: noInlinesProfile, 198 ignore: regexp.MustCompile("unknown"), 199 wantFm: true, 200 wantSampleFuncs: allNoInlinesSampleFuncs, 201 }, 202 { 203 name: "ignore matches function names", 204 profile: noInlinesProfile, 205 ignore: regexp.MustCompile("fun1"), 206 wantFm: true, 207 wantIm: true, 208 wantSampleFuncs: []string{ 209 "fun7 fun8: 3", 210 }, 211 }, 212 { 213 name: "ignore matches file names", 214 profile: noInlinesProfile, 215 ignore: regexp.MustCompile("file1"), 216 wantFm: true, 217 wantIm: true, 218 wantSampleFuncs: []string{ 219 "fun7 fun8: 3", 220 }, 221 }, 222 { 223 name: "ignore matches mapping names", 224 profile: noInlinesProfile, 225 ignore: regexp.MustCompile("map1"), 226 wantFm: true, 227 wantIm: true, 228 wantSampleFuncs: []string{ 229 "fun0 fun1 fun2 fun3: 1", 230 "fun4 fun5 fun1 fun6: 2", 231 "fun7 fun8: 3", 232 }, 233 }, 234 { 235 name: "ignore matches inline functions", 236 profile: inlinesProfile, 237 ignore: regexp.MustCompile("fun5"), 238 wantFm: true, 239 wantIm: true, 240 wantSampleFuncs: []string{ 241 "fun0 fun1 fun2 fun3: 1", 242 }, 243 }, 244 // Show 245 { 246 name: "show with no matches", 247 profile: noInlinesProfile, 248 show: regexp.MustCompile("unknown"), 249 wantFm: true, 250 }, 251 { 252 name: "show matches function names", 253 profile: noInlinesProfile, 254 show: regexp.MustCompile("fun1|fun2"), 255 wantFm: true, 256 wantSm: true, 257 wantSampleFuncs: []string{ 258 "fun1 fun2: 1", 259 "fun1: 2", 260 "fun10: 4", 261 }, 262 }, 263 { 264 name: "show matches file names", 265 profile: noInlinesProfile, 266 show: regexp.MustCompile("file1|file3"), 267 wantFm: true, 268 wantSm: true, 269 wantSampleFuncs: []string{ 270 "fun1 fun3: 1", 271 "fun1: 2", 272 "fun10: 4", 273 }, 274 }, 275 { 276 name: "show matches mapping names", 277 profile: noInlinesProfile, 278 show: regexp.MustCompile("map1"), 279 wantFm: true, 280 wantSm: true, 281 wantSampleFuncs: []string{ 282 "fun10: 4", 283 }, 284 }, 285 { 286 name: "show matches inline functions", 287 profile: inlinesProfile, 288 show: regexp.MustCompile("fun[03]"), 289 wantFm: true, 290 wantSm: true, 291 wantSampleFuncs: []string{ 292 "fun0 fun3: 1", 293 }, 294 }, 295 { 296 name: "show keeps all lines when matching both mapping and function", 297 profile: inlinesProfile, 298 show: regexp.MustCompile("map0|fun5"), 299 wantFm: true, 300 wantSm: true, 301 wantSampleFuncs: []string{ 302 "fun0 fun1 fun2 fun3: 1", 303 "fun4 fun5 fun6: 2", 304 }, 305 }, 306 // Hide 307 { 308 name: "hide with no matches", 309 profile: noInlinesProfile, 310 hide: regexp.MustCompile("unknown"), 311 wantFm: true, 312 wantSampleFuncs: allNoInlinesSampleFuncs, 313 }, 314 { 315 name: "hide matches function names", 316 profile: noInlinesProfile, 317 hide: regexp.MustCompile("fun1|fun2"), 318 wantFm: true, 319 wantHm: true, 320 wantSampleFuncs: []string{ 321 "fun0 fun3: 1", 322 "fun4 fun5 fun6: 2", 323 "fun7 fun8: 3", 324 "fun9 fun4 fun7: 4", 325 }, 326 }, 327 { 328 name: "hide matches file names", 329 profile: noInlinesProfile, 330 hide: regexp.MustCompile("file1|file3"), 331 wantFm: true, 332 wantHm: true, 333 wantSampleFuncs: []string{ 334 "fun0 fun2: 1", 335 "fun4 fun5 fun6: 2", 336 "fun7 fun8: 3", 337 "fun9 fun4 fun7: 4", 338 }, 339 }, 340 { 341 name: "hide matches mapping names", 342 profile: noInlinesProfile, 343 hide: regexp.MustCompile("map1"), 344 wantFm: true, 345 wantHm: true, 346 wantSampleFuncs: []string{ 347 "fun0 fun1 fun2 fun3: 1", 348 "fun4 fun5 fun1 fun6: 2", 349 "fun7 fun8: 3", 350 "fun9 fun4 fun7: 4", 351 }, 352 }, 353 { 354 name: "hide matches inline functions", 355 profile: inlinesProfile, 356 hide: regexp.MustCompile("fun[125]"), 357 wantFm: true, 358 wantHm: true, 359 wantSampleFuncs: []string{ 360 "fun0 fun3: 1", 361 "fun4 fun6: 2", 362 }, 363 }, 364 { 365 name: "hide drops all lines when matching both mapping and function", 366 profile: inlinesProfile, 367 hide: regexp.MustCompile("map0|fun5"), 368 wantFm: true, 369 wantHm: true, 370 }, 371 // Compound filters 372 { 373 name: "hides a stack matched by both focus and ignore", 374 profile: noInlinesProfile, 375 focus: regexp.MustCompile("fun1|fun7"), 376 ignore: regexp.MustCompile("fun1"), 377 wantFm: true, 378 wantIm: true, 379 wantSampleFuncs: []string{ 380 "fun7 fun8: 3", 381 }, 382 }, 383 { 384 name: "hides a function if both show and hide match it", 385 profile: noInlinesProfile, 386 show: regexp.MustCompile("fun1"), 387 hide: regexp.MustCompile("fun10"), 388 wantFm: true, 389 wantSm: true, 390 wantHm: true, 391 wantSampleFuncs: []string{ 392 "fun1: 1", 393 "fun1: 2", 394 }, 395 }, 396 } { 397 t.Run(tc.name, func(t *testing.T) { 398 p := tc.profile.Copy() 399 fm, im, hm, sm := p.FilterSamplesByName(tc.focus, tc.ignore, tc.hide, tc.show) 400 401 type match struct{ fm, im, hm, sm bool } 402 if got, want := (match{fm: fm, im: im, hm: hm, sm: sm}), (match{fm: tc.wantFm, im: tc.wantIm, hm: tc.wantHm, sm: tc.wantSm}); got != want { 403 t.Errorf("match got %+v want %+v", got, want) 404 } 405 406 if got, want := strings.Join(sampleFuncs(p), "\n")+"\n", strings.Join(tc.wantSampleFuncs, "\n")+"\n"; got != want { 407 diff, err := proftest.Diff([]byte(want), []byte(got)) 408 if err != nil { 409 t.Fatalf("failed to get diff: %v", err) 410 } 411 t.Errorf("FilterSamplesByName: got diff(want->got):\n%s", diff) 412 } 413 }) 414 } 415} 416 417func TestShowFrom(t *testing.T) { 418 for _, tc := range []struct { 419 name string 420 profile *Profile 421 showFrom *regexp.Regexp 422 // wantMatch is the expected return value. 423 wantMatch bool 424 // wantSampleFuncs contains expected stack functions and sample value after 425 // filtering, in the same order as in the profile. The format is as 426 // returned by sampleFuncs function below, which is "callee caller: <num>". 427 wantSampleFuncs []string 428 }{ 429 { 430 name: "nil showFrom keeps all frames", 431 profile: noInlinesProfile, 432 wantMatch: false, 433 wantSampleFuncs: allNoInlinesSampleFuncs, 434 }, 435 { 436 name: "showFrom with no matches drops all samples", 437 profile: noInlinesProfile, 438 showFrom: regexp.MustCompile("unknown"), 439 wantMatch: false, 440 }, 441 { 442 name: "showFrom matches function names", 443 profile: noInlinesProfile, 444 showFrom: regexp.MustCompile("fun1"), 445 wantMatch: true, 446 wantSampleFuncs: []string{ 447 "fun0 fun1: 1", 448 "fun4 fun5 fun1: 2", 449 "fun9 fun4 fun10: 4", 450 }, 451 }, 452 { 453 name: "showFrom matches file names", 454 profile: noInlinesProfile, 455 showFrom: regexp.MustCompile("file1"), 456 wantMatch: true, 457 wantSampleFuncs: []string{ 458 "fun0 fun1: 1", 459 "fun4 fun5 fun1: 2", 460 "fun9 fun4 fun10: 4", 461 }, 462 }, 463 { 464 name: "showFrom matches mapping names", 465 profile: noInlinesProfile, 466 showFrom: regexp.MustCompile("map1"), 467 wantMatch: true, 468 wantSampleFuncs: []string{ 469 "fun9 fun4 fun10: 4", 470 }, 471 }, 472 { 473 name: "showFrom drops frames above highest of multiple matches", 474 profile: noInlinesProfile, 475 showFrom: regexp.MustCompile("fun[12]"), 476 wantMatch: true, 477 wantSampleFuncs: []string{ 478 "fun0 fun1 fun2: 1", 479 "fun4 fun5 fun1: 2", 480 "fun9 fun4 fun10: 4", 481 }, 482 }, 483 { 484 name: "showFrom matches inline functions", 485 profile: inlinesProfile, 486 showFrom: regexp.MustCompile("fun0|fun5"), 487 wantMatch: true, 488 wantSampleFuncs: []string{ 489 "fun0: 1", 490 "fun4 fun5: 2", 491 }, 492 }, 493 { 494 name: "showFrom drops frames above highest of multiple inline matches", 495 profile: inlinesProfile, 496 showFrom: regexp.MustCompile("fun[1245]"), 497 wantMatch: true, 498 wantSampleFuncs: []string{ 499 "fun0 fun1 fun2: 1", 500 "fun4 fun5: 2", 501 }, 502 }, 503 { 504 name: "showFrom keeps all lines when matching mapping and function", 505 profile: inlinesProfile, 506 showFrom: regexp.MustCompile("map0|fun5"), 507 wantMatch: true, 508 wantSampleFuncs: []string{ 509 "fun0 fun1 fun2 fun3: 1", 510 "fun4 fun5 fun6: 2", 511 }, 512 }, 513 { 514 name: "showFrom matches location with empty lines", 515 profile: emptyLinesProfile, 516 showFrom: regexp.MustCompile("map1"), 517 wantMatch: true, 518 wantSampleFuncs: []string{ 519 ": 2", 520 }, 521 }, 522 } { 523 t.Run(tc.name, func(t *testing.T) { 524 p := tc.profile.Copy() 525 526 if gotMatch := p.ShowFrom(tc.showFrom); gotMatch != tc.wantMatch { 527 t.Errorf("match got %+v, want %+v", gotMatch, tc.wantMatch) 528 } 529 530 if got, want := strings.Join(sampleFuncs(p), "\n")+"\n", strings.Join(tc.wantSampleFuncs, "\n")+"\n"; got != want { 531 diff, err := proftest.Diff([]byte(want), []byte(got)) 532 if err != nil { 533 t.Fatalf("failed to get diff: %v", err) 534 } 535 t.Errorf("profile samples got diff(want->got):\n%s", diff) 536 } 537 }) 538 } 539} 540 541// sampleFuncs returns a slice of strings where each string represents one 542// profile sample in the format "<fun1> <fun2> <fun3>: <value>". This allows 543// the expected values for test cases to be specified in human-readable 544// strings. 545func sampleFuncs(p *Profile) []string { 546 var ret []string 547 for _, s := range p.Sample { 548 var funcs []string 549 for _, loc := range s.Location { 550 for _, line := range loc.Line { 551 funcs = append(funcs, line.Function.Name) 552 } 553 } 554 ret = append(ret, fmt.Sprintf("%s: %d", strings.Join(funcs, " "), s.Value[0])) 555 } 556 return ret 557} 558 559func TestTagFilter(t *testing.T) { 560 // Perform several forms of tag filtering on the test profile. 561 562 type filterTestcase struct { 563 include, exclude *regexp.Regexp 564 im, em bool 565 count int 566 } 567 568 countTags := func(p *Profile) map[string]bool { 569 tags := make(map[string]bool) 570 571 for _, s := range p.Sample { 572 for l := range s.Label { 573 tags[l] = true 574 } 575 for l := range s.NumLabel { 576 tags[l] = true 577 } 578 } 579 return tags 580 } 581 582 for tx, tc := range []filterTestcase{ 583 {nil, nil, true, false, 3}, 584 {regexp.MustCompile("notfound"), nil, false, false, 0}, 585 {regexp.MustCompile("key1"), nil, true, false, 1}, 586 {nil, regexp.MustCompile("key[12]"), true, true, 1}, 587 } { 588 prof := testProfile1.Copy() 589 gim, gem := prof.FilterTagsByName(tc.include, tc.exclude) 590 if gim != tc.im { 591 t.Errorf("Filter #%d, got include match=%v, want %v", tx, gim, tc.im) 592 } 593 if gem != tc.em { 594 t.Errorf("Filter #%d, got exclude match=%v, want %v", tx, gem, tc.em) 595 } 596 if tags := countTags(prof); len(tags) != tc.count { 597 t.Errorf("Filter #%d, got %d tags[%v], want %d", tx, len(tags), tags, tc.count) 598 } 599 } 600} 601