소스 검색

Add show_from profile filter. (#372)

lvng 7 년 전
부모
커밋
a50cffb15f

+ 2
- 0
doc/README.md 파일 보기

@@ -94,6 +94,8 @@ Some common pprof options are:
94 94
   *regex*.
95 95
 * **-ignore= _regex_:** Do not include samples that include a report entry matching
96 96
   *regex*.
97
+* **-show\_from= _regex_:** Do not show entries above the first one that 
98
+  matches *regex*.
97 99
 * **-show= _regex_:** Only show entries that match *regex*.
98 100
 * **-hide= _regex_:** Do not show entries that match *regex*.
99 101
 

+ 4
- 0
internal/driver/commands.go 파일 보기

@@ -186,6 +186,10 @@ var pprofVariables = variables{
186 186
 		"Only show nodes matching regexp",
187 187
 		"If set, only show nodes that match this location.",
188 188
 		"Matching includes the function name, filename or object name.")},
189
+	"show_from": &variable{stringKind, "", "", helpText(
190
+		"Drops functions above the highest matched frame.",
191
+		"If set, all frames above the highest match are dropped from every sample.",
192
+		"Matching includes the function name, filename or object name.")},
189 193
 	"tagfocus": &variable{stringKind, "", "", helpText(
190 194
 		"Restricts to samples with tags in range or matched by regexp",
191 195
 		"Use name=value syntax to limit the matching to a specific tag.",

+ 9
- 10
internal/driver/driver.go 파일 보기

@@ -150,11 +150,11 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
150 150
 }
151 151
 
152 152
 func applyCommandOverrides(cmd []string, v variables) variables {
153
-	trim, focus, tagfocus, hide := v["trim"].boolValue(), true, true, true
153
+	trim, tagfilter, filter := v["trim"].boolValue(), true, true
154 154
 
155 155
 	switch cmd[0] {
156 156
 	case "proto", "raw":
157
-		trim, focus, tagfocus, hide = false, false, false, false
157
+		trim, tagfilter, filter = false, false, false
158 158
 		v.set("addresses", "t")
159 159
 	case "callgrind", "kcachegrind":
160 160
 		trim = false
@@ -163,7 +163,7 @@ func applyCommandOverrides(cmd []string, v variables) variables {
163 163
 		trim = false
164 164
 		v.set("addressnoinlines", "t")
165 165
 	case "peek":
166
-		trim, focus, hide = false, false, false
166
+		trim, filter = false, false
167 167
 	case "list":
168 168
 		v.set("nodecount", "0")
169 169
 		v.set("lines", "t")
@@ -181,17 +181,16 @@ func applyCommandOverrides(cmd []string, v variables) variables {
181 181
 		v.set("nodefraction", "0")
182 182
 		v.set("edgefraction", "0")
183 183
 	}
184
-	if !focus {
185
-		v.set("focus", "")
186
-		v.set("ignore", "")
187
-	}
188
-	if !tagfocus {
184
+	if !tagfilter {
189 185
 		v.set("tagfocus", "")
190 186
 		v.set("tagignore", "")
191 187
 	}
192
-	if !hide {
188
+	if !filter {
189
+		v.set("focus", "")
190
+		v.set("ignore", "")
193 191
 		v.set("hide", "")
194 192
 		v.set("show", "")
193
+		v.set("show_from", "")
195 194
 	}
196 195
 	return v
197 196
 }
@@ -242,7 +241,7 @@ func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars var
242 241
 	}
243 242
 
244 243
 	var filters []string
245
-	for _, k := range []string{"focus", "ignore", "hide", "show", "tagfocus", "tagignore", "tagshow", "taghide"} {
244
+	for _, k := range []string{"focus", "ignore", "hide", "show", "show_from", "tagfocus", "tagignore", "tagshow", "taghide"} {
246 245
 		v := vars[k].value
247 246
 		if v != "" {
248 247
 			filters = append(filters, k+"="+v)

+ 4
- 0
internal/driver/driver_focus.go 파일 보기

@@ -33,6 +33,7 @@ func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, v variab
33 33
 	ignore, err := compileRegexOption("ignore", v["ignore"].value, err)
34 34
 	hide, err := compileRegexOption("hide", v["hide"].value, err)
35 35
 	show, err := compileRegexOption("show", v["show"].value, err)
36
+	showfrom, err := compileRegexOption("show_from", v["show_from"].value, err)
36 37
 	tagfocus, err := compileTagFilter("tagfocus", v["tagfocus"].value, numLabelUnits, ui, err)
37 38
 	tagignore, err := compileTagFilter("tagignore", v["tagignore"].value, numLabelUnits, ui, err)
38 39
 	prunefrom, err := compileRegexOption("prune_from", v["prune_from"].value, err)
@@ -46,6 +47,9 @@ func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, v variab
46 47
 	warnNoMatches(hide == nil || hm, "Hide", ui)
47 48
 	warnNoMatches(show == nil || hnm, "Show", ui)
48 49
 
50
+	sfm := prof.ShowFrom(showfrom)
51
+	warnNoMatches(showfrom == nil || sfm, "ShowFrom", ui)
52
+
49 53
 	tfm, tim := prof.FilterSamplesByTag(tagfocus, tagignore)
50 54
 	warnNoMatches(tagfocus == nil || tfm, "TagFocus", ui)
51 55
 	warnNoMatches(tagignore == nil || tim, "TagIgnore", ui)

+ 4
- 0
internal/driver/driver_test.go 파일 보기

@@ -65,6 +65,7 @@ func TestParse(t *testing.T) {
65 65
 		{"topproto,lines,cum,hide=mangled[X3]0", "cpu"},
66 66
 		{"tree,lines,cum,focus=[24]00", "heap"},
67 67
 		{"tree,relative_percentages,cum,focus=[24]00", "heap"},
68
+		{"tree,lines,cum,show_from=line2", "cpu"},
68 69
 		{"callgrind", "cpu"},
69 70
 		{"callgrind,call_tree", "cpu"},
70 71
 		{"callgrind", "heap"},
@@ -261,6 +262,9 @@ func solutionFilename(source string, f *testFlags) string {
261 262
 	if f.strings["ignore"] != "" || f.strings["tagignore"] != "" {
262 263
 		name = append(name, "ignore")
263 264
 	}
265
+	if f.strings["show_from"] != "" {
266
+		name = append(name, "show_from")
267
+	}
264 268
 	name = addString(name, f, []string{"hide", "show"})
265 269
 	if f.strings["unit"] != "minimum" {
266 270
 		name = addString(name, f, []string{"unit"})

+ 16
- 0
internal/driver/testdata/pprof.cpu.cum.lines.tree.show_from 파일 보기

@@ -0,0 +1,16 @@
1
+Active filters:
2
+   show_from=line2
3
+Showing nodes accounting for 1.01s, 90.18% of 1.12s total
4
+----------------------------------------------------------+-------------
5
+      flat  flat%   sum%        cum   cum%   calls calls% + context 	 	 
6
+----------------------------------------------------------+-------------
7
+         0     0%     0%      1.01s 90.18%                | line2000 testdata/file2000.src:4
8
+                                             1.01s   100% |   line2001 testdata/file2000.src:9 (inline)
9
+----------------------------------------------------------+-------------
10
+                                             1.01s   100% |   line2000 testdata/file2000.src:4 (inline)
11
+     0.01s  0.89%  0.89%      1.01s 90.18%                | line2001 testdata/file2000.src:9
12
+                                                1s 99.01% |   line1000 testdata/file1000.src:1
13
+----------------------------------------------------------+-------------
14
+                                                1s   100% |   line2001 testdata/file2000.src:9
15
+        1s 89.29% 90.18%         1s 89.29%                | line1000 testdata/file1000.src:1
16
+----------------------------------------------------------+-------------

+ 65
- 0
profile/filter.go 파일 보기

@@ -74,6 +74,71 @@ func (p *Profile) FilterSamplesByName(focus, ignore, hide, show *regexp.Regexp)
74 74
 	return
75 75
 }
76 76
 
77
+// ShowFrom drops all stack frames above the highest matching frame and returns
78
+// whether a match was found. If showFrom is nil it returns false and does not
79
+// modify the profile.
80
+//
81
+// Example: consider a sample with frames [A, B, C, B], where A is the root.
82
+// ShowFrom(nil) returns false and has frames [A, B, C, B].
83
+// ShowFrom(A) returns true and has frames [A, B, C, B].
84
+// ShowFrom(B) returns true and has frames [B, C, B].
85
+// ShowFrom(C) returns true and has frames [C, B].
86
+// ShowFrom(D) returns false and drops the sample because no frames remain.
87
+func (p *Profile) ShowFrom(showFrom *regexp.Regexp) (matched bool) {
88
+	if showFrom == nil {
89
+		return false
90
+	}
91
+	// showFromLocs stores location IDs that matched ShowFrom.
92
+	showFromLocs := make(map[uint64]bool)
93
+	// Apply to locations.
94
+	for _, loc := range p.Location {
95
+		if filterShowFromLocation(loc, showFrom) {
96
+			showFromLocs[loc.ID] = true
97
+			matched = true
98
+		}
99
+	}
100
+	// For all samples, strip locations after the highest matching one.
101
+	s := make([]*Sample, 0, len(p.Sample))
102
+	for _, sample := range p.Sample {
103
+		for i := len(sample.Location) - 1; i >= 0; i-- {
104
+			if showFromLocs[sample.Location[i].ID] {
105
+				sample.Location = sample.Location[:i+1]
106
+				s = append(s, sample)
107
+				break
108
+			}
109
+		}
110
+	}
111
+	p.Sample = s
112
+	return matched
113
+}
114
+
115
+// filterShowFromLocation tests a showFrom regex against a location, removes
116
+// lines after the last match and returns whether a match was found. If the
117
+// mapping is matched, then all lines are kept.
118
+func filterShowFromLocation(loc *Location, showFrom *regexp.Regexp) bool {
119
+	if m := loc.Mapping; m != nil && showFrom.MatchString(m.File) {
120
+		return true
121
+	}
122
+	if i := loc.lastMatchedLineIndex(showFrom); i >= 0 {
123
+		loc.Line = loc.Line[:i+1]
124
+		return true
125
+	}
126
+	return false
127
+}
128
+
129
+// lastMatchedLineIndex returns the index of the last line that matches a regex,
130
+// or -1 if no match is found.
131
+func (loc *Location) lastMatchedLineIndex(re *regexp.Regexp) int {
132
+	for i := len(loc.Line) - 1; i >= 0; i-- {
133
+		if fn := loc.Line[i].Function; fn != nil {
134
+			if re.MatchString(fn.Name) || re.MatchString(fn.Filename) {
135
+				return i
136
+			}
137
+		}
138
+	}
139
+	return -1
140
+}
141
+
77 142
 // FilterTagsByName filters the tags in a profile and only keeps
78 143
 // tags that match show and not hide.
79 144
 func (p *Profile) FilterTagsByName(show, hide *regexp.Regexp) (sm, hm bool) {

+ 147
- 1
profile/filter_test.go 파일 보기

@@ -101,7 +101,29 @@ var inlinesProfile = &Profile{
101 101
 	},
102 102
 }
103 103
 
104
-func TestFilter(t *testing.T) {
104
+var 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
+
110
+var 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
+
126
+func TestFilterSamplesByName(t *testing.T) {
105 127
 	for _, tc := range []struct {
106 128
 		// name is the name of the test case.
107 129
 		name string
@@ -392,6 +414,130 @@ func TestFilter(t *testing.T) {
392 414
 	}
393 415
 }
394 416
 
417
+func 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
+
395 541
 // sampleFuncs returns a slice of strings where each string represents one
396 542
 // profile sample in the format "<fun1> <fun2> <fun3>: <value>". This allows
397 543
 // the expected values for test cases to be specifed in human-readable strings.