Skip to content

Commit

Permalink
Add show_from profile filter. (google#372)
Browse files Browse the repository at this point in the history
  • Loading branch information
lvng authored and Gabriel Marin committed Dec 17, 2020
1 parent d0bf4fb commit faf2502
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 11 deletions.
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 {
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

0 comments on commit faf2502

Please sign in to comment.