1// Copyright 2014 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
17// Implements methods to filter samples from profiles.
18
19import "regexp"
20
21// FilterSamplesByName filters the samples in a profile and only keeps
22// samples where at least one frame matches focus but none match ignore.
23// Returns true is the corresponding regexp matched at least one sample.
24func (p *Profile) FilterSamplesByName(focus, ignore, hide, show *regexp.Regexp) (fm, im, hm, hnm bool) {
25	if focus == nil && ignore == nil && hide == nil && show == nil {
26		fm = true // Missing focus implies a match
27		return
28	}
29	focusOrIgnore := make(map[uint64]bool)
30	hidden := make(map[uint64]bool)
31	for _, l := range p.Location {
32		if ignore != nil && l.matchesName(ignore) {
33			im = true
34			focusOrIgnore[l.ID] = false
35		} else if focus == nil || l.matchesName(focus) {
36			fm = true
37			focusOrIgnore[l.ID] = true
38		}
39
40		if hide != nil && l.matchesName(hide) {
41			hm = true
42			l.Line = l.unmatchedLines(hide)
43			if len(l.Line) == 0 {
44				hidden[l.ID] = true
45			}
46		}
47		if show != nil {
48			l.Line = l.matchedLines(show)
49			if len(l.Line) == 0 {
50				hidden[l.ID] = true
51			} else {
52				hnm = true
53			}
54		}
55	}
56
57	s := make([]*Sample, 0, len(p.Sample))
58	for _, sample := range p.Sample {
59		if focusedAndNotIgnored(sample.Location, focusOrIgnore) {
60			if len(hidden) > 0 {
61				var locs []*Location
62				for _, loc := range sample.Location {
63					if !hidden[loc.ID] {
64						locs = append(locs, loc)
65					}
66				}
67				if len(locs) == 0 {
68					// Remove sample with no locations (by not adding it to s).
69					continue
70				}
71				sample.Location = locs
72			}
73			s = append(s, sample)
74		}
75	}
76	p.Sample = s
77
78	return
79}
80
81// ShowFrom drops all stack frames above the highest matching frame and returns
82// whether a match was found. If showFrom is nil it returns false and does not
83// modify the profile.
84//
85// Example: consider a sample with frames [A, B, C, B], where A is the root.
86// ShowFrom(nil) returns false and has frames [A, B, C, B].
87// ShowFrom(A) returns true and has frames [A, B, C, B].
88// ShowFrom(B) returns true and has frames [B, C, B].
89// ShowFrom(C) returns true and has frames [C, B].
90// ShowFrom(D) returns false and drops the sample because no frames remain.
91func (p *Profile) ShowFrom(showFrom *regexp.Regexp) (matched bool) {
92	if showFrom == nil {
93		return false
94	}
95	// showFromLocs stores location IDs that matched ShowFrom.
96	showFromLocs := make(map[uint64]bool)
97	// Apply to locations.
98	for _, loc := range p.Location {
99		if filterShowFromLocation(loc, showFrom) {
100			showFromLocs[loc.ID] = true
101			matched = true
102		}
103	}
104	// For all samples, strip locations after the highest matching one.
105	s := make([]*Sample, 0, len(p.Sample))
106	for _, sample := range p.Sample {
107		for i := len(sample.Location) - 1; i >= 0; i-- {
108			if showFromLocs[sample.Location[i].ID] {
109				sample.Location = sample.Location[:i+1]
110				s = append(s, sample)
111				break
112			}
113		}
114	}
115	p.Sample = s
116	return matched
117}
118
119// filterShowFromLocation tests a showFrom regex against a location, removes
120// lines after the last match and returns whether a match was found. If the
121// mapping is matched, then all lines are kept.
122func filterShowFromLocation(loc *Location, showFrom *regexp.Regexp) bool {
123	if m := loc.Mapping; m != nil && showFrom.MatchString(m.File) {
124		return true
125	}
126	if i := loc.lastMatchedLineIndex(showFrom); i >= 0 {
127		loc.Line = loc.Line[:i+1]
128		return true
129	}
130	return false
131}
132
133// lastMatchedLineIndex returns the index of the last line that matches a regex,
134// or -1 if no match is found.
135func (loc *Location) lastMatchedLineIndex(re *regexp.Regexp) int {
136	for i := len(loc.Line) - 1; i >= 0; i-- {
137		if fn := loc.Line[i].Function; fn != nil {
138			if re.MatchString(fn.Name) || re.MatchString(fn.Filename) {
139				return i
140			}
141		}
142	}
143	return -1
144}
145
146// FilterTagsByName filters the tags in a profile and only keeps
147// tags that match show and not hide.
148func (p *Profile) FilterTagsByName(show, hide *regexp.Regexp) (sm, hm bool) {
149	matchRemove := func(name string) bool {
150		matchShow := show == nil || show.MatchString(name)
151		matchHide := hide != nil && hide.MatchString(name)
152
153		if matchShow {
154			sm = true
155		}
156		if matchHide {
157			hm = true
158		}
159		return !matchShow || matchHide
160	}
161	for _, s := range p.Sample {
162		for lab := range s.Label {
163			if matchRemove(lab) {
164				delete(s.Label, lab)
165			}
166		}
167		for lab := range s.NumLabel {
168			if matchRemove(lab) {
169				delete(s.NumLabel, lab)
170			}
171		}
172	}
173	return
174}
175
176// matchesName returns whether the location matches the regular
177// expression. It checks any available function names, file names, and
178// mapping object filename.
179func (loc *Location) matchesName(re *regexp.Regexp) bool {
180	for _, ln := range loc.Line {
181		if fn := ln.Function; fn != nil {
182			if re.MatchString(fn.Name) || re.MatchString(fn.Filename) {
183				return true
184			}
185		}
186	}
187	if m := loc.Mapping; m != nil && re.MatchString(m.File) {
188		return true
189	}
190	return false
191}
192
193// unmatchedLines returns the lines in the location that do not match
194// the regular expression.
195func (loc *Location) unmatchedLines(re *regexp.Regexp) []Line {
196	if m := loc.Mapping; m != nil && re.MatchString(m.File) {
197		return nil
198	}
199	var lines []Line
200	for _, ln := range loc.Line {
201		if fn := ln.Function; fn != nil {
202			if re.MatchString(fn.Name) || re.MatchString(fn.Filename) {
203				continue
204			}
205		}
206		lines = append(lines, ln)
207	}
208	return lines
209}
210
211// matchedLines returns the lines in the location that match
212// the regular expression.
213func (loc *Location) matchedLines(re *regexp.Regexp) []Line {
214	if m := loc.Mapping; m != nil && re.MatchString(m.File) {
215		return loc.Line
216	}
217	var lines []Line
218	for _, ln := range loc.Line {
219		if fn := ln.Function; fn != nil {
220			if !re.MatchString(fn.Name) && !re.MatchString(fn.Filename) {
221				continue
222			}
223		}
224		lines = append(lines, ln)
225	}
226	return lines
227}
228
229// focusedAndNotIgnored looks up a slice of ids against a map of
230// focused/ignored locations. The map only contains locations that are
231// explicitly focused or ignored. Returns whether there is at least
232// one focused location but no ignored locations.
233func focusedAndNotIgnored(locs []*Location, m map[uint64]bool) bool {
234	var f bool
235	for _, loc := range locs {
236		if focus, focusOrIgnore := m[loc.ID]; focusOrIgnore {
237			if focus {
238				// Found focused location. Must keep searching in case there
239				// is an ignored one as well.
240				f = true
241			} else {
242				// Found ignored location. Can return false right away.
243				return false
244			}
245		}
246	}
247	return f
248}
249
250// TagMatch selects tags for filtering
251type TagMatch func(s *Sample) bool
252
253// FilterSamplesByTag removes all samples from the profile, except
254// those that match focus and do not match the ignore regular
255// expression.
256func (p *Profile) FilterSamplesByTag(focus, ignore TagMatch) (fm, im bool) {
257	samples := make([]*Sample, 0, len(p.Sample))
258	for _, s := range p.Sample {
259		focused, ignored := true, false
260		if focus != nil {
261			focused = focus(s)
262		}
263		if ignore != nil {
264			ignored = ignore(s)
265		}
266		fm = fm || focused
267		im = im || ignored
268		if focused && !ignored {
269			samples = append(samples, s)
270		}
271	}
272	p.Sample = samples
273	return
274}
275