1 # 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 5 import json 6 import logging 7 import os 8 import StringIO 9 10 from autotest_lib.client.common_lib import error, utils 11 from autotest_lib.client.common_lib.cros import dev_server 12 13 14 TELEMETRY_RUN_BENCHMARKS_SCRIPT = 'tools/perf/run_benchmark' 15 TELEMETRY_RUN_TESTS_SCRIPT = 'tools/telemetry/run_tests' 16 TELEMETRY_RUN_GPU_TESTS_SCRIPT = 'content/test/gpu/run_gpu_integration_test.py' 17 TELEMETRY_TIMEOUT_MINS = 150 18 19 DUT_CHROME_ROOT = '/usr/local/telemetry/src' 20 21 # Result Statuses 22 SUCCESS_STATUS = 'SUCCESS' 23 WARNING_STATUS = 'WARNING' 24 FAILED_STATUS = 'FAILED' 25 26 # A list of telemetry tests that cannot run on dut. 27 ON_DUT_BLACKLIST = [ 28 'loading.desktop', # crbug/882299 29 'rendering.desktop', # crbug/882291 30 'system_health.memory_desktop', # crbug/874386 31 ] 32 33 class TelemetryResult(object): 34 """Class to represent the results of a telemetry run. 35 36 This class represents the results of a telemetry run, whether it ran 37 successful, failed or had warnings. 38 """ 39 40 41 def __init__(self, exit_code=0, stdout='', stderr=''): 42 """Initializes this TelemetryResultObject instance. 43 44 @param status: Status of the telemtry run. 45 @param stdout: Stdout of the telemetry run. 46 @param stderr: Stderr of the telemetry run. 47 """ 48 if exit_code == 0: 49 self.status = SUCCESS_STATUS 50 else: 51 self.status = FAILED_STATUS 52 53 self._stdout = stdout 54 self._stderr = stderr 55 self.output = '\n'.join([stdout, stderr]) 56 57 58 class TelemetryRunner(object): 59 """Class responsible for telemetry for a given build. 60 61 This class will extract and install telemetry on the devserver and is 62 responsible for executing the telemetry benchmarks and returning their 63 output to the caller. 64 """ 65 66 def __init__(self, host, local=False, telemetry_on_dut=True): 67 """Initializes this telemetry runner instance. 68 69 If telemetry is not installed for this build, it will be. 70 71 Basically, the following commands on the local pc on which test_that 72 will be executed, depending on the 4 possible combinations of 73 local x telemetry_on_dut: 74 75 local=True, telemetry_on_dut=False: 76 run_benchmark --browser=cros-chrome --remote=[dut] [test] 77 78 local=True, telemetry_on_dut=True: 79 ssh [dut] run_benchmark --browser=system [test] 80 81 local=False, telemetry_on_dut=False: 82 ssh [devserver] run_benchmark --browser=cros-chrome --remote=[dut] [test] 83 84 local=False, telemetry_on_dut=True: 85 ssh [devserver] ssh [dut] run_benchmark --browser=system [test] 86 87 @param host: Host where the test will be run. 88 @param local: If set, no devserver will be used, test will be run 89 locally. 90 If not set, "ssh [devserver] " will be appended to test 91 commands. 92 @param telemetry_on_dut: If set, telemetry itself (the test harness) 93 will run on dut. 94 It decides browser=[system|cros-chrome] 95 """ 96 self._host = host 97 self._devserver = None 98 self._telemetry_path = None 99 self._telemetry_on_dut = telemetry_on_dut 100 # TODO (llozano crbug.com/324964). Remove conditional code. 101 # Use a class hierarchy instead. 102 if local: 103 self._setup_local_telemetry() 104 else: 105 self._setup_devserver_telemetry() 106 107 logging.debug('Telemetry Path: %s', self._telemetry_path) 108 109 110 def _setup_devserver_telemetry(self): 111 """Setup Telemetry to use the devserver.""" 112 logging.debug('Setting up telemetry for devserver testing') 113 logging.debug('Grabbing build from AFE.') 114 info = self._host.host_info_store.get() 115 if not info.build: 116 logging.error('Unable to locate build label for host: %s.', 117 self._host.host_port) 118 raise error.AutotestError('Failed to grab build for host %s.' % 119 self._host.host_port) 120 121 logging.debug('Setting up telemetry for build: %s', info.build) 122 123 self._devserver = dev_server.ImageServer.resolve( 124 info.build, hostname=self._host.hostname) 125 self._devserver.stage_artifacts(info.build, ['autotest_packages']) 126 self._telemetry_path = self._devserver.setup_telemetry(build=info.build) 127 128 129 def _setup_local_telemetry(self): 130 """Setup Telemetry to use local path to its sources. 131 132 First look for chrome source root, either externally mounted, or inside 133 the chroot. Prefer chrome-src-internal source tree to chrome-src. 134 """ 135 TELEMETRY_DIR = 'src' 136 CHROME_LOCAL_SRC = '/var/cache/chromeos-cache/distfiles/target/' 137 CHROME_EXTERNAL_SRC = os.path.expanduser('~/chrome_root/') 138 139 logging.debug('Setting up telemetry for local testing') 140 141 sources_list = ('chrome-src-internal', 'chrome-src') 142 dir_list = [CHROME_EXTERNAL_SRC] 143 dir_list.extend( 144 [os.path.join(CHROME_LOCAL_SRC, x) for x in sources_list]) 145 if 'CHROME_ROOT' in os.environ: 146 dir_list.insert(0, os.environ['CHROME_ROOT']) 147 148 telemetry_src = '' 149 for dir in dir_list: 150 if os.path.exists(dir): 151 telemetry_src = os.path.join(dir, TELEMETRY_DIR) 152 break 153 else: 154 raise error.TestError('Telemetry source directory not found.') 155 156 self._devserver = None 157 self._telemetry_path = telemetry_src 158 159 160 def _get_telemetry_cmd(self, script, test_or_benchmark, *args): 161 """Build command to execute telemetry based on script and benchmark. 162 163 @param script: Telemetry script we want to run. For example: 164 [path_to_telemetry_src]/src/tools/telemetry/run_tests. 165 @param test_or_benchmark: Name of the test or benchmark we want to run, 166 with the page_set (if required) as part of 167 the string. 168 @param args: additional list of arguments to pass to the script. 169 170 @returns Full telemetry command to execute the script. 171 """ 172 telemetry_cmd = [] 173 if self._devserver: 174 devserver_hostname = self._devserver.hostname 175 telemetry_cmd.extend(['ssh', devserver_hostname]) 176 177 if self._telemetry_on_dut: 178 telemetry_cmd.extend( 179 [self._host.ssh_command(alive_interval=900, 180 connection_attempts=4), 181 'python', 182 script, 183 '--verbose', 184 '--output-format=chartjson', 185 '--output-dir=%s' % DUT_CHROME_ROOT, 186 '--browser=system']) 187 else: 188 telemetry_cmd.extend( 189 ['python', 190 script, 191 '--verbose', 192 '--browser=cros-chrome', 193 '--output-format=chartjson', 194 '--output-dir=%s' % self._telemetry_path, 195 '--remote=%s' % self._host.host_port]) 196 telemetry_cmd.extend(args) 197 telemetry_cmd.append(test_or_benchmark) 198 199 return ' '.join(telemetry_cmd) 200 201 202 def _scp_telemetry_results_cmd(self, perf_results_dir): 203 """Build command to copy the telemetry results from the devserver. 204 205 @param perf_results_dir: directory path where test output is to be 206 collected. 207 @returns SCP command to copy the results json to the specified directory. 208 """ 209 if not perf_results_dir: 210 return '' 211 212 scp_cmd = ['scp'] 213 if self._telemetry_on_dut: 214 scp_cmd.append(self._host.make_ssh_options(alive_interval=900, 215 connection_attempts=4)) 216 if not self._host.is_default_port: 217 scp_cmd.append('-P %d' % self._host.port) 218 src = 'root@%s:%s/results-chart.json' % (self._host.hostname, 219 DUT_CHROME_ROOT) 220 else: 221 devserver_hostname = '' 222 if self._devserver: 223 devserver_hostname = self._devserver.hostname + ':' 224 src = '%s%s/results-chart.json' % (devserver_hostname, 225 self._telemetry_path) 226 227 scp_cmd.extend([src, perf_results_dir]) 228 return ' '.join(scp_cmd) 229 230 231 def _run_cmd(self, cmd): 232 """Execute an command in a external shell and capture the output. 233 234 @param cmd: String of is a valid shell command. 235 236 @returns The standard out, standard error and the integer exit code of 237 the executed command. 238 """ 239 logging.debug('Running: %s', cmd) 240 241 output = StringIO.StringIO() 242 error_output = StringIO.StringIO() 243 exit_code = 0 244 try: 245 result = utils.run(cmd, stdout_tee=output, 246 stderr_tee=error_output, 247 timeout=TELEMETRY_TIMEOUT_MINS*60) 248 exit_code = result.exit_status 249 except error.CmdError as e: 250 logging.debug('Error occurred executing.') 251 exit_code = e.result_obj.exit_status 252 253 stdout = output.getvalue() 254 stderr = error_output.getvalue() 255 logging.debug('Completed with exit code: %d.\nstdout:%s\n' 256 'stderr:%s', exit_code, stdout, stderr) 257 return stdout, stderr, exit_code 258 259 260 def _run_telemetry(self, script, test_or_benchmark, *args): 261 """Runs telemetry on a dut. 262 263 @param script: Telemetry script we want to run. For example: 264 [path_to_telemetry_src]/src/tools/telemetry/run_tests. 265 @param test_or_benchmark: Name of the test or benchmark we want to run, 266 with the page_set (if required) as part of the 267 string. 268 @param args: additional list of arguments to pass to the script. 269 270 @returns A TelemetryResult Instance with the results of this telemetry 271 execution. 272 """ 273 # TODO (sbasi crbug.com/239933) add support for incognito mode. 274 275 telemetry_cmd = self._get_telemetry_cmd(script, 276 test_or_benchmark, 277 *args) 278 logging.debug('Running Telemetry: %s', telemetry_cmd) 279 280 stdout, stderr, exit_code = self._run_cmd(telemetry_cmd) 281 282 return TelemetryResult(exit_code=exit_code, stdout=stdout, 283 stderr=stderr) 284 285 286 def _run_scp(self, perf_results_dir): 287 """Runs telemetry on a dut. 288 289 @param perf_results_dir: The local directory that results are being 290 collected. 291 """ 292 scp_cmd = self._scp_telemetry_results_cmd(perf_results_dir) 293 logging.debug('Retrieving Results: %s', scp_cmd) 294 _, _, exit_code = self._run_cmd(scp_cmd) 295 if exit_code != 0: 296 raise error.TestFail('Unable to retrieve results.') 297 298 299 def _run_test(self, script, test, *args): 300 """Runs a telemetry test on a dut. 301 302 @param script: Which telemetry test script we want to run. Can be 303 telemetry's base test script or the Chrome OS specific 304 test script. 305 @param test: Telemetry test we want to run. 306 @param args: additional list of arguments to pass to the script. 307 308 @returns A TelemetryResult Instance with the results of this telemetry 309 execution. 310 """ 311 logging.debug('Running telemetry test: %s', test) 312 telemetry_script = os.path.join(self._telemetry_path, script) 313 result = self._run_telemetry(telemetry_script, test, *args) 314 if result.status is FAILED_STATUS: 315 raise error.TestFail('Telemetry test %s failed.' % test) 316 return result 317 318 319 def run_telemetry_test(self, test, *args): 320 """Runs a telemetry test on a dut. 321 322 @param test: Telemetry test we want to run. 323 @param args: additional list of arguments to pass to the telemetry 324 execution script. 325 326 @returns A TelemetryResult Instance with the results of this telemetry 327 execution. 328 """ 329 return self._run_test(TELEMETRY_RUN_TESTS_SCRIPT, test, *args) 330 331 332 def run_telemetry_benchmark(self, benchmark, perf_value_writer=None, 333 *args): 334 """Runs a telemetry benchmark on a dut. 335 336 @param benchmark: Benchmark we want to run. 337 @param perf_value_writer: Should be an instance with the function 338 output_perf_value(), if None, no perf value 339 will be written. Typically this will be the 340 job object from an autotest test. 341 @param args: additional list of arguments to pass to the telemetry 342 execution script. 343 344 @returns A TelemetryResult Instance with the results of this telemetry 345 execution. 346 """ 347 logging.debug('Running telemetry benchmark: %s', benchmark) 348 349 if benchmark in ON_DUT_BLACKLIST: 350 self._telemetry_on_dut = False 351 352 if self._telemetry_on_dut: 353 telemetry_script = os.path.join(DUT_CHROME_ROOT, 354 TELEMETRY_RUN_BENCHMARKS_SCRIPT) 355 self._ensure_deps(self._host, benchmark) 356 else: 357 telemetry_script = os.path.join(self._telemetry_path, 358 TELEMETRY_RUN_BENCHMARKS_SCRIPT) 359 360 result = self._run_telemetry(telemetry_script, benchmark, *args) 361 362 if result.status is WARNING_STATUS: 363 raise error.TestWarn('Telemetry Benchmark: %s' 364 ' exited with Warnings.' % benchmark) 365 if result.status is FAILED_STATUS: 366 raise error.TestFail('Telemetry Benchmark: %s' 367 ' failed to run.' % benchmark) 368 if perf_value_writer: 369 self._run_scp(perf_value_writer.resultsdir) 370 return result 371 372 373 def run_gpu_integration_test(self, test, *args): 374 """Runs a gpu test on a dut. 375 376 @param test: Gpu test we want to run. 377 @param args: additional list of arguments to pass to the telemetry 378 execution script. 379 380 @returns A TelemetryResult instance with the results of this telemetry 381 execution. 382 """ 383 script = os.path.join(DUT_CHROME_ROOT, 384 TELEMETRY_RUN_GPU_TESTS_SCRIPT) 385 cmd = [] 386 if self._devserver: 387 devserver_hostname = self._devserver.hostname 388 cmd.extend(['ssh', devserver_hostname]) 389 390 cmd.extend( 391 [self._host.ssh_command(alive_interval=900, connection_attempts=4), 392 'python', script]) 393 cmd.extend(args) 394 cmd.append(test) 395 cmd = ' '.join(cmd) 396 stdout, stderr, exit_code = self._run_cmd(cmd) 397 398 if exit_code: 399 raise error.TestFail('Gpu Integration Test: %s' 400 ' failed to run.' % test) 401 402 return TelemetryResult(exit_code=exit_code, stdout=stdout, 403 stderr=stderr) 404 405 406 def _ensure_deps(self, dut, test_name): 407 """ 408 Ensure the dependencies are locally available on DUT. 409 410 @param dut: The autotest host object representing DUT. 411 @param test_name: Name of the telemetry test. 412 """ 413 # Get DEPs using host's telemetry. 414 # Example output, fetch_benchmark_deps.py --output-deps=deps octane: 415 # {'octane': ['tools/perf/page_sets/data/octane_002.wprgo']} 416 perf_path = os.path.join(self._telemetry_path, 'tools', 'perf') 417 deps_path = os.path.join(perf_path, 'fetch_benchmark_deps_result.json') 418 fetch_path = os.path.join(perf_path, 'fetch_benchmark_deps.py') 419 format_fetch = ('python %s --output-deps=%s %s') 420 command_fetch = format_fetch % (fetch_path, deps_path, test_name) 421 command_get = 'cat %s' % deps_path 422 423 if self._devserver: 424 devserver_hostname = self._devserver.url().split( 425 'http://')[1].split(':')[0] 426 command_fetch = 'ssh %s %s' % (devserver_hostname, command_fetch) 427 command_get = 'ssh %s %s' % (devserver_hostname, command_get) 428 429 logging.info('Getting DEPs: %s', command_fetch) 430 _, _, exit_code = self._run_cmd(command_fetch) 431 if exit_code != 0: 432 raise error.TestFail('Error occurred while fetching DEPs.') 433 stdout, _, exit_code = self._run_cmd(command_get) 434 if exit_code != 0: 435 raise error.TestFail('Error occurred while getting DEPs.') 436 437 # Download DEPs to DUT. 438 # send_file() relies on rsync over ssh. Couldn't be better. 439 deps = json.loads(stdout) 440 for dep in deps[test_name]: 441 src = os.path.join(self._telemetry_path, dep) 442 dst = os.path.join(DUT_CHROME_ROOT, dep) 443 if self._devserver: 444 logging.info('Copying: %s -> %s', src, dst) 445 rsync_cmd = utils.sh_escape('rsync %s %s %s:%s' % 446 (self._host.rsync_options(), src, 447 self._host.hostname, dst)) 448 utils.run('ssh %s "%s"' % (devserver_hostname, rsync_cmd)) 449 else: 450 if not os.path.isfile(src): 451 raise error.TestFail('Error occurred while saving DEPs.') 452 logging.info('Copying: %s -> %s', src, dst) 453 dut.send_file(src, dst) 454