blob: e16e8262d8787181b784fecc817c70ae473edc71 [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
Simran Basiaaf9ab22013-07-02 12:30:21 -070016TELEMETRY_RUN_BENCHMARKS_SCRIPT = 'tools/perf/run_measurement'
Simran Basi833814b2013-01-29 13:13:43 -080017TELEMETRY_RUN_TESTS_SCRIPT = 'tools/telemetry/run_tests'
Simran Basi1dbfc132013-05-02 10:11:02 -070018TELEMETRY_RUN_CROS_TESTS_SCRIPT = 'chrome/test/telemetry/run_cros_tests'
Simran Basiee9e8602013-03-19 11:52:18 -070019TELEMETRY_TIMEOUT_MINS = 60
Simran Basi833814b2013-01-29 13:13:43 -080020
21# Result Statuses
22SUCCESS_STATUS = 'SUCCESS'
23WARNING_STATUS = 'WARNING'
24FAILED_STATUS = 'FAILED'
25
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070026# Regex for the RESULT output lines understood by chrome buildbot.
27# Keep in sync with chromium/tools/build/scripts/slave/process_log_utils.py.
28RESULTS_REGEX = re.compile(r'(?P<IMPORTANT>\*)?RESULT '
29 '(?P<GRAPH>[^:]*): (?P<TRACE>[^=]*)= '
30 '(?P<VALUE>[\{\[]?[-\d\., ]+[\}\]]?)('
31 ' ?(?P<UNITS>.+))?')
32
33# Constants pertaining to perf keys generated from Telemetry test results.
34PERF_KEY_TELEMETRY_PREFIX = 'TELEMETRY'
35PERF_KEY_DELIMITER = '--'
36
Simran Basi833814b2013-01-29 13:13:43 -080037
38class TelemetryResult(object):
39 """Class to represent the results of a telemetry run.
40
41 This class represents the results of a telemetry run, whether it ran
42 successful, failed or had warnings.
43 """
44
45
46 def __init__(self, exit_code=0, stdout='', stderr=''):
47 """Initializes this TelemetryResultObject instance.
48
49 @param status: Status of the telemtry run.
50 @param stdout: Stdout of the telemetry run.
51 @param stderr: Stderr of the telemetry run.
52 """
53 if exit_code == 0:
54 self.status = SUCCESS_STATUS
55 else:
56 self.status = FAILED_STATUS
57
58 self.perf_keyvals = {}
59 self._stdout = stdout
60 self._stderr = stderr
61 self.output = '\n'.join([stdout, stderr])
62
63
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070064 def _cleanup_perf_string(self, str):
65 """Clean up a perf-related string by removing illegal characters.
Simran Basi833814b2013-01-29 13:13:43 -080066
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070067 Perf keys stored in the chromeOS database may contain only letters,
68 numbers, underscores, periods, and dashes. Transform an inputted
69 string so that any illegal characters are replaced by underscores.
70
71 @param str: The perf string to clean up.
72
73 @return The cleaned-up perf string.
74 """
75 return re.sub(r'[^\w.-]', '_', str)
76
77
78 def _cleanup_units_string(self, units):
79 """Cleanup a units string.
80
81 Given a string representing units for a perf measurement, clean it up
82 by replacing certain illegal characters with meaningful alternatives.
83 Any other illegal characters should then be replaced with underscores.
Simran Basi833814b2013-01-29 13:13:43 -080084
85 Examples:
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070086 count/time -> count_per_time
87 % -> percent
88 units! --> units_
89 score (bigger is better) -> score__bigger_is_better_
90 score (runs/s) -> score__runs_per_s_
Simran Basi833814b2013-01-29 13:13:43 -080091
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070092 @param units: The units string to clean up.
Simran Basi833814b2013-01-29 13:13:43 -080093
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070094 @return The cleaned-up units string.
Simran Basi833814b2013-01-29 13:13:43 -080095 """
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070096 if '%' in units:
97 units = units.replace('%', 'percent')
Simran Basi833814b2013-01-29 13:13:43 -080098 if '/' in units:
99 units = units.replace('/','_per_')
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700100 return self._cleanup_perf_string(units)
Simran Basi833814b2013-01-29 13:13:43 -0800101
102
103 def parse_benchmark_results(self):
104 """Parse the results of a telemetry benchmark run.
105
106 Stdout has the format of CSV at the top and then the output repeated
107 in RESULT block format below.
108
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700109 The lines of interest start with the substring "RESULT". These are
110 specially-formatted perf data lines that are interpreted by chrome
111 builbot (when the Telemetry tests run for chrome desktop) and are
112 parsed to extract perf data that can then be displayed on a perf
113 dashboard. This format is documented in the docstring of class
114 GraphingLogProcessor in this file in the chrome tree:
Simran Basi833814b2013-01-29 13:13:43 -0800115
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700116 chromium/tools/build/scripts/slave/process_log_utils.py
Simran Basi833814b2013-01-29 13:13:43 -0800117
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700118 Example RESULT output lines:
119 RESULT average_commit_time_by_url: http___www.ebay.com= 8.86528 ms
120 RESULT CodeLoad: CodeLoad= 6343 score (bigger is better)
121 RESULT ai-astar: ai-astar= [614,527,523,471,530,523,577,625,614,538] ms
122
123 Currently for chromeOS, we can only associate a single perf key (string)
124 with a perf value. That string can only contain letters, numbers,
125 dashes, periods, and underscores, as defined by write_keyval() in:
126
127 chromeos/src/third_party/autotest/files/client/common_lib/
128 base_utils.py
129
130 We therefore parse each RESULT line, clean up the strings to remove any
131 illegal characters not accepted by chromeOS, and construct a perf key
132 string based on the parsed components of the RESULT line (with each
133 component separated by a special delimiter). We prefix the perf key
134 with the substring "TELEMETRY" to identify it as a telemetry-formatted
135 perf key.
Simran Basi833814b2013-01-29 13:13:43 -0800136
137 Stderr has the format of Warnings/Tracebacks. There is always a default
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700138 warning of the display enviornment setting, followed by warnings of
Simran Basi833814b2013-01-29 13:13:43 -0800139 page timeouts or a traceback.
140
141 If there are any other warnings we flag the test as warning. If there
142 is a traceback we consider this test a failure.
Simran Basi833814b2013-01-29 13:13:43 -0800143 """
Simran Basi833814b2013-01-29 13:13:43 -0800144 if not self._stdout:
145 # Nothing in stdout implies a test failure.
146 logging.error('No stdout, test failed.')
147 self.status = FAILED_STATUS
148 return
149
150 stdout_lines = self._stdout.splitlines()
Simran Basi833814b2013-01-29 13:13:43 -0800151 for line in stdout_lines:
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700152 results_match = RESULTS_REGEX.search(line)
153 if not results_match:
Simran Basi833814b2013-01-29 13:13:43 -0800154 continue
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700155
156 match_dict = results_match.groupdict()
157 graph_name = self._cleanup_perf_string(match_dict['GRAPH'].strip())
158 trace_name = self._cleanup_perf_string(match_dict['TRACE'].strip())
159 units = self._cleanup_units_string(
160 (match_dict['UNITS'] or 'units').strip())
161 value = match_dict['VALUE'].strip()
162 unused_important = match_dict['IMPORTANT'] or False # Unused now.
163
164 if value.startswith('['):
165 # A list of values, e.g., "[12,15,8,7,16]". Extract just the
166 # numbers, compute the average and use that. In this example,
167 # we'd get 12+15+8+7+16 / 5 --> 11.6.
168 value_list = [float(x) for x in value.strip('[],').split(',')]
169 value = float(sum(value_list)) / len(value_list)
170 elif value.startswith('{'):
171 # A single value along with a standard deviation, e.g.,
172 # "{34.2,2.15}". Extract just the value itself and use that.
173 # In this example, we'd get 34.2.
174 value_list = [float(x) for x in value.strip('{},').split(',')]
175 value = value_list[0] # Position 0 is the value.
176
177 perf_key = PERF_KEY_DELIMITER.join(
178 [PERF_KEY_TELEMETRY_PREFIX, graph_name, trace_name, units])
179 self.perf_keyvals[perf_key] = str(value)
180
181 pp = pprint.PrettyPrinter(indent=2)
182 logging.debug('Perf Keyvals: %s', pp.pformat(self.perf_keyvals))
Simran Basi833814b2013-01-29 13:13:43 -0800183
184 if self.status is SUCCESS_STATUS:
185 return
186
187 # Otherwise check if simply a Warning occurred or a Failure,
188 # i.e. a Traceback is listed.
189 self.status = WARNING_STATUS
190 for line in self._stderr.splitlines():
191 if line.startswith('Traceback'):
192 self.status = FAILED_STATUS
193
194
195class TelemetryRunner(object):
196 """Class responsible for telemetry for a given build.
197
198 This class will extract and install telemetry on the devserver and is
199 responsible for executing the telemetry benchmarks and returning their
200 output to the caller.
201 """
202
203 def __init__(self, host):
204 """Initializes this telemetry runner instance.
205
206 If telemetry is not installed for this build, it will be.
207 """
208 self._host = host
209 logging.debug('Grabbing build from AFE.')
210
211 build = host.get_build()
212 if not build:
213 logging.error('Unable to locate build label for host: %s.',
214 self._host.hostname)
215 raise error.AutotestError('Failed to grab build for host %s.' %
216 self._host.hostname)
217
218 logging.debug('Setting up telemetry for build: %s', build)
219
220 self._devserver = dev_server.ImageServer.resolve(build)
221 self._telemetry_path = self._devserver.setup_telemetry(build=build)
222 logging.debug('Telemetry Path: %s',self._telemetry_path)
223
224
225 def _run_telemetry(self, script, test_or_benchmark):
226 """Runs telemetry on a dut.
227
228 @param script: Telemetry script we want to run. For example:
229 [path_to_telemetry_src]/src/tools/telemetry/run_tests
230 @param test_or_benchmark: Name of the test or benchmark we want to run,
231 with the page_set (if required) as part of the
232 string.
233
234 @returns A TelemetryResult Instance with the results of this telemetry
235 execution.
236 """
237 devserver_hostname = self._devserver.url().split(
238 'http://')[1].split(':')[0]
Simran Basi1dbfc132013-05-02 10:11:02 -0700239 # TODO (sbasi crbug.com/239933) add support for incognito mode.
Simran Basi833814b2013-01-29 13:13:43 -0800240 telemetry_args = ['ssh',
241 devserver_hostname,
242 'python',
243 script,
244 '--browser=cros-chrome',
245 '--remote=%s' % self._host.hostname,
246 test_or_benchmark]
247
248 logging.debug('Running Telemetry: %s', ' '.join(telemetry_args))
249 output = StringIO.StringIO()
250 error_output = StringIO.StringIO()
251 exit_code = 0
252 try:
253 result = utils.run(' '.join(telemetry_args), stdout_tee=output,
254 stderr_tee=error_output,
255 timeout=TELEMETRY_TIMEOUT_MINS*60)
256 exit_code = result.exit_status
257 except error.CmdError as e:
258 # Telemetry returned a return code of not 0; for benchmarks this
259 # can be due to a timeout on one of the pages of the page set and
260 # we may still have data on the rest. For a test however this
261 # indicates failure.
262 logging.debug('Error occurred executing telemetry.')
263 exit_code = e.result_obj.exit_status
264
265 stdout = output.getvalue()
266 stderr = error_output.getvalue()
267 logging.debug('Telemetry completed with exit code: %d.\nstdout:%s\n'
268 'stderr:%s', exit_code, stdout, stderr)
269
270 return TelemetryResult(exit_code=exit_code, stdout=stdout,
271 stderr=stderr)
272
273
Simran Basi1dbfc132013-05-02 10:11:02 -0700274 def _run_test(self, script, test):
275 """Runs a telemetry test on a dut.
276
277 @param script: Which telemetry test script we want to run. Can be
278 telemetry's base test script or the Chrome OS specific
279 test script.
280 @param test: Telemetry test we want to run.
281
282 @returns A TelemetryResult Instance with the results of this telemetry
283 execution.
284 """
285 logging.debug('Running telemetry test: %s', test)
286 telemetry_script = os.path.join(self._telemetry_path, script)
287 result = self._run_telemetry(telemetry_script, test)
288 if result.status is FAILED_STATUS:
289 raise error.TestFail('Telemetry test: %s failed.',
290 test)
291 return result
292
293
Simran Basi833814b2013-01-29 13:13:43 -0800294 def run_telemetry_test(self, test):
295 """Runs a telemetry test on a dut.
296
297 @param test: Telemetry test we want to run.
298
299 @returns A TelemetryResult Instance with the results of this telemetry
300 execution.
301 """
Simran Basi1dbfc132013-05-02 10:11:02 -0700302 return self._run_test(TELEMETRY_RUN_TESTS_SCRIPT, test)
303
304
305 def run_cros_telemetry_test(self, test):
306 """Runs a cros specific telemetry test on a dut.
307
308 @param test: Telemetry test we want to run.
309
310 @returns A TelemetryResult instance with the results of this telemetry
311 execution.
312 """
313 return self._run_test(TELEMETRY_RUN_CROS_TESTS_SCRIPT, test)
Simran Basi833814b2013-01-29 13:13:43 -0800314
315
316 def run_telemetry_benchmark(self, benchmark, page_set, keyval_writer=None):
317 """Runs a telemetry benchmark on a dut.
318
319 @param benchmark: Benchmark we want to run.
320 @param page_set: Page set we want to use.
321 @param keyval_writer: Should be a instance with the function
322 write_perf_keyval(), if None, no keyvals will be
323 written. Typically this will be the job object
324 from a autotest test.
325
326 @returns A TelemetryResult Instance with the results of this telemetry
327 execution.
328 """
329 logging.debug('Running telemetry benchmark: %s with page set: %s.',
330 benchmark, page_set)
331 telemetry_script = os.path.join(self._telemetry_path,
332 TELEMETRY_RUN_BENCHMARKS_SCRIPT)
333 page_set_path = os.path.join(self._telemetry_path,
334 'tools/perf/page_sets/%s' % page_set)
335 benchmark_with_pageset = ' '.join([benchmark, page_set_path])
336 result = self._run_telemetry(telemetry_script, benchmark_with_pageset)
337 result.parse_benchmark_results()
338
339 if keyval_writer:
340 keyval_writer.write_perf_keyval(result.perf_keyvals)
341
342 if result.status is WARNING_STATUS:
343 raise error.TestWarn('Telemetry Benchmark: %s with page set: %s'
344 ' exited with Warnings.' % (benchmark,
345 page_set))
346 if result.status is FAILED_STATUS:
347 raise error.TestFail('Telemetry Benchmark: %s with page set: %s'
348 ' failed to run.' % (benchmark,
349 page_set))
350
351 return result