blob: 93527154732c56956376dde959094363886aadce [file] [log] [blame]
Simran Basi833814b2013-01-29 13:13:43 -08001# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import logging
6import os
Dennis Jeffreyc42fd302013-04-17 11:57:51 -07007import pprint
Simran Basi833814b2013-01-29 13:13:43 -08008import re
9import StringIO
10
11import common
12from autotest_lib.client.common_lib import error, utils
13from autotest_lib.client.common_lib.cros import dev_server
14
15
16TELEMETRY_RUN_BENCHMARKS_SCRIPT = 'tools/perf/run_multipage_benchmarks'
17TELEMETRY_RUN_TESTS_SCRIPT = 'tools/telemetry/run_tests'
Simran Basiee9e8602013-03-19 11:52:18 -070018TELEMETRY_TIMEOUT_MINS = 60
Simran Basi833814b2013-01-29 13:13:43 -080019
20# Result Statuses
21SUCCESS_STATUS = 'SUCCESS'
22WARNING_STATUS = 'WARNING'
23FAILED_STATUS = 'FAILED'
24
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070025# Regex for the RESULT output lines understood by chrome buildbot.
26# Keep in sync with chromium/tools/build/scripts/slave/process_log_utils.py.
27RESULTS_REGEX = re.compile(r'(?P<IMPORTANT>\*)?RESULT '
28 '(?P<GRAPH>[^:]*): (?P<TRACE>[^=]*)= '
29 '(?P<VALUE>[\{\[]?[-\d\., ]+[\}\]]?)('
30 ' ?(?P<UNITS>.+))?')
31
32# Constants pertaining to perf keys generated from Telemetry test results.
33PERF_KEY_TELEMETRY_PREFIX = 'TELEMETRY'
34PERF_KEY_DELIMITER = '--'
35
Simran Basi833814b2013-01-29 13:13:43 -080036
37class TelemetryResult(object):
38 """Class to represent the results of a telemetry run.
39
40 This class represents the results of a telemetry run, whether it ran
41 successful, failed or had warnings.
42 """
43
44
45 def __init__(self, exit_code=0, stdout='', stderr=''):
46 """Initializes this TelemetryResultObject instance.
47
48 @param status: Status of the telemtry run.
49 @param stdout: Stdout of the telemetry run.
50 @param stderr: Stderr of the telemetry run.
51 """
52 if exit_code == 0:
53 self.status = SUCCESS_STATUS
54 else:
55 self.status = FAILED_STATUS
56
57 self.perf_keyvals = {}
58 self._stdout = stdout
59 self._stderr = stderr
60 self.output = '\n'.join([stdout, stderr])
61
62
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070063 def _cleanup_perf_string(self, str):
64 """Clean up a perf-related string by removing illegal characters.
Simran Basi833814b2013-01-29 13:13:43 -080065
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070066 Perf keys stored in the chromeOS database may contain only letters,
67 numbers, underscores, periods, and dashes. Transform an inputted
68 string so that any illegal characters are replaced by underscores.
69
70 @param str: The perf string to clean up.
71
72 @return The cleaned-up perf string.
73 """
74 return re.sub(r'[^\w.-]', '_', str)
75
76
77 def _cleanup_units_string(self, units):
78 """Cleanup a units string.
79
80 Given a string representing units for a perf measurement, clean it up
81 by replacing certain illegal characters with meaningful alternatives.
82 Any other illegal characters should then be replaced with underscores.
Simran Basi833814b2013-01-29 13:13:43 -080083
84 Examples:
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070085 count/time -> count_per_time
86 % -> percent
87 units! --> units_
88 score (bigger is better) -> score__bigger_is_better_
89 score (runs/s) -> score__runs_per_s_
Simran Basi833814b2013-01-29 13:13:43 -080090
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070091 @param units: The units string to clean up.
Simran Basi833814b2013-01-29 13:13:43 -080092
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070093 @return The cleaned-up units string.
Simran Basi833814b2013-01-29 13:13:43 -080094 """
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070095 if '%' in units:
96 units = units.replace('%', 'percent')
Simran Basi833814b2013-01-29 13:13:43 -080097 if '/' in units:
98 units = units.replace('/','_per_')
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070099 return self._cleanup_perf_string(units)
Simran Basi833814b2013-01-29 13:13:43 -0800100
101
102 def parse_benchmark_results(self):
103 """Parse the results of a telemetry benchmark run.
104
105 Stdout has the format of CSV at the top and then the output repeated
106 in RESULT block format below.
107
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700108 The lines of interest start with the substring "RESULT". These are
109 specially-formatted perf data lines that are interpreted by chrome
110 builbot (when the Telemetry tests run for chrome desktop) and are
111 parsed to extract perf data that can then be displayed on a perf
112 dashboard. This format is documented in the docstring of class
113 GraphingLogProcessor in this file in the chrome tree:
Simran Basi833814b2013-01-29 13:13:43 -0800114
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700115 chromium/tools/build/scripts/slave/process_log_utils.py
Simran Basi833814b2013-01-29 13:13:43 -0800116
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700117 Example RESULT output lines:
118 RESULT average_commit_time_by_url: http___www.ebay.com= 8.86528 ms
119 RESULT CodeLoad: CodeLoad= 6343 score (bigger is better)
120 RESULT ai-astar: ai-astar= [614,527,523,471,530,523,577,625,614,538] ms
121
122 Currently for chromeOS, we can only associate a single perf key (string)
123 with a perf value. That string can only contain letters, numbers,
124 dashes, periods, and underscores, as defined by write_keyval() in:
125
126 chromeos/src/third_party/autotest/files/client/common_lib/
127 base_utils.py
128
129 We therefore parse each RESULT line, clean up the strings to remove any
130 illegal characters not accepted by chromeOS, and construct a perf key
131 string based on the parsed components of the RESULT line (with each
132 component separated by a special delimiter). We prefix the perf key
133 with the substring "TELEMETRY" to identify it as a telemetry-formatted
134 perf key.
Simran Basi833814b2013-01-29 13:13:43 -0800135
136 Stderr has the format of Warnings/Tracebacks. There is always a default
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700137 warning of the display enviornment setting, followed by warnings of
Simran Basi833814b2013-01-29 13:13:43 -0800138 page timeouts or a traceback.
139
140 If there are any other warnings we flag the test as warning. If there
141 is a traceback we consider this test a failure.
Simran Basi833814b2013-01-29 13:13:43 -0800142 """
Simran Basi833814b2013-01-29 13:13:43 -0800143 if not self._stdout:
144 # Nothing in stdout implies a test failure.
145 logging.error('No stdout, test failed.')
146 self.status = FAILED_STATUS
147 return
148
149 stdout_lines = self._stdout.splitlines()
Simran Basi833814b2013-01-29 13:13:43 -0800150 for line in stdout_lines:
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700151 results_match = RESULTS_REGEX.search(line)
152 if not results_match:
Simran Basi833814b2013-01-29 13:13:43 -0800153 continue
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700154
155 match_dict = results_match.groupdict()
156 graph_name = self._cleanup_perf_string(match_dict['GRAPH'].strip())
157 trace_name = self._cleanup_perf_string(match_dict['TRACE'].strip())
158 units = self._cleanup_units_string(
159 (match_dict['UNITS'] or 'units').strip())
160 value = match_dict['VALUE'].strip()
161 unused_important = match_dict['IMPORTANT'] or False # Unused now.
162
163 if value.startswith('['):
164 # A list of values, e.g., "[12,15,8,7,16]". Extract just the
165 # numbers, compute the average and use that. In this example,
166 # we'd get 12+15+8+7+16 / 5 --> 11.6.
167 value_list = [float(x) for x in value.strip('[],').split(',')]
168 value = float(sum(value_list)) / len(value_list)
169 elif value.startswith('{'):
170 # A single value along with a standard deviation, e.g.,
171 # "{34.2,2.15}". Extract just the value itself and use that.
172 # In this example, we'd get 34.2.
173 value_list = [float(x) for x in value.strip('{},').split(',')]
174 value = value_list[0] # Position 0 is the value.
175
176 perf_key = PERF_KEY_DELIMITER.join(
177 [PERF_KEY_TELEMETRY_PREFIX, graph_name, trace_name, units])
178 self.perf_keyvals[perf_key] = str(value)
179
180 pp = pprint.PrettyPrinter(indent=2)
181 logging.debug('Perf Keyvals: %s', pp.pformat(self.perf_keyvals))
Simran Basi833814b2013-01-29 13:13:43 -0800182
183 if self.status is SUCCESS_STATUS:
184 return
185
186 # Otherwise check if simply a Warning occurred or a Failure,
187 # i.e. a Traceback is listed.
188 self.status = WARNING_STATUS
189 for line in self._stderr.splitlines():
190 if line.startswith('Traceback'):
191 self.status = FAILED_STATUS
192
193
194class TelemetryRunner(object):
195 """Class responsible for telemetry for a given build.
196
197 This class will extract and install telemetry on the devserver and is
198 responsible for executing the telemetry benchmarks and returning their
199 output to the caller.
200 """
201
202 def __init__(self, host):
203 """Initializes this telemetry runner instance.
204
205 If telemetry is not installed for this build, it will be.
206 """
207 self._host = host
208 logging.debug('Grabbing build from AFE.')
209
210 build = host.get_build()
211 if not build:
212 logging.error('Unable to locate build label for host: %s.',
213 self._host.hostname)
214 raise error.AutotestError('Failed to grab build for host %s.' %
215 self._host.hostname)
216
217 logging.debug('Setting up telemetry for build: %s', build)
218
219 self._devserver = dev_server.ImageServer.resolve(build)
220 self._telemetry_path = self._devserver.setup_telemetry(build=build)
221 logging.debug('Telemetry Path: %s',self._telemetry_path)
222
223
224 def _run_telemetry(self, script, test_or_benchmark):
225 """Runs telemetry on a dut.
226
227 @param script: Telemetry script we want to run. For example:
228 [path_to_telemetry_src]/src/tools/telemetry/run_tests
229 @param test_or_benchmark: Name of the test or benchmark we want to run,
230 with the page_set (if required) as part of the
231 string.
232
233 @returns A TelemetryResult Instance with the results of this telemetry
234 execution.
235 """
236 devserver_hostname = self._devserver.url().split(
237 'http://')[1].split(':')[0]
238 telemetry_args = ['ssh',
239 devserver_hostname,
240 'python',
241 script,
242 '--browser=cros-chrome',
243 '--remote=%s' % self._host.hostname,
244 test_or_benchmark]
245
246 logging.debug('Running Telemetry: %s', ' '.join(telemetry_args))
247 output = StringIO.StringIO()
248 error_output = StringIO.StringIO()
249 exit_code = 0
250 try:
251 result = utils.run(' '.join(telemetry_args), stdout_tee=output,
252 stderr_tee=error_output,
253 timeout=TELEMETRY_TIMEOUT_MINS*60)
254 exit_code = result.exit_status
255 except error.CmdError as e:
256 # Telemetry returned a return code of not 0; for benchmarks this
257 # can be due to a timeout on one of the pages of the page set and
258 # we may still have data on the rest. For a test however this
259 # indicates failure.
260 logging.debug('Error occurred executing telemetry.')
261 exit_code = e.result_obj.exit_status
262
263 stdout = output.getvalue()
264 stderr = error_output.getvalue()
265 logging.debug('Telemetry completed with exit code: %d.\nstdout:%s\n'
266 'stderr:%s', exit_code, stdout, stderr)
267
268 return TelemetryResult(exit_code=exit_code, stdout=stdout,
269 stderr=stderr)
270
271
272 def run_telemetry_test(self, test):
273 """Runs a telemetry test on a dut.
274
275 @param test: Telemetry test we want to run.
276
277 @returns A TelemetryResult Instance with the results of this telemetry
278 execution.
279 """
280 logging.debug('Running telemetry test: %s', test)
281 telemetry_script = os.path.join(self._telemetry_path,
282 TELEMETRY_RUN_TESTS_SCRIPT)
283 result = self._run_telemetry(telemetry_script, test)
284 if result.status is FAILED_STATUS:
285 raise error.TestFail('Telemetry test: %s failed.',
286 test)
287 return result
288
289
290 def run_telemetry_benchmark(self, benchmark, page_set, keyval_writer=None):
291 """Runs a telemetry benchmark on a dut.
292
293 @param benchmark: Benchmark we want to run.
294 @param page_set: Page set we want to use.
295 @param keyval_writer: Should be a instance with the function
296 write_perf_keyval(), if None, no keyvals will be
297 written. Typically this will be the job object
298 from a autotest test.
299
300 @returns A TelemetryResult Instance with the results of this telemetry
301 execution.
302 """
303 logging.debug('Running telemetry benchmark: %s with page set: %s.',
304 benchmark, page_set)
305 telemetry_script = os.path.join(self._telemetry_path,
306 TELEMETRY_RUN_BENCHMARKS_SCRIPT)
307 page_set_path = os.path.join(self._telemetry_path,
308 'tools/perf/page_sets/%s' % page_set)
309 benchmark_with_pageset = ' '.join([benchmark, page_set_path])
310 result = self._run_telemetry(telemetry_script, benchmark_with_pageset)
311 result.parse_benchmark_results()
312
313 if keyval_writer:
314 keyval_writer.write_perf_keyval(result.perf_keyvals)
315
316 if result.status is WARNING_STATUS:
317 raise error.TestWarn('Telemetry Benchmark: %s with page set: %s'
318 ' exited with Warnings.' % (benchmark,
319 page_set))
320 if result.status is FAILED_STATUS:
321 raise error.TestFail('Telemetry Benchmark: %s with page set: %s'
322 ' failed to run.' % (benchmark,
323 page_set))
324
325 return result