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 |