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	focusOrIgnore := make(map[uint64]bool)
26	hidden := make(map[uint64]bool)
27	for _, l := range p.Location {
28		if ignore != nil && l.matchesName(ignore) {
29			im = true
30			focusOrIgnore[l.ID] = false
31		} else if focus == nil || l.matchesName(focus) {
32			fm = true
33			focusOrIgnore[l.ID] = true
34		}
35
36		if hide != nil && l.matchesName(hide) {
37			hm = true
38			l.Line = l.unmatchedLines(hide)
39			if len(l.Line) == 0 {
40				hidden[l.ID] = true
41			}
42		}
43		if show != nil {
44			l.Line = l.matchedLines(show)
45			if len(l.Line) == 0 {
46				hidden[l.ID] = true
47			} else {
48				hnm = true
49			}
50		}
51	}
52
53	s := make([]*Sample, 0, len(p.Sample))
54	for _, sample := range p.Sample {
55		if focusedAndNotIgnored(sample.Location, focusOrIgnore) {
56			if len(hidden) > 0 {
57				var locs []*Location
58				for _, loc := range sample.Location {
59					if !hidden[loc.ID] {
60						locs = append(locs, loc)
61					}
62				}
63				if len(locs) == 0 {
64					// Remove sample with no locations (by not adding it to s).
65					continue
66				}
67				sample.Location = locs
68			}
69			s = append(s, sample)
70		}
71	}
72	p.Sample = s
73
74	return
75}
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.
87func (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.
118func 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.
131func (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
142// FilterTagsByName filters the tags in a profile and only keeps
143// tags that match show and not hide.
144func (p *Profile) FilterTagsByName(show, hide *regexp.Regexp) (sm, hm bool) {
145	matchRemove := func(name string) bool {
146		matchShow := show == nil || show.MatchString(name)
147		matchHide := hide != nil && hide.MatchString(name)
148
149		if matchShow {
150			sm = true
151		}
152		if matchHide {
153			hm = true
154		}
155		return !matchShow || matchHide
156	}
157	for _, s := range p.Sample {
158		for lab := range s.Label {
159			if matchRemove(lab) {
160				delete(s.Label, lab)
161			}
162		}
163		for lab := range s.NumLabel {
164			if matchRemove(lab) {
165				delete(s.NumLabel, lab)
166			}
167		}
168	}
169	return
170}
171
172// matchesName returns whether the location matches the regular
173// expression. It checks any available function names, file names, and
174// mapping object filename.
175func (loc *Location) matchesName(re *regexp.Regexp) bool {
176	for _, ln := range loc.Line {
177		if fn := ln.Function; fn != nil {
178			if re.MatchString(fn.Name) || re.MatchString(fn.Filename) {
179				return true
180			}
181		}
182	}
183	if m := loc.Mapping; m != nil && re.MatchString(m.File) {
184		return true
185	}
186	return false
187}
188
189// unmatchedLines returns the lines in the location that do not match
190// the regular expression.
191func (loc *Location) unmatchedLines(re *regexp.Regexp) []Line {
192	if m := loc.Mapping; m != nil && re.MatchString(m.File) {
193		return nil
194	}
195	var lines []Line
196	for _, ln := range loc.Line {
197		if fn := ln.Function; fn != nil {
198			if re.MatchString(fn.Name) || re.MatchString(fn.Filename) {
199				continue
200			}
201		}
202		lines = append(lines, ln)
203	}
204	return lines
205}
206
207// matchedLines returns the lines in the location that match
208// the regular expression.
209func (loc *Location) matchedLines(re *regexp.Regexp) []Line {
210	if m := loc.Mapping; m != nil && re.MatchString(m.File) {
211		return loc.Line
212	}
213	var lines []Line
214	for _, ln := range loc.Line {
215		if fn := ln.Function; fn != nil {
216			if !re.MatchString(fn.Name) && !re.MatchString(fn.Filename) {
217				continue
218			}
219		}
220		lines = append(lines, ln)
221	}
222	return lines
223}
224
225// focusedAndNotIgnored looks up a slice of ids against a map of
226// focused/ignored locations. The map only contains locations that are
227// explicitly focused or ignored. Returns whether there is at least
228// one focused location but no ignored locations.
229func focusedAndNotIgnored(locs []*Location, m map[uint64]bool) bool {
230	var f bool
231	for _, loc := range locs {
232		if focus, focusOrIgnore := m[loc.ID]; focusOrIgnore {
233			if focus {
234				// Found focused location. Must keep searching in case there
235				// is an ignored one as well.
236				f = true
237			} else {
238				// Found ignored location. Can return false right away.
239				return false
240			}
241		}
242	}
243	return f
244}
245
246// TagMatch selects tags for filtering
247type TagMatch func(s *Sample) bool
248
249// FilterSamplesByTag removes all samples from the profile, except
250// those that match focus and do not match the ignore regular
251// expression.
252func (p *Profile) FilterSamplesByTag(focus, ignore TagMatch) (fm, im bool) {
253	samples := make([]*Sample, 0, len(p.Sample))
254	for _, s := range p.Sample {
255		focused, ignored := true, false
256		if focus != nil {
257			focused = focus(s)
258		}
259		if ignore != nil {
260			ignored = ignore(s)
261		}
262		fm = fm || focused
263		im = im || ignored
264		if focused && !ignored {
265			samples = append(samples, s)
266		}
267	}
268	p.Sample = samples
269	return
270}
271