blob: 1c26f57ac02ef15925430e6851a6c160fc688033 [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
Dave Tu6a404e62013-11-05 15:54:48 -080016TELEMETRY_RUN_BENCHMARKS_SCRIPT = 'tools/perf/run_benchmark'
Simran Basi1dbfc132013-05-02 10:11:02 -070017TELEMETRY_RUN_CROS_TESTS_SCRIPT = 'chrome/test/telemetry/run_cros_tests'
Ilja Friedelf2473802014-03-28 17:54:34 -070018TELEMETRY_RUN_GPU_TESTS_SCRIPT = 'content/test/gpu/run_gpu_test.py'
Ilja H. Friedel086bc3f2014-02-27 22:17:55 -080019TELEMETRY_RUN_TESTS_SCRIPT = 'tools/telemetry/run_tests'
Achuith Bhandarkar124e4732014-01-21 15:27:54 -080020TELEMETRY_TIMEOUT_MINS = 120
Simran Basi833814b2013-01-29 13:13:43 -080021
22# Result Statuses
23SUCCESS_STATUS = 'SUCCESS'
24WARNING_STATUS = 'WARNING'
25FAILED_STATUS = 'FAILED'
26
Dennis Jeffreyc42fd302013-04-17 11:57:51 -070027# Regex for the RESULT output lines understood by chrome buildbot.
28# Keep in sync with chromium/tools/build/scripts/slave/process_log_utils.py.
29RESULTS_REGEX = re.compile(r'(?P<IMPORTANT>\*)?RESULT '
30 '(?P<GRAPH>[^:]*): (?P<TRACE>[^=]*)= '
31 '(?P<VALUE>[\{\[]?[-\d\., ]+[\}\]]?)('
32 ' ?(?P<UNITS>.+))?')
33
Simran Basi833814b2013-01-29 13:13:43 -080034
35class TelemetryResult(object):
36 """Class to represent the results of a telemetry run.
37
38 This class represents the results of a telemetry run, whether it ran
39 successful, failed or had warnings.
40 """
41
42
43 def __init__(self, exit_code=0, stdout='', stderr=''):
44 """Initializes this TelemetryResultObject instance.
45
46 @param status: Status of the telemtry run.
47 @param stdout: Stdout of the telemetry run.
48 @param stderr: Stderr of the telemetry run.
49 """
50 if exit_code == 0:
51 self.status = SUCCESS_STATUS
52 else:
53 self.status = FAILED_STATUS
54
Fang Denge689e712013-11-13 18:27:06 -080055 # A list of perf values, e.g.
56 # [{'graph': 'graphA', 'trace': 'page_load_time',
57 # 'units': 'secs', 'value':0.5}, ...]
58 self.perf_data = []
Simran Basi833814b2013-01-29 13:13:43 -080059 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
Dave Tu6a404e62013-11-05 15:54:48 -0800106 Stdout has the output in RESULT block format below.
Simran Basi833814b2013-01-29 13:13:43 -0800107
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.
Fang Denge689e712013-11-13 18:27:06 -0800175 elif re.search('^\d+$', value):
176 value = int(value)
177 else:
178 value = float(value)
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700179
Fang Denge689e712013-11-13 18:27:06 -0800180 self.perf_data.append({'graph':graph_name, 'trace': trace_name,
181 'units': units, 'value': value})
Dennis Jeffreyc42fd302013-04-17 11:57:51 -0700182
183 pp = pprint.PrettyPrinter(indent=2)
Fang Denge689e712013-11-13 18:27:06 -0800184 logging.debug('Perf values: %s', pp.pformat(self.perf_data))
Simran Basi833814b2013-01-29 13:13:43 -0800185
186 if self.status is SUCCESS_STATUS:
187 return
188
189 # Otherwise check if simply a Warning occurred or a Failure,
190 # i.e. a Traceback is listed.
191 self.status = WARNING_STATUS
192 for line in self._stderr.splitlines():
193 if line.startswith('Traceback'):
194 self.status = FAILED_STATUS
195
196
197class TelemetryRunner(object):
198 """Class responsible for telemetry for a given build.
199
200 This class will extract and install telemetry on the devserver and is
201 responsible for executing the telemetry benchmarks and returning their
202 output to the caller.
203 """
204
Luis Lozano23ae3192013-11-08 16:22:46 -0800205 def __init__(self, host, local=False):
Simran Basi833814b2013-01-29 13:13:43 -0800206 """Initializes this telemetry runner instance.
207
208 If telemetry is not installed for this build, it will be.
Luis Lozano23ae3192013-11-08 16:22:46 -0800209
210 @param host: Host where the test will be run.
211 @param local: If set, no devserver will be used, test will be run
212 locally.
Simran Basi833814b2013-01-29 13:13:43 -0800213 """
214 self._host = host
Luis Lozano23ae3192013-11-08 16:22:46 -0800215
216 # 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)
241 self._telemetry_path = self._devserver.setup_telemetry(build=build)
Luis Lozano23ae3192013-11-08 16:22:46 -0800242
243
244 def _setup_local_telemetry(self):
245 """Setup Telemetry to use local path to its sources.
246
247 First look for chrome source root, either externally mounted, or inside
248 the chroot. Prefer chrome-src-internal source tree to chrome-src.
249 """
250 TELEMETRY_DIR = 'src'
251 CHROME_LOCAL_SRC = '/var/cache/chromeos-cache/distfiles/target/'
252 CHROME_EXTERNAL_SRC = '~/chrome_root/'
253
254 logging.debug('Setting up telemetry for local testing')
255
256 sources_list = ('chrome-src-internal', 'chrome-src')
257 dir_list = [os.path.join(CHROME_EXTERNAL_SRC, x) for x in sources_list]
258 dir_list.extend(
259 [os.path.join(CHROME_LOCAL_SRC, x) for x in sources_list])
260 if 'CHROME_ROOT' in os.environ:
261 dir_list.insert(0, os.environ['CHROME_ROOT'])
262
263 telemetry_src = ''
264 for dir in dir_list:
265 if os.path.exists(dir):
266 telemetry_src = os.path.join(dir, TELEMETRY_DIR)
267 break
268 else:
269 raise error.TestError('Telemetry source directory not found.')
270
271 self._devserver = None
272 self._telemetry_path = telemetry_src
273
274
275 def _get_telemetry_cmd(self, script, test_or_benchmark):
276 """Build command to execute telemetry based on script and benchmark.
277
278 @param script: Telemetry script we want to run. For example:
279 [path_to_telemetry_src]/src/tools/telemetry/run_tests.
280 @param test_or_benchmark: Name of the test or benchmark we want to run,
281 with the page_set (if required) as part of
282 the string.
283 @returns Full telemetry command to execute the script.
284 """
285 telemetry_cmd = []
286 if self._devserver:
287 devserver_hostname = self._devserver.url().split(
288 'http://')[1].split(':')[0]
289 telemetry_cmd.extend(['ssh', devserver_hostname])
290
291 telemetry_cmd.extend(
292 ['python',
293 script,
294 '--browser=cros-chrome',
295 '--remote=%s' % self._host.hostname,
296 test_or_benchmark])
297 return telemetry_cmd
Simran Basi833814b2013-01-29 13:13:43 -0800298
299
300 def _run_telemetry(self, script, test_or_benchmark):
301 """Runs telemetry on a dut.
302
303 @param script: Telemetry script we want to run. For example:
Luis Lozano23ae3192013-11-08 16:22:46 -0800304 [path_to_telemetry_src]/src/tools/telemetry/run_tests.
Simran Basi833814b2013-01-29 13:13:43 -0800305 @param test_or_benchmark: Name of the test or benchmark we want to run,
306 with the page_set (if required) as part of the
307 string.
308
309 @returns A TelemetryResult Instance with the results of this telemetry
310 execution.
311 """
Simran Basi1dbfc132013-05-02 10:11:02 -0700312 # TODO (sbasi crbug.com/239933) add support for incognito mode.
Simran Basi833814b2013-01-29 13:13:43 -0800313
Luis Lozano23ae3192013-11-08 16:22:46 -0800314 telemetry_cmd = self._get_telemetry_cmd(script, test_or_benchmark)
315 logging.debug('Running Telemetry: %s', ' '.join(telemetry_cmd))
316
Simran Basi833814b2013-01-29 13:13:43 -0800317 output = StringIO.StringIO()
318 error_output = StringIO.StringIO()
319 exit_code = 0
320 try:
Luis Lozano23ae3192013-11-08 16:22:46 -0800321 result = utils.run(' '.join(telemetry_cmd), stdout_tee=output,
Simran Basi833814b2013-01-29 13:13:43 -0800322 stderr_tee=error_output,
323 timeout=TELEMETRY_TIMEOUT_MINS*60)
324 exit_code = result.exit_status
325 except error.CmdError as e:
326 # Telemetry returned a return code of not 0; for benchmarks this
327 # can be due to a timeout on one of the pages of the page set and
328 # we may still have data on the rest. For a test however this
329 # indicates failure.
330 logging.debug('Error occurred executing telemetry.')
331 exit_code = e.result_obj.exit_status
332
333 stdout = output.getvalue()
334 stderr = error_output.getvalue()
335 logging.debug('Telemetry completed with exit code: %d.\nstdout:%s\n'
336 'stderr:%s', exit_code, stdout, stderr)
337
338 return TelemetryResult(exit_code=exit_code, stdout=stdout,
339 stderr=stderr)
340
341
Simran Basi1dbfc132013-05-02 10:11:02 -0700342 def _run_test(self, script, test):
343 """Runs a telemetry test on a dut.
344
345 @param script: Which telemetry test script we want to run. Can be
346 telemetry's base test script or the Chrome OS specific
347 test script.
348 @param test: Telemetry test we want to run.
349
350 @returns A TelemetryResult Instance with the results of this telemetry
351 execution.
352 """
353 logging.debug('Running telemetry test: %s', test)
354 telemetry_script = os.path.join(self._telemetry_path, script)
355 result = self._run_telemetry(telemetry_script, test)
356 if result.status is FAILED_STATUS:
357 raise error.TestFail('Telemetry test: %s failed.',
358 test)
359 return result
360
361
Simran Basi833814b2013-01-29 13:13:43 -0800362 def run_telemetry_test(self, test):
363 """Runs a telemetry test on a dut.
364
365 @param test: Telemetry test we want to run.
366
367 @returns A TelemetryResult Instance with the results of this telemetry
368 execution.
369 """
Simran Basi1dbfc132013-05-02 10:11:02 -0700370 return self._run_test(TELEMETRY_RUN_TESTS_SCRIPT, test)
371
372
373 def run_cros_telemetry_test(self, test):
374 """Runs a cros specific telemetry test on a dut.
375
376 @param test: Telemetry test we want to run.
377
378 @returns A TelemetryResult instance with the results of this telemetry
379 execution.
380 """
381 return self._run_test(TELEMETRY_RUN_CROS_TESTS_SCRIPT, test)
Simran Basi833814b2013-01-29 13:13:43 -0800382
383
Ilja H. Friedel086bc3f2014-02-27 22:17:55 -0800384 def run_gpu_test(self, test):
385 """Runs a gpu test on a dut.
386
387 @param test: Gpu test we want to run.
388
389 @returns A TelemetryResult instance with the results of this telemetry
390 execution.
391 """
392 return self._run_test(TELEMETRY_RUN_GPU_TESTS_SCRIPT, test)
393
394
Fang Denge689e712013-11-13 18:27:06 -0800395 @staticmethod
396 def _output_perf_value(perf_value_writer, perf_data):
397 """Output perf values to result dir.
398
399 The perf values will be output to the result dir and
400 be subsequently uploaded to perf dashboard.
401
402 @param perf_value_writer: Should be an instance with the function
403 output_perf_value(), if None, no perf value
404 will be written. Typically this will be the
405 job object from an autotest test.
406 @param perf_data: A list of perf values, each value is
407 a dictionary that looks like
408 {'graph':'GraphA', 'trace':'metric1',
409 'units':'secs', 'value':0.5}
410 """
411 for perf_value in perf_data:
412 perf_value_writer.output_perf_value(
413 description=perf_value['trace'],
414 value=perf_value['value'],
415 units=perf_value['units'],
416 graph=perf_value['graph'])
417
418
419 def run_telemetry_benchmark(self, benchmark, perf_value_writer=None):
Simran Basi833814b2013-01-29 13:13:43 -0800420 """Runs a telemetry benchmark on a dut.
421
422 @param benchmark: Benchmark we want to run.
Fang Denge689e712013-11-13 18:27:06 -0800423 @param perf_value_writer: Should be an instance with the function
424 output_perf_value(), if None, no perf value
425 will be written. Typically this will be the
426 job object from an autotest test.
Simran Basi833814b2013-01-29 13:13:43 -0800427
428 @returns A TelemetryResult Instance with the results of this telemetry
429 execution.
430 """
Dave Tu6a404e62013-11-05 15:54:48 -0800431 logging.debug('Running telemetry benchmark: %s', benchmark)
Simran Basi833814b2013-01-29 13:13:43 -0800432 telemetry_script = os.path.join(self._telemetry_path,
433 TELEMETRY_RUN_BENCHMARKS_SCRIPT)
Dave Tu6a404e62013-11-05 15:54:48 -0800434 result = self._run_telemetry(telemetry_script, benchmark)
Simran Basi833814b2013-01-29 13:13:43 -0800435 result.parse_benchmark_results()
436
Fang Denge689e712013-11-13 18:27:06 -0800437 if perf_value_writer:
438 self._output_perf_value(perf_value_writer, result.perf_data)
Simran Basi833814b2013-01-29 13:13:43 -0800439
440 if result.status is WARNING_STATUS:
Dave Tu6a404e62013-11-05 15:54:48 -0800441 raise error.TestWarn('Telemetry Benchmark: %s'
442 ' exited with Warnings.' % benchmark)
Simran Basi833814b2013-01-29 13:13:43 -0800443 if result.status is FAILED_STATUS:
Dave Tu6a404e62013-11-05 15:54:48 -0800444 raise error.TestFail('Telemetry Benchmark: %s'
445 ' failed to run.' % benchmark)
Simran Basi833814b2013-01-29 13:13:43 -0800446
447 return result