Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add show_from profile filter. #372

Merged
merged 11 commits into from
May 8, 2018
2 changes: 2 additions & 0 deletions doc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ Some common pprof options are:
*regex*.
* **-ignore= _regex_:** Do not include samples that include a report entry matching
*regex*.
* **-show\_from= _regex_:** Do not show entries above the first one that
matches *regex*.
* **-show= _regex_:** Only show entries that match *regex*.
* **-hide= _regex_:** Do not show entries that match *regex*.

Expand Down
4 changes: 4 additions & 0 deletions internal/driver/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ var pprofVariables = variables{
"Only show nodes matching regexp",
"If set, only show nodes that match this location.",
"Matching includes the function name, filename or object name.")},
"show_from": &variable{stringKind, "", "", helpText(
"Drops functions above the highest matched frame.",
"If set, all frames above the highest match are dropped from every sample.",
"Matching includes the function name, filename or object name.")},
"tagfocus": &variable{stringKind, "", "", helpText(
"Restricts to samples with tags in range or matched by regexp",
"Use name=value syntax to limit the matching to a specific tag.",
Expand Down
19 changes: 9 additions & 10 deletions internal/driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,11 +150,11 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
}

func applyCommandOverrides(cmd []string, v variables) variables {
trim, focus, tagfocus, hide := v["trim"].boolValue(), true, true, true
trim, tagfilter, filter := v["trim"].boolValue(), true, true

switch cmd[0] {
case "proto", "raw":
trim, focus, tagfocus, hide = false, false, false, false
trim, tagfilter, filter = false, false, false
v.set("addresses", "t")
case "callgrind", "kcachegrind":
trim = false
Expand All @@ -163,7 +163,7 @@ func applyCommandOverrides(cmd []string, v variables) variables {
trim = false
v.set("addressnoinlines", "t")
case "peek":
trim, focus, hide = false, false, false
trim, filter = false, false
case "list":
v.set("nodecount", "0")
v.set("lines", "t")
Expand All @@ -181,17 +181,16 @@ func applyCommandOverrides(cmd []string, v variables) variables {
v.set("nodefraction", "0")
v.set("edgefraction", "0")
}
if !focus {
v.set("focus", "")
v.set("ignore", "")
}
if !tagfocus {
if !tagfilter {
v.set("tagfocus", "")
v.set("tagignore", "")
}
if !hide {
if !filter {
v.set("focus", "")
v.set("ignore", "")
v.set("hide", "")
v.set("show", "")
v.set("show_from", "")
}
return v
}
Expand Down Expand Up @@ -242,7 +241,7 @@ func reportOptions(p *profile.Profile, numLabelUnits map[string]string, vars var
}

var filters []string
for _, k := range []string{"focus", "ignore", "hide", "show", "tagfocus", "tagignore", "tagshow", "taghide"} {
for _, k := range []string{"focus", "ignore", "hide", "show", "show_from", "tagfocus", "tagignore", "tagshow", "taghide"} {
v := vars[k].value
if v != "" {
filters = append(filters, k+"="+v)
Expand Down
4 changes: 4 additions & 0 deletions internal/driver/driver_focus.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, v variab
ignore, err := compileRegexOption("ignore", v["ignore"].value, err)
hide, err := compileRegexOption("hide", v["hide"].value, err)
show, err := compileRegexOption("show", v["show"].value, err)
showfrom, err := compileRegexOption("show_from", v["show_from"].value, err)
tagfocus, err := compileTagFilter("tagfocus", v["tagfocus"].value, numLabelUnits, ui, err)
tagignore, err := compileTagFilter("tagignore", v["tagignore"].value, numLabelUnits, ui, err)
prunefrom, err := compileRegexOption("prune_from", v["prune_from"].value, err)
Expand All @@ -46,6 +47,9 @@ func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, v variab
warnNoMatches(hide == nil || hm, "Hide", ui)
warnNoMatches(show == nil || hnm, "Show", ui)

sfm := prof.ShowFrom(showfrom)
warnNoMatches(showfrom == nil || sfm, "ShowFrom", ui)

tfm, tim := prof.FilterSamplesByTag(tagfocus, tagignore)
warnNoMatches(tagfocus == nil || tfm, "TagFocus", ui)
warnNoMatches(tagignore == nil || tim, "TagIgnore", ui)
Expand Down
4 changes: 4 additions & 0 deletions internal/driver/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func TestParse(t *testing.T) {
{"topproto,lines,cum,hide=mangled[X3]0", "cpu"},
{"tree,lines,cum,focus=[24]00", "heap"},
{"tree,relative_percentages,cum,focus=[24]00", "heap"},
{"tree,lines,cum,show_from=line2", "cpu"},
{"callgrind", "cpu"},
{"callgrind,call_tree", "cpu"},
{"callgrind", "heap"},
Expand Down Expand Up @@ -261,6 +262,9 @@ func solutionFilename(source string, f *testFlags) string {
if f.strings["ignore"] != "" || f.strings["tagignore"] != "" {
name = append(name, "ignore")
}
if f.strings["show_from"] != "" {
name = append(name, "show_from")
}
name = addString(name, f, []string{"hide", "show"})
if f.strings["unit"] != "minimum" {
name = addString(name, f, []string{"unit"})
Expand Down
16 changes: 16 additions & 0 deletions internal/driver/testdata/pprof.cpu.cum.lines.tree.show_from
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Active filters:
show_from=line2
Showing nodes accounting for 1.01s, 90.18% of 1.12s total
----------------------------------------------------------+-------------
flat flat% sum% cum cum% calls calls% + context
----------------------------------------------------------+-------------
0 0% 0% 1.01s 90.18% | line2000 testdata/file2000.src:4
1.01s 100% | line2001 testdata/file2000.src:9 (inline)
----------------------------------------------------------+-------------
1.01s 100% | line2000 testdata/file2000.src:4 (inline)
0.01s 0.89% 0.89% 1.01s 90.18% | line2001 testdata/file2000.src:9
1s 99.01% | line1000 testdata/file1000.src:1
----------------------------------------------------------+-------------
1s 100% | line2001 testdata/file2000.src:9
1s 89.29% 90.18% 1s 89.29% | line1000 testdata/file1000.src:1
----------------------------------------------------------+-------------
65 changes: 65 additions & 0 deletions profile/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,71 @@ func (p *Profile) FilterSamplesByName(focus, ignore, hide, show *regexp.Regexp)
return
}

// ShowFrom drops all stack frames above the highest matching frame and returns
// whether a match was found. If showFrom is nil it returns false and does not
// modify the profile.
//
// Example: consider a sample with frames [A, B, C, B], where A is the root.
// ShowFrom(nil) returns false and has frames [A, B, C, B].
// ShowFrom(A) returns true and has frames [A, B, C, B].
// ShowFrom(B) returns true and has frames [B, C, B].
// ShowFrom(C) returns true and has frames [C, B].
// ShowFrom(D) returns false and drops the sample because no frames remain.
func (p *Profile) ShowFrom(showFrom *regexp.Regexp) (matched bool) {
if showFrom == nil {
return false
}
// showFromLocs stores location IDs that matched ShowFrom.
showFromLocs := make(map[uint64]bool)
// Apply to locations.
for _, loc := range p.Location {
if filterShowFromLocation(loc, showFrom) {
showFromLocs[loc.ID] = true
matched = true
}
}
// For all samples, strip locations after the highest matching one.
s := make([]*Sample, 0, len(p.Sample))
for _, sample := range p.Sample {
for i := len(sample.Location) - 1; i >= 0; i-- {
if showFromLocs[sample.Location[i].ID] {
sample.Location = sample.Location[:i+1]
s = append(s, sample)
break
}
}
}
p.Sample = s
return matched
}

// filterShowFromLocation tests a showFrom regex against a location, removes
// lines after the last match and returns whether a match was found. If the
// mapping is matched, then all lines are kept.
func filterShowFromLocation(loc *Location, showFrom *regexp.Regexp) bool {
if m := loc.Mapping; m != nil && showFrom.MatchString(m.File) {
return true
}
if i := loc.lastMatchedLineIndex(showFrom); i >= 0 {
loc.Line = loc.Line[:i+1]
return true
}
return false
}

// lastMatchedLineIndex returns the index of the last line that matches a regex,
// or -1 if no match is found.
func (loc *Location) lastMatchedLineIndex(re *regexp.Regexp) int {
for i := len(loc.Line) - 1; i >= 0; i-- {
if fn := loc.Line[i].Function; fn != nil {
if re.MatchString(fn.Name) || re.MatchString(fn.Filename) {
return i
}
}
}
return -1
}

// FilterTagsByName filters the tags in a profile and only keeps
// tags that match show and not hide.
func (p *Profile) FilterTagsByName(show, hide *regexp.Regexp) (sm, hm bool) {
Expand Down
148 changes: 147 additions & 1 deletion profile/filter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,29 @@ var inlinesProfile = &Profile{
},
}

func TestFilter(t *testing.T) {
var emptyLinesLocs = []*Location{
{ID: 1, Mapping: mappings[0], Address: 0x1000, Line: []Line{{Function: functions[0], Line: 1}, {Function: functions[1], Line: 1}}},
{ID: 2, Mapping: mappings[0], Address: 0x2000, Line: []Line{}},
{ID: 3, Mapping: mappings[1], Address: 0x2000, Line: []Line{}},
}

var emptyLinesProfile = &Profile{
TimeNanos: 10000,
PeriodType: &ValueType{Type: "cpu", Unit: "milliseconds"},
Period: 1,
DurationNanos: 10e9,
SampleType: []*ValueType{{Type: "samples", Unit: "count"}},
Mapping: mappings,
Function: functions,
Location: emptyLinesLocs,
Sample: []*Sample{
{Value: []int64{1}, Location: []*Location{emptyLinesLocs[0], emptyLinesLocs[1]}},
{Value: []int64{2}, Location: []*Location{emptyLinesLocs[2]}},
{Value: []int64{3}, Location: []*Location{}},
},
}

func TestFilterSamplesByName(t *testing.T) {
for _, tc := range []struct {
// name is the name of the test case.
name string
Expand Down Expand Up @@ -392,6 +414,130 @@ func TestFilter(t *testing.T) {
}
}

func TestShowFrom(t *testing.T) {
for _, tc := range []struct {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have test cases that check that "show from" shows frames from the highest match? It is not obvious from the test names that any test does that. Need a test for both locations and lines.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added two test cases. one for matching multiple frames without inlines, and one with inlines.

name string
profile *Profile
showFrom *regexp.Regexp
// wantMatch is the expected return value.
wantMatch bool
// wantSampleFuncs contains expected stack functions and sample value after
// filtering, in the same order as in the profile. The format is as
// returned by sampleFuncs function below, which is "callee caller: <num>".
wantSampleFuncs []string
}{
{
name: "nil showFrom keeps all frames",
profile: noInlinesProfile,
wantMatch: false,
wantSampleFuncs: allNoInlinesSampleFuncs,
},
{
name: "showFrom with no matches drops all samples",
profile: noInlinesProfile,
showFrom: regexp.MustCompile("unknown"),
wantMatch: false,
},
{
name: "showFrom matches function names",
profile: noInlinesProfile,
showFrom: regexp.MustCompile("fun1"),
wantMatch: true,
wantSampleFuncs: []string{
"fun0 fun1: 1",
"fun4 fun5 fun1: 2",
"fun9 fun4 fun10: 4",
},
},
{
name: "showFrom matches file names",
profile: noInlinesProfile,
showFrom: regexp.MustCompile("file1"),
wantMatch: true,
wantSampleFuncs: []string{
"fun0 fun1: 1",
"fun4 fun5 fun1: 2",
"fun9 fun4 fun10: 4",
},
},
{
name: "showFrom matches mapping names",
profile: noInlinesProfile,
showFrom: regexp.MustCompile("map1"),
wantMatch: true,
wantSampleFuncs: []string{
"fun9 fun4 fun10: 4",
},
},
{
name: "showFrom drops frames above highest of multiple matches",
profile: noInlinesProfile,
showFrom: regexp.MustCompile("fun[12]"),
wantMatch: true,
wantSampleFuncs: []string{
"fun0 fun1 fun2: 1",
"fun4 fun5 fun1: 2",
"fun9 fun4 fun10: 4",
},
},
{
name: "showFrom matches inline functions",
profile: inlinesProfile,
showFrom: regexp.MustCompile("fun0|fun5"),
wantMatch: true,
wantSampleFuncs: []string{
"fun0: 1",
"fun4 fun5: 2",
},
},
{
name: "showFrom drops frames above highest of multiple inline matches",
profile: inlinesProfile,
showFrom: regexp.MustCompile("fun[1245]"),
wantMatch: true,
wantSampleFuncs: []string{
"fun0 fun1 fun2: 1",
"fun4 fun5: 2",
},
},
{
name: "showFrom keeps all lines when matching mapping and function",
profile: inlinesProfile,
showFrom: regexp.MustCompile("map0|fun5"),
wantMatch: true,
wantSampleFuncs: []string{
"fun0 fun1 fun2 fun3: 1",
"fun4 fun5 fun6: 2",
},
},
{
name: "showFrom matches location with empty lines",
profile: emptyLinesProfile,
showFrom: regexp.MustCompile("map1"),
wantMatch: true,
wantSampleFuncs: []string{
": 2",
},
},
} {
t.Run(tc.name, func(t *testing.T) {
p := tc.profile.Copy()

if gotMatch := p.ShowFrom(tc.showFrom); gotMatch != tc.wantMatch {
t.Errorf("match got %+v, want %+v", gotMatch, tc.wantMatch)
}

if got, want := strings.Join(sampleFuncs(p), "\n")+"\n", strings.Join(tc.wantSampleFuncs, "\n")+"\n"; got != want {
diff, err := proftest.Diff([]byte(want), []byte(got))
if err != nil {
t.Fatalf("failed to get diff: %v", err)
}
t.Errorf("profile samples got diff(want->got):\n%s", diff)
}
})
}
}

// sampleFuncs returns a slice of strings where each string represents one
// profile sample in the format "<fun1> <fun2> <fun3>: <value>". This allows
// the expected values for test cases to be specifed in human-readable strings.
Expand Down