diff --git a/internal/driver/driver_focus.go b/internal/driver/driver_focus.go index ba5b502a..b17eca6a 100644 --- a/internal/driver/driver_focus.go +++ b/internal/driver/driver_focus.go @@ -40,7 +40,7 @@ func applyFocus(prof *profile.Profile, numLabelUnits map[string]string, v variab return err } - fm, im, hm, hnm := prof.FilterSamplesByName(focus, ignore, hide, show) + fm, im, _, _, hnm, hm := prof.FilterSamplesByName(focus, ignore, nil, nil, show, hide) warnNoMatches(focus == nil || fm, "Focus", ui) warnNoMatches(ignore == nil || im, "Ignore", ui) warnNoMatches(hide == nil || hm, "Hide", ui) diff --git a/profile/filter.go b/profile/filter.go index e68e93ae..51079e76 100644 --- a/profile/filter.go +++ b/profile/filter.go @@ -21,9 +21,19 @@ import "regexp" // FilterSamplesByName filters the samples in a profile and only keeps // samples where at least one frame matches focus but none match ignore. // Returns true is the corresponding regexp matched at least one sample. -func (p *Profile) FilterSamplesByName(focus, ignore, hide, show *regexp.Regexp) (fm, im, hm, hnm bool) { +func (p *Profile) FilterSamplesByName(focus, ignore, showFrom, hideFrom, show, hide *regexp.Regexp) (fm, im, sfm, hfm, sm, hm bool) { + if focus == nil && ignore == nil && showFrom == nil && hideFrom == nil && show == nil && hide == nil { + fm = true + return + } + focusOrIgnore := make(map[uint64]bool) hidden := make(map[uint64]bool) + // showFromLocs stores location IDs that matched ShowFrom. + showFromLocs := make(map[uint64]bool) + // hideFromLocs stores location IDs that matched HideFrom. + hideFromLocs := make(map[uint64]bool) + for _, l := range p.Location { if ignore != nil && l.matchesName(ignore) { im = true @@ -33,47 +43,158 @@ func (p *Profile) FilterSamplesByName(focus, ignore, hide, show *regexp.Regexp) focusOrIgnore[l.ID] = true } - if hide != nil && l.matchesName(hide) { - hm = true - l.Line = l.unmatchedLines(hide) - if len(l.Line) == 0 { - hidden[l.ID] = true + drop, sfml, hfml, sml, hml := filterLocation(l, showFrom, hideFrom, show, hide) + if drop { + hidden[l.ID] = true + } + if sfml { + sfm = true + showFromLocs[l.ID] = true + } + if hfml { + hfm = true + hideFromLocs[l.ID] = true + } + sm = sm || sml + hm = hm || hml + } + + mayHideFrames := len(hidden) != 0 || showFrom != nil || len(hideFromLocs) != 0 + + filteredSamples := make([]*Sample, 0, len(p.Sample)) + for _, sample := range p.Sample { + if !focusedAndNotIgnored(sample.Location, focusOrIgnore) { + continue + } + + if !mayHideFrames { + filteredSamples = append(filteredSamples, sample) + continue + } + + var pathIndex int + if hideFrom != nil { + if i := lastLocationIndex(sample.Location, hideFromLocs); i >= 0 { + pathIndex = i } } - if show != nil { - l.Line = l.matchedLines(show) - if len(l.Line) == 0 { - hidden[l.ID] = true + + var lastPathIndex = len(sample.Location) + if showFrom != nil { + if i := lastLocationIndex(sample.Location, showFromLocs); i >= 0 { + lastPathIndex = i + 1 } else { - hnm = true + lastPathIndex = 0 + } + } + + var filteredPath []*Location + for ; pathIndex < lastPathIndex; pathIndex++ { + location := sample.Location[pathIndex] + if !hidden[location.ID] { + filteredPath = append(filteredPath, location) } } + + if len(filteredPath) != 0 { + sample.Location = filteredPath + filteredSamples = append(filteredSamples, sample) + } + } + p.Sample = filteredSamples + return +} + +// filterLocation prunes a location's lines based on filters and returns +// whether each filter was matched, and whether or not to drop the location. +func filterLocation(location *Location, showFrom, hideFrom, show, hide *regexp.Regexp) (drop, sfml, hfml, sml, hml bool) { + if len(location.Line) == 0 { + return filterEmptyLocation(location, showFrom, hideFrom, show, hide) } - s := make([]*Sample, 0, len(p.Sample)) - for _, sample := range p.Sample { - if focusedAndNotIgnored(sample.Location, focusOrIgnore) { - if len(hidden) > 0 { - var locs []*Location - for _, loc := range sample.Location { - if !hidden[loc.ID] { - locs = append(locs, loc) - } - } - if len(locs) == 0 { - // Remove sample with no locations (by not adding it to s). - continue - } - sample.Location = locs + // TODO: These filter regexes are tested twice, first to determine the return + // values, and again to prune the lines. This happens because we want to test + // each filter against all lines before any pruning is done, to avoid missing + // any matches that were removed by a previous filter. + if show != nil && location.matchesName(show) { + sml = true + } + if hide != nil && location.matchesName(hide) { + hml = true + } + + showFromIndex := len(location.Line) + if showFrom != nil { + if location.matchesMapping(showFrom) { + sfml = true + } else if i := location.lastMatchedLineIndex(showFrom); i >= 0 { + sfml = true + showFromIndex = i + 1 + } + } + + hideFromIndex := 0 + if hideFrom != nil { + if location.matchesMapping(hideFrom) { + hfml = true + hideFromIndex = showFromIndex + } else if i := location.lastMatchedLineIndex(hideFrom); i >= 0 { + hfml = true + hideFromIndex = i + 1 + if hideFromIndex > showFromIndex { + hideFromIndex = showFromIndex } - s = append(s, sample) } } - p.Sample = s + location.Line = location.Line[hideFromIndex:showFromIndex] + if show != nil { + location.Line = location.matchedLines(show) + } + if hide != nil { + location.Line = location.unmatchedLines(hide) + } + + drop = len(location.Line) == 0 return } +// filterEmptyLocation handles the special case of locations with zero lines. +// the filters are only tested against the mapping, and the location is only +// dropped if explicitly matched by the filters. +func filterEmptyLocation(location *Location, showFrom, hideFrom, show, hide *regexp.Regexp) (drop, sfml, hfml, sml, hml bool) { + if showFrom != nil && location.matchesMapping(showFrom) { + sfml = true + } + if hideFrom != nil && location.matchesMapping(hideFrom) { + drop = true + hfml = true + } + if show != nil { + if location.matchesMapping(show) { + sml = true + } else { + drop = true + } + } + if hide != nil && location.matchesMapping(hide) { + drop = true + hml = true + } + return +} + +// lastLocationIndex returns the index of the last location who's ID is in the +// map. +func lastLocationIndex(path []*Location, matchedIDs map[uint64]bool) int { + for i := len(path) - 1; i >= 0; i-- { + if matchedIDs[path[i].ID] { + 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) { @@ -121,6 +242,27 @@ func (loc *Location) matchesName(re *regexp.Regexp) bool { return false } +// matchesMapping returns whether a regex matches the location's mapping. +func (loc *Location) matchesMapping(re *regexp.Regexp) bool { + if m := loc.Mapping; m != nil && re != nil && re.MatchString(m.File) { + return true + } + return false +} + +// lastMatchedLineIndex returns the last line index 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 +} + // unmatchedLines returns the lines in the location that do not match // the regular expression. func (loc *Location) unmatchedLines(re *regexp.Regexp) []Line { diff --git a/profile/filter_test.go b/profile/filter_test.go index c25887a4..ea010d9d 100644 --- a/profile/filter_test.go +++ b/profile/filter_test.go @@ -101,6 +101,34 @@ var inlinesProfile = &Profile{ }, } +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{}}, + }, +} + +var allEmptyLinesSampleFuncs = []string{ + "fun0 fun1: 1", + ": 2", + ": 3", +} + func TestFilter(t *testing.T) { for _, tc := range []struct { // name is the name of the test case. @@ -108,9 +136,9 @@ func TestFilter(t *testing.T) { // profile is the profile that gets filtered. profile *Profile // These are the inputs to FilterSamplesByName(). - focus, ignore, hide, show *regexp.Regexp + focus, ignore, showFrom, hideFrom, hide, show *regexp.Regexp // want{F,I,S,H}m are expected return values from FilterSamplesByName. - wantFm, wantIm, wantSm, wantHm bool + wantFm, wantIm, wantSfm, wantHfm, wantSm, wantHm 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: ". @@ -123,6 +151,12 @@ func TestFilter(t *testing.T) { wantFm: true, wantSampleFuncs: allNoInlinesSampleFuncs, }, + { + name: "empty filters keep all frames with empty lines", + profile: emptyLinesProfile, + wantFm: true, + wantSampleFuncs: allEmptyLinesSampleFuncs, + }, // Focus { name: "focus with no matches", @@ -169,6 +203,76 @@ func TestFilter(t *testing.T) { "fun4 fun5 fun6: 2", }, }, + { + name: "focus matches location with empty lines", + profile: emptyLinesProfile, + focus: regexp.MustCompile("map1"), + wantFm: true, + wantSampleFuncs: []string{ + ": 2", + }, + }, + // Focus with other filters + { + name: "focus and ignore", + profile: noInlinesProfile, + focus: regexp.MustCompile("fun1|fun7"), + ignore: regexp.MustCompile("fun1"), + wantFm: true, + wantIm: true, + wantSampleFuncs: []string{ + "fun7 fun8: 3", + }, + }, + { + name: "focus and showFrom", + profile: noInlinesProfile, + focus: regexp.MustCompile("fun1"), + showFrom: regexp.MustCompile("fun2|fun8"), + wantFm: true, + wantSfm: true, + wantSampleFuncs: []string{ + "fun0 fun1 fun2: 1", + }, + }, + { + name: "focus and hideFrom", + profile: noInlinesProfile, + focus: regexp.MustCompile("fun1"), + hideFrom: regexp.MustCompile("fun2|fun7"), + wantFm: true, + wantHfm: true, + wantSampleFuncs: []string{ + "fun3: 1", + "fun4 fun5 fun1 fun6: 2", + }, + }, + { + name: "focus and hide", + profile: noInlinesProfile, + focus: regexp.MustCompile("fun1"), + hide: regexp.MustCompile("fun1"), + wantFm: true, + wantHm: true, + wantSampleFuncs: []string{ + "fun0 fun2 fun3: 1", + "fun4 fun5 fun6: 2", + "fun9 fun4 fun7: 4", + }, + }, + { + name: "focus and show", + profile: noInlinesProfile, + focus: regexp.MustCompile("fun1"), + show: regexp.MustCompile("fun1"), + wantFm: true, + wantSm: true, + wantSampleFuncs: []string{ + "fun1: 1", + "fun1: 2", + "fun10: 4", + }, + }, // Ignore { name: "ignore with no matches matches all samples", @@ -219,6 +323,339 @@ func TestFilter(t *testing.T) { "fun0 fun1 fun2 fun3: 1", }, }, + { + name: "ignore matches location with empty lines", + profile: emptyLinesProfile, + ignore: regexp.MustCompile("map1"), + wantFm: true, + wantIm: true, + wantSampleFuncs: []string{ + "fun0 fun1: 1", + }, + }, + // Ignore with other filters + { + name: "ignore and showFrom", + profile: noInlinesProfile, + ignore: regexp.MustCompile("fun2"), + showFrom: regexp.MustCompile("fun1"), + wantFm: true, + wantIm: true, + wantSfm: true, + wantSampleFuncs: []string{ + "fun4 fun5 fun1: 2", + "fun9 fun4 fun10: 4", + }, + }, + { + name: "ignore and hideFrom", + profile: noInlinesProfile, + ignore: regexp.MustCompile("fun2"), + hideFrom: regexp.MustCompile("fun1"), + wantFm: true, + wantIm: true, + wantHfm: true, + wantSampleFuncs: []string{ + "fun6: 2", + "fun7 fun8: 3", + "fun7: 4", + }, + }, + { + name: "ignore and hide", + profile: noInlinesProfile, + ignore: regexp.MustCompile("fun2"), + hide: regexp.MustCompile("fun1"), + wantFm: true, + wantIm: true, + wantHm: true, + wantSampleFuncs: []string{ + "fun4 fun5 fun6: 2", + "fun7 fun8: 3", + "fun9 fun4 fun7: 4", + }, + }, + { + name: "ignore and show", + profile: noInlinesProfile, + ignore: regexp.MustCompile("fun2"), + show: regexp.MustCompile("fun1"), + wantFm: true, + wantIm: true, + wantSm: true, + wantSampleFuncs: []string{ + "fun1: 2", + "fun10: 4", + }, + }, + // ShowFrom + { + name: "showFrom with no matches drops all samples", + profile: noInlinesProfile, + showFrom: regexp.MustCompile("unknown"), + wantFm: true, + }, + { + name: "showFrom matches function names", + profile: noInlinesProfile, + showFrom: regexp.MustCompile("fun1"), + wantFm: true, + wantSfm: 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"), + wantFm: true, + wantSfm: 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"), + wantFm: true, + wantSfm: true, + wantSampleFuncs: []string{ + "fun9 fun4 fun10: 4", + }, + }, + { + name: "showFrom matches inline functions", + profile: inlinesProfile, + showFrom: regexp.MustCompile("fun0|fun5"), + wantFm: true, + wantSfm: true, + wantSampleFuncs: []string{ + "fun0: 1", + "fun4 fun5: 2", + }, + }, + { + name: "showFrom keeps all lines when matching mapping and function", + profile: inlinesProfile, + showFrom: regexp.MustCompile("map0|fun5"), + wantFm: true, + wantSfm: 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"), + wantFm: true, + wantSfm: true, + wantSampleFuncs: []string{ + ": 2", + }, + }, + // ShowFrom with other filters + { + name: "showFrom and hideFrom", + profile: noInlinesProfile, + showFrom: regexp.MustCompile("fun2|fun5|fun9"), + hideFrom: regexp.MustCompile("fun0|fun6|fun9"), + wantFm: true, + wantSfm: true, + wantHfm: true, + wantSampleFuncs: []string{ + "fun1 fun2: 1", + }, + }, + { + name: "showFrom and hide", + profile: noInlinesProfile, + showFrom: regexp.MustCompile("fun1"), + hide: regexp.MustCompile("fun2|fun4"), + wantFm: true, + wantSfm: true, + wantHm: true, + wantSampleFuncs: []string{ + "fun0 fun1: 1", + "fun5 fun1: 2", + "fun9 fun10: 4", + }, + }, + { + name: "showFrom and show", + profile: noInlinesProfile, + showFrom: regexp.MustCompile("fun1"), + show: regexp.MustCompile("fun3|fun5|fun10"), + wantFm: true, + wantSfm: true, + wantSm: true, + wantSampleFuncs: []string{ + "fun5: 2", + "fun10: 4", + }, + }, + { + name: "showFrom and hide inline", + profile: inlinesProfile, + showFrom: regexp.MustCompile("fun2|fun5"), + hide: regexp.MustCompile("fun0|fun4"), + wantFm: true, + wantSfm: true, + wantHm: true, + wantSampleFuncs: []string{ + "fun1 fun2: 1", + "fun5: 2", + }, + }, + { + name: "showFrom and show inline", + profile: inlinesProfile, + showFrom: regexp.MustCompile("fun2|fun3|fun5|"), + show: regexp.MustCompile("fun1|fun5"), + wantFm: true, + wantSfm: true, + wantSm: true, + wantSampleFuncs: []string{ + "fun1: 1", + "fun5: 2", + }, + }, + // HideFrom + { + name: "hideFrom with no matches keeps all samples", + profile: noInlinesProfile, + hideFrom: regexp.MustCompile("unknown"), + wantFm: true, + wantSampleFuncs: allNoInlinesSampleFuncs, + }, + { + name: "hideFrom matches function names", + profile: noInlinesProfile, + hideFrom: regexp.MustCompile("fun1"), + wantFm: true, + wantHfm: true, + wantSampleFuncs: []string{ + "fun2 fun3: 1", + "fun6: 2", + "fun7 fun8: 3", + "fun7: 4", + }, + }, + { + name: "hideFrom matches file names", + profile: noInlinesProfile, + hideFrom: regexp.MustCompile("file1"), + wantFm: true, + wantHfm: true, + wantSampleFuncs: []string{ + "fun2 fun3: 1", + "fun6: 2", + "fun7 fun8: 3", + "fun7: 4", + }, + }, + { + name: "hideFrom matches mapping names", + profile: noInlinesProfile, + hideFrom: regexp.MustCompile("map1"), + wantFm: true, + wantHfm: true, + wantSampleFuncs: []string{ + "fun0 fun1 fun2 fun3: 1", + "fun4 fun5 fun1 fun6: 2", + "fun7 fun8: 3", + "fun7: 4", + }, + }, + { + name: "hideFrom matches inline functions", + profile: inlinesProfile, + hideFrom: regexp.MustCompile("fun0|fun5"), + wantFm: true, + wantHfm: true, + wantSampleFuncs: []string{ + "fun1 fun2 fun3: 1", + "fun6: 2", + }, + }, + { + name: "hideFrom drops all lines when matching mapping and function", + profile: inlinesProfile, + hideFrom: regexp.MustCompile("map0|fun5"), + wantFm: true, + wantHfm: true, + }, + { + name: "hideFrom matches location with empty lines", + profile: emptyLinesProfile, + hideFrom: regexp.MustCompile("map1"), + wantFm: true, + wantHfm: true, + wantSampleFuncs: []string{ + "fun0 fun1: 1", + }, + }, + // HideFrom with other filters + { + name: "hideFrom and hide", + profile: noInlinesProfile, + hideFrom: regexp.MustCompile("fun1"), + hide: regexp.MustCompile("fun2|fun4|fun10"), + wantFm: true, + wantHfm: true, + wantHm: true, + wantSampleFuncs: []string{ + "fun3: 1", + "fun6: 2", + "fun7 fun8: 3", + "fun7: 4", + }, + }, + { + name: "hideFrom and show", + profile: noInlinesProfile, + hideFrom: regexp.MustCompile("fun1"), + show: regexp.MustCompile("fun0|fun6|fun10"), + wantFm: true, + wantHfm: true, + wantSm: true, + wantSampleFuncs: []string{ + "fun6: 2", + }, + }, + { + name: "hideFrom and hide inline", + profile: inlinesProfile, + hideFrom: regexp.MustCompile("fun1|fun4"), + hide: regexp.MustCompile("fun3|fun5"), + wantFm: true, + wantHfm: true, + wantHm: true, + wantSampleFuncs: []string{ + "fun2: 1", + "fun6: 2", + }, + }, + { + name: "hideFrom and show inline", + profile: inlinesProfile, + hideFrom: regexp.MustCompile("fun1|fun4"), + show: regexp.MustCompile("fun3|fun4|fun6"), + wantFm: true, + wantHfm: true, + wantSm: true, + wantSampleFuncs: []string{ + "fun3: 1", + "fun6: 2", + }, + }, // Show { name: "show with no matches", @@ -281,6 +718,31 @@ func TestFilter(t *testing.T) { "fun4 fun5 fun6: 2", }, }, + { + name: "show matches location with empty lines", + profile: emptyLinesProfile, + show: regexp.MustCompile("fun1|map1"), + wantFm: true, + wantSm: true, + wantSampleFuncs: []string{ + "fun1: 1", + ": 2", + }, + }, + // Show with other filters + { + name: "show and hide", + profile: noInlinesProfile, + show: regexp.MustCompile("fun1|fun2"), + hide: regexp.MustCompile("fun10"), + wantFm: true, + wantSm: true, + wantHm: true, + wantSampleFuncs: []string{ + "fun1 fun2: 1", + "fun1: 2", + }, + }, // Hide { name: "hide with no matches", @@ -346,39 +808,40 @@ func TestFilter(t *testing.T) { wantFm: true, wantHm: true, }, - // Compound filters { - name: "hides a stack matched by both focus and ignore", - profile: noInlinesProfile, - focus: regexp.MustCompile("fun1|fun7"), - ignore: regexp.MustCompile("fun1"), + name: "hide matches location with empty lines", + profile: emptyLinesProfile, + hide: regexp.MustCompile("fun1|map1"), wantFm: true, - wantIm: true, + wantHm: true, wantSampleFuncs: []string{ - "fun7 fun8: 3", + "fun0: 1", }, }, + // Compound filters { - name: "hides a function if both show and hide match it", - profile: noInlinesProfile, - show: regexp.MustCompile("fun1"), - hide: regexp.MustCompile("fun10"), - wantFm: true, - wantSm: true, - wantHm: true, + name: "focus showFrom hide", + profile: noInlinesProfile, + focus: regexp.MustCompile("fun3"), + showFrom: regexp.MustCompile("fun2"), + hide: regexp.MustCompile("fun1"), + wantFm: true, + wantSfm: true, + wantHm: true, wantSampleFuncs: []string{ - "fun1: 1", - "fun1: 2", + "fun0 fun2: 1", }, }, } { t.Run(tc.name, func(t *testing.T) { p := tc.profile.Copy() - fm, im, hm, sm := p.FilterSamplesByName(tc.focus, tc.ignore, tc.hide, tc.show) + fm, im, sfm, hfm, sm, hm := p.FilterSamplesByName(tc.focus, tc.ignore, tc.showFrom, tc.hideFrom, tc.show, tc.hide) - type match struct{ fm, im, hm, sm bool } - 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 { - t.Errorf("match got %+v want %+v", got, want) + type match struct{ fm, im, sfm, hfm, sm, hm bool } + gotMatch := match{fm, im, sfm, hfm, sm, hm} + wantMatch := match{tc.wantFm, tc.wantIm, tc.wantSfm, tc.wantHfm, tc.wantSm, tc.wantHm} + if gotMatch != wantMatch { + t.Errorf("match got %+v want %+v", gotMatch, wantMatch) } if got, want := strings.Join(sampleFuncs(p), "\n")+"\n", strings.Join(tc.wantSampleFuncs, "\n")+"\n"; got != want {