| OLD | NEW |
| 1 # Copyright 2015 The Chromium Authors. All rights reserved. | 1 # Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import os | 5 import os |
| 6 import re | 6 import re |
| 7 import time | 7 import time |
| 8 | 8 |
| 9 from . import parse_metric | |
| 10 | |
| 11 | 9 |
| 12 class Metric(object): # pragma: no cover | 10 class Metric(object): # pragma: no cover |
| 13 OLD_STYLE_DELIMITER = '-' | 11 OLD_STYLE_DELIMITER = '-' |
| 14 NEW_STYLE_DELIMITER = '@@' | 12 NEW_STYLE_DELIMITER = '@@' |
| 15 | 13 |
| 16 def __init__(self, metric_string): | 14 def __init__(self, metric_string): |
| 17 parts = metric_string.split('/') | 15 parts = metric_string.split('/') |
| 18 self.chart_name = None | 16 self.chart_name = None |
| 19 self.interaction_record_name = None | 17 self.interaction_record_name = None |
| 20 self.trace_name = None | 18 self.trace_name = None |
| (...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 65 # processed -- that is, \t is converted to a tab character, etc. Hence we | 63 # processed -- that is, \t is converted to a tab character, etc. Hence we |
| 66 # use a placeholder with no backslashes and later replace with str.replace. | 64 # use a placeholder with no backslashes and later replace with str.replace. |
| 67 command = out_dir_regex.sub(placeholder, command) | 65 command = out_dir_regex.sub(placeholder, command) |
| 68 return command.replace(placeholder, new_arg) | 66 return command.replace(placeholder, new_arg) |
| 69 | 67 |
| 70 | 68 |
| 71 def _is_telemetry_command(command): | 69 def _is_telemetry_command(command): |
| 72 """Attempts to discern whether or not a given command is running telemetry.""" | 70 """Attempts to discern whether or not a given command is running telemetry.""" |
| 73 return 'run_benchmark' in command | 71 return 'run_benchmark' in command |
| 74 | 72 |
| 75 | |
| 76 def run_perf_test(api, test_config, **kwargs): | 73 def run_perf_test(api, test_config, **kwargs): |
| 77 """Runs the command N times and parses a metric from the output.""" | 74 """Runs the command N times and parses a metric from the output.""" |
| 78 # TODO(prasadv): Consider extracting out the body of the for loop into | 75 # TODO(prasadv): Consider extracting out the body of the for loop into |
| 79 # a helper method, or extract the metric-extraction to make this more | 76 # a helper method, or extract the metric-extraction to make this more |
| 80 # cleaner. | 77 # cleaner. |
| 81 limit = test_config['max_time_minutes'] * kwargs.get('time_multiplier', 1) | 78 limit = test_config['max_time_minutes'] * kwargs.get('time_multiplier', 1) |
| 82 run_results = {'measured_values': [], 'errors': set()} | 79 results = {'valueset_paths': [], 'chartjson_paths': [], 'errors': set(), |
| 83 values = run_results['measured_values'] | 80 'retcodes': [], 'stdout_paths': [], 'output': []} |
| 84 metric = test_config.get('metric') | 81 metric = test_config.get('metric') |
| 85 retcodes = [] | |
| 86 output_for_all_runs = [] | |
| 87 temp_dir = None | 82 temp_dir = None |
| 88 repeat_cnt = test_config['repeat_count'] | 83 repeat_count = test_config['repeat_count'] |
| 89 | 84 |
| 90 command = test_config['command'] | 85 command = test_config['command'] |
| 91 use_chartjson = bool('chartjson' in command) | 86 use_chartjson = bool('chartjson' in command) |
| 87 use_valueset = bool('valueset' in command) |
| 88 use_buildbot = bool('buildbot' in command) |
| 92 is_telemetry = _is_telemetry_command(command) | 89 is_telemetry = _is_telemetry_command(command) |
| 93 start_time = time.time() | 90 start_time = time.time() |
| 94 | 91 |
| 95 if api.m.chromium.c.TARGET_PLATFORM == 'android' and is_telemetry: | 92 if api.m.chromium.c.TARGET_PLATFORM == 'android' and is_telemetry: |
| 96 device_serial_number = api.device_to_test; | 93 device_serial_number = api.device_to_test; |
| 97 if device_serial_number: | 94 if device_serial_number: |
| 98 command += ' --device ' + device_serial_number # pragma: no cover | 95 command += ' --device ' + device_serial_number # pragma: no cover |
| 99 | 96 |
| 100 for i in range(repeat_cnt): | 97 for i in range(repeat_count): |
| 101 elapsed_minutes = (time.time() - start_time) / 60.0 | 98 elapsed_minutes = (time.time() - start_time) / 60.0 |
| 102 # A limit of 0 means 'no timeout set'. | 99 # A limit of 0 means 'no timeout set'. |
| 103 if limit and elapsed_minutes >= limit: # pragma: no cover | 100 if limit and elapsed_minutes >= limit: # pragma: no cover |
| 104 break | 101 break |
| 105 if is_telemetry: | 102 if is_telemetry: |
| 106 if i == 0 and kwargs.get('reset_on_first_run'): | 103 if i == 0 and kwargs.get('reset_on_first_run'): |
| 107 command += ' --reset-results' | 104 command += ' --reset-results' |
| 108 if i == repeat_cnt - 1 and kwargs.get('upload_on_last_run'): | 105 if i == repeat_count - 1 and kwargs.get('upload_on_last_run'): |
| 109 command += ' --upload-results' | 106 command += ' --upload-results' |
| 110 if kwargs.get('results_label'): | 107 if kwargs.get('results_label'): |
| 111 command += ' --results-label=%s' % kwargs.get('results_label') | 108 command += ' --results-label=%s' % kwargs.get('results_label') |
| 112 if use_chartjson: # pragma: no cover | 109 temp_dir = api.m.path.mkdtemp('perf-test-output') |
| 113 temp_dir = api.m.path.mkdtemp('perf-test-output') | 110 if use_chartjson or use_valueset: # pragma: no cover |
| 114 command = _set_output_dir(command, str(temp_dir)) | 111 command = _set_output_dir(command, str(temp_dir)) |
| 115 results_path = temp_dir.join('results-chart.json') | 112 chartjson_path = temp_dir.join('results-chart.json') |
| 113 valueset_path = temp_dir.join('results-valueset.json') |
| 116 | 114 |
| 117 step_name = "Performance Test%s %d of %d" % ( | 115 step_name = "Performance Test%s %d of %d" % ( |
| 118 ' (%s)' % kwargs['name'] if 'name' in kwargs else '', i + 1, repeat_cnt) | 116 ' (%s)' % kwargs['name'] if 'name' in kwargs else '', i + 1, repeat_coun
t) |
| 119 if api.m.platform.is_linux: | 117 if api.m.platform.is_linux: |
| 120 os.environ['CHROME_DEVEL_SANDBOX'] = api.m.path.join( | 118 os.environ['CHROME_DEVEL_SANDBOX'] = api.m.path.join( |
| 121 '/opt', 'chromium', 'chrome_sandbox') | 119 '/opt', 'chromium', 'chrome_sandbox') |
| 122 out, err, retcode = _run_command(api, command, step_name) | 120 out, err, retcode = _run_command(api, command, step_name, **kwargs) |
| 121 results['output'].append(out or '') |
| 122 if out: |
| 123 # Write stdout to a local temp location for possible buildbot parsing |
| 124 stdout_path = temp_dir.join('results.txt') |
| 125 api.m.file.write('write buildbot output to disk', stdout_path, out) |
| 126 results['stdout_paths'].append(stdout_path) |
| 123 | 127 |
| 124 if out is None and err is None: | 128 if metric: |
| 125 # dummy value when running test TODO: replace with a mock | |
| 126 values.append(0) | |
| 127 elif metric: # pragma: no cover | |
| 128 if use_chartjson: | 129 if use_chartjson: |
| 129 step_result = api.m.json.read( | 130 try: |
| 130 'Reading chartjson results', results_path) | 131 step_result = api.m.json.read( |
| 131 has_valid_value, value = find_values( | 132 'Reading chartjson results', chartjson_path) |
| 132 step_result.json.output, Metric(metric)) | 133 except api.m.step.StepFailure: # pragma: no cover |
| 133 else: | 134 pass |
| 134 has_valid_value, value = parse_metric.parse_metric( | 135 else: |
| 135 out, err, metric.split('/')) | 136 if step_result.json.output: # pragma: no cover |
| 136 output_for_all_runs.append(out) | 137 results['chartjson_paths'].append(chartjson_path) |
| 137 if has_valid_value: | 138 if use_valueset: |
| 138 values.extend(value) | 139 try: |
| 139 else: | 140 step_result = api.m.json.read( |
| 140 # This means the metric was not found in the output. | 141 'Reading valueset results', valueset_path, |
| 141 if not retcode: | 142 step_test_data=lambda: api.m.json.test_api.output( |
| 142 # If all tests passed, but the metric was not found, this means that | 143 {'dummy':'dict'})) |
| 143 # something changed on the test, or the given metric name was | 144 except api.m.step.StepFailure: # pragma: no cover |
| 144 # incorrect, we need to surface this on the bisector. | 145 pass |
| 145 run_results['errors'].add('MISSING_METRIC') | 146 else: |
| 146 else: | 147 if step_result.json.output: |
| 147 output_for_all_runs.append(out) | 148 results['valueset_paths'].append(valueset_path) |
| 148 retcodes.append(retcode) | 149 results['retcodes'].append(retcode) |
| 149 | 150 return results |
| 150 return run_results, output_for_all_runs, retcodes | |
| 151 | |
| 152 | |
| 153 def find_values(results, metric): # pragma: no cover | |
| 154 """Tries to extract the given metric from the given results. | |
| 155 | |
| 156 This method tries several different possible chart names depending | |
| 157 on the given metric. | |
| 158 | |
| 159 Args: | |
| 160 results: The chartjson dict. | |
| 161 metric: A Metric instance. | |
| 162 | |
| 163 Returns: | |
| 164 A pair (has_valid_value, value), where has_valid_value is a boolean, | |
| 165 and value is the value(s) extracted from the results. | |
| 166 """ | |
| 167 has_valid_value, value, _ = parse_metric.parse_chartjson_metric( | |
| 168 results, metric.as_pair()) | |
| 169 if has_valid_value: | |
| 170 return True, value | |
| 171 | |
| 172 # TODO(eakuefner): Get rid of this fallback when bisect uses ToT Telemetry. | |
| 173 has_valid_value, value, _ = parse_metric.parse_chartjson_metric( | |
| 174 results, metric.as_pair(Metric.OLD_STYLE_DELIMITER)) | |
| 175 if has_valid_value: | |
| 176 return True, value | |
| 177 | |
| 178 # If we still haven't found a valid value, it's possible that the metric was | |
| 179 # specified as interaction-chart/trace or interaction-chart/interaction-chart, | |
| 180 # and the chartjson chart names use @@ as the separator between interaction | |
| 181 # and chart names. | |
| 182 if Metric.OLD_STYLE_DELIMITER not in metric.chart_name: | |
| 183 return False, [] # Give up; no results found. | |
| 184 interaction, chart = metric.chart_name.split(Metric.OLD_STYLE_DELIMITER, 1) | |
| 185 metric.interaction_record_name = interaction | |
| 186 metric.chart_name = chart | |
| 187 has_valid_value, value, _ = parse_metric.parse_chartjson_metric( | |
| 188 results, metric.as_pair()) | |
| 189 return has_valid_value, value | |
| 190 | 151 |
| 191 def _rebase_path(api, file_path): | 152 def _rebase_path(api, file_path): |
| 192 """Attempts to make an absolute path for the command. | 153 """Attempts to make an absolute path for the command. |
| 193 | 154 |
| 194 We want to pass to runtest.py an absolute path if possible. | 155 We want to pass to runtest.py an absolute path if possible. |
| 195 """ | 156 """ |
| 196 if (file_path.startswith('src/') or file_path.startswith('./src/')): | 157 if (file_path.startswith('src/') or file_path.startswith('./src/')): |
| 197 return api.m.path['checkout'].join( | 158 return api.m.path['checkout'].join( |
| 198 *file_path.split('src', 1)[1].split('/')[1:]) | 159 *file_path.split('src', 1)[1].split('/')[1:]) |
| 199 elif (file_path.startswith('src\\') or | 160 elif (file_path.startswith('src\\') or |
| 200 file_path.startswith('.\\src\\')): # pragma: no cover | 161 file_path.startswith('.\\src\\')): # pragma: no cover |
| 201 return api.m.path['checkout'].join( | 162 return api.m.path['checkout'].join( |
| 202 *file_path.split('src', 1)[1].split('\\')[1:]) | 163 *file_path.split('src', 1)[1].split('\\')[1:]) |
| 203 return file_path | 164 return file_path |
| 204 | 165 |
| 205 def _run_command(api, command, step_name): | 166 def _run_command(api, command, step_name, **kwargs): |
| 206 command_parts = command.split() | 167 command_parts = command.split() |
| 207 stdout = api.m.raw_io.output() | 168 stdout = api.m.raw_io.output() |
| 208 stderr = api.m.raw_io.output() | 169 stderr = api.m.raw_io.output() |
| 209 | 170 |
| 171 inner_kwargs = {} |
| 172 if 'step_test_data' in kwargs: |
| 173 inner_kwargs['step_test_data'] = kwargs['step_test_data'] |
| 210 # TODO(prasadv): Remove this once bisect runs are no longer running | 174 # TODO(prasadv): Remove this once bisect runs are no longer running |
| 211 # against revisions from February 2016 or earlier. | 175 # against revisions from February 2016 or earlier. |
| 212 kwargs = {} | |
| 213 if 'android-chrome' in command: # pragma: no cover | 176 if 'android-chrome' in command: # pragma: no cover |
| 214 kwargs['env'] = {'CHROMIUM_OUTPUT_DIR': api.m.chromium.output_dir} | 177 inner_kwargs['env'] = {'CHROMIUM_OUTPUT_DIR': api.m.chromium.output_dir} |
| 215 | 178 |
| 216 # By default, we assume that the test to run is an executable binary. In the | 179 # By default, we assume that the test to run is an executable binary. In the |
| 217 # case of python scripts, runtest.py will guess based on the extension. | 180 # case of python scripts, runtest.py will guess based on the extension. |
| 218 python_mode = False | 181 python_mode = False |
| 219 if command_parts[0] == 'python': # pragma: no cover | 182 if command_parts[0] == 'python': # pragma: no cover |
| 220 # Dashboard prepends the command with 'python' when on windows, however, it | 183 # Dashboard prepends the command with 'python' when on windows, however, it |
| 221 # is not necessary to pass this along to the runtest.py invocation. | 184 # is not necessary to pass this along to the runtest.py invocation. |
| 222 # TODO(robertocn): Remove this clause when dashboard stops sending python as | 185 # TODO(robertocn): Remove this clause when dashboard stops sending python as |
| 223 # part of the command. | 186 # part of the command. |
| 224 # https://github.com/catapult-project/catapult/issues/2283 | 187 # https://github.com/catapult-project/catapult/issues/2283 |
| 225 command_parts = command_parts[1:] | 188 command_parts = command_parts[1:] |
| 226 python_mode = True | 189 python_mode = True |
| 227 elif _is_telemetry_command(command): | 190 elif _is_telemetry_command(command): |
| 228 # run_benchmark is a python script without an extension, hence we force | 191 # run_benchmark is a python script without an extension, hence we force |
| 229 # python mode. | 192 # python mode. |
| 230 python_mode = True | 193 python_mode = True |
| 231 try: | 194 try: |
| 232 step_result = api.m.chromium.runtest( | 195 step_result = api.m.chromium.runtest( |
| 233 test=_rebase_path(api, command_parts[0]), | 196 test=_rebase_path(api, command_parts[0]), |
| 234 args=command_parts[1:], | 197 args=command_parts[1:], |
| 235 xvfb=True, | 198 xvfb=True, |
| 236 name=step_name, | 199 name=step_name, |
| 237 python_mode=python_mode, | 200 python_mode=python_mode, |
| 238 stdout=stdout, | 201 stdout=stdout, |
| 239 stderr=stderr, | 202 stderr=stderr, |
| 240 **kwargs) | 203 **inner_kwargs) |
| 241 step_result.presentation.logs['Captured Output'] = ( | 204 step_result.presentation.logs['Captured Output'] = ( |
| 242 step_result.stdout or '').splitlines() | 205 step_result.stdout or '').splitlines() |
| 243 except api.m.step.StepFailure as sf: | 206 except api.m.step.StepFailure as sf: |
| 244 sf.result.presentation.logs['Failure Output'] = ( | 207 sf.result.presentation.logs['Failure Output'] = ( |
| 245 sf.result.stdout or '').splitlines() | 208 sf.result.stdout or '').splitlines() |
| 246 if sf.result.stderr: # pragma: no cover | 209 if sf.result.stderr: # pragma: no cover |
| 247 sf.result.presentation.logs['stderr'] = ( | 210 sf.result.presentation.logs['stderr'] = ( |
| 248 sf.result.stderr).splitlines() | 211 sf.result.stderr).splitlines() |
| 249 return sf.result.stdout, sf.result.stderr, sf.result.retcode | 212 return sf.result.stdout, sf.result.stderr, sf.result.retcode |
| 250 return step_result.stdout, step_result.stderr, step_result.retcode | 213 return step_result.stdout, step_result.stderr, step_result.retcode |
| OLD | NEW |