blob: bc7a7d32d8d22fea04771354869c705c4534b705 [file] [log] [blame]
Derek Beckett5fb683c2020-08-19 15:24:13 -07001# Lint as: python2, python3
Simran Basi833814b2013-01-29 13:13:43 -08002# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Derek Beckett5fb683c2020-08-19 15:24:13 -07006from __future__ import absolute_import
7from __future__ import division
8from __future__ import print_function
9
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -080010import abc
Kuo-Hsin Yang4a006172018-04-25 14:44:55 +080011import json
Simran Basi833814b2013-01-29 13:13:43 -080012import logging
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +080013import numbers
Simran Basi833814b2013-01-29 13:13:43 -080014import os
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -080015import random
Chinglin Yu0adaf112020-01-20 14:48:20 +080016import tempfile
Derek Beckett5fb683c2020-08-19 15:24:13 -070017import six
Simran Basi833814b2013-01-29 13:13:43 -080018
Zhizhou Yang65d37042020-01-08 17:44:12 -080019import numpy
20
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -080021import common
Simran Basi833814b2013-01-29 13:13:43 -080022from autotest_lib.client.common_lib import error, utils
23from autotest_lib.client.common_lib.cros import dev_server
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -080024from autotest_lib.server.cros import telemetry_setup
Simran Basi833814b2013-01-29 13:13:43 -080025
Dave Tu6a404e62013-11-05 15:54:48 -080026TELEMETRY_RUN_BENCHMARKS_SCRIPT = 'tools/perf/run_benchmark'
Ilja H. Friedel086bc3f2014-02-27 22:17:55 -080027TELEMETRY_RUN_TESTS_SCRIPT = 'tools/telemetry/run_tests'
Gurchetan Singhfaf75e92017-04-17 18:09:44 -070028TELEMETRY_RUN_GPU_TESTS_SCRIPT = 'content/test/gpu/run_gpu_integration_test.py'
Mao Huangc9642d72017-09-28 16:50:02 +080029TELEMETRY_TIMEOUT_MINS = 150
Simran Basi833814b2013-01-29 13:13:43 -080030
Ting-Yuan Huange5b19132016-03-22 13:02:41 +080031DUT_CHROME_ROOT = '/usr/local/telemetry/src'
32
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +080033CHART_JSON_RESULT = 'results-chart.json'
34HISTOGRAM_SET_RESULT = 'histograms.json'
Zhizhou Yang65d37042020-01-08 17:44:12 -080035PROFILE_ARTIFACTS = 'artifacts'
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +080036
Simran Basi833814b2013-01-29 13:13:43 -080037# Result Statuses
38SUCCESS_STATUS = 'SUCCESS'
39WARNING_STATUS = 'WARNING'
40FAILED_STATUS = 'FAILED'
41
Kuo-Hsin Yang07da7b62018-08-08 16:56:06 +080042# A list of telemetry tests that cannot run on dut.
Derek Beckett6b7cc0f2020-11-30 13:15:26 -080043ON_DUT_BLOCKLIST = [
Zhizhou Yang65d37042020-01-08 17:44:12 -080044 'loading.desktop', # crbug/882299
45 'rendering.desktop', # crbug/882291
Kuo-Hsin Yange0915472018-09-10 10:36:16 +080046]
Simran Basi833814b2013-01-29 13:13:43 -080047
Zhizhou Yang65d37042020-01-08 17:44:12 -080048
Simran Basi833814b2013-01-29 13:13:43 -080049class TelemetryResult(object):
50 """Class to represent the results of a telemetry run.
51
52 This class represents the results of a telemetry run, whether it ran
53 successful, failed or had warnings.
54 """
55
Simran Basi833814b2013-01-29 13:13:43 -080056 def __init__(self, exit_code=0, stdout='', stderr=''):
57 """Initializes this TelemetryResultObject instance.
58
59 @param status: Status of the telemtry run.
60 @param stdout: Stdout of the telemetry run.
61 @param stderr: Stderr of the telemetry run.
62 """
63 if exit_code == 0:
64 self.status = SUCCESS_STATUS
65 else:
66 self.status = FAILED_STATUS
67
Simran Basi833814b2013-01-29 13:13:43 -080068 self._stdout = stdout
69 self._stderr = stderr
70 self.output = '\n'.join([stdout, stderr])
71
72
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -080073class TelemetryRunnerFactory(object):
74 """A factory class to determine TelemetryRunner subclass to be used.
75
76 The TelemetryRunner class, today, has various ways to execute the telemetry
77 test. The test can be executed locally (using a tool like test_that) or can
78 be executed in the Lab environment - for this usecase, either the drone OR
79 the devserver can be used.
80
81 A Factory class offloads this determination overhead from the clients. Users
82 of the TelemetryRunner class are highly encouraged to go through this
83 Factory class while determining the correct TelemetryRunner subclass.
84 """
85
86 def get_runner(self, host, local=False, telemetry_on_dut=True):
87 """Method to determine which TelemetryRunner subclass to use."""
88 if local:
89 return LocalTelemetryRunner(host, telemetry_on_dut)
90 else:
91 # TODO(crbug.com/165407): Once all telemetry tests are being
92 # executed on the drone, deprecate DevserverTelemetryRunner class.
93 #
94 # In case of Lab tests, we want to move all telemetry test execution
95 # from the devserver to the drone. But a slow rollout is key, as we
96 # are not sure what kind of problems moving telemetry to drone will
97 # cause. The determiner is a random number between 1 to 100 and the
98 # cutoff essentially decides the "percentage" of tests that will
99 # executed on the drone. Every once in a while (may be once or twice
100 # a week), this cutoff value will be increased until it reaches 100
101 # i.e 100% of telemetry tests are executed on the drones and 0%
102 # tests are executed on the devserver. Once this has reached, the
103 # DevserverTelemetryRunner can be safely deprecated.
104 determiner = random.randint(1, 100)
105 cutoff = 25
106 if determiner <= cutoff:
107 return DroneTelemetryRunner(host, telemetry_on_dut)
108 else:
109 return DevserverTelemetryRunner(host, telemetry_on_dut)
110
111
112class TelemetryRunner(six.with_metaclass(abc.ABCMeta, object)):
Simran Basi833814b2013-01-29 13:13:43 -0800113 """Class responsible for telemetry for a given build.
114
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800115 This class will extract and install telemetry environment and is
Simran Basi833814b2013-01-29 13:13:43 -0800116 responsible for executing the telemetry benchmarks and returning their
117 output to the caller.
118 """
119
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800120 def __init__(self, host, telemetry_on_dut=True):
Simran Basi833814b2013-01-29 13:13:43 -0800121 """Initializes this telemetry runner instance.
122
123 If telemetry is not installed for this build, it will be.
Luis Lozano23ae3192013-11-08 16:22:46 -0800124
125 @param host: Host where the test will be run.
Ting-Yuan Huange5b19132016-03-22 13:02:41 +0800126 @param telemetry_on_dut: If set, telemetry itself (the test harness)
127 will run on dut.
128 It decides browser=[system|cros-chrome]
Simran Basi833814b2013-01-29 13:13:43 -0800129 """
130 self._host = host
Ilja H. Friedelc7bf3102014-05-13 17:31:25 -0700131 self._telemetry_path = None
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800132 self._devserver = None
Zhizhou Yang65d37042020-01-08 17:44:12 -0800133 self._perf_value_writer = None
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800134 self._setup_telemetry()
Ting-Yuan Huange5b19132016-03-22 13:02:41 +0800135 self._telemetry_on_dut = telemetry_on_dut
Chinglin Yu0adaf112020-01-20 14:48:20 +0800136 self._benchmark_deps = None
Luis Lozano23ae3192013-11-08 16:22:46 -0800137 logging.debug('Telemetry Path: %s', self._telemetry_path)
138
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800139 def __enter__(self):
140 """Called while entering context manager; does nothing."""
141 return self
Simran Basi833814b2013-01-29 13:13:43 -0800142
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800143 def __exit__(self, exc_type, exc_value, traceback):
144 """Called while exiting context manager."""
Simran Basi833814b2013-01-29 13:13:43 -0800145
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800146 @abc.abstractmethod
147 def _setup_telemetry(self):
148 """Set up telemetry environment."""
Luis Lozano23ae3192013-11-08 16:22:46 -0800149
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800150 def _get_telemetry_cmd(self, script, test_or_benchmark, output_format,
Zhizhou Yangf32fda02019-09-25 14:01:45 -0700151 *args, **kwargs):
Luis Lozano23ae3192013-11-08 16:22:46 -0800152 """Build command to execute telemetry based on script and benchmark.
153
154 @param script: Telemetry script we want to run. For example:
155 [path_to_telemetry_src]/src/tools/telemetry/run_tests.
156 @param test_or_benchmark: Name of the test or benchmark we want to run,
157 with the page_set (if required) as part of
158 the string.
Zhizhou Yang65d37042020-01-08 17:44:12 -0800159 @param output_format: Format of the json result file: histogram or
160 chart-json.
Luis Lozano814c7182015-09-08 11:20:47 -0700161 @param args: additional list of arguments to pass to the script.
Zhizhou Yangf32fda02019-09-25 14:01:45 -0700162 @param kwargs: additional list of keyword arguments to pass to the
163 script.
Luis Lozano814c7182015-09-08 11:20:47 -0700164
Luis Lozano23ae3192013-11-08 16:22:46 -0800165 @returns Full telemetry command to execute the script.
166 """
167 telemetry_cmd = []
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800168 # TODO(crbug.com/165407): All conditional blocks that are dependent on
169 # "self_devserver" must be deleted once all telemetry tests are
170 # migrated to the drone and DevserverTelemetryRunner is deprecated.
Luis Lozano23ae3192013-11-08 16:22:46 -0800171 if self._devserver:
Allen Lia5cfb972016-12-27 17:17:22 -0800172 devserver_hostname = self._devserver.hostname
Luis Lozano23ae3192013-11-08 16:22:46 -0800173 telemetry_cmd.extend(['ssh', devserver_hostname])
174
Zhizhou Yangf32fda02019-09-25 14:01:45 -0700175 no_verbose = kwargs.get('no_verbose', False)
176
Zhizhou Yang65d37042020-01-08 17:44:12 -0800177 output_dir = (DUT_CHROME_ROOT
178 if self._telemetry_on_dut else self._telemetry_path)
179 # Create a temp directory to hold single test run.
180 if self._perf_value_writer:
181 output_dir = os.path.join(
182 output_dir, self._perf_value_writer.tmpdir.strip('/'))
183
Ting-Yuan Huange5b19132016-03-22 13:02:41 +0800184 if self._telemetry_on_dut:
Denis Nikitinfdc2c712019-11-15 15:33:52 -0800185 telemetry_cmd.extend([
Zhizhou Yang65d37042020-01-08 17:44:12 -0800186 self._host.ssh_command(
187 alive_interval=900, connection_attempts=4),
188 'python2',
189 script,
190 '--output-format=%s' % output_format,
191 '--output-dir=%s' % output_dir,
192 '--browser=system',
Denis Nikitinfdc2c712019-11-15 15:33:52 -0800193 ])
Ting-Yuan Huange5b19132016-03-22 13:02:41 +0800194 else:
Denis Nikitinfdc2c712019-11-15 15:33:52 -0800195 telemetry_cmd.extend([
Zhizhou Yang65d37042020-01-08 17:44:12 -0800196 'python2',
197 script,
198 '--browser=cros-chrome',
199 '--output-format=%s' % output_format,
200 '--output-dir=%s' % output_dir,
Tiancong Wangbb13ed32020-03-25 16:50:28 -0700201 '--remote=%s' % self._host.hostname,
Denis Nikitinfdc2c712019-11-15 15:33:52 -0800202 ])
Tiancong Wangbb13ed32020-03-25 16:50:28 -0700203 if self._host.host_port != self._host.hostname:
204 # If the user specify a different port for the DUT, we should
205 # use different telemetry argument to set it up.
206 #
207 # e.g. When user is running experiments with ssh port
208 # forwarding, they specify remote as 127.0.0.1:2222. Now
209 # host_port is 127.0.0.1:2222 and hostname is 127.0.0.1
210 # port is 2222
211 telemetry_cmd.append('--remote-ssh-port=%s' % self._host.port)
212
Zhizhou Yangf32fda02019-09-25 14:01:45 -0700213 if not no_verbose:
214 telemetry_cmd.append('--verbose')
Luis Lozano814c7182015-09-08 11:20:47 -0700215 telemetry_cmd.extend(args)
216 telemetry_cmd.append(test_or_benchmark)
217
Keith Haddow1e5c7012016-03-09 16:05:37 -0800218 return ' '.join(telemetry_cmd)
219
Zhizhou Yang65d37042020-01-08 17:44:12 -0800220 def _scp_telemetry_results_cmd(self, perf_results_dir, output_format,
221 artifacts):
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800222 """Build command to copy the telemetry results from the work directory.
Keith Haddow1e5c7012016-03-09 16:05:37 -0800223
224 @param perf_results_dir: directory path where test output is to be
225 collected.
Zhizhou Yang65d37042020-01-08 17:44:12 -0800226 @param output_format: Format of the json result file: histogram or
227 chart-json.
228 @param artifacts: Whether we want to copy artifacts directory.
229
230 @returns SCP command to copy the results json to the specified
231 directory.
Keith Haddow1e5c7012016-03-09 16:05:37 -0800232 """
Dean Liaoe3e75f62017-11-14 10:36:43 +0800233 if not perf_results_dir:
234 return ''
235
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800236 output_filename = CHART_JSON_RESULT
237 if output_format == 'histograms':
238 output_filename = HISTOGRAM_SET_RESULT
Zhizhou Yang65d37042020-01-08 17:44:12 -0800239 scp_cmd = []
Dean Liaoe3e75f62017-11-14 10:36:43 +0800240 if self._telemetry_on_dut:
Zhizhou Yang65d37042020-01-08 17:44:12 -0800241 scp_cmd.extend(['scp', '-r'])
242 scp_cmd.append(
243 self._host.make_ssh_options(
244 alive_interval=900, connection_attempts=4))
Dean Liaoe3e75f62017-11-14 10:36:43 +0800245 if not self._host.is_default_port:
246 scp_cmd.append('-P %d' % self._host.port)
Zhizhou Yang65d37042020-01-08 17:44:12 -0800247 src = 'root@%s:%s' % (self._host.hostname, DUT_CHROME_ROOT)
Dean Liaoe3e75f62017-11-14 10:36:43 +0800248 else:
Zhizhou Yang65d37042020-01-08 17:44:12 -0800249 # Use rsync --remove-source-file to move rather than copy from
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800250 # work dir. This is because each run will generate certain artifacts
Zhizhou Yang65d37042020-01-08 17:44:12 -0800251 # and will not be removed after, making result size getting larger.
252 # We don't do this for results on DUT because 1) rsync doesn't work
253 # 2) DUT will be reflashed frequently and no need to worry about
254 # result size.
255 scp_cmd.extend(['rsync', '-avz', '--remove-source-files'])
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800256 # TODO(crbug.com/165407): All conditional blocks that are dependent
257 # on "self._devserver" must be deleted once all telemetry tests
258 # are migrated to the drone and DevserverTelemetryRunner is
259 # deprecated.
Dean Liaoe3e75f62017-11-14 10:36:43 +0800260 devserver_hostname = ''
Ricky Liangd186f3e2016-03-15 16:50:55 +0800261 if self._devserver:
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800262 devserver_hostname = self._devserver.hostname
263 devserver_hostname += ':'
Zhizhou Yang65d37042020-01-08 17:44:12 -0800264 src = '%s%s' % (devserver_hostname, self._telemetry_path)
Keith Haddow1e5c7012016-03-09 16:05:37 -0800265
Zhizhou Yang65d37042020-01-08 17:44:12 -0800266 if self._perf_value_writer:
267 src = os.path.join(src, self._perf_value_writer.tmpdir.strip('/'))
268
269 scp_cmd.append(os.path.join(src, output_filename))
270
271 # Copy artifacts back to result directory if needed.
272 if artifacts:
273 scp_cmd.append(os.path.join(src, PROFILE_ARTIFACTS))
274
275 scp_cmd.append(perf_results_dir)
Keith Haddow1e5c7012016-03-09 16:05:37 -0800276 return ' '.join(scp_cmd)
277
Keith Haddow1e5c7012016-03-09 16:05:37 -0800278 def _run_cmd(self, cmd):
279 """Execute an command in a external shell and capture the output.
280
281 @param cmd: String of is a valid shell command.
282
283 @returns The standard out, standard error and the integer exit code of
284 the executed command.
285 """
286 logging.debug('Running: %s', cmd)
287
Derek Beckett5fb683c2020-08-19 15:24:13 -0700288 output = six.StringIO()
289 error_output = six.StringIO()
Keith Haddow1e5c7012016-03-09 16:05:37 -0800290 exit_code = 0
291 try:
Zhizhou Yang65d37042020-01-08 17:44:12 -0800292 result = utils.run(
293 cmd,
294 stdout_tee=output,
295 stderr_tee=error_output,
296 timeout=TELEMETRY_TIMEOUT_MINS * 60)
Keith Haddow1e5c7012016-03-09 16:05:37 -0800297 exit_code = result.exit_status
298 except error.CmdError as e:
299 logging.debug('Error occurred executing.')
300 exit_code = e.result_obj.exit_status
301
302 stdout = output.getvalue()
303 stderr = error_output.getvalue()
304 logging.debug('Completed with exit code: %d.\nstdout:%s\n'
305 'stderr:%s', exit_code, stdout, stderr)
306 return stdout, stderr, exit_code
Simran Basi833814b2013-01-29 13:13:43 -0800307
Zhizhou Yang65d37042020-01-08 17:44:12 -0800308 def _run_telemetry(self, script, test_or_benchmark, output_format, *args,
309 **kwargs):
Simran Basi833814b2013-01-29 13:13:43 -0800310 """Runs telemetry on a dut.
311
312 @param script: Telemetry script we want to run. For example:
Luis Lozano23ae3192013-11-08 16:22:46 -0800313 [path_to_telemetry_src]/src/tools/telemetry/run_tests.
Simran Basi833814b2013-01-29 13:13:43 -0800314 @param test_or_benchmark: Name of the test or benchmark we want to run,
315 with the page_set (if required) as part of the
316 string.
Luis Lozano814c7182015-09-08 11:20:47 -0700317 @param args: additional list of arguments to pass to the script.
Zhizhou Yangf32fda02019-09-25 14:01:45 -0700318 @param kwargs: additional list of keyword arguments to pass to the
319 script.
Simran Basi833814b2013-01-29 13:13:43 -0800320
321 @returns A TelemetryResult Instance with the results of this telemetry
322 execution.
323 """
Simran Basi1dbfc132013-05-02 10:11:02 -0700324 # TODO (sbasi crbug.com/239933) add support for incognito mode.
Simran Basi833814b2013-01-29 13:13:43 -0800325
Zhizhou Yang65d37042020-01-08 17:44:12 -0800326 telemetry_cmd = self._get_telemetry_cmd(script, test_or_benchmark,
327 output_format, *args, **kwargs)
Zhizhou Yangf32fda02019-09-25 14:01:45 -0700328 logging.info('Running Telemetry: %s', telemetry_cmd)
Luis Lozano23ae3192013-11-08 16:22:46 -0800329
Keith Haddow1e5c7012016-03-09 16:05:37 -0800330 stdout, stderr, exit_code = self._run_cmd(telemetry_cmd)
Simran Basi833814b2013-01-29 13:13:43 -0800331
Zhizhou Yang65d37042020-01-08 17:44:12 -0800332 return TelemetryResult(
333 exit_code=exit_code, stdout=stdout, stderr=stderr)
Simran Basi833814b2013-01-29 13:13:43 -0800334
Zhizhou Yang65d37042020-01-08 17:44:12 -0800335 def _run_scp(self, perf_results_dir, output_format, artifacts=False):
Keith Haddow1e5c7012016-03-09 16:05:37 -0800336 """Runs telemetry on a dut.
337
338 @param perf_results_dir: The local directory that results are being
339 collected.
Zhizhou Yang65d37042020-01-08 17:44:12 -0800340 @param output_format: Format of the json result file.
341 @param artifacts: Whether we want to copy artifacts directory.
Keith Haddow1e5c7012016-03-09 16:05:37 -0800342 """
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800343 scp_cmd = self._scp_telemetry_results_cmd(perf_results_dir,
Zhizhou Yang65d37042020-01-08 17:44:12 -0800344 output_format, artifacts)
Keith Haddow1e5c7012016-03-09 16:05:37 -0800345 logging.debug('Retrieving Results: %s', scp_cmd)
Dean Liaoe4773c72017-11-09 16:15:38 +0800346 _, _, exit_code = self._run_cmd(scp_cmd)
347 if exit_code != 0:
348 raise error.TestFail('Unable to retrieve results.')
Keith Haddow1e5c7012016-03-09 16:05:37 -0800349
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800350 if output_format == 'histograms':
351 # Converts to chart json format.
352 input_filename = os.path.join(perf_results_dir,
353 HISTOGRAM_SET_RESULT)
Zhizhou Yang65d37042020-01-08 17:44:12 -0800354 output_filename = os.path.join(perf_results_dir, CHART_JSON_RESULT)
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800355 histograms = json.loads(open(input_filename).read())
356 chartjson = TelemetryRunner.convert_chart_json(histograms)
357 with open(output_filename, 'w') as fout:
358 fout.write(json.dumps(chartjson, indent=2))
Keith Haddow1e5c7012016-03-09 16:05:37 -0800359
Luis Lozano814c7182015-09-08 11:20:47 -0700360 def _run_test(self, script, test, *args):
Simran Basi1dbfc132013-05-02 10:11:02 -0700361 """Runs a telemetry test on a dut.
362
363 @param script: Which telemetry test script we want to run. Can be
364 telemetry's base test script or the Chrome OS specific
365 test script.
366 @param test: Telemetry test we want to run.
Luis Lozano814c7182015-09-08 11:20:47 -0700367 @param args: additional list of arguments to pass to the script.
Simran Basi1dbfc132013-05-02 10:11:02 -0700368
369 @returns A TelemetryResult Instance with the results of this telemetry
370 execution.
371 """
372 logging.debug('Running telemetry test: %s', test)
373 telemetry_script = os.path.join(self._telemetry_path, script)
Zhizhou Yang65d37042020-01-08 17:44:12 -0800374 result = self._run_telemetry(telemetry_script, test, 'chartjson',
375 *args)
Simran Basi1dbfc132013-05-02 10:11:02 -0700376 if result.status is FAILED_STATUS:
Ilja H. Friedelc7bf3102014-05-13 17:31:25 -0700377 raise error.TestFail('Telemetry test %s failed.' % test)
Simran Basi1dbfc132013-05-02 10:11:02 -0700378 return result
379
Luis Lozano814c7182015-09-08 11:20:47 -0700380 def run_telemetry_test(self, test, *args):
Simran Basi833814b2013-01-29 13:13:43 -0800381 """Runs a telemetry test on a dut.
382
383 @param test: Telemetry test we want to run.
Luis Lozano814c7182015-09-08 11:20:47 -0700384 @param args: additional list of arguments to pass to the telemetry
385 execution script.
Simran Basi833814b2013-01-29 13:13:43 -0800386
387 @returns A TelemetryResult Instance with the results of this telemetry
388 execution.
389 """
Luis Lozano814c7182015-09-08 11:20:47 -0700390 return self._run_test(TELEMETRY_RUN_TESTS_SCRIPT, test, *args)
Simran Basi1dbfc132013-05-02 10:11:02 -0700391
Zhizhou Yangf32fda02019-09-25 14:01:45 -0700392 def run_telemetry_benchmark(self,
393 benchmark,
394 perf_value_writer=None,
395 *args,
396 **kwargs):
Simran Basi833814b2013-01-29 13:13:43 -0800397 """Runs a telemetry benchmark on a dut.
398
399 @param benchmark: Benchmark we want to run.
Fang Denge689e712013-11-13 18:27:06 -0800400 @param perf_value_writer: Should be an instance with the function
401 output_perf_value(), if None, no perf value
402 will be written. Typically this will be the
403 job object from an autotest test.
Luis Lozano814c7182015-09-08 11:20:47 -0700404 @param args: additional list of arguments to pass to the telemetry
405 execution script.
Zhizhou Yangf32fda02019-09-25 14:01:45 -0700406 @param kwargs: additional list of keyword arguments to pass to the
407 telemetry execution script.
Simran Basi833814b2013-01-29 13:13:43 -0800408
409 @returns A TelemetryResult Instance with the results of this telemetry
410 execution.
411 """
Dave Tu6a404e62013-11-05 15:54:48 -0800412 logging.debug('Running telemetry benchmark: %s', benchmark)
Ting-Yuan Huange5b19132016-03-22 13:02:41 +0800413
Zhizhou Yang65d37042020-01-08 17:44:12 -0800414 self._perf_value_writer = perf_value_writer
415
Derek Beckett6b7cc0f2020-11-30 13:15:26 -0800416 if benchmark in ON_DUT_BLOCKLIST:
Ting-Yuan Huange5b19132016-03-22 13:02:41 +0800417 self._telemetry_on_dut = False
418
Zhizhou Yangf32fda02019-09-25 14:01:45 -0700419 output_format = kwargs.get('ex_output_format', '')
420
421 if not output_format:
Kuo-Hsin Yang72901f32019-10-28 14:24:58 +0800422 output_format = 'histograms'
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800423
Ting-Yuan Huange5b19132016-03-22 13:02:41 +0800424 if self._telemetry_on_dut:
425 telemetry_script = os.path.join(DUT_CHROME_ROOT,
426 TELEMETRY_RUN_BENCHMARKS_SCRIPT)
427 self._ensure_deps(self._host, benchmark)
428 else:
429 telemetry_script = os.path.join(self._telemetry_path,
430 TELEMETRY_RUN_BENCHMARKS_SCRIPT)
431
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800432 result = self._run_telemetry(telemetry_script, benchmark,
Zhizhou Yangf32fda02019-09-25 14:01:45 -0700433 output_format, *args, **kwargs)
Simran Basi833814b2013-01-29 13:13:43 -0800434
435 if result.status is WARNING_STATUS:
Dave Tu6a404e62013-11-05 15:54:48 -0800436 raise error.TestWarn('Telemetry Benchmark: %s'
Zhizhou Yangf32fda02019-09-25 14:01:45 -0700437 ' exited with Warnings.\nOutput:\n%s\n' %
438 (benchmark, result.output))
439 elif result.status is FAILED_STATUS:
Dave Tu6a404e62013-11-05 15:54:48 -0800440 raise error.TestFail('Telemetry Benchmark: %s'
Zhizhou Yangf32fda02019-09-25 14:01:45 -0700441 ' failed to run.\nOutput:\n%s\n' %
442 (benchmark, result.output))
443 elif '[ PASSED ] 0 tests.' in result.output:
444 raise error.TestWarn('Telemetry Benchmark: %s exited successfully,'
445 ' but no test actually passed.\nOutput\n%s\n'
446 % (benchmark, result.output))
Keith Haddow1e5c7012016-03-09 16:05:37 -0800447 if perf_value_writer:
Zhizhou Yang65d37042020-01-08 17:44:12 -0800448 artifacts = kwargs.get('artifacts', False)
449 self._run_scp(perf_value_writer.resultsdir, output_format,
450 artifacts)
Simran Basi833814b2013-01-29 13:13:43 -0800451 return result
Ting-Yuan Huange5b19132016-03-22 13:02:41 +0800452
Gurchetan Singhfaf75e92017-04-17 18:09:44 -0700453 def run_gpu_integration_test(self, test, *args):
454 """Runs a gpu test on a dut.
455
456 @param test: Gpu test we want to run.
457 @param args: additional list of arguments to pass to the telemetry
458 execution script.
459
Drew Davenport84395922018-09-10 10:42:37 -0600460 @returns A TelemetryResult instance with the results of this telemetry
461 execution.
Gurchetan Singhfaf75e92017-04-17 18:09:44 -0700462 """
Zhizhou Yang65d37042020-01-08 17:44:12 -0800463 script = os.path.join(DUT_CHROME_ROOT, TELEMETRY_RUN_GPU_TESTS_SCRIPT)
Gurchetan Singhfaf75e92017-04-17 18:09:44 -0700464 cmd = []
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800465 # TODO(crbug.com/165407): All conditional blocks that are dependent on
466 # "self._devserver" must be deleted once all telemetry tests are
467 # migrated to the drone and DevserverTelemetryRunner is deprecated.
Gurchetan Singhfaf75e92017-04-17 18:09:44 -0700468 if self._devserver:
469 devserver_hostname = self._devserver.hostname
470 cmd.extend(['ssh', devserver_hostname])
471
Zhizhou Yang65d37042020-01-08 17:44:12 -0800472 cmd.extend([
473 self._host.ssh_command(
474 alive_interval=900, connection_attempts=4), 'python2',
475 script
476 ])
Gurchetan Singhfaf75e92017-04-17 18:09:44 -0700477 cmd.extend(args)
478 cmd.append(test)
479 cmd = ' '.join(cmd)
480 stdout, stderr, exit_code = self._run_cmd(cmd)
481
Drew Davenport84395922018-09-10 10:42:37 -0600482 if exit_code:
483 raise error.TestFail('Gpu Integration Test: %s'
484 ' failed to run.' % test)
485
Zhizhou Yang65d37042020-01-08 17:44:12 -0800486 return TelemetryResult(
487 exit_code=exit_code, stdout=stdout, stderr=stderr)
Gurchetan Singhfaf75e92017-04-17 18:09:44 -0700488
Ting-Yuan Huange5b19132016-03-22 13:02:41 +0800489 def _ensure_deps(self, dut, test_name):
490 """
491 Ensure the dependencies are locally available on DUT.
492
493 @param dut: The autotest host object representing DUT.
494 @param test_name: Name of the telemetry test.
495 """
496 # Get DEPs using host's telemetry.
Kuo-Hsin Yang4a006172018-04-25 14:44:55 +0800497 # Example output, fetch_benchmark_deps.py --output-deps=deps octane:
498 # {'octane': ['tools/perf/page_sets/data/octane_002.wprgo']}
Zhizhou Yang65d37042020-01-08 17:44:12 -0800499 fetch_path = os.path.join(self._telemetry_path, 'tools', 'perf',
500 'fetch_benchmark_deps.py')
Chinglin Yu0adaf112020-01-20 14:48:20 +0800501 # Use a temporary file for |deps_path| to avoid race conditions. The
502 # created temporary file is assigned to |self._benchmark_deps| to make
503 # it valid until |self| is destroyed.
504 self._benchmark_deps = tempfile.NamedTemporaryFile(
Zhizhou Yang65d37042020-01-08 17:44:12 -0800505 prefix='fetch_benchmark_deps_result.', suffix='.json')
Chinglin Yu0adaf112020-01-20 14:48:20 +0800506 deps_path = self._benchmark_deps.name
Kuo-Hsin Yang66324262019-12-10 10:36:34 +0800507 format_fetch = ('python2 %s --output-deps=%s %s')
Kuo-Hsin Yang4a006172018-04-25 14:44:55 +0800508 command_fetch = format_fetch % (fetch_path, deps_path, test_name)
509 command_get = 'cat %s' % deps_path
Ting-Yuan Huange5b19132016-03-22 13:02:41 +0800510
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800511 # TODO(crbug.com/165407): All conditional blocks that are dependent on
512 # "self._devserver" must be deleted once all telemetry tests are
513 # migrated to the drone and DevserverTelemetryRunner is deprecated.
Ting-Yuan Huange5b19132016-03-22 13:02:41 +0800514 if self._devserver:
515 devserver_hostname = self._devserver.url().split(
516 'http://')[1].split(':')[0]
Kuo-Hsin Yang4a006172018-04-25 14:44:55 +0800517 command_fetch = 'ssh %s %s' % (devserver_hostname, command_fetch)
518 command_get = 'ssh %s %s' % (devserver_hostname, command_get)
Ting-Yuan Huange5b19132016-03-22 13:02:41 +0800519
Kuo-Hsin Yang4a006172018-04-25 14:44:55 +0800520 logging.info('Getting DEPs: %s', command_fetch)
521 _, _, exit_code = self._run_cmd(command_fetch)
522 if exit_code != 0:
523 raise error.TestFail('Error occurred while fetching DEPs.')
524 stdout, _, exit_code = self._run_cmd(command_get)
525 if exit_code != 0:
Ting-Yuan Huange5b19132016-03-22 13:02:41 +0800526 raise error.TestFail('Error occurred while getting DEPs.')
527
528 # Download DEPs to DUT.
529 # send_file() relies on rsync over ssh. Couldn't be better.
Kuo-Hsin Yang4a006172018-04-25 14:44:55 +0800530 deps = json.loads(stdout)
531 for dep in deps[test_name]:
Ting-Yuan Huange5b19132016-03-22 13:02:41 +0800532 src = os.path.join(self._telemetry_path, dep)
533 dst = os.path.join(DUT_CHROME_ROOT, dep)
Ting-Yuan Huang8a2c7f72016-03-28 22:01:07 +0800534 if self._devserver:
535 logging.info('Copying: %s -> %s', src, dst)
Zhizhou Yang65d37042020-01-08 17:44:12 -0800536 rsync_cmd = utils.sh_escape(
537 'rsync %s %s %s:%s' % (self._host.rsync_options(), src,
538 self._host.hostname, dst))
Chung-yih Wangfd8eb242017-12-09 19:23:04 +0800539 utils.run('ssh %s "%s"' % (devserver_hostname, rsync_cmd))
Ting-Yuan Huang8a2c7f72016-03-28 22:01:07 +0800540 else:
541 if not os.path.isfile(src):
542 raise error.TestFail('Error occurred while saving DEPs.')
543 logging.info('Copying: %s -> %s', src, dst)
544 dut.send_file(src, dst)
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800545
546 @staticmethod
547 def convert_chart_json(histogram_set):
548 """
549 Convert from histogram set to chart json format.
550
551 @param histogram_set: result in histogram set format.
552
553 @returns result in chart json format.
554 """
555 value_map = {}
556
557 # Gets generic set values.
558 for obj in histogram_set:
559 if 'type' in obj and obj['type'] == 'GenericSet':
560 value_map[obj['guid']] = obj['values']
561
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800562 charts = {}
563 benchmark_name = ''
564 benchmark_desc = ''
565
566 # Checks the unit test for how this conversion works.
567 for obj in histogram_set:
568 if 'name' not in obj or 'sampleValues' not in obj:
569 continue
570 metric_name = obj['name']
571 diagnostics = obj['diagnostics']
Derek Beckett5fb683c2020-08-19 15:24:13 -0700572 if 'stories' in diagnostics:
Kuo-Hsin Yang72901f32019-10-28 14:24:58 +0800573 story_name = value_map[diagnostics['stories']][0]
574 else:
575 story_name = 'default'
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800576 local_benchmark_name = value_map[diagnostics['benchmarks']][0]
Kuo-Hsin Yang99622172019-09-16 20:02:53 +0800577 if benchmark_name == '':
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800578 benchmark_name = local_benchmark_name
Derek Beckett5fb683c2020-08-19 15:24:13 -0700579 if 'benchmarkDescriptions' in diagnostics:
Kuo-Hsin Yang72901f32019-10-28 14:24:58 +0800580 benchmark_desc = value_map[
Zhizhou Yang65d37042020-01-08 17:44:12 -0800581 diagnostics['benchmarkDescriptions']][0]
Kuo-Hsin Yang72901f32019-10-28 14:24:58 +0800582 if benchmark_name != local_benchmark_name:
Zhizhou Yang65d37042020-01-08 17:44:12 -0800583 logging.warning(
584 'There are more than 1 benchmark names in the'
585 'result. old: %s, new: %s', benchmark_name,
586 local_benchmark_name)
Kuo-Hsin Yang72901f32019-10-28 14:24:58 +0800587 continue
Kuo-Hsin Yang99622172019-09-16 20:02:53 +0800588
589 unit = obj['unit']
590 smaller_postfixes = ('_smallerIsBetter', '-')
591 bigger_postfixes = ('_biggerIsBetter', '+')
592 all_postfixes = smaller_postfixes + bigger_postfixes
593
Kuo-Hsin Yange01ff602019-06-14 11:33:31 +0800594 improvement = 'up'
Kuo-Hsin Yang99622172019-09-16 20:02:53 +0800595 for postfix in smaller_postfixes:
Zhizhou Yang65d37042020-01-08 17:44:12 -0800596 if unit.endswith(postfix):
597 improvement = 'down'
Kuo-Hsin Yang99622172019-09-16 20:02:53 +0800598 for postfix in all_postfixes:
Zhizhou Yang65d37042020-01-08 17:44:12 -0800599 if unit.endswith(postfix):
600 unit = unit[:-len(postfix)]
601 break
Kuo-Hsin Yang99622172019-09-16 20:02:53 +0800602
603 if unit == 'unitless':
Zhizhou Yang65d37042020-01-08 17:44:12 -0800604 unit = 'score'
Kuo-Hsin Yang99622172019-09-16 20:02:53 +0800605
Zhizhou Yang65d37042020-01-08 17:44:12 -0800606 values = [
607 x for x in obj['sampleValues']
608 if isinstance(x, numbers.Number)
609 ]
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800610 if metric_name not in charts:
611 charts[metric_name] = {}
612 charts[metric_name][story_name] = {
Zhizhou Yang65d37042020-01-08 17:44:12 -0800613 'improvement_direction': improvement,
614 'name': metric_name,
615 'std': numpy.std(values),
616 'type': 'list_of_scalar_values',
617 'units': unit,
618 'values': values
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800619 }
620
621 # Adds summaries.
622 for metric_name in charts:
623 values = []
624 metric_content = charts[metric_name]
625 for story_name in metric_content:
626 story_content = metric_content[story_name]
627 values += story_content['values']
628 metric_type = story_content['type']
629 units = story_content['units']
Kuo-Hsin Yange01ff602019-06-14 11:33:31 +0800630 improvement = story_content['improvement_direction']
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800631 values.sort()
632 std = numpy.std(values)
633 metric_content['summary'] = {
Zhizhou Yang65d37042020-01-08 17:44:12 -0800634 'improvement_direction': improvement,
635 'name': metric_name,
636 'std': std,
637 'type': metric_type,
638 'units': units,
639 'values': values
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800640 }
641
642 benchmark_metadata = {
Zhizhou Yang65d37042020-01-08 17:44:12 -0800643 'description': benchmark_desc,
644 'name': benchmark_name,
645 'type': 'telemetry_benchmark'
Kuo-Hsin Yang14d002b2019-06-06 13:18:04 +0800646 }
Kuo-Hsin Yange01ff602019-06-14 11:33:31 +0800647 return {
Zhizhou Yang65d37042020-01-08 17:44:12 -0800648 'benchmark_description': benchmark_desc,
649 'benchmark_metadata': benchmark_metadata,
650 'benchmark_name': benchmark_name,
651 'charts': charts,
652 'format_version': 1.0
Kuo-Hsin Yange01ff602019-06-14 11:33:31 +0800653 }
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800654
655
656class LocalTelemetryRunner(TelemetryRunner):
657 """Specialized TelemetryRunner to handle local telemetry test runs."""
658
659 def __init__(self, *args, **kwargs):
660 """Initialize LocalTelemetryRunner.
661
662 The telemetry test will run locally. Depending on whether
663 telemetry_on_dut is True or False, there can be possible combinations
664 for the execution of this test:
665
666 telemetry_on_dut=False:
667 python2 run_benchmark --browser=cros-chrome --remote=[dut] [test]
668
669 telemetry_on_dut=True:
670 ssh [dut] python2 run_benchmark --browser=system [test]
671
672 @param args: The list of arguments to be passed. See Base class for a
673 complete list of accepted arguments.
674 @param kwargs: Any keyword arguments to be passed. See Base class for a
675 complete list of accepted keyword arguments.
676 """
677 super(LocalTelemetryRunner, self).__init__(*args, **kwargs)
678
679 def _setup_telemetry(self):
680 """Setup Telemetry to use local path to its sources.
681
682 First look for chrome source root, either externally mounted, or inside
683 the chroot. Prefer chrome-src-internal source tree to chrome-src.
684 """
685 TELEMETRY_DIR = 'src'
686 CHROME_LOCAL_SRC = '/var/cache/chromeos-cache/distfiles/target/'
687 CHROME_EXTERNAL_SRC = os.path.expanduser('~/chrome_root/')
688
689 logging.debug('Setting up telemetry for local testing')
690
691 sources_list = ('chrome-src-internal', 'chrome-src')
692 dir_list = [CHROME_EXTERNAL_SRC]
693 dir_list.extend(
694 [os.path.join(CHROME_LOCAL_SRC, x) for x in sources_list])
695 if 'CHROME_ROOT' in os.environ:
696 dir_list.insert(0, os.environ['CHROME_ROOT'])
697
698 telemetry_src = ''
699 for dir in dir_list:
700 if os.path.exists(dir):
701 telemetry_src = os.path.join(dir, TELEMETRY_DIR)
702 break
703 else:
704 raise error.TestError('Telemetry source directory not found.')
705
706 self._telemetry_path = telemetry_src
707
708
709class DroneTelemetryRunner(TelemetryRunner):
710 """Handle telemetry test setup on the drone.
711
712 Users of this class are strongly advised to use this class as a context
713 manager. Since the setup for telemetry environment happens on the drone, it
714 is imperative that this setup be cleaned up once the test is done. Using
715 this class as a context manager will transfer the burden of clean up from
716 the user to Python.
717 """
718
719 def __init__(self, *args, **kwargs):
720 """Initialize DroneTelemetryRunner.
721
722 The telemetry test will run on the drone. Depending on whether
723 telemetry_on_dut is True or False, there can be possible combinations
724 for the execution of this test:
725
726 telemetry_on_dut=False:
727 python2 run_benchmark --browser=cros-chrome --remote=[dut] [test]
728
729 telemetry_on_dut=True:
730 ssh [dut] python2 run_benchmark --browser=system [test]
731
732 @param args: The list of arguments to be passed. See Base class for a
733 complete list of accepted arguments.
734 @param kwargs: Any keyword arguments to be passed. See Base class for a
735 complete list of accepted keyword arguments.
736 """
737 self._telemetry_setup = None
738 super(DroneTelemetryRunner, self).__init__(*args, **kwargs)
739
740 def __enter__(self):
741 """Called while entering context manager; does nothing."""
742 return self
743
744 def __exit__(self, exc_type, exc_value, traceback):
745 """Called while exiting context manager; cleans up temp files."""
746 logging.info('Cleaning up the telemetry environment on the drone.')
747 self._telemetry_setup.Cleanup()
748
749 def _setup_telemetry(self):
750 """Setup Telemetry on the drone."""
751 logging.debug('Setting up telemetry on the drone')
752 info = self._host.host_info_store.get()
753 if not info.build:
754 logging.error('Unable to locate build label for host: %s.',
755 self._host.host_port)
756 raise error.AutotestError('Failed to grab build for host %s.' %
757 self._host.host_port)
758
759 logging.debug('Setting up telemetry for build: %s', info.build)
760 try:
761 self._telemetry_setup = telemetry_setup.TelemetrySetup(
Sanika Kulkarni211a4ef2021-01-29 13:23:44 -0800762 hostname=self._host.hostname, build=info.build)
Sanika Kulkarni78dd7f82021-01-08 11:47:23 -0800763 self._telemetry_path = self._telemetry_setup.Setup()
764 except telemetry_setup.TelemetrySetupError as e:
765 raise error.AutotestError('Telemetry Environment could not be '
766 'setup: %s.' % e)
767
768
769class DevserverTelemetryRunner(TelemetryRunner):
770 """Handle telemetry test setup and execution on the devserver."""
771
772 def __init__(self, *args, **kwargs):
773 """Initialize DevserverTelemetryRunner.
774
775 The telemetry test will run on the devserver. Depending on whether
776 telemetry_on_dut is True or False, there can be possible combinations
777 for the execution of this test:
778
779 telemetry_on_dut=False:
780 ssh [devserver] python2 run_benchmark --browser=cros-chrome
781 --remote=[dut] [test]
782
783 telemetry_on_dut=True:
784 ssh [devserver] ssh [dut] python2 run_benchmark --browser=system [test]
785
786 @param args: The list of arguments to be passed. See Base class for a
787 complete list of accepted arguments.
788 @param kwargs: Any keyword arguments to be passed. See Base class for a
789 complete list of accepted keyword arguments.
790 """
791 super(DevserverTelemetryRunner, self).__init__(*args, **kwargs)
792
793 def _setup_telemetry(self):
794 """Setup Telemetry to use the devserver."""
795 logging.debug('Setting up telemetry for devserver testing')
796 logging.debug('Grabbing build from AFE.')
797 info = self._host.host_info_store.get()
798 if not info.build:
799 logging.error('Unable to locate build label for host: %s.',
800 self._host.host_port)
801 raise error.AutotestError('Failed to grab build for host %s.' %
802 self._host.host_port)
803 logging.debug('Setting up telemetry for build: %s', info.build)
804 self._devserver = dev_server.ImageServer.resolve(
805 info.build, hostname=self._host.hostname)
806 self._devserver.stage_artifacts(info.build, ['autotest_packages'])
807 self._telemetry_path = self._devserver.setup_telemetry(
808 build=info.build)