| # -*- coding: utf-8 -*- |
| # Copyright 2017 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Runs autotest on DUT and gets result for performance evaluation.""" |
| |
| from __future__ import division |
| from __future__ import print_function |
| |
| import os |
| import shutil |
| |
| from chromite.cros_bisect import common |
| from chromite.cros_bisect import evaluator |
| from chromite.cros_bisect import simple_chrome_builder |
| from chromite.lib import cros_build_lib |
| from chromite.lib import cros_logging as logging |
| from chromite.lib import json_lib |
| from chromite.lib import osutils |
| from chromite.lib import path_util |
| from chromite.lib import remote_access |
| from chromite.lib import repo_util |
| |
| |
| class AutotestEvaluator(evaluator.Evaluator): |
| """Evaluates performance by running autotest test. |
| |
| It first try running autotest from DUT (via ssh command). If it fails to run |
| (e.g. first running the test on the DUT), it then runs "test_that" inside |
| CrOS's chrome_sdk to pack test package and push it to DUT to execute. |
| |
| After autotest done running, it grabs JSON report file and finds the metric |
| to watch (currently support float value). |
| """ |
| AUTOTEST_BASE = '/usr/local/autotest' |
| AUTOTEST_CLIENT = os.path.join(AUTOTEST_BASE, 'bin', 'autotest_client') |
| REQUIRED_ARGS = evaluator.Evaluator.REQUIRED_ARGS + ( |
| 'board', 'chromium_dir', 'cros_dir', 'test_name', 'metric', |
| 'metric_take_average', 'eval_passing_only') |
| CROS_DIR = 'cros' |
| RESULT_FILENAME = 'results-chart.json' |
| |
| def __init__(self, options): |
| """Constructor. |
| |
| Args: |
| options: In addition to the flags required by the base class, need to |
| specify: |
| * board: CrOS board name (used for running host side autotest). |
| * chromium_dir: Optional. If specified, use the chromium repo the path |
| points to. Otherwise, use base_dir/chromium/src. |
| * cros_dir: Optional. If specified, use it to enter CrOS chroot to |
| run autotest "test_that" command. Otherwise, use base_dir/cros. |
| * test_name: Autotest name to run. |
| * metric: Metric to look up. |
| * metric_take_average: If set, take average value of the metric. |
| * eval_passing_only: If set, use existing perf result only if test was |
| passing. |
| """ |
| super(AutotestEvaluator, self).__init__(options) |
| self.board = options.board |
| self.test_name = options.test_name |
| self.metric = options.metric |
| self.metric_take_average = options.metric_take_average |
| self.eval_passing_only = options.eval_passing_only |
| # Used for entering chroot. Some autotest depends on CHROME_ROOT being set. |
| if options.chromium_dir: |
| self.chromium_dir = options.chromium_dir |
| else: |
| self.chromium_dir = os.path.join(self.base_dir, |
| simple_chrome_builder.CHROMIUM_DIR) |
| |
| if options.cros_dir: |
| self.cros_dir = options.cros_dir |
| else: |
| self.cros_dir = os.path.join(self.base_dir, self.CROS_DIR) |
| |
| def RunTestFromDut(self, remote, report_file): |
| """Runs autotest from DUT. |
| |
| It runs autotest from DUT directly. It can only be used after the test was |
| deployed/run using "test_that" from host. |
| |
| Args: |
| remote: DUT for running test (refer lib.commandline.Device). |
| report_file: Benchmark report to store (host side). |
| |
| Returns: |
| False if sanity check fails, i.e. autotest control file missing. |
| If autotest ran successfully, or --eval-failsafe is set, it returns |
| if the test report is retrieved from DUT. Otherwise, False. |
| """ |
| def RetrieveReport(dut, remote_report_file): |
| """Retrieves report from DUT to local. |
| |
| Args: |
| dut: a RemoteAccess object to access DUT. |
| remote_report_file: path of the report on DUT to retrieve. |
| |
| Returns: |
| True if a report is copied from DUT to local path (report_file). |
| """ |
| logging.info('Copy report from DUT(%s:%s) to %s', |
| dut.remote_host, remote_report_file, report_file) |
| scp_result = dut.ScpToLocal(remote_report_file, report_file, |
| check=False) |
| if scp_result.returncode != 0: |
| logging.error('Failed to copy report from DUT(%s:%s) to host(%s)', |
| dut.remote_host, remote_report_file, report_file) |
| return False |
| return True |
| |
| # TODO(deanliao): Deal with the case that test control file is not the |
| # same as below. |
| test_target = os.path.join(self.AUTOTEST_BASE, 'tests', self.test_name, |
| 'control') |
| |
| remote_report_file = os.path.join( |
| self.AUTOTEST_BASE, 'results', 'default', self.test_name, 'results', |
| self.RESULT_FILENAME) |
| |
| with osutils.TempDir() as temp_dir: |
| dut = remote_access.RemoteAccess( |
| remote.hostname, temp_dir, port=remote.port, username=remote.username) |
| |
| run_test_command = [self.AUTOTEST_CLIENT, test_target] |
| logging.info('Run autotest from DUT %s: %s', dut.remote_host, |
| cros_build_lib.CmdToStr(run_test_command)) |
| |
| # Make sure that both self.AUTOTEST_CLIENT and test_target exist. |
| sanity_check_result = dut.RemoteSh(['ls'] + run_test_command, |
| check=False, ssh_error_ok=True) |
| if sanity_check_result.returncode != 0: |
| logging.info('Failed to run autotest from DUT %s: One of %s does not ' |
| 'exist.', dut.remote_host, run_test_command) |
| return False |
| |
| run_test_result = dut.RemoteSh(run_test_command, check=False, |
| ssh_error_ok=True) |
| run_test_returncode = run_test_result.returncode |
| |
| if run_test_returncode != 0: |
| logging.info('Run failed (returncode: %d)', run_test_returncode) |
| if self.eval_passing_only: |
| return False |
| else: |
| logging.info('Ran successfully.') |
| return RetrieveReport(dut, remote_report_file) |
| |
| def LookupReportFile(self): |
| """Looks up autotest report file. |
| |
| It looks up results-chart.json under chroot's /tmp/test_that_latest. |
| |
| Returns: |
| Path to report file. None if not found. |
| """ |
| # Default result dir: /tmp/test_that_latest |
| results_dir = self.ResolvePathFromChroot(os.path.join( |
| '/tmp', 'test_that_latest', 'results-1-%s' % self.test_name)) |
| # Invoking "find" command is faster than using os.walkdir(). |
| try: |
| command_result = cros_build_lib.run( |
| ['find', '.', '-name', self.RESULT_FILENAME], |
| cwd=results_dir, capture_output=True, encoding='utf-8') |
| except cros_build_lib.RunCommandError as e: |
| logging.error('Failed to look up %s under %s: %s', self.RESULT_FILENAME, |
| results_dir, e) |
| return None |
| if not command_result.output: |
| logging.error('Failed to look up %s under %s', self.RESULT_FILENAME, |
| results_dir) |
| return None |
| report_file_under_results_dir = ( |
| command_result.output.splitlines()[0].strip()) |
| return os.path.normpath( |
| os.path.join(results_dir, report_file_under_results_dir)) |
| |
| def SetupCrosRepo(self): |
| """Gets the ChromeOS source code. |
| |
| It is used to enter cros_sdk to run autotest by executing "test_that" |
| command. |
| """ |
| osutils.SafeMakedirs(self.cros_dir) |
| repo = repo_util.Repository.Initialize( |
| self.cros_dir, |
| 'https://chromium.googlesource.com/chromiumos/manifest.git', |
| repo_url='https://chromium.googlesource.com/external/repo.git') |
| repo.Sync(jobs=8) |
| |
| def MaySetupBoard(self): |
| """Checks if /build/${board} exists. Sets it up if not. |
| |
| Returns: |
| False if setup_board or build_package failed. True otherwise. |
| """ |
| if not os.path.isdir(self.cros_dir): |
| logging.notice('ChromeOS source: %s does not exist, set it up', |
| self.cros_dir) |
| self.SetupCrosRepo() |
| |
| board_path = self.ResolvePathFromChroot(os.path.join('/build', self.board)) |
| if os.path.isdir(board_path): |
| return True |
| |
| try: |
| self.RunCommandInsideCrosSdk(['setup_board', '--board', self.board]) |
| except cros_build_lib.RunCommandError as e: |
| logging.error('Failed to setup_board for %s: %s', self.board, e) |
| return False |
| |
| try: |
| self.RunCommandInsideCrosSdk(['./build_packages', '--board', self.board]) |
| except cros_build_lib.RunCommandError as e: |
| logging.error('Failed to build_package for %s: %s', self.board, e) |
| return False |
| return True |
| |
| def ResolvePathFromChroot(self, path_inside_chroot): |
| """Resolves path from chroot. |
| |
| Args: |
| path_inside_chroot: path inside chroot. |
| |
| Returns: |
| Path outside chroot which points to the given path inside chroot. |
| """ |
| return path_util.ChrootPathResolver(source_path=self.cros_dir).FromChroot( |
| path_inside_chroot) |
| |
| def RunCommandInsideCrosSdk(self, command): |
| """Runs command inside cros_sdk. |
| |
| The cros_sdk it used is under self.cros_dir. And its chrome_root is set to |
| self.chrome_root. |
| |
| Args: |
| command: command as a list of arguments. |
| |
| Returns: |
| A CommandResult object. |
| |
| Raises: |
| RunCommandError: Raised on error. |
| """ |
| # --chrome_root is needed for autotests running Telemetry. |
| # --no-ns-pid is used to prevent the program receiving SIGTTIN (e.g. go to |
| # background and stopped) when asking user input. |
| chroot_args = ['--chrome_root', self.chromium_dir, |
| '--no-ns-pid'] |
| return cros_build_lib.run(command, enter_chroot=True, |
| chroot_args=chroot_args, cwd=self.cros_dir) |
| |
| def RunTestFromHost(self, remote, report_file_to_store): |
| """Runs autotest from host. |
| |
| It uses test_that tool in CrOS chroot to deploy autotest to DUT and run it. |
| |
| Args: |
| remote: DUT for running test (refer lib.commandline.Device). |
| report_file_to_store: Benchmark report to store. |
| |
| Returns: |
| False if sanity check fails, i.e. setup_board inside chroot fails. |
| If autotest ran successfully inside chroot, or --eval-failsafe is set, it |
| returns if the test report is retrieved from chroot. Otherwise, False. |
| """ |
| if not self.MaySetupBoard(): |
| return False |
| |
| run_autotest = ['test_that', |
| '-b', self.board, |
| '--fast', |
| '--args', 'local=True', |
| remote.raw, |
| self.test_name] |
| |
| try: |
| self.RunCommandInsideCrosSdk(run_autotest) |
| except cros_build_lib.RunCommandError as e: |
| if self.eval_passing_only: |
| logging.error('Failed to run autotest: %s', e) |
| return False |
| |
| report_file_in_chroot = self.LookupReportFile() |
| if not report_file_in_chroot: |
| logging.error('Failed to run autotest: report file not found') |
| return False |
| |
| try: |
| shutil.copyfile(report_file_in_chroot, report_file_to_store) |
| except Exception as e: |
| logging.error('Failed to retrieve report from chroot %s to %s', |
| report_file_in_chroot, report_file_to_store) |
| return False |
| return True |
| |
| def GetAutotestMetricValue(self, report_file): |
| """Gets metric value from autotest benchmark report. |
| |
| Report example: |
| {"avg_fps_1000_fishes": { |
| "summary": { |
| "units": "fps", |
| "type": "scalar", |
| "value": 56.733810392225671, |
| "improvement_direction": "up" |
| }, |
| ..., |
| }, |
| ..., |
| } |
| self.metric = "avg_fps_1000_fishes/summary/value" |
| |
| Args: |
| report_file: Path to benchmark report. |
| |
| Returns: |
| Metric value in benchmark report. |
| None if self.metric is undefined or metric does not exist in the report. |
| """ |
| if not self.metric: |
| return None |
| |
| report = json_lib.ParseJsonFileWithComments(report_file) |
| metric_value = json_lib.GetNestedDictValue(report, self.metric.split('/')) |
| if metric_value is None: |
| logging.error('Cannot get metric %s from %s', self.metric, report_file) |
| return None |
| if self.metric_take_average: |
| return sum(metric_value) / len(metric_value) |
| return metric_value |
| |
| def GetReportPath(self, build_label, nth_eval, repeat): |
| """Obtains report file path. |
| |
| Args: |
| build_label: current build label to run the evaluation. |
| nth_eval: n-th evaluation. |
| repeat: #repeat. |
| |
| Returns: |
| Report file path. |
| """ |
| return os.path.join( |
| self.report_base_dir, |
| 'results-chart.%s.%d-%d.json' % (build_label, nth_eval, repeat)) |
| |
| def Evaluate(self, remote, build_label, repeat=1): |
| """Runs autotest N-times on DUT and extracts the designated metric values. |
| |
| Args: |
| remote: DUT to evaluate (refer lib.commandline.Device). |
| build_label: Build label used for part of report filename and log message. |
| repeat: Run test for N times. Default 1. |
| |
| Returns: |
| Score object stores a list of autotest running results. |
| """ |
| if repeat == 1: |
| times_str = 'once' |
| elif repeat == 2: |
| times_str = 'twice' |
| else: |
| times_str = '%d times' % repeat |
| logging.info( |
| 'Evaluating build %s performance on DUT %s by running autotest %s %s ' |
| 'to get metric %s', |
| build_label, remote.raw, self.test_name, times_str, self.metric) |
| |
| score_list = [] |
| for nth in range(repeat): |
| report_file = self.GetReportPath(build_label, nth + 1, repeat) |
| score = self._EvaluateOnce(remote, report_file) |
| if score is None: |
| return common.Score() |
| logging.info( |
| 'Run autotest %d/%d. Got result: %s:%s = %.3f (build:%s DUT:%s).', |
| nth + 1, repeat, self.test_name, self.metric, score, build_label, |
| remote.raw) |
| score_list.append(score) |
| |
| scores = common.Score(score_list) |
| logging.info( |
| 'Successfully ran autotest %d times. Arithmetic mean(%s:%s) = %.3f', |
| repeat, self.test_name, self.metric, scores.mean) |
| return scores |
| |
| def _EvaluateOnce(self, remote, report_file): |
| """Runs autotest on DUT once and extracts the designated metric value.""" |
| success = self.RunTestFromDut(remote, report_file) |
| if not success: |
| logging.info('Failed to run autotest from DUT. Failover to run autotest ' |
| 'from host using "test_that" command.') |
| success = self.RunTestFromHost(remote, report_file) |
| if not success: |
| logging.error('Failed to run autotest.') |
| return None |
| return self.GetAutotestMetricValue(report_file) |
| |
| def CheckLastEvaluate(self, build_label, repeat=1): |
| """Checks if previous evaluate report is available. |
| |
| Args: |
| build_label: Build label used for part of report filename and log message. |
| repeat: Run test for N times. Default 1. |
| |
| Returns: |
| Score object stores a list of autotest running results if report |
| available and reuse_eval is set. |
| Score() otherwise. |
| """ |
| if not self.reuse_eval: |
| return common.Score() |
| |
| score_list = [] |
| for nth in range(repeat): |
| report_file = self.GetReportPath(build_label, nth + 1, repeat) |
| if not os.path.isfile(report_file): |
| return common.Score() |
| score = self.GetAutotestMetricValue(report_file) |
| if score is None: |
| return common.Score() |
| score_list.append(score) |
| scores = common.Score(score_list) |
| logging.info( |
| 'Used archived autotest result. Arithmetic mean(%s:%s) = ' |
| '%.3f (build:%s)', |
| self.test_name, self.metric, scores.mean, build_label) |
| return scores |