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