blob: 747a2a3f024d51ef7edc67810ed991b1ba4f4923 [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
Simran Basi833814b2013-01-29 13:13:43 -080011from autotest_lib.client.common_lib import error, utils
12from autotest_lib.client.common_lib.cros import dev_server
13
14
Dave Tu6a404e62013-11-05 15:54:48 -080015TELEMETRY_RUN_BENCHMARKS_SCRIPT = 'tools/perf/run_benchmark'
Simran Basi1dbfc132013-05-02 10:11:02 -070016TELEMETRY_RUN_CROS_TESTS_SCRIPT = 'chrome/test/telemetry/run_cros_tests'
Ilja Friedelf2473802014-03-28 17:54:34 -070017TELEMETRY_RUN_GPU_TESTS_SCRIPT = 'content/test/gpu/run_gpu_test.py'
Ilja H. Friedel086bc3f2014-02-27 22:17:55 -080018TELEMETRY_RUN_TESTS_SCRIPT = 'tools/telemetry/run_tests'
Achuith Bhandarkar124e4732014-01-21 15:27:54 -080019TELEMETRY_TIMEOUT_MINS = 120
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
Simran Basi833814b2013-01-29 13:13:43 -080033
34class TelemetryResult(object):
35 """Class to represent the results of a telemetry run.
36
37 This class represents the results of a telemetry run, whether it ran
38 successful, failed or had warnings.
39 """
40
41
42 def __init__(self, exit_code=0, stdout='', stderr=''):
43 """Initializes this TelemetryResultObject instance.
44
45 @param status: Status of the telemtry run.
46 @param stdout: Stdout of the telemetry run.
47 @param stderr: Stderr of the telemetry run.
48 """
49 if exit_code == 0:
50 self.status = SUCCESS_STATUS
51 else:
52 self.status = FAILED_STATUS
53
Fang Denge689e712013-11-13 18:27:06 -080054 # A list of perf values, e.g.
55 # [{'graph': 'graphA', 'trace': 'page_load_time',
56 # 'units': 'secs', 'value':0.5}, ...]
57 self.perf_data = []
Simran Basi833814b2013-01-29 13:13:43 -080058 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
Dave Tu6a404e62013-11-05 15:54:48 -0800105 Stdout has the output in RESULT block format below.
Simran Basi833814b2013-01-29 13:13:43 -0800106
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700107 The lines of interest start with the substring "RESULT". These are
108 specially-formatted perf data lines that are interpreted by chrome
109 builbot (when the Telemetry tests run for chrome desktop) and are
110 parsed to extract perf data that can then be displayed on a perf
111 dashboard. This format is documented in the docstring of class
112 GraphingLogProcessor in this file in the chrome tree:
Simran Basi833814b2013-01-29 13:13:43 -0800113
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700114 chromium/tools/build/scripts/slave/process_log_utils.py
Simran Basi833814b2013-01-29 13:13:43 -0800115
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700116 Example RESULT output lines:
117 RESULT average_commit_time_by_url: http___www.ebay.com= 8.86528 ms
118 RESULT CodeLoad: CodeLoad= 6343 score (bigger is better)
119 RESULT ai-astar: ai-astar= [614,527,523,471,530,523,577,625,614,538] ms
120
121 Currently for chromeOS, we can only associate a single perf key (string)
122 with a perf value. That string can only contain letters, numbers,
123 dashes, periods, and underscores, as defined by write_keyval() in:
124
125 chromeos/src/third_party/autotest/files/client/common_lib/
126 base_utils.py
127
128 We therefore parse each RESULT line, clean up the strings to remove any
129 illegal characters not accepted by chromeOS, and construct a perf key
130 string based on the parsed components of the RESULT line (with each
131 component separated by a special delimiter). We prefix the perf key
132 with the substring "TELEMETRY" to identify it as a telemetry-formatted
133 perf key.
Simran Basi833814b2013-01-29 13:13:43 -0800134
135 Stderr has the format of Warnings/Tracebacks. There is always a default
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700136 warning of the display enviornment setting, followed by warnings of
Simran Basi833814b2013-01-29 13:13:43 -0800137 page timeouts or a traceback.
138
139 If there are any other warnings we flag the test as warning. If there
140 is a traceback we consider this test a failure.
Simran Basi833814b2013-01-29 13:13:43 -0800141 """
Simran Basi833814b2013-01-29 13:13:43 -0800142 if not self._stdout:
143 # Nothing in stdout implies a test failure.
144 logging.error('No stdout, test failed.')
145 self.status = FAILED_STATUS
146 return
147
148 stdout_lines = self._stdout.splitlines()
Simran Basi833814b2013-01-29 13:13:43 -0800149 for line in stdout_lines:
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700150 results_match = RESULTS_REGEX.search(line)
151 if not results_match:
Simran Basi833814b2013-01-29 13:13:43 -0800152 continue
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700153
154 match_dict = results_match.groupdict()
155 graph_name = self._cleanup_perf_string(match_dict['GRAPH'].strip())
156 trace_name = self._cleanup_perf_string(match_dict['TRACE'].strip())
157 units = self._cleanup_units_string(
158 (match_dict['UNITS'] or 'units').strip())
159 value = match_dict['VALUE'].strip()
160 unused_important = match_dict['IMPORTANT'] or False # Unused now.
161
162 if value.startswith('['):
163 # A list of values, e.g., "[12,15,8,7,16]". Extract just the
164 # numbers, compute the average and use that. In this example,
165 # we'd get 12+15+8+7+16 / 5 --> 11.6.
166 value_list = [float(x) for x in value.strip('[],').split(',')]
167 value = float(sum(value_list)) / len(value_list)
168 elif value.startswith('{'):
169 # A single value along with a standard deviation, e.g.,
170 # "{34.2,2.15}". Extract just the value itself and use that.
171 # In this example, we'd get 34.2.
172 value_list = [float(x) for x in value.strip('{},').split(',')]
173 value = value_list[0] # Position 0 is the value.
Fang Denge689e712013-11-13 18:27:06 -0800174 elif re.search('^\d+$', value):
175 value = int(value)
176 else:
177 value = float(value)
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700178
Fang Denge689e712013-11-13 18:27:06 -0800179 self.perf_data.append({'graph':graph_name, 'trace': trace_name,
180 'units': units, 'value': value})
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700181
182 pp = pprint.PrettyPrinter(indent=2)
Fang Denge689e712013-11-13 18:27:06 -0800183 logging.debug('Perf values: %s', pp.pformat(self.perf_data))
Simran Basi833814b2013-01-29 13:13:43 -0800184
185 if self.status is SUCCESS_STATUS:
186 return
187
188 # Otherwise check if simply a Warning occurred or a Failure,
189 # i.e. a Traceback is listed.
190 self.status = WARNING_STATUS
191 for line in self._stderr.splitlines():
192 if line.startswith('Traceback'):
193 self.status = FAILED_STATUS
194
195
196class TelemetryRunner(object):
197 """Class responsible for telemetry for a given build.
198
199 This class will extract and install telemetry on the devserver and is
200 responsible for executing the telemetry benchmarks and returning their
201 output to the caller.
202 """
203
Luis Lozano23ae3192013-11-08 16:22:46 -0800204 def __init__(self, host, local=False):
Simran Basi833814b2013-01-29 13:13:43 -0800205 """Initializes this telemetry runner instance.
206
207 If telemetry is not installed for this build, it will be.
Luis Lozano23ae3192013-11-08 16:22:46 -0800208
209 @param host: Host where the test will be run.
210 @param local: If set, no devserver will be used, test will be run
211 locally.
Simran Basi833814b2013-01-29 13:13:43 -0800212 """
213 self._host = host
Ilja H. Friedelc7bf3102014-05-13 17:31:25 -0700214 self._devserver = None
215 self._telemetry_path = None
Luis Lozano23ae3192013-11-08 16:22:46 -0800216 # TODO (llozano crbug.com/324964). Remove conditional code.
217 # Use a class hierarchy instead.
218 if local:
219 self._setup_local_telemetry()
220 else:
221 self._setup_devserver_telemetry()
222
223 logging.debug('Telemetry Path: %s', self._telemetry_path)
224
225
226 def _setup_devserver_telemetry(self):
227 """Setup Telemetry to use the devserver."""
228 logging.debug('Setting up telemetry for devserver testing')
Simran Basi833814b2013-01-29 13:13:43 -0800229 logging.debug('Grabbing build from AFE.')
230
Luis Lozano23ae3192013-11-08 16:22:46 -0800231 build = self._host.get_build()
Simran Basi833814b2013-01-29 13:13:43 -0800232 if not build:
233 logging.error('Unable to locate build label for host: %s.',
234 self._host.hostname)
235 raise error.AutotestError('Failed to grab build for host %s.' %
236 self._host.hostname)
237
238 logging.debug('Setting up telemetry for build: %s', build)
239
240 self._devserver = dev_server.ImageServer.resolve(build)
Simran Basicf81f682014-12-03 16:31:39 -0800241 self._devserver.stage_artifacts(build, ['autotest_packages'])
Simran Basi833814b2013-01-29 13:13:43 -0800242 self._telemetry_path = self._devserver.setup_telemetry(build=build)
Luis Lozano23ae3192013-11-08 16:22:46 -0800243
244
245 def _setup_local_telemetry(self):
246 """Setup Telemetry to use local path to its sources.
247
248 First look for chrome source root, either externally mounted, or inside
249 the chroot. Prefer chrome-src-internal source tree to chrome-src.
250 """
251 TELEMETRY_DIR = 'src'
252 CHROME_LOCAL_SRC = '/var/cache/chromeos-cache/distfiles/target/'
Josh Triplett05208c92014-07-17 13:21:29 -0700253 CHROME_EXTERNAL_SRC = os.path.expanduser('~/chrome_root/')
Luis Lozano23ae3192013-11-08 16:22:46 -0800254
255 logging.debug('Setting up telemetry for local testing')
256
257 sources_list = ('chrome-src-internal', 'chrome-src')
Josh Triplett05208c92014-07-17 13:21:29 -0700258 dir_list = [CHROME_EXTERNAL_SRC]
Luis Lozano23ae3192013-11-08 16:22:46 -0800259 dir_list.extend(
260 [os.path.join(CHROME_LOCAL_SRC, x) for x in sources_list])
261 if 'CHROME_ROOT' in os.environ:
262 dir_list.insert(0, os.environ['CHROME_ROOT'])
263
264 telemetry_src = ''
265 for dir in dir_list:
266 if os.path.exists(dir):
267 telemetry_src = os.path.join(dir, TELEMETRY_DIR)
268 break
269 else:
270 raise error.TestError('Telemetry source directory not found.')
271
272 self._devserver = None
273 self._telemetry_path = telemetry_src
274
275
276 def _get_telemetry_cmd(self, script, test_or_benchmark):
277 """Build command to execute telemetry based on script and benchmark.
278
279 @param script: Telemetry script we want to run. For example:
280 [path_to_telemetry_src]/src/tools/telemetry/run_tests.
281 @param test_or_benchmark: Name of the test or benchmark we want to run,
282 with the page_set (if required) as part of
283 the string.
284 @returns Full telemetry command to execute the script.
285 """
286 telemetry_cmd = []
287 if self._devserver:
288 devserver_hostname = self._devserver.url().split(
289 'http://')[1].split(':')[0]
290 telemetry_cmd.extend(['ssh', devserver_hostname])
291
292 telemetry_cmd.extend(
293 ['python',
294 script,
Ilja H. Friedel6965bd82014-05-20 18:29:15 -0700295 '--verbose',
Luis Lozano23ae3192013-11-08 16:22:46 -0800296 '--browser=cros-chrome',
297 '--remote=%s' % self._host.hostname,
298 test_or_benchmark])
299 return telemetry_cmd
Simran Basi833814b2013-01-29 13:13:43 -0800300
301
302 def _run_telemetry(self, script, test_or_benchmark):
303 """Runs telemetry on a dut.
304
305 @param script: Telemetry script we want to run. For example:
Luis Lozano23ae3192013-11-08 16:22:46 -0800306 [path_to_telemetry_src]/src/tools/telemetry/run_tests.
Simran Basi833814b2013-01-29 13:13:43 -0800307 @param test_or_benchmark: Name of the test or benchmark we want to run,
308 with the page_set (if required) as part of the
309 string.
310
311 @returns A TelemetryResult Instance with the results of this telemetry
312 execution.
313 """
Simran Basi1dbfc132013-05-02 10:11:02 -0700314 # TODO (sbasi crbug.com/239933) add support for incognito mode.
Simran Basi833814b2013-01-29 13:13:43 -0800315
Luis Lozano23ae3192013-11-08 16:22:46 -0800316 telemetry_cmd = self._get_telemetry_cmd(script, test_or_benchmark)
317 logging.debug('Running Telemetry: %s', ' '.join(telemetry_cmd))
318
Simran Basi833814b2013-01-29 13:13:43 -0800319 output = StringIO.StringIO()
320 error_output = StringIO.StringIO()
321 exit_code = 0
322 try:
Luis Lozano23ae3192013-11-08 16:22:46 -0800323 result = utils.run(' '.join(telemetry_cmd), stdout_tee=output,
Simran Basi833814b2013-01-29 13:13:43 -0800324 stderr_tee=error_output,
325 timeout=TELEMETRY_TIMEOUT_MINS*60)
326 exit_code = result.exit_status
327 except error.CmdError as e:
328 # Telemetry returned a return code of not 0; for benchmarks this
329 # can be due to a timeout on one of the pages of the page set and
330 # we may still have data on the rest. For a test however this
331 # indicates failure.
332 logging.debug('Error occurred executing telemetry.')
333 exit_code = e.result_obj.exit_status
334
335 stdout = output.getvalue()
336 stderr = error_output.getvalue()
337 logging.debug('Telemetry completed with exit code: %d.\nstdout:%s\n'
338 'stderr:%s', exit_code, stdout, stderr)
339
340 return TelemetryResult(exit_code=exit_code, stdout=stdout,
341 stderr=stderr)
342
343
Simran Basi1dbfc132013-05-02 10:11:02 -0700344 def _run_test(self, script, test):
345 """Runs a telemetry test on a dut.
346
347 @param script: Which telemetry test script we want to run. Can be
348 telemetry's base test script or the Chrome OS specific
349 test script.
350 @param test: Telemetry test we want to run.
351
352 @returns A TelemetryResult Instance with the results of this telemetry
353 execution.
354 """
355 logging.debug('Running telemetry test: %s', test)
356 telemetry_script = os.path.join(self._telemetry_path, script)
357 result = self._run_telemetry(telemetry_script, test)
358 if result.status is FAILED_STATUS:
Ilja H. Friedelc7bf3102014-05-13 17:31:25 -0700359 raise error.TestFail('Telemetry test %s failed.' % test)
Simran Basi1dbfc132013-05-02 10:11:02 -0700360 return result
361
362
Simran Basi833814b2013-01-29 13:13:43 -0800363 def run_telemetry_test(self, test):
364 """Runs a telemetry test on a dut.
365
366 @param test: Telemetry test we want to run.
367
368 @returns A TelemetryResult Instance with the results of this telemetry
369 execution.
370 """
Simran Basi1dbfc132013-05-02 10:11:02 -0700371 return self._run_test(TELEMETRY_RUN_TESTS_SCRIPT, test)
372
373
374 def run_cros_telemetry_test(self, test):
375 """Runs a cros specific telemetry test on a dut.
376
377 @param test: Telemetry test we want to run.
378
379 @returns A TelemetryResult instance with the results of this telemetry
380 execution.
381 """
382 return self._run_test(TELEMETRY_RUN_CROS_TESTS_SCRIPT, test)
Simran Basi833814b2013-01-29 13:13:43 -0800383
384
Ilja H. Friedel086bc3f2014-02-27 22:17:55 -0800385 def run_gpu_test(self, test):
386 """Runs a gpu test on a dut.
387
388 @param test: Gpu test we want to run.
389
390 @returns A TelemetryResult instance with the results of this telemetry
391 execution.
392 """
393 return self._run_test(TELEMETRY_RUN_GPU_TESTS_SCRIPT, test)
394
395
Fang Denge689e712013-11-13 18:27:06 -0800396 @staticmethod
397 def _output_perf_value(perf_value_writer, perf_data):
398 """Output perf values to result dir.
399
400 The perf values will be output to the result dir and
401 be subsequently uploaded to perf dashboard.
402
403 @param perf_value_writer: Should be an instance with the function
404 output_perf_value(), if None, no perf value
405 will be written. Typically this will be the
406 job object from an autotest test.
407 @param perf_data: A list of perf values, each value is
408 a dictionary that looks like
409 {'graph':'GraphA', 'trace':'metric1',
410 'units':'secs', 'value':0.5}
411 """
412 for perf_value in perf_data:
413 perf_value_writer.output_perf_value(
414 description=perf_value['trace'],
415 value=perf_value['value'],
416 units=perf_value['units'],
417 graph=perf_value['graph'])
418
419
420 def run_telemetry_benchmark(self, benchmark, perf_value_writer=None):
Simran Basi833814b2013-01-29 13:13:43 -0800421 """Runs a telemetry benchmark on a dut.
422
423 @param benchmark: Benchmark we want to run.
Fang Denge689e712013-11-13 18:27:06 -0800424 @param perf_value_writer: Should be an instance with the function
425 output_perf_value(), if None, no perf value
426 will be written. Typically this will be the
427 job object from an autotest test.
Simran Basi833814b2013-01-29 13:13:43 -0800428
429 @returns A TelemetryResult Instance with the results of this telemetry
430 execution.
431 """
Dave Tu6a404e62013-11-05 15:54:48 -0800432 logging.debug('Running telemetry benchmark: %s', benchmark)
Simran Basi833814b2013-01-29 13:13:43 -0800433 telemetry_script = os.path.join(self._telemetry_path,
434 TELEMETRY_RUN_BENCHMARKS_SCRIPT)
Dave Tu6a404e62013-11-05 15:54:48 -0800435 result = self._run_telemetry(telemetry_script, benchmark)
Simran Basi833814b2013-01-29 13:13:43 -0800436 result.parse_benchmark_results()
437
Fang Denge689e712013-11-13 18:27:06 -0800438 if perf_value_writer:
439 self._output_perf_value(perf_value_writer, result.perf_data)
Simran Basi833814b2013-01-29 13:13:43 -0800440
441 if result.status is WARNING_STATUS:
Dave Tu6a404e62013-11-05 15:54:48 -0800442 raise error.TestWarn('Telemetry Benchmark: %s'
443 ' exited with Warnings.' % benchmark)
Simran Basi833814b2013-01-29 13:13:43 -0800444 if result.status is FAILED_STATUS:
Dave Tu6a404e62013-11-05 15:54:48 -0800445 raise error.TestFail('Telemetry Benchmark: %s'
446 ' failed to run.' % benchmark)
Simran Basi833814b2013-01-29 13:13:43 -0800447
448 return result