| #!/usr/bin/env vpython3 | 
 | # Copyright 2017 The Chromium Authors | 
 | # Use of this source code is governed by a BSD-style license that can be | 
 | # found in the LICENSE file. | 
 | """Runs telemetry benchmarks and gtest perf tests. | 
 |  | 
 | If optional argument --isolated-script-test-output=[FILENAME] is passed | 
 | to the script, json is written to that file in the format detailed in | 
 | //docs/testing/json-test-results-format.md. | 
 |  | 
 | If optional argument --isolated-script-test-filter=[TEST_NAMES] is passed to | 
 | the script, it should be a  double-colon-separated ("::") list of test names, | 
 | to run just that subset of tests. | 
 |  | 
 | This script is intended to be the base command invoked by the isolate, | 
 | followed by a subsequent Python script. It could be generalized to | 
 | invoke an arbitrary executable. | 
 | It currently runs several benchmarks. The benchmarks it will execute are | 
 | based on the shard it is running on and the sharding_map_path. | 
 |  | 
 | If this is executed with a gtest perf test, the flag --non-telemetry | 
 | has to be passed in to the script so the script knows it is running | 
 | an executable and not the run_benchmark command. | 
 |  | 
 | This script merges test results from all the benchmarks into the one | 
 | output.json file. The test results and perf results are also put in separate | 
 | directories per benchmark. Two files will be present in each directory; | 
 | perf_results.json, which is the perf specific results (with unenforced format, | 
 | could be histogram or graph json), and test_results.json. | 
 |  | 
 | TESTING: | 
 | To test changes to this script, please run unit tests: | 
 | $ cd testing/scripts | 
 | $ vpython3 run_performance_tests_unittest.py | 
 |  | 
 | Run end-to-end tests: | 
 | $ cd tools/perf | 
 | $ ./run_tests ScriptsSmokeTest.testRunPerformanceTests | 
 | """ | 
 |  | 
 | import argparse | 
 | from collections import deque, OrderedDict | 
 | import datetime | 
 | import json | 
 | import os | 
 | import pathlib | 
 | import shutil | 
 | import sys | 
 | import time | 
 | import tempfile | 
 | import traceback | 
 |  | 
 | # vpython-provided modules. | 
 | # pylint: disable=import-error | 
 | import six | 
 | import requests | 
 | # pylint: enable=import-error | 
 |  | 
 | import common | 
 |  | 
 | CHROMIUM_SRC_DIR = pathlib.Path(__file__).absolute().parents[2] | 
 | RELEASE_DIR = CHROMIUM_SRC_DIR / 'out/Release' | 
 |  | 
 | PERF_DIR = CHROMIUM_SRC_DIR / 'tools/perf' | 
 | sys.path.append(str(PERF_DIR)) | 
 | # //tools/perf imports. | 
 | if (PERF_DIR / 'crossbench_result_converter.py').exists(): | 
 |   # Optional import needed to run crossbench. | 
 |   import crossbench_result_converter | 
 | else: | 
 |   print('Optional crossbench_result_converter not available.') | 
 | import generate_legacy_perf_dashboard_json | 
 | from core import path_util | 
 | from core import results_merger | 
 |  | 
 | sys.path.append(str(CHROMIUM_SRC_DIR / 'testing')) | 
 | # //testing imports. | 
 | import xvfb | 
 | import test_env | 
 |  | 
 | THIRD_PARTY_DIR = CHROMIUM_SRC_DIR / 'third_party' | 
 | CATAPULT_DIR = THIRD_PARTY_DIR / 'catapult' | 
 | TELEMETRY_DIR = CATAPULT_DIR / 'telemetry' | 
 | # //third_party/catapult/telemetry imports. | 
 | if TELEMETRY_DIR.exists() and (CATAPULT_DIR / 'common').exists(): | 
 |   # Telemetry is required on perf infra, but not present on some environments. | 
 |   sys.path.append(str(TELEMETRY_DIR)) | 
 |   from telemetry.internal.browser import browser_finder | 
 |   from telemetry.internal.browser import browser_options | 
 |   from telemetry.core import util | 
 |   from telemetry.internal.util import binary_manager | 
 | else: | 
 |   print('Optional telemetry library not available.') | 
 |  | 
 | SHARD_MAPS_DIR = CHROMIUM_SRC_DIR / 'tools/perf/core/shard_maps' | 
 | CROSSBENCH_TOOL = CHROMIUM_SRC_DIR / 'third_party/crossbench/cb.py' | 
 | ADB_TOOL = THIRD_PARTY_DIR / 'android_sdk/public/platform-tools/adb' | 
 | BUNDLETOOL = THIRD_PARTY_DIR / 'android_build_tools/bundletool/cipd/bundletool.jar'  # pylint: disable=line-too-long | 
 | GSUTIL_DIR = THIRD_PARTY_DIR / 'catapult/third_party/gsutil' | 
 | PAGE_SETS_DATA = CHROMIUM_SRC_DIR / 'tools/perf/page_sets/data' | 
 | PERF_TOOLS = ['benchmarks', 'executables', 'crossbench'] | 
 |  | 
 | # See https://crbug.com/923564. | 
 | # We want to switch over to using histograms for everything, but converting from | 
 | # the format output by gtest perf tests to histograms has introduced several | 
 | # problems. So, only perform the conversion on tests that are whitelisted and | 
 | # are okay with potentially encountering issues. | 
 | GTEST_CONVERSION_WHITELIST = [ | 
 |     'angle_perftests', | 
 |     'base_perftests', | 
 |     'blink_heap_perftests', | 
 |     'blink_platform_perftests', | 
 |     'cc_perftests', | 
 |     'components_perftests', | 
 |     'command_buffer_perftests', | 
 |     'dawn_perf_tests', | 
 |     'gpu_perftests', | 
 |     'load_library_perf_tests', | 
 |     'net_perftests', | 
 |     'browser_tests', | 
 |     'services_perftests', | 
 |     # TODO(jmadill): Remove once migrated. http://anglebug.com/5124 | 
 |     'standalone_angle_perftests', | 
 |     'sync_performance_tests', | 
 |     'tracing_perftests', | 
 |     'views_perftests', | 
 |     'viz_perftests', | 
 |     'wayland_client_perftests', | 
 |     'xr.vr.common_perftests', | 
 | ] | 
 |  | 
 | # pylint: disable=useless-object-inheritance | 
 |  | 
 |  | 
 | class OutputFilePaths(object): | 
 |   """Provide paths to where results outputs should be written. | 
 |  | 
 |   The process_perf_results.py merge script later will pull all of these | 
 |   together, so that's why they aren't in the standard locations. Also, | 
 |   note that because of the OBBS (One Build Bot Step), Telemetry | 
 |   has multiple tests running on a single shard, so we need to prefix | 
 |   these locations with a directory named by the benchmark name. | 
 |   """ | 
 |  | 
 |   def __init__(self, isolated_out_dir, perf_test_name): | 
 |     self.name = perf_test_name | 
 |     self.benchmark_path = os.path.join(isolated_out_dir, perf_test_name) | 
 |  | 
 |   def SetUp(self): | 
 |     if os.path.exists(self.benchmark_path): | 
 |       shutil.rmtree(self.benchmark_path) | 
 |     os.makedirs(self.benchmark_path) | 
 |     return self | 
 |  | 
 |   @property | 
 |   def perf_results(self): | 
 |     return os.path.join(self.benchmark_path, 'perf_results.json') | 
 |  | 
 |   @property | 
 |   def test_results(self): | 
 |     return os.path.join(self.benchmark_path, 'test_results.json') | 
 |  | 
 |   @property | 
 |   def logs(self): | 
 |     return os.path.join(self.benchmark_path, 'benchmark_log.txt') | 
 |  | 
 |   @property | 
 |   def csv_perf_results(self): | 
 |     """Path for csv perf results. | 
 |  | 
 |     Note that the chrome.perf waterfall uses the json histogram perf results | 
 |     exclusively. csv_perf_results are implemented here in case a user script | 
 |     passes --output-format=csv. | 
 |     """ | 
 |     return os.path.join(self.benchmark_path, 'perf_results.csv') | 
 |  | 
 |  | 
 | def print_start(step, attempt=None): | 
 |   if attempt is None: | 
 |     attempt_str = '' | 
 |   else: | 
 |     attempt_str = f' (attempt #{attempt})' | 
 |   print(f'\n### {step}{attempt_str} {datetime.datetime.now()} ###') | 
 |  | 
 |  | 
 | def print_duration(step, start): | 
 |   print('Duration of %s: %d seconds' % (step, time.time() - start)) | 
 |  | 
 |  | 
 | def IsWindows(): | 
 |   return sys.platform == 'cygwin' or sys.platform.startswith('win') | 
 |  | 
 |  | 
 | def get_abs_user_path(user_path): | 
 |   if user_path is None: | 
 |     return None | 
 |   return os.path.abspath(os.path.expanduser(user_path)) | 
 |  | 
 |  | 
 | class GtestCommandGenerator(object): | 
 |  | 
 |   def __init__(self, | 
 |                options, | 
 |                override_executable=None, | 
 |                additional_flags=None, | 
 |                ignore_shard_env_vars=False): | 
 |     self._options = options | 
 |     self._override_executable = override_executable | 
 |     self._additional_flags = additional_flags or [] | 
 |     self._ignore_shard_env_vars = ignore_shard_env_vars | 
 |  | 
 |   def generate(self, output_dir): | 
 |     """Generate the command to run to start the gtest perf test. | 
 |  | 
 |     Returns: | 
 |       list of strings, the executable and its arguments. | 
 |     """ | 
 |     return ([self._get_executable()] + self._generate_filter_args() + | 
 |             self._generate_repeat_args() + | 
 |             self._generate_also_run_disabled_tests_args() + | 
 |             self._generate_output_args(output_dir) + | 
 |             self._generate_shard_args() + self._get_additional_flags()) | 
 |  | 
 |   @property | 
 |   def ignore_shard_env_vars(self): | 
 |     return self._ignore_shard_env_vars | 
 |  | 
 |   @property | 
 |   def executable_name(self): | 
 |     """Gets the platform-independent name of the executable.""" | 
 |     return self._override_executable or self._options.executable | 
 |  | 
 |   def _get_executable(self): | 
 |     executable = str(self.executable_name) | 
 |     if IsWindows(): | 
 |       return r'.\%s.exe' % executable | 
 |     return './%s' % executable | 
 |  | 
 |   def _get_additional_flags(self): | 
 |     return self._additional_flags | 
 |  | 
 |   def _generate_shard_args(self): | 
 |     """Teach the gtest to ignore the environment variables. | 
 |  | 
 |     GTEST_SHARD_INDEX and GTEST_TOTAL_SHARDS will confuse the gtest | 
 |     and convince it to only run some of its tests. Instead run all | 
 |     of them. | 
 |     """ | 
 |     if self._ignore_shard_env_vars: | 
 |       return ['--test-launcher-total-shards=1', '--test-launcher-shard-index=0'] | 
 |     return [] | 
 |  | 
 |   def _generate_filter_args(self): | 
 |     if self._options.isolated_script_test_filter: | 
 |       filter_list = common.extract_filter_list( | 
 |           self._options.isolated_script_test_filter) | 
 |       return ['--gtest_filter=' + ':'.join(filter_list)] | 
 |     return [] | 
 |  | 
 |   # pylint: disable=no-self-use | 
 |   def _generate_repeat_args(self): | 
 |     # TODO(crbug.com/40608634): Support --isolated-script-test-repeat. | 
 |     return [] | 
 |   # pylint: enable=no-self-use | 
 |  | 
 |   # pylint: disable=no-self-use | 
 |   def _generate_also_run_disabled_tests_args(self): | 
 |     # TODO(crbug.com/40608634): Support | 
 |     # --isolated-script-test-also-run-disabled-tests. | 
 |     return [] | 
 |   # pylint: enable=no-self-use | 
 |  | 
 |   def _generate_output_args(self, output_dir): | 
 |     output_args = [] | 
 |     if self._options.use_gtest_benchmark_script: | 
 |       output_args.append('--output-dir=' + output_dir) | 
 |     # These flags are to make sure that test output perf metrics in the log. | 
 |     if '--verbose' not in self._get_additional_flags(): | 
 |       output_args.append('--verbose') | 
 |     if ('--test-launcher-print-test-stdio=always' | 
 |         not in self._get_additional_flags()): | 
 |       output_args.append('--test-launcher-print-test-stdio=always') | 
 |     return output_args | 
 |  | 
 |  | 
 | def write_simple_test_results(return_code, output_filepath, benchmark_name): | 
 |   # TODO(crbug.com/40144432): Fix to output | 
 |   # https://chromium.googlesource.com/chromium/src/+/main/docs/testing/json_test_results_format.md | 
 |   # for each test rather than this summary. | 
 |   # Append the shard index to the end of the name so that the merge script | 
 |   # doesn't blow up trying to merge unmergeable results. | 
 |   benchmark_name += '_shard_%s' % os.environ.get('GTEST_SHARD_INDEX', '0') | 
 |   output_json = { | 
 |       'tests': { | 
 |           benchmark_name: { | 
 |               'expected': 'PASS', | 
 |               'actual': 'FAIL' if return_code else 'PASS', | 
 |               'is_unexpected': bool(return_code), | 
 |           }, | 
 |       }, | 
 |       'interrupted': False, | 
 |       'path_delimiter': '/', | 
 |       'version': 3, | 
 |       'seconds_since_epoch': time.time(), | 
 |       'num_failures_by_type': { | 
 |           'FAIL': 1 if return_code else 0, | 
 |           'PASS': 0 if return_code else 1, | 
 |       }, | 
 |   } | 
 |   with open(output_filepath, 'w') as fh: | 
 |     json.dump(output_json, fh) | 
 |  | 
 |  | 
 | def upload_simple_test_results(return_code, benchmark_name): | 
 |   # TODO(crbug.com/40144432): Fix to upload results for each test rather than | 
 |   # this summary. | 
 |   try: | 
 |     with open(os.environ['LUCI_CONTEXT']) as f: | 
 |       sink = json.load(f)['result_sink'] | 
 |   except KeyError: | 
 |     return | 
 |  | 
 |   if return_code: | 
 |     summary = '<p>Benchmark failed with status code %d</p>' % return_code | 
 |   else: | 
 |     summary = '<p>Benchmark passed</p>' | 
 |  | 
 |   result_json = { | 
 |       'testResults': [{ | 
 |           'testId': benchmark_name, | 
 |           'expected': not return_code, | 
 |           'status': 'FAIL' if return_code else 'PASS', | 
 |           'summaryHtml': summary, | 
 |           'tags': [{ | 
 |               'key': 'exit_code', | 
 |               'value': str(return_code) | 
 |           }], | 
 |       }] | 
 |   } | 
 |  | 
 |   res = requests.post( | 
 |       url='http://%s/prpc/luci.resultsink.v1.Sink/ReportTestResults' % | 
 |       sink['address'], | 
 |       headers={ | 
 |           'Content-Type': 'application/json', | 
 |           'Accept': 'application/json', | 
 |           'Authorization': 'ResultSink %s' % sink['auth_token'], | 
 |       }, | 
 |       data=json.dumps(result_json)) | 
 |   res.raise_for_status() | 
 |  | 
 |  | 
 | def execute_gtest_perf_test(command_generator, | 
 |                             output_paths, | 
 |                             use_xvfb=False, | 
 |                             is_unittest=False, | 
 |                             results_label=None): | 
 |   start = time.time() | 
 |  | 
 |   env = os.environ.copy() | 
 |   env['CHROME_HEADLESS'] = '1' | 
 |   # TODO(crbug.com/40153230): Some gtests do not implements the | 
 |   # unit_test_launcher.cc. As a result, they will not respect the arguments | 
 |   # added by _generate_shard_args() and will still use the values of | 
 |   # GTEST_SHARD_INDEX and GTEST_TOTAL_SHARDS to run part of the tests. | 
 |   # Removing those environment variables as a workaround. | 
 |   if command_generator.ignore_shard_env_vars: | 
 |     if 'GTEST_TOTAL_SHARDS' in env: | 
 |       env.pop('GTEST_TOTAL_SHARDS') | 
 |     if 'GTEST_SHARD_INDEX' in env: | 
 |       env.pop('GTEST_SHARD_INDEX') | 
 |  | 
 |   return_code = 0 | 
 |   try: | 
 |     command = command_generator.generate(output_paths.benchmark_path) | 
 |     if use_xvfb: | 
 |       # When running with xvfb, we currently output both to stdout and to the | 
 |       # file. It would be better to only output to the file to keep the logs | 
 |       # clean. | 
 |       return_code = xvfb.run_executable(command, | 
 |                                         env, | 
 |                                         stdoutfile=output_paths.logs) | 
 |     else: | 
 |       with open(output_paths.logs, 'w') as handle: | 
 |         try: | 
 |           return_code = test_env.run_command_output_to_handle(command, | 
 |                                                               handle, | 
 |                                                               env=env) | 
 |         except OSError as e: | 
 |           print('Command to run gtest perf test %s failed with an OSError: %s' % | 
 |                 (output_paths.name, e)) | 
 |           return_code = 1 | 
 |     if (not os.path.exists(output_paths.perf_results) | 
 |         and os.path.exists(output_paths.logs)): | 
 |       # Get the correct json format from the stdout to write to the perf | 
 |       # results file if gtest does not generate one. | 
 |       results_processor = generate_legacy_perf_dashboard_json.\ | 
 |           LegacyResultsProcessor() | 
 |       graph_json_string = results_processor.GenerateJsonResults( | 
 |           output_paths.logs) | 
 |       with open(output_paths.perf_results, 'w') as fh: | 
 |         fh.write(graph_json_string) | 
 |   except Exception:  # pylint: disable=broad-except | 
 |     traceback.print_exc() | 
 |     return_code = 1 | 
 |   if os.path.exists(output_paths.perf_results): | 
 |     executable_name = command_generator.executable_name | 
 |     if executable_name.startswith('bin/run_'): | 
 |       # The executable is a wrapper used by Fuchsia. Remove the prefix to get | 
 |       # the actual executable name. | 
 |       executable_name = executable_name[8:] | 
 |     if executable_name in GTEST_CONVERSION_WHITELIST: | 
 |       # //third_party/catapult/tracing imports. | 
 |       with path_util.SysPath(path_util.GetTracingDir()): | 
 |         # pylint: disable=no-name-in-module,import-outside-toplevel | 
 |         from tracing.value import gtest_json_converter | 
 |         # pylint: enable=no-name-in-module,import-outside-toplevel | 
 |       gtest_json_converter.ConvertGtestJsonFile(output_paths.perf_results, | 
 |                                                 label=results_label) | 
 |   else: | 
 |     print('ERROR: gtest perf test %s did not generate perf output' % | 
 |           output_paths.name) | 
 |     return_code = 1 | 
 |   write_simple_test_results(return_code, output_paths.test_results, | 
 |                             output_paths.name) | 
 |   if not is_unittest: | 
 |     upload_simple_test_results(return_code, output_paths.name) | 
 |  | 
 |   print_duration('executing gtest %s' % command_generator.executable_name, | 
 |                  start) | 
 |  | 
 |   return return_code | 
 |  | 
 |  | 
 | class _TelemetryFilterArgument(object): | 
 |  | 
 |   def __init__(self, filter_string): | 
 |     self.benchmark, self.story = filter_string.split('/') | 
 |  | 
 |  | 
 | class TelemetryCommandGenerator(object): | 
 |  | 
 |   def __init__(self, | 
 |                benchmark, | 
 |                options, | 
 |                story_selection_config=None, | 
 |                is_reference=False): | 
 |     self.benchmark = benchmark | 
 |     self._options = options | 
 |     self._story_selection_config = story_selection_config | 
 |     self._is_reference = is_reference | 
 |  | 
 |   def generate(self, output_dir): | 
 |     """Generate the command to run to start the benchmark. | 
 |  | 
 |     Args: | 
 |       output_dir: The directory to configure the command to put output files | 
 |         into. | 
 |  | 
 |     Returns: | 
 |       list of strings, the executable and its arguments. | 
 |     """ | 
 |     return ( | 
 |         [sys.executable] + self._options.executable.split(' ') + | 
 |         [self.benchmark] + self._generate_filter_args() + | 
 |         self._generate_also_run_disabled_tests_args() + | 
 |         self._generate_output_args(output_dir) + | 
 |         self._generate_story_selection_args() + | 
 |         # passthrough args must be before reference args and repeat args: | 
 |         # crbug.com/928928, crbug.com/894254#c78 | 
 |         self._get_passthrough_args() + self._generate_syslog_args() + | 
 |         self._generate_repeat_args() + self._generate_reference_build_args() + | 
 |         self._generate_results_label_args()) | 
 |  | 
 |   def _get_passthrough_args(self): | 
 |     return self._options.passthrough_args | 
 |  | 
 |   def _generate_filter_args(self): | 
 |     if self._options.isolated_script_test_filter: | 
 |       filter_list = common.extract_filter_list( | 
 |           self._options.isolated_script_test_filter) | 
 |       filter_arguments = [_TelemetryFilterArgument(f) for f in filter_list] | 
 |       applicable_stories = [ | 
 |           f.story for f in filter_arguments if f.benchmark == self.benchmark | 
 |       ] | 
 |       # Need to convert this to a valid regex. | 
 |       filter_regex = '(' + '|'.join(applicable_stories) + ')' | 
 |       return ['--story-filter=' + filter_regex] | 
 |     return [] | 
 |  | 
 |   def _generate_repeat_args(self): | 
 |     pageset_repeat = None | 
 |     if self._options.isolated_script_test_repeat: | 
 |       pageset_repeat = self._options.isolated_script_test_repeat | 
 |     elif (self._story_selection_config is not None | 
 |           and self._story_selection_config.get('pageset_repeat')): | 
 |       pageset_repeat = self._story_selection_config.get('pageset_repeat') | 
 |  | 
 |     if pageset_repeat: | 
 |       return ['--pageset-repeat=' + str(pageset_repeat)] | 
 |     return [] | 
 |  | 
 |   def _generate_also_run_disabled_tests_args(self): | 
 |     if self._options.isolated_script_test_also_run_disabled_tests: | 
 |       return ['--also-run-disabled-tests'] | 
 |     return [] | 
 |  | 
 |   def _generate_output_args(self, output_dir): | 
 |     if self._options.no_output_conversion: | 
 |       return ['--output-format=none', '--output-dir=' + output_dir] | 
 |     return [ | 
 |         '--output-format=json-test-results', '--output-format=histograms', | 
 |         '--output-dir=' + output_dir | 
 |     ] | 
 |  | 
 |   def _generate_story_selection_args(self): | 
 |     """Returns arguments that limit the stories to be run inside the benchmark. | 
 |     """ | 
 |     selection_args = [] | 
 |     if self._story_selection_config: | 
 |       if 'begin' in self._story_selection_config: | 
 |         selection_args.append('--story-shard-begin-index=%d' % | 
 |                               (self._story_selection_config['begin'])) | 
 |       if 'end' in self._story_selection_config: | 
 |         selection_args.append('--story-shard-end-index=%d' % | 
 |                               (self._story_selection_config['end'])) | 
 |       if 'sections' in self._story_selection_config: | 
 |         range_string = _generate_story_index_ranges( | 
 |             self._story_selection_config['sections']) | 
 |         if range_string: | 
 |           selection_args.append('--story-shard-indexes=%s' % range_string) | 
 |       if self._story_selection_config.get('abridged', True): | 
 |         selection_args.append('--run-abridged-story-set') | 
 |     return selection_args | 
 |  | 
 |   def _generate_syslog_args(self): | 
 |     if self._options.per_test_logs_dir: | 
 |       isolated_out_dir = os.path.dirname( | 
 |           self._options.isolated_script_test_output) | 
 |       return ['--logs-dir', os.path.join(isolated_out_dir, self.benchmark)] | 
 |     return [] | 
 |  | 
 |   def _generate_reference_build_args(self): | 
 |     if self._is_reference: | 
 |       reference_browser_flag = '--browser=reference' | 
 |       # TODO(crbug.com/40113070): Make the logic generic once more reference | 
 |       # settings are added | 
 |       if '--browser=android-chrome-bundle' in self._get_passthrough_args(): | 
 |         reference_browser_flag = '--browser=reference-android-chrome-bundle' | 
 |       return [reference_browser_flag, '--max-failures=5'] | 
 |     return [] | 
 |  | 
 |   def _generate_results_label_args(self): | 
 |     if self._options.results_label: | 
 |       return ['--results-label=' + self._options.results_label] | 
 |     return [] | 
 |  | 
 |  | 
 | def _generate_story_index_ranges(sections): | 
 |   range_string = '' | 
 |   for section in sections: | 
 |     begin = section.get('begin', '') | 
 |     end = section.get('end', '') | 
 |     # If there only one story in the range, we only keep its index. | 
 |     # In general, we expect either begin or end, or both. | 
 |     if begin != '' and end != '' and end - begin == 1: | 
 |       new_range = str(begin) | 
 |     elif begin != '' or end != '': | 
 |       new_range = '%s-%s' % (str(begin), str(end)) | 
 |     else: | 
 |       raise ValueError('Index ranges in "sections" in shard map should have' | 
 |                        'at least one of "begin" and "end": %s' % str(section)) | 
 |     if range_string: | 
 |       range_string += ',%s' % new_range | 
 |     else: | 
 |       range_string = new_range | 
 |   return range_string | 
 |  | 
 |  | 
 | def execute_telemetry_benchmark(command_generator, | 
 |                                 output_paths, | 
 |                                 use_xvfb=False, | 
 |                                 return_exit_code_zero=False, | 
 |                                 no_output_conversion=False): | 
 |   start = time.time() | 
 |  | 
 |   env = os.environ.copy() | 
 |   env['CHROME_HEADLESS'] = '1' | 
 |  | 
 |   return_code = 1 | 
 |   temp_dir = tempfile.mkdtemp('telemetry') | 
 |   infra_failure = False | 
 |   try: | 
 |     command = command_generator.generate(temp_dir) | 
 |     if use_xvfb: | 
 |       # When running with xvfb, we currently output both to stdout and to the | 
 |       # file. It would be better to only output to the file to keep the logs | 
 |       # clean. | 
 |       return_code = xvfb.run_executable(command, | 
 |                                         env=env, | 
 |                                         stdoutfile=output_paths.logs) | 
 |     else: | 
 |       with open(output_paths.logs, 'w') as handle: | 
 |         return_code = test_env.run_command_output_to_handle(command, | 
 |                                                             handle, | 
 |                                                             env=env) | 
 |     expected_results_filename = os.path.join(temp_dir, 'test-results.json') | 
 |     if os.path.exists(expected_results_filename): | 
 |       shutil.move(expected_results_filename, output_paths.test_results) | 
 |     else: | 
 |       common.write_interrupted_test_results_to(output_paths.test_results, start) | 
 |  | 
 |     if not no_output_conversion: | 
 |       expected_perf_filename = os.path.join(temp_dir, 'histograms.json') | 
 |       if os.path.exists(expected_perf_filename): | 
 |         shutil.move(expected_perf_filename, output_paths.perf_results) | 
 |       elif return_code: | 
 |         print(f'The benchmark failed with status code {return_code}, ' | 
 |               'and did not produce perf results output. ' | 
 |               'Check benchmark output for more details.') | 
 |       else: | 
 |         print('The benchmark returned a success status code, ' | 
 |               'but did not product perf results output.') | 
 |  | 
 |     csv_file_path = os.path.join(temp_dir, 'results.csv') | 
 |     if os.path.isfile(csv_file_path): | 
 |       shutil.move(csv_file_path, output_paths.csv_perf_results) | 
 |   except Exception:  # pylint: disable=broad-except | 
 |     print('The following exception may have prevented the code from ' | 
 |           'outputing structured test results and perf results output:') | 
 |     print(traceback.format_exc()) | 
 |     infra_failure = True | 
 |   finally: | 
 |     # On swarming bots, don't remove output directory, since Result Sink might | 
 |     # still be uploading files to Result DB. Also, swarming bots automatically | 
 |     # clean up at the end of each task. | 
 |     if 'SWARMING_TASK_ID' not in os.environ: | 
 |       # Add ignore_errors=True because otherwise rmtree may fail due to leaky | 
 |       # processes of tests are still holding opened handles to files under | 
 |       # |tempfile_dir|. For example, see crbug.com/865896 | 
 |       shutil.rmtree(temp_dir, ignore_errors=True) | 
 |  | 
 |   print_duration('executing benchmark %s' % command_generator.benchmark, start) | 
 |  | 
 |   if infra_failure: | 
 |     print('There was an infrastructure error encountered during the run. ' | 
 |           'Please check the logs above for details') | 
 |     return 1 | 
 |  | 
 |   # Telemetry sets exit code to -1 to indicate that no stories were run. This | 
 |   # becomes 255 on linux because linux doesn't support -1 so it does modulo: | 
 |   # -1 % 256 == 255. | 
 |   # TODO(crbug.com/40105219): Make 111 be the exit code that means | 
 |   # "no stories were run.". | 
 |   if return_code in (111, -1, 255): | 
 |     print('Exit code %s indicates that no stories were run, so we are marking ' | 
 |           'this as a success.' % return_code) | 
 |     return 0 | 
 |   if return_code: | 
 |     if return_exit_code_zero: | 
 |       print('run_benchmark returned exit code ' + str(return_code) + | 
 |             ' which indicates there were test failures in the run.') | 
 |       return 0 | 
 |     return return_code | 
 |   return 0 | 
 |  | 
 |  | 
 | def load_map_file(map_file, isolated_out_dir): | 
 |   """Loads the shard map file and copies it to isolated_out_dir.""" | 
 |   if not os.path.exists(map_file): | 
 |     map_file_path = SHARD_MAPS_DIR / map_file | 
 |     if map_file_path.exists(): | 
 |       map_file = str(map_file_path) | 
 |     else: | 
 |       raise Exception(f'Test shard map file not found: {map_file_path}') | 
 |   copy_map_file_to_out_dir(map_file, isolated_out_dir) | 
 |   with open(map_file) as f: | 
 |     return json.load(f) | 
 |  | 
 |  | 
 | def load_map_string(map_string, isolated_out_dir): | 
 |   """Loads the dynamic shard map string and writes it to isolated_out_dir.""" | 
 |   if not map_string: | 
 |     raise Exception('Use `--dynamic-shardmap` to pass the dynamic shard map') | 
 |   shard_map = json.loads(map_string, object_pairs_hook=OrderedDict) | 
 |   with tempfile.NamedTemporaryFile(mode='wb', delete=False) as tmp: | 
 |     tmp.write(bytes(map_string, 'utf-8')) | 
 |     tmp.close() | 
 |     copy_map_file_to_out_dir(tmp.name, isolated_out_dir) | 
 |   return shard_map | 
 |  | 
 |  | 
 | def copy_map_file_to_out_dir(map_file, isolated_out_dir): | 
 |   """Copies the sharding map file to isolated_out_dir for later collection.""" | 
 |   if not os.path.exists(isolated_out_dir): | 
 |     os.makedirs(isolated_out_dir) | 
 |   shutil.copyfile(map_file, | 
 |                   os.path.join(isolated_out_dir, 'benchmarks_shard_map.json')) | 
 |  | 
 |  | 
 | def fetch_binary_path(dependency_name, os_name='linux', arch='x86_64'): | 
 |   if binary_manager.NeedsInit(): | 
 |     binary_manager.InitDependencyManager(None) | 
 |   return binary_manager.FetchPath(dependency_name, os_name=os_name, arch=arch) | 
 |  | 
 |  | 
 | class CrossbenchTest(object): | 
 |   """This class is for running Crossbench tests. | 
 |  | 
 |   To run Crossbench tests, pass the relative path to `cb.py` as the | 
 |   `executable` argument, followed by `--benchmarks` to specify the | 
 |   target benchmark. The remaining Crossbench arguments are optional, | 
 |   and any passed arguments are sent to the `cb.py` for executing the test. | 
 |  | 
 |   Shard map: Use `crossbench-benchmarks` node in each shard group with a | 
 |   dictionary of benchmark names and, optionally, a list of arguments that need | 
 |   to pass through the `cb.py` tool. See `linux-perf-fyi_map.json` for examples. | 
 |  | 
 |   Example: | 
 |     ./run_performance_tests.py ../../third_party/crossbench/cb.py \ | 
 |     --isolated-script-test-output=/tmp/crossbench/ \ | 
 |     --benchmarks=speedometer \ | 
 |     --browser=../../out/linux/chrome \ | 
 |     --repeat=1 --probe='profiling' --story='jQuery.*' | 
 |   """ | 
 |  | 
 |   EXECUTABLE = 'cb.py' | 
 |   OUTDIR = '--out-dir=%s/output' | 
 |   CHROME_BROWSER = '--browser=%s' | 
 |   ANDROID_HJSON = ('{browser:"%s", driver:{type:"Android", ' | 
 |                    f'adb_bin:"{ADB_TOOL}", ' | 
 |                    f'bundletool:"{BUNDLETOOL}' | 
 |                    '"}}') | 
 |   STORY_LABEL = 'default' | 
 |   BENCHMARK_FILESERVERS = { | 
 |       'speedometer_3.1': 'third_party/speedometer/v3.1', | 
 |       'speedometer_3.0': 'third_party/speedometer/v3.0', | 
 |       'speedometer_3': 'third_party/speedometer/v3.1', | 
 |       'speedometer_2.1': 'third_party/speedometer/v2.1', | 
 |       'speedometer_2.0': 'third_party/speedometer/v2.0', | 
 |       'speedometer_2': 'third_party/speedometer/v2.1', | 
 |       'jetstream_2.2': 'third_party/jetstream/v2.2', | 
 |       'jetstream_2': 'third_party/jetstream/v2.2', | 
 |       'motionmark_1.3': 'third_party/blink/perf_tests/MotionMark' | 
 |   } | 
 |  | 
 |   def __init__(self, options, isolated_out_dir): | 
 |     self.options = options | 
 |     self._parse_arguments() | 
 |     self.isolated_out_dir = isolated_out_dir | 
 |     self.is_chrome = (not self.cb_options.official_browser | 
 |                       or self.cb_options.official_browser.startswith('chrome')) | 
 |     self.env = self._create_env_arg() | 
 |     if self.options.luci_chromium: | 
 |       # In luci.chromium the Chrome and driver are in the user path. | 
 |       self.browser = '--browser=%s' % get_abs_user_path('chrome') | 
 |       driver_path = get_abs_user_path('chromedriver') | 
 |       self.driver_path_arg = [f'--driver-path={driver_path}'] | 
 |       self.is_android = False | 
 |     else: | 
 |       browser_arg = _get_browser_arg(options.passthrough_args) | 
 |       self.is_android = _is_android(browser_arg) | 
 |       self._find_browser(browser_arg) | 
 |     self.network = self._get_network_arg(options.passthrough_args) | 
 |  | 
 |   def _parse_arguments(self): | 
 |     parser = argparse.ArgumentParser() | 
 |     parser.add_argument('--official-browser', | 
 |                         type=str, | 
 |                         required=False, | 
 |                         help='Use official build of the browser') | 
 |     self.cb_options, self.options.passthrough_args = parser.parse_known_args( | 
 |         self.options.passthrough_args) | 
 |  | 
 |   def _get_network_arg(self, args): | 
 |     if _arg := _get_arg(args, '--network='): | 
 |       return [_arg] | 
 |     if _arg := _get_arg(args, '--fileserver'): | 
 |       return self._create_fileserver_network(_arg) | 
 |     if _get_arg(args, '--wpr'): | 
 |       return self._create_wpr_network(args) | 
 |     if self.options.benchmarks.startswith('motionmark') and not self.is_android: | 
 |       # TODO(crbug.com/413452730): Enable local file server in all platforms. | 
 |       return [] | 
 |     if ((self.options.benchmarks in self.BENCHMARK_FILESERVERS) | 
 |         and not (self.options.benchmarks.startswith('speedometer') | 
 |                  and sys.platform == 'darwin')): | 
 |       # Use file server when it is available. | 
 |       arg = '--fileserver' | 
 |       args.append(arg) | 
 |       return self._create_fileserver_network(arg) | 
 |     return [] | 
 |  | 
 |   def _create_env_arg(self): | 
 |     if (self.options.benchmarks.startswith('motionmark') | 
 |         and sys.platform == 'darwin'): | 
 |       # Set screen refresh rate to 60Hz on Mac due to crbug.com/415318275. | 
 |       return ['--env={screen_refresh_rate:60}'] | 
 |     return [] | 
 |  | 
 |   def _create_fileserver_network(self, arg): | 
 |     if '=' in arg: | 
 |       fileserver_path = arg.split('=', 1)[1] | 
 |     else: | 
 |       benchmark = self.options.benchmarks | 
 |       if benchmark not in self.BENCHMARK_FILESERVERS: | 
 |         raise ValueError(f'fileserver does not support {benchmark}') | 
 |       fileserver_path = self.BENCHMARK_FILESERVERS.get(benchmark) | 
 |     fileserver_relative_path = str(CHROMIUM_SRC_DIR / fileserver_path) | 
 |     # Replacing --fileserver with --network. | 
 |     self.options.passthrough_args.remove(arg) | 
 |     return [ | 
 |         _create_network_json('local', | 
 |                              path=fileserver_relative_path, | 
 |                              url='http://localhost:0') | 
 |     ] | 
 |  | 
 |   def _create_wpr_network(self, args): | 
 |     wpr_arg = _get_arg(args, '--wpr') | 
 |     if wpr_arg and '=' in wpr_arg: | 
 |       wpr_name = wpr_arg.split('=', 1)[1] | 
 |     else: | 
 |       raise ValueError('The archive file path is missing!') | 
 |     archive = str(PAGE_SETS_DATA / wpr_name) | 
 |     if (wpr_go := fetch_binary_path('wpr_go')) is None: | 
 |       raise ValueError(f'wpr_go not found: {wpr_go}') | 
 |     if wpr_arg: | 
 |       # Replacing --wpr with --network. | 
 |       self.options.passthrough_args.remove(wpr_arg) | 
 |     skip_injection_arg = '--skip-wpr-script-injection' | 
 |     skip_injection = _get_arg(args, skip_injection_arg) | 
 |     if skip_injection: | 
 |       self.options.passthrough_args.remove(skip_injection_arg) | 
 |     return [ | 
 |         _create_network_json('wpr', | 
 |                              path=archive, | 
 |                              wpr_go_bin=wpr_go, | 
 |                              skip_injection=bool(skip_injection)) | 
 |     ] | 
 |  | 
 |   def _check_for_embedder_arg(self): | 
 |     embedder_arg = _get_arg(self.options.passthrough_args, '--embedder=') | 
 |     if embedder_arg: | 
 |       embedder_package_name = embedder_arg.split('=', 1)[1] | 
 |       # This will affect browser arg, but is not to be passed by itself | 
 |       self.options.passthrough_args.remove(embedder_arg) | 
 |       return embedder_package_name | 
 |     return None | 
 |  | 
 |   def _find_browser(self, browser_arg): | 
 |     # Replacing --browser with the generated self.browser. | 
 |     self.options.passthrough_args = [ | 
 |         arg for arg in self.options.passthrough_args | 
 |         if not arg.startswith('--browser=') | 
 |     ] | 
 |     if self.cb_options.official_browser: | 
 |       if self.is_android: | 
 |         android_json = self.ANDROID_HJSON % self.cb_options.official_browser | 
 |         self.browser = self.CHROME_BROWSER % android_json | 
 |       else: | 
 |         self.browser = self.CHROME_BROWSER % self.cb_options.official_browser | 
 |       self.driver_path_arg = [] | 
 |       return | 
 |     self.driver_path_arg = self._find_chromedriver() | 
 |     if '/' in browser_arg or '\\' in browser_arg: | 
 |       # The --browser arg looks like a path. Use it as-is. | 
 |       self.browser = self.CHROME_BROWSER % browser_arg | 
 |       return | 
 |     options = browser_options.BrowserFinderOptions() | 
 |     options.chrome_root = CHROMIUM_SRC_DIR | 
 |     parser = options.CreateParser() | 
 |     parser.parse_args([self.CHROME_BROWSER % browser_arg]) | 
 |     possible_browser = browser_finder.FindBrowser(options) | 
 |     if not possible_browser: | 
 |       raise ValueError(f'Unable to find Chrome browser of type: {browser_arg}') | 
 |     if self.is_android: | 
 |       # Check for an arg with embedder package name to override browser (WV) | 
 |       browser_app = (self._check_for_embedder_arg() | 
 |                      or possible_browser.settings.package) | 
 |       android_json = self.ANDROID_HJSON % browser_app | 
 |       self.browser = self.CHROME_BROWSER % android_json | 
 |     else: | 
 |       assert hasattr(possible_browser, 'local_executable') | 
 |       self.browser = self.CHROME_BROWSER % possible_browser.local_executable | 
 |  | 
 |   def _find_chromedriver(self): | 
 |     path = 'clang_x64' if self.is_android else '.' | 
 |     abspath = pathlib.Path(path).absolute() | 
 |     if ((driver_path := (abspath / 'chromedriver')).exists() | 
 |         or (driver_path := (abspath / 'chromedriver.exe')).exists()): | 
 |       return [f'--driver-path={driver_path}'] | 
 |     # Unable to find ChromeDriver, will rely on crossbench to download one. | 
 |     return [] | 
 |  | 
 |   def _get_default_args(self): | 
 |     default_args = ['--no-symlinks'] | 
 |     if self.is_chrome: | 
 |       # Required until crbug.com/41491492 and crbug.com/346323630 are fixed. | 
 |       default_args.append('--enable-features=DisablePrivacySandboxPrompts') | 
 |     if self.is_chrome and not self.is_android: | 
 |       # See http://shortn/_xGSaVM9P5g | 
 |       default_args.append('--enable-field-trial-config') | 
 |     if self.options.luci_chromium: | 
 |       default_args.append('--headless') | 
 |     return default_args | 
 |  | 
 |   def _generate_command_list(self, benchmark, benchmark_args, working_dir): | 
 |     return (['vpython3', '-Xutf8'] + [self.options.executable] + [benchmark] + | 
 |             ['--env-validation=throw'] + [self.OUTDIR % working_dir] + | 
 |             [self.browser] + benchmark_args + self.driver_path_arg + | 
 |             self.network + self.env + self._get_default_args()) | 
 |  | 
 |   def execute_benchmark(self, | 
 |                         benchmark, | 
 |                         display_name, | 
 |                         benchmark_args, | 
 |                         is_unittest=False): | 
 |     start = time.time() | 
 |  | 
 |     env = os.environ.copy() | 
 |     env['CHROME_HEADLESS'] = '1' | 
 |     env['PATH'] = f"{GSUTIL_DIR}{';' if IsWindows() else ':'}{env['PATH']}" | 
 |  | 
 |     return_code = 1 | 
 |     output_paths = OutputFilePaths(self.isolated_out_dir, display_name).SetUp() | 
 |     infra_failure = False | 
 |     try: | 
 |       command = self._generate_command_list(benchmark, benchmark_args, | 
 |                                             output_paths.benchmark_path) | 
 |       if self.options.xvfb: | 
 |         # When running with xvfb, we currently output both to stdout and to the | 
 |         # file. It would be better to only output to the file to keep the logs | 
 |         # clean. | 
 |         return_code = xvfb.run_executable(command, | 
 |                                           env=env, | 
 |                                           stdoutfile=output_paths.logs) | 
 |       else: | 
 |         with open(output_paths.logs, 'w') as handle: | 
 |           return_code = test_env.run_command_output_to_handle(command, | 
 |                                                               handle, | 
 |                                                               env=env) | 
 |  | 
 |       if return_code == 0 or self.options.ignore_benchmark_exit_code: | 
 |         crossbench_result_converter.convert( | 
 |             pathlib.Path(output_paths.benchmark_path) / 'output', | 
 |             pathlib.Path(output_paths.perf_results), display_name, | 
 |             self.STORY_LABEL, self.options.results_label) | 
 |       if return_code and os.path.exists(output_paths.logs): | 
 |         # To avoid printing too large log file, we print the last 100 lines. | 
 |         bottom_of_log = deque(maxlen=100) | 
 |         with open(output_paths.logs, 'r') as handle: | 
 |           for line in handle: | 
 |             if line.strip(): | 
 |               bottom_of_log.append(line.replace('\n', '')) | 
 |         print(f'The last 100 lines of {output_paths.logs}:') | 
 |         while bottom_of_log: | 
 |           print(f'    {bottom_of_log.popleft()}') | 
 |         print('See the complete logs in the CAS Outputs') | 
 |     except Exception:  # pylint: disable=broad-except | 
 |       print('The following exception may have prevented the code from ' | 
 |             'outputing structured test results and perf results output:') | 
 |       print(traceback.format_exc()) | 
 |       infra_failure = True | 
 |  | 
 |     if self.options.luci_chromium: | 
 |       write_simple_test_results(return_code, | 
 |                                 self.options.isolated_script_test_output, | 
 |                                 display_name) | 
 |     else: | 
 |       write_simple_test_results(return_code, output_paths.test_results, | 
 |                                 display_name) | 
 |       if not is_unittest: | 
 |         upload_simple_test_results(return_code, display_name) | 
 |  | 
 |     print_duration(f'Executing benchmark: {benchmark}', start) | 
 |  | 
 |     if infra_failure: | 
 |       print('There was an infrastructure error encountered during the run. ' | 
 |             'Please check the logs above for details') | 
 |       return 1 | 
 |  | 
 |     if return_code and self.options.ignore_benchmark_exit_code: | 
 |       print(f'crossbench returned exit code {return_code}' | 
 |             ' which indicates there were test failures in the run.') | 
 |       return 0 | 
 |     return return_code | 
 |  | 
 |   def execute(self): | 
 |     if not self.options.benchmarks: | 
 |       raise Exception('Please use the --benchmarks to specify the benchmark.') | 
 |     if ',' in self.options.benchmarks: | 
 |       raise Exception('No support to run multiple benchmarks at this time.') | 
 |     return self.execute_benchmark( | 
 |         self.options.benchmarks, | 
 |         (self.options.benchmark_display_name or self.options.benchmarks), | 
 |         self.options.passthrough_args) | 
 |  | 
 |  | 
 | def _create_network_json(config_type, | 
 |                          path, | 
 |                          url=None, | 
 |                          wpr_go_bin=None, | 
 |                          skip_injection=False): | 
 |   network_dict = {'type': config_type} | 
 |   network_dict['path'] = path | 
 |   if url: | 
 |     network_dict['url'] = url | 
 |   if wpr_go_bin: | 
 |     network_dict['wpr_go_bin'] = wpr_go_bin | 
 |   if skip_injection: | 
 |     network_dict['skip_injection'] = True | 
 |   network_json = json.dumps(network_dict) | 
 |   return f'--network={network_json}' | 
 |  | 
 |  | 
 | def _get_browser_arg(args): | 
 |   browser_arg = _get_arg(args, '--browser=', must_exists=True) | 
 |   return browser_arg.split('=', 1)[1] | 
 |  | 
 |  | 
 | def _get_arg(args, arg, must_exists=False): | 
 |   if _args := [a for a in args if a.startswith(arg)]: | 
 |     if len(_args) != 1: | 
 |       raise ValueError(f'Expects exactly one {arg} on command line') | 
 |     return _args[0] | 
 |   if must_exists: | 
 |     raise ValueError(f'{arg} argument is missing!') | 
 |   return [] | 
 |  | 
 |  | 
 | def _is_android(browser_arg): | 
 |   """Is the test running on an Android device. | 
 |  | 
 |   See third_party/catapult/telemetry/telemetry/internal/backends/android_browser_backend_settings.py  # pylint: disable=line-too-long | 
 |   """ | 
 |   return browser_arg.lower().startswith('android') | 
 |  | 
 |  | 
 | def parse_arguments(args): | 
 |   parser = argparse.ArgumentParser() | 
 |   parser.add_argument('executable', help='The name of the executable to run.') | 
 |   parser.add_argument('--isolated-script-test-output', required=True) | 
 |   # The following two flags may be passed in sometimes by Pinpoint | 
 |   # or by the recipe, but they don't do anything. crbug.com/927482. | 
 |   parser.add_argument('--isolated-script-test-chartjson-output', required=False) | 
 |   parser.add_argument('--isolated-script-test-perf-output', required=False) | 
 |  | 
 |   parser.add_argument('--isolated-script-test-filter', type=str, required=False) | 
 |  | 
 |   # Note that the following three arguments are only supported by Telemetry | 
 |   # tests right now. See crbug.com/920002. | 
 |   parser.add_argument('--isolated-script-test-repeat', type=int, required=False) | 
 |   parser.add_argument( | 
 |       '--isolated-script-test-launcher-retry-limit', | 
 |       type=int, | 
 |       required=False, | 
 |       choices=[0])  # Telemetry does not support retries. crbug.com/894254#c21 | 
 |   parser.add_argument('--isolated-script-test-also-run-disabled-tests', | 
 |                       default=False, | 
 |                       action='store_true', | 
 |                       required=False) | 
 |   parser.add_argument('--xvfb', help='Start xvfb.', action='store_true') | 
 |   parser.add_argument('--non-telemetry', | 
 |                       help='Type of perf test', | 
 |                       type=bool, | 
 |                       default=False) | 
 |   parser.add_argument('--gtest-benchmark-name', | 
 |                       help='Name of the gtest benchmark', | 
 |                       type=str, | 
 |                       required=False) | 
 |   parser.add_argument('--use-gtest-benchmark-script', | 
 |                       help='Whether gtest is invoked via benchmark script.', | 
 |                       default=False, | 
 |                       action='store_true') | 
 |  | 
 |   parser.add_argument('--benchmarks', | 
 |                       help='Comma separated list of benchmark names' | 
 |                       ' to run in lieu of indexing into our benchmark bot maps', | 
 |                       required=False) | 
 |   parser.add_argument('--benchmark-display-name', | 
 |                       help='Benchmark name displayed to the user,' | 
 |                       ' supported with crossbench only', | 
 |                       required=False) | 
 |   # Added to address android flakiness. | 
 |   parser.add_argument('--benchmark-max-runs', | 
 |                       help='Max number of benchmark runs until it succeeds.', | 
 |                       type=int, | 
 |                       required=False, | 
 |                       default=1) | 
 |   # crbug.com/1236245: This allows for per-benchmark device logs. | 
 |   parser.add_argument('--per-test-logs-dir', | 
 |                       help='Require --logs-dir args for test', | 
 |                       required=False, | 
 |                       default=False, | 
 |                       action='store_true') | 
 |   # Some executions may have a different sharding scheme and/or set of tests. | 
 |   # These files must live in src/tools/perf/core/shard_maps | 
 |   parser.add_argument('--test-shard-map-filename', type=str, required=False) | 
 |   parser.add_argument('--run-ref-build', | 
 |                       help='Run test on reference browser', | 
 |                       action='store_true') | 
 |   parser.add_argument('--passthrough-arg', | 
 |                       help='Arguments to pass directly through to the test ' | 
 |                       'executable.', | 
 |                       action='append', | 
 |                       dest='passthrough_args', | 
 |                       default=[]) | 
 |   parser.add_argument('--use-dynamic-shards', | 
 |                       help='If set, use dynamic shardmap instead of the file.', | 
 |                       action='store_true', | 
 |                       required=False) | 
 |   parser.add_argument('--dynamic-shardmap', | 
 |                       help='The dynamically generated shardmap string used to ' | 
 |                       'replace the static shardmap file.', | 
 |                       type=str, | 
 |                       required=False) | 
 |   parser.add_argument('--ignore-benchmark-exit-code', | 
 |                       help='If set, return an exit code 0 even if there' + | 
 |                       ' are benchmark failures', | 
 |                       action='store_true', | 
 |                       required=False) | 
 |   parser.add_argument('--results-label', | 
 |                       help='If set for a non-telemetry test, adds label to' + | 
 |                       ' the result histograms.', | 
 |                       type=str, | 
 |                       required=False) | 
 |   parser.add_argument('--no-output-conversion', | 
 |                       help='If supplied, trace conversion is not done.', | 
 |                       action='store_true', | 
 |                       required=False, | 
 |                       default=False) | 
 |   parser.add_argument('--luci-chromium', | 
 |                       help='Whether the test runs in `luci.chromium` (CQ/CI).', | 
 |                       action='store_true', | 
 |                       required=False, | 
 |                       default=False) | 
 |   options, leftover_args = parser.parse_known_args(args) | 
 |   options.passthrough_args.extend(leftover_args) | 
 |   return options | 
 |  | 
 |  | 
 | def _set_cwd(): | 
 |   """Change current working directory to build output directory. | 
 |  | 
 |   On perf waterfall, the recipe sets the current working directory to the chrome | 
 |   build output directory. Pinpoint, on the other hand, does not know where the | 
 |   build output directory is, so it is hardcoded to set current working directory | 
 |   to out/Release. This used to be correct most of the time, but this has been | 
 |   changed by https://crbug.com/355218109, causing various problems (e.g., | 
 |   https://crbug.com/377748127). This function attempts to detect such cases and | 
 |   change the current working directory to chrome output directory. | 
 |   """ | 
 |  | 
 |   # If the current directory is named out/Release and is empty, we are likely | 
 |   # running on Pinpoint with a wrong working directory. | 
 |   cwd = pathlib.Path.cwd() | 
 |   if list(cwd.iterdir()): | 
 |     return | 
 |   if cwd.name != 'Release' or cwd.parent.name != 'out': | 
 |     return | 
 |  | 
 |   print(f'Current directory {cwd} is empty, attempting to find build output') | 
 |   candidates = [] | 
 |   for build_dir in util.GetBuildDirectories(): | 
 |     path = pathlib.Path(build_dir).resolve() | 
 |     if path.exists() and list(path.iterdir()): | 
 |       candidates.append(path) | 
 |  | 
 |   if len(candidates) != 1: | 
 |     if not candidates: | 
 |       print('No build output directory found') | 
 |     else: | 
 |       print(f'Multiple build output directories found: {candidates}') | 
 |     raise RuntimeError( | 
 |         'Unable to find build output. Please change to the build output ' | 
 |         'directory before running this script.') | 
 |  | 
 |   print(f'Changing current directory to {candidates[0]}') | 
 |   os.chdir(candidates[0]) | 
 |  | 
 |  | 
 | def main(sys_args): | 
 |   sys.stdout.reconfigure(line_buffering=True) | 
 |   _set_cwd() | 
 |   args = sys_args[1:]  # Skip program name. | 
 |   options = parse_arguments(args) | 
 |   isolated_out_dir = os.path.dirname(options.isolated_script_test_output) | 
 |   overall_return_code = 0 | 
 |   # This is a list of test results files to be merged into a standard | 
 |   # output.json file for use by infrastructure including FindIt. | 
 |   # This list should not contain reference build runs | 
 |   # since we do not monitor those. Also, merging test reference build results | 
 |   # with standard build results may not work properly. | 
 |   test_results_files = [] | 
 |  | 
 |   print('Running a series of performance test subprocesses. Logs, performance\n' | 
 |         'results, and test results JSON will be saved in a subfolder of the\n' | 
 |         'isolated output directory. Inside the hash marks in the following\n' | 
 |         'lines is the name of the subfolder to find results in.\n') | 
 |  | 
 |   if options.use_dynamic_shards: | 
 |     shard_map = load_map_string(options.dynamic_shardmap, isolated_out_dir) | 
 |     overall_return_code = _run_benchmarks_on_shardmap(shard_map, options, | 
 |                                                       isolated_out_dir, | 
 |                                                       test_results_files) | 
 |   elif options.test_shard_map_filename: | 
 |     shard_map = load_map_file(options.test_shard_map_filename, isolated_out_dir) | 
 |     overall_return_code = _run_benchmarks_on_shardmap(shard_map, options, | 
 |                                                       isolated_out_dir, | 
 |                                                       test_results_files) | 
 |   elif options.executable.endswith(CrossbenchTest.EXECUTABLE): | 
 |     assert options.benchmark_max_runs == 1, ( | 
 |         'Benchmark rerun is not supported with CrossbenchTest.') | 
 |     overall_return_code = CrossbenchTest(options, isolated_out_dir).execute() | 
 |   elif options.non_telemetry: | 
 |     assert options.benchmark_max_runs == 1, ( | 
 |         'Benchmark rerun is not supported in non telemetry tests.') | 
 |     benchmark_name = options.gtest_benchmark_name | 
 |     passthrough_args = options.passthrough_args | 
 |     # crbug/1146949#c15 | 
 |     # In the case that pinpoint passes all arguments to swarming through http | 
 |     # request, the passthrough_args are converted into a comma-separated string. | 
 |     if passthrough_args and isinstance(passthrough_args, six.text_type): | 
 |       passthrough_args = passthrough_args.split(',') | 
 |     # With --non-telemetry, the gtest executable file path will be passed in as | 
 |     # options.executable, which is different from running on shard map. Thus, | 
 |     # we don't override executable as we do in running on shard map. | 
 |     command_generator = GtestCommandGenerator(options, | 
 |                                               additional_flags=passthrough_args, | 
 |                                               ignore_shard_env_vars=True) | 
 |     # Fallback to use the name of the executable if flag isn't set. | 
 |     # TODO(crbug.com/40588014): remove fallback logic and raise parser error if | 
 |     # --non-telemetry is set but --gtest-benchmark-name is not set once pinpoint | 
 |     # is converted to always pass --gtest-benchmark-name flag. | 
 |     if not benchmark_name: | 
 |       benchmark_name = options.executable | 
 |     output_paths = OutputFilePaths(isolated_out_dir, benchmark_name).SetUp() | 
 |     print_start(benchmark_name) | 
 |     overall_return_code = execute_gtest_perf_test( | 
 |         command_generator, | 
 |         output_paths, | 
 |         options.xvfb, | 
 |         results_label=options.results_label) | 
 |     test_results_files.append(output_paths.test_results) | 
 |   elif options.benchmarks: | 
 |     benchmarks = options.benchmarks.split(',') | 
 |     for benchmark in benchmarks: | 
 |       command_generator = TelemetryCommandGenerator(benchmark, options) | 
 |       for run_num in range(options.benchmark_max_runs): | 
 |         print_start(benchmark, run_num) | 
 |         output_paths = OutputFilePaths(isolated_out_dir, benchmark).SetUp() | 
 |         return_code = execute_telemetry_benchmark( | 
 |             command_generator, | 
 |             output_paths, | 
 |             options.xvfb, | 
 |             options.ignore_benchmark_exit_code, | 
 |             no_output_conversion=options.no_output_conversion) | 
 |         if return_code == 0: | 
 |           break | 
 |       overall_return_code = return_code or overall_return_code | 
 |       test_results_files.append(output_paths.test_results) | 
 |     if options.run_ref_build: | 
 |       print('Not running reference build. --run-ref-build argument is only ' | 
 |             'supported for sharded benchmarks. It is simple to support ' | 
 |             'this for unsharded --benchmarks if needed.') | 
 |   else: | 
 |     raise Exception('Telemetry tests must provide either a shard map or a ' | 
 |                     '--benchmarks list so that we know which stories to run.') | 
 |  | 
 |   # Dumping the test results. | 
 |   if test_results_files: | 
 |     test_results_list = [] | 
 |     for test_results_file in test_results_files: | 
 |       if os.path.exists(test_results_file): | 
 |         with open(test_results_file, 'r') as fh: | 
 |           test_results_list.append(json.load(fh)) | 
 |     merged_test_results = results_merger.merge_test_results(test_results_list) | 
 |     with open(options.isolated_script_test_output, 'w') as f: | 
 |       json.dump(merged_test_results, f) | 
 |  | 
 |   return overall_return_code | 
 |  | 
 |  | 
 | def _run_benchmarks_on_shardmap(shard_map, options, isolated_out_dir, | 
 |                                 test_results_files): | 
 |   overall_return_code = 0 | 
 |   # TODO(crbug.com/40631538): shard environment variables are not specified | 
 |   # for single-shard shard runs. | 
 |   if 'GTEST_SHARD_INDEX' not in os.environ and '1' in shard_map.keys(): | 
 |     raise Exception( | 
 |         'Setting GTEST_SHARD_INDEX environment variable is required ' | 
 |         'when you use a shard map.') | 
 |   shard_index = os.environ.get('GTEST_SHARD_INDEX', '0') | 
 |   shard_configuration = shard_map[shard_index] | 
 |   if not [x for x in shard_configuration if x in PERF_TOOLS]: | 
 |     raise Exception( | 
 |         f'None of {",".join(PERF_TOOLS)} presented in the shard map') | 
 |   if 'benchmarks' in shard_configuration: | 
 |     benchmarks_and_configs = shard_configuration['benchmarks'] | 
 |     for (benchmark, story_selection_config) in benchmarks_and_configs.items(): | 
 |       # Need to run the benchmark on both latest browser and reference | 
 |       # build. | 
 |       command_generator = TelemetryCommandGenerator( | 
 |           benchmark, options, story_selection_config=story_selection_config) | 
 |       for run_num in range(options.benchmark_max_runs): | 
 |         output_paths = OutputFilePaths(isolated_out_dir, benchmark).SetUp() | 
 |         print_start(benchmark, run_num) | 
 |         return_code = execute_telemetry_benchmark( | 
 |             command_generator, | 
 |             output_paths, | 
 |             options.xvfb, | 
 |             options.ignore_benchmark_exit_code, | 
 |             no_output_conversion=options.no_output_conversion) | 
 |         if return_code == 0: | 
 |           break | 
 |       overall_return_code = return_code or overall_return_code | 
 |       test_results_files.append(output_paths.test_results) | 
 |       if options.run_ref_build: | 
 |         reference_benchmark_foldername = benchmark + '.reference' | 
 |         reference_output_paths = OutputFilePaths( | 
 |             isolated_out_dir, reference_benchmark_foldername).SetUp() | 
 |         reference_command_generator = TelemetryCommandGenerator( | 
 |             benchmark, | 
 |             options, | 
 |             story_selection_config=story_selection_config, | 
 |             is_reference=True) | 
 |         print_start(reference_benchmark_foldername) | 
 |         # We intentionally ignore the return code and test results of the | 
 |         # reference build. | 
 |         execute_telemetry_benchmark( | 
 |             reference_command_generator, | 
 |             reference_output_paths, | 
 |             options.xvfb, | 
 |             options.ignore_benchmark_exit_code, | 
 |             no_output_conversion=options.no_output_conversion) | 
 |   if 'executables' in shard_configuration: | 
 |     names_and_configs = shard_configuration['executables'] | 
 |     for (name, configuration) in names_and_configs.items(): | 
 |       additional_flags = [] | 
 |       if 'arguments' in configuration: | 
 |         additional_flags = configuration['arguments'] | 
 |       command_generator = GtestCommandGenerator( | 
 |           options, | 
 |           override_executable=configuration['path'], | 
 |           additional_flags=additional_flags, | 
 |           ignore_shard_env_vars=True) | 
 |       for run_num in range(options.benchmark_max_runs): | 
 |         output_paths = OutputFilePaths(isolated_out_dir, name).SetUp() | 
 |         print_start(name, run_num) | 
 |         return_code = execute_gtest_perf_test(command_generator, output_paths, | 
 |                                               options.xvfb) | 
 |         if return_code == 0: | 
 |           break | 
 |       overall_return_code = return_code or overall_return_code | 
 |       test_results_files.append(output_paths.test_results) | 
 |   if 'crossbench' in shard_configuration: | 
 |     benchmarks = shard_configuration['crossbench'] | 
 |     # Overwriting the "run_benchmark" with the Crossbench tool. | 
 |     options.executable = str(CROSSBENCH_TOOL) | 
 |     original_passthrough_args = options.passthrough_args.copy() | 
 |     for benchmark, benchmark_config in benchmarks.items(): | 
 |       display_name = benchmark_config.get('display_name', benchmark) | 
 |       if benchmark_args := benchmark_config.get('arguments', []): | 
 |         options.passthrough_args.extend(benchmark_args) | 
 |       options.benchmarks = benchmark | 
 |       crossbench_test = CrossbenchTest(options, isolated_out_dir) | 
 |       # CrossbenchTest may filter some arguments. | 
 |       benchmark_args = [ | 
 |           x for x in benchmark_args if x in options.passthrough_args | 
 |       ] | 
 |       for run_num in range(options.benchmark_max_runs): | 
 |         print_start(display_name, run_num) | 
 |         return_code = crossbench_test.execute_benchmark(benchmark, display_name, | 
 |                                                         benchmark_args) | 
 |         if return_code == 0: | 
 |           break | 
 |       overall_return_code = return_code or overall_return_code | 
 |       test_results_files.append( | 
 |           OutputFilePaths(isolated_out_dir, display_name).test_results) | 
 |       options.passthrough_args = original_passthrough_args.copy() | 
 |  | 
 |   return overall_return_code | 
 |  | 
 |  | 
 | # This is not really a "script test" so does not need to manually add | 
 | # any additional compile targets. | 
 | def main_compile_targets(args): | 
 |   json.dump([], args.output) | 
 |  | 
 |  | 
 | if __name__ == '__main__': | 
 |   # Conform minimally to the protocol defined by ScriptTest. | 
 |   if 'compile_targets' in sys.argv: | 
 |     funcs = { | 
 |         'run': None, | 
 |         'compile_targets': main_compile_targets, | 
 |     } | 
 |     sys.exit(common.run_script(sys.argv[1:], funcs)) | 
 |  | 
 |   sys.exit(main(sys.argv)) |