blob: 636d5ba591d62d07a5bba9f976f261ffdd0f5202 [file] [log] [blame]
Dennis Kempin19e972b2013-06-20 13:21:38 -07001#! /usr/bin/env python
2# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
Harry Cutts0edf1572020-01-21 15:42:10 -08005
Harry Cutts85378ee2020-02-07 15:53:46 -08006from __future__ import absolute_import
7from __future__ import division
Harry Cutts0edf1572020-01-21 15:42:10 -08008from __future__ import print_function
9
Dennis Kempind5b59022013-07-17 14:12:55 -070010from collections import Counter, defaultdict, namedtuple, OrderedDict
11from itertools import chain
Dennis Kempin19e972b2013-06-20 13:21:38 -070012from mtreplay import MTReplay
Harry Cutts85378ee2020-02-07 15:53:46 -080013from .queryengine import Query, QueryEngine, QueryMatch, QueryResult
Dennis Kempin19e972b2013-06-20 13:21:38 -070014import re
Dennis Kempin19e972b2013-06-20 13:21:38 -070015
16
Dennis Kempind5b59022013-07-17 14:12:55 -070017class MTStatQuery(Query):
18 """ Searches for MTStat lines.
Dennis Kempin253ee052013-07-01 14:58:22 -070019
Dennis Kempind5b59022013-07-17 14:12:55 -070020 An MTStat line looks like 'MTStat:124.321:Key=Value' and is used
21 for generating statistics and searching of numeric values.
22 """
23 line_regex = re.compile('MTStat:([0-9]+\.[0-9]+):(\w+)([=:])(\w+)')
24 line_match = namedtuple("LineMatch", "timestamp key operator value")
Dennis Kempin253ee052013-07-01 14:58:22 -070025
Dennis Kempind5b59022013-07-17 14:12:55 -070026 @classmethod
27 def MatchLine(cls, line):
28 match = cls.line_regex.match(line)
Dennis Kempin6e03a432013-06-25 09:00:53 -070029 if match:
Dennis Kempind5b59022013-07-17 14:12:55 -070030 return cls.line_match(
31 timestamp=match.group(1),
32 key=match.group(2),
33 operator=match.group(3),
34 value=match.group(4)
35 )
36 return None
Dennis Kempin253ee052013-07-01 14:58:22 -070037
Dennis Kempind5b59022013-07-17 14:12:55 -070038 # supported operators for queries
Dennis Kempin253ee052013-07-01 14:58:22 -070039 search_operators = {
40 ">": lambda a, b: float(a) > float(b),
41 ">=": lambda a, b: float(a) >= float(b),
42 "<": lambda a, b: float(a) < float(b),
43 "<=": lambda a, b: float(a) <= float(b),
44 "=": lambda a, b: str(a) == str(b),
45 "==": lambda a, b: str(a) == str(b),
46 "!=": lambda a, b: str(a) != str(b)
47 }
48
Dennis Kempind5b59022013-07-17 14:12:55 -070049 def __init__(self, query=None, capture_logs=False):
50 self.query = query
51 self.capture_logs = capture_logs
52 # parse search query
53 if self.query:
54 # example: Key >= 32
55 query_regex = re.compile('\s*(\w+)\s*([<>=!]+)\s*([0-9a-zA-Z]+)\s*')
56 match = query_regex.match(query)
57 if not match:
58 raise ValueError(query, " is not a valid query")
59 self.key = match.group(1)
60 self.op = match.group(2)
61 self.value = match.group(3)
Dennis Kempin253ee052013-07-01 14:58:22 -070062
Dennis Kempind5b59022013-07-17 14:12:55 -070063 def FindMatches(self, replay_results, filename):
64 # find MTStat lines in log
65 lines = replay_results.gestures_log.splitlines()
66 lines = filter(lambda line: MTStatQuery.MatchLine(line), lines)
67 all_matches = map(lambda line: MTStatMatch(line, filename), lines)
68
69 # samples are denoted by the : operator, all of them are returned.
70 samples = filter(lambda match: match.operator == ":", all_matches)
71
72 # updates are denoted by the = operator and force only the last
73 # update to be returned.
74 updates = filter(lambda match: match.operator == "=", all_matches)
75 last_updates = dict([(update.key, update) for update in updates])
76
77 matches = samples + last_updates.values()
78
79 # filter by search query if requested
80 if self.query:
81 matches = filter(self.Match, matches)
82 return matches
83
84 def Match(self, match):
85 op = MTStatQuery.search_operators[self.op]
86 return (match.key == self.key and
87 op(match.value, self.value))
88
89
90class MTStatMatch(QueryMatch):
91 def __init__(self, line, file):
92 match = MTStatQuery.MatchLine(line)
93 QueryMatch.__init__(self, file, float(match.timestamp), line)
94 self.key = match.key
95 self.operator = match.operator
96 self.value = match.value
97
98
99class RegexQuery(Query):
100 """ Searches the raw gestures log with a regular expression """
101 def __init__(self, query):
102 self.regex = re.compile(query)
103 self.capture_logs = True
104
105 def FindMatches(self, replay_results, filename):
106 lines = replay_results.gestures_log.splitlines()
107 lines = filter(lambda line: self.regex.match(line), lines)
108 matches = map(lambda line: QueryMatch(filename, None, line), lines)
109 return matches
110
111
112class GesturesQuery(Query):
113 """ Searches for gestures with matching type """
114 def __init__(self, type, capture_logs):
115 self.type = type
116 self.capture_logs = capture_logs
117
118 def FindMatches(self, replay_results, filename):
119 gestures = replay_results.gestures.gestures
120 if self.type:
121 gestures = filter(lambda g: g.type == self.type, gestures)
122 return map(lambda g: GesturesMatch(filename, g), gestures)
123
124
125class GesturesMatch(QueryMatch):
126 def __init__(self, filename, gesture):
127 QueryMatch.__init__(self, filename, gesture.start, str(gesture))
128 self.gesture = gesture
129
130
131class GesturesDiffQuery(GesturesQuery):
132 """ Compare gestures to 'original' QueryResult and search for changes """
133 def __init__(self, type, original):
134 self.type = type
135 self.original = original
136 self.capture_logs = True
137
138 def FindMatches(self, replay_results, filename):
139 self_matches = GesturesQuery.FindMatches(self, replay_results, filename)
140 original_matches = self.original.matches
141
142 size = min(len(original_matches), len(self_matches))
143 matches = []
144 for i in range(size):
145 if str(self_matches[i].gesture) != str(original_matches[i].gesture):
146 match = GesturesDiffMatch(filename, self_matches[i].gesture,
147 original_matches[i].gesture)
148 matches.append(match)
149 return matches
150
151
152class GesturesDiffMatch(GesturesMatch):
153 def __init__(self, filename, new, original):
154 GesturesMatch.__init__(self, filename, new)
155 self.original = original
156
157 def __str__(self):
158 return "%f: %s != %s" % (self.timestamp, str(self.gesture),
159 str(self.original))
160
161
162class MTStat(object):
Dennis Kempin7432eb02014-03-18 13:41:41 -0700163 def SearchChanges(self, type=None, number=None, platform=None, parallel=True):
Dennis Kempind5b59022013-07-17 14:12:55 -0700164 """ Search for changed gestures.
165
166 This command will compare the gestures output of the HEAD version of
167 the gestures library with the local version.
168 Optionally the type of gestures to look at can be specified by 'type'
Dennis Kempin253ee052013-07-01 14:58:22 -0700169 """
Dennis Kempind5b59022013-07-17 14:12:55 -0700170 engine = QueryEngine()
Dennis Kempin7432eb02014-03-18 13:41:41 -0700171 files = engine.SelectFiles(number, platform)
Dennis Kempin253ee052013-07-01 14:58:22 -0700172
Dennis Kempind5b59022013-07-17 14:12:55 -0700173 MTReplay().Recompile(head=True)
174 gestures_query = GesturesQuery(type, False)
175 original_results = engine.Execute(files, gestures_query, parallel=parallel)
Dennis Kempin253ee052013-07-01 14:58:22 -0700176
Dennis Kempin253ee052013-07-01 14:58:22 -0700177 MTReplay().Recompile()
Dennis Kempind5b59022013-07-17 14:12:55 -0700178 diff_queries =dict([
179 (file, GesturesDiffQuery(type, original_results[file]))
180 for file in files if file in original_results])
181 return engine.Execute(files, diff_queries, parallel=parallel)
Dennis Kempin253ee052013-07-01 14:58:22 -0700182
Dennis Kempind5b59022013-07-17 14:12:55 -0700183 def Search(self, search=None, gestures=None, regex=None,
Dennis Kempin7432eb02014-03-18 13:41:41 -0700184 number=None, platform=None, parallel=True):
Dennis Kempin253ee052013-07-01 14:58:22 -0700185 """ Search for occurences of a specific tag or regex.
186
Dennis Kempind5b59022013-07-17 14:12:55 -0700187 Specify either a 'search' or a 'regex'. Search queries are formatted
Dennis Kempin253ee052013-07-01 14:58:22 -0700188 in a simple "key operator value" format. For example:
189 "MyKey > 5" will return all matches where MyKey has a value
190 of greater than 5.
191 Supported operators are: >, <, >=, <=, =, !=
192
193 number: optional number of random reports to use
194 parallel: use parallel processing
195
196 returns a dictionary of lists containing the matches
197 for each file.
198 """
Dennis Kempind5b59022013-07-17 14:12:55 -0700199 engine = QueryEngine()
Dennis Kempin7432eb02014-03-18 13:41:41 -0700200 files = engine.SelectFiles(number, platform)
Dennis Kempin253ee052013-07-01 14:58:22 -0700201
Dennis Kempind5b59022013-07-17 14:12:55 -0700202 if search:
203 query = MTStatQuery(search, True)
204 elif regex:
205 query = RegexQuery(regex)
206 elif gestures:
207 query = GesturesQuery(gestures, True)
208 else:
209 return None
Dennis Kempin253ee052013-07-01 14:58:22 -0700210
Dennis Kempind5b59022013-07-17 14:12:55 -0700211 return engine.Execute(files, query, parallel=parallel)
Dennis Kempin253ee052013-07-01 14:58:22 -0700212
Dennis Kempin7432eb02014-03-18 13:41:41 -0700213 def GatherStats(self, number=None, platform=None, parallel=True, num_bins=10):
Dennis Kempin253ee052013-07-01 14:58:22 -0700214 """ Gathers stats on feedback reports.
215
216 Returns a dictionary with a histogram for each recorded key.
217 """
Dennis Kempind5b59022013-07-17 14:12:55 -0700218 engine = QueryEngine()
Dennis Kempin7432eb02014-03-18 13:41:41 -0700219 files = engine.SelectFiles(number, platform)
Dennis Kempind5b59022013-07-17 14:12:55 -0700220 results = engine.Execute(files, MTStatQuery(), parallel=parallel)
Dennis Kempin253ee052013-07-01 14:58:22 -0700221
222 # gather values for each key in a list
Dennis Kempind5b59022013-07-17 14:12:55 -0700223 all_matches = chain(*[result.matches for result in results.values()])
224 value_collection = defaultdict(list)
225 for match in all_matches:
226 value_collection[match.key].append(match.value)
Dennis Kempin253ee052013-07-01 14:58:22 -0700227
228 # build histograms
229 histograms = {}
230 for key, values in value_collection.items():
231 histograms[key] = self._Histogram(values, num_bins)
Dennis Kempind5b59022013-07-17 14:12:55 -0700232 return OrderedDict(sorted(histograms.items(), key=lambda t: t[0]))
Dennis Kempin253ee052013-07-01 14:58:22 -0700233
Sean O'Brienfd204da2017-05-02 15:13:11 -0700234 def Download(self, num, parallel=True):
235 QueryEngine().Download(num, parallel)
Dennis Kempin5457a752013-07-25 09:20:03 -0700236
Dennis Kempin253ee052013-07-01 14:58:22 -0700237 def _Histogram(self, values, num_bins):
238 def RawHistogram(values):
Dennis Kempind5b59022013-07-17 14:12:55 -0700239 return OrderedDict(Counter(values))
Dennis Kempin253ee052013-07-01 14:58:22 -0700240
241 # convert all items to integers.
242 integers = []
243 for value in values:
244 try:
245 integers.append(int(value))
246 except:
247 # not an integer.
248 return RawHistogram(values)
249
250 # don't condense lists that are already small enough
251 if len(set(integers)) <= num_bins:
252 return RawHistogram(integers)
253
254 # all integer values, use bins for histogram
Dennis Kempind5b59022013-07-17 14:12:55 -0700255 histogram = OrderedDict()
Dennis Kempin253ee052013-07-01 14:58:22 -0700256 integers = sorted(integers)
257
258 # calculate bin size (append one at the end to include last value)
259 begin = integers[0]
260 end = integers[-1] + 1
261 bin_size = float(end - begin) / float(num_bins)
262
263 # remove each bins integers from the list and count them.
264 for i in range(num_bins):
265 high_v = round((i + 1) * bin_size) + begin
266
267 filtered = filter(lambda i: i >= high_v, integers)
268 histogram["<%d" % high_v] = len(integers) - len(filtered)
269 integers = filtered
270 return histogram