OLD | NEW |
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2013 The Chromium Authors. All rights reserved. |
3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
5 | 5 |
6 """Run Performance Test Bisect Tool | 6 """Run Performance Test Bisect Tool |
7 | 7 |
8 This script is used by a trybot to run the src/tools/bisect-perf-regression.py | 8 This script is used by a trybot to run the src/tools/bisect-perf-regression.py |
9 script with the parameters specified in run-bisect-perf-regression.cfg. It will | 9 script with the parameters specified in run-bisect-perf-regression.cfg. It will |
10 check out a copy of the depot in a subdirectory 'bisect' of the working | 10 check out a copy of the depot in a subdirectory 'bisect' of the working |
11 directory provided, and run the bisect-perf-regression.py script there. | 11 directory provided, and run the bisect-perf-regression.py script there. |
12 | 12 |
13 """ | 13 """ |
14 | 14 |
15 import imp | 15 import imp |
16 import optparse | 16 import optparse |
17 import os | 17 import os |
18 import subprocess | 18 import subprocess |
19 import sys | 19 import sys |
20 import traceback | 20 import traceback |
21 | 21 |
| 22 import bisect_utils |
| 23 bisect = imp.load_source('bisect-perf-regression', |
| 24 os.path.join(os.path.abspath(os.path.dirname(sys.argv[0])), |
| 25 'bisect-perf-regression.py')) |
| 26 |
| 27 |
22 CROS_BOARD_ENV = 'BISECT_CROS_BOARD' | 28 CROS_BOARD_ENV = 'BISECT_CROS_BOARD' |
23 CROS_IP_ENV = 'BISECT_CROS_IP' | 29 CROS_IP_ENV = 'BISECT_CROS_IP' |
24 | 30 |
25 def LoadConfigFile(path_to_file): | 31 |
26 """Attempts to load the file 'run-bisect-perf-regression.cfg' as a module | 32 class Goma(object): |
| 33 |
| 34 def __init__(self, path_to_goma): |
| 35 self._abs_path_to_goma = None |
| 36 self._abs_path_to_goma_file = None |
| 37 if path_to_goma: |
| 38 self._abs_path_to_goma = os.path.abspath(path_to_goma) |
| 39 self._abs_path_to_goma_file = self._GetExecutablePath( |
| 40 self._abs_path_to_goma) |
| 41 |
| 42 def __enter__(self): |
| 43 if self._HasGOMAPath(): |
| 44 self._SetupAndStart() |
| 45 return self |
| 46 |
| 47 def __exit__(self, *_): |
| 48 if self._HasGOMAPath(): |
| 49 self._Stop() |
| 50 |
| 51 def _HasGOMAPath(self): |
| 52 return bool(self._abs_path_to_goma) |
| 53 |
| 54 def _GetExecutablePath(self, path_to_goma): |
| 55 if os.name == 'nt': |
| 56 return os.path.join(path_to_goma, 'goma_ctl.bat') |
| 57 else: |
| 58 return os.path.join(path_to_goma, 'goma_ctl.sh') |
| 59 |
| 60 def _SetupEnvVars(self): |
| 61 if os.name == 'nt': |
| 62 os.environ['CC'] = (os.path.join(self._abs_path_to_goma, 'gomacc.exe') + |
| 63 ' cl.exe') |
| 64 os.environ['CXX'] = (os.path.join(self._abs_path_to_goma, 'gomacc.exe') + |
| 65 ' cl.exe') |
| 66 else: |
| 67 os.environ['PATH'] = os.pathsep.join([self._abs_path_to_goma, |
| 68 os.environ['PATH']]) |
| 69 |
| 70 def _SetupAndStart(self): |
| 71 """Sets up GOMA and launches it. |
| 72 |
| 73 Args: |
| 74 path_to_goma: Path to goma directory. |
| 75 |
| 76 Returns: |
| 77 True if successful.""" |
| 78 self._SetupEnvVars() |
| 79 |
| 80 # Sometimes goma is lingering around if something went bad on a previous |
| 81 # run. Stop it before starting a new process. Can ignore the return code |
| 82 # since it will return an error if it wasn't running. |
| 83 self._Stop() |
| 84 |
| 85 if subprocess.call([self._abs_path_to_goma_file, 'start']): |
| 86 raise RuntimeError('GOMA failed to start.') |
| 87 |
| 88 def _Stop(self): |
| 89 subprocess.call([self._abs_path_to_goma_file, 'stop']) |
| 90 |
| 91 |
| 92 |
| 93 def _LoadConfigFile(path_to_file): |
| 94 """Attempts to load the specified config file as a module |
27 and grab the global config dict. | 95 and grab the global config dict. |
28 | 96 |
29 Args: | 97 Args: |
30 path_to_file: Path to the run-bisect-perf-regression.cfg file. | 98 path_to_file: Path to the file. |
31 | 99 |
32 Returns: | 100 Returns: |
33 The config dict which should be formatted as follows: | 101 The config dict which should be formatted as follows: |
34 {'command': string, 'good_revision': string, 'bad_revision': string | 102 {'command': string, 'good_revision': string, 'bad_revision': string |
35 'metric': string}. | 103 'metric': string, etc...}. |
36 Returns None on failure. | 104 Returns None on failure. |
37 """ | 105 """ |
38 try: | 106 try: |
39 local_vars = {} | 107 local_vars = {} |
40 execfile(os.path.join(path_to_file, 'run-bisect-perf-regression.cfg'), | 108 execfile(path_to_file, local_vars) |
41 local_vars) | |
42 | 109 |
43 return local_vars['config'] | 110 return local_vars['config'] |
44 except: | 111 except: |
45 print | 112 print |
46 traceback.print_exc() | 113 traceback.print_exc() |
47 print | 114 print |
48 return None | 115 return None |
49 | 116 |
50 | 117 |
51 def RunBisectionScript(config, working_directory, path_to_file, path_to_goma): | 118 def _OutputFailedResults(text_to_print): |
| 119 bisect_utils.OutputAnnotationStepStart('Results - Failed') |
| 120 print |
| 121 print text_to_print |
| 122 print |
| 123 bisect_utils.OutputAnnotationStepClosed() |
| 124 |
| 125 |
| 126 def _CreateBisectOptionsFromConfig(config): |
| 127 opts_dict = {} |
| 128 opts_dict['command'] = config['command'] |
| 129 opts_dict['metric'] = config['metric'] |
| 130 |
| 131 if config['repeat_count']: |
| 132 opts_dict['repeat_test_count'] = int(config['repeat_count']) |
| 133 |
| 134 if config['truncate_percent']: |
| 135 opts_dict['truncate_percent'] = int(config['truncate_percent']) |
| 136 |
| 137 if config['max_time_minutes']: |
| 138 opts_dict['max_time_minutes'] = int(config['max_time_minutes']) |
| 139 |
| 140 if config.has_key('use_goma'): |
| 141 opts_dict['use_goma'] = config['use_goma'] |
| 142 |
| 143 opts_dict['build_preference'] = 'ninja' |
| 144 opts_dict['output_buildbot_annotations'] = True |
| 145 |
| 146 if '--browser=cros' in config['command']: |
| 147 opts_dict['target_platform'] = 'cros' |
| 148 |
| 149 if os.environ[CROS_BOARD_ENV] and os.environ[CROS_IP_ENV]: |
| 150 opts_dict['cros_board'] = os.environ[CROS_BOARD_ENV] |
| 151 opts_dict['cros_remote_ip'] = os.environ[CROS_IP_ENV] |
| 152 else: |
| 153 raise RuntimeError('Cros build selected, but BISECT_CROS_IP or' |
| 154 'BISECT_CROS_BOARD undefined.') |
| 155 elif 'android' in config['command']: |
| 156 opts_dict['target_platform'] = 'android' |
| 157 |
| 158 return bisect.BisectOptions.FromDict(opts_dict) |
| 159 |
| 160 |
| 161 def _RunPerformanceTest(config, path_to_file): |
| 162 # Bisect script expects to be run from src |
| 163 os.chdir(os.path.join(path_to_file, '..')) |
| 164 |
| 165 bisect_utils.OutputAnnotationStepStart('Building With Patch') |
| 166 |
| 167 opts = _CreateBisectOptionsFromConfig(config) |
| 168 b = bisect.BisectPerformanceMetrics(None, opts) |
| 169 |
| 170 if bisect_utils.RunGClient(['runhooks']): |
| 171 raise RuntimeError('Failed to run gclient runhooks') |
| 172 |
| 173 if not b.BuildCurrentRevision('chromium'): |
| 174 raise RuntimeError('Patched version failed to build.') |
| 175 |
| 176 bisect_utils.OutputAnnotationStepClosed() |
| 177 bisect_utils.OutputAnnotationStepStart('Running With Patch') |
| 178 |
| 179 results_with_patch = b.RunPerformanceTestAndParseResults( |
| 180 opts.command, opts.metric, reset_on_first_run=True, results_label='Patch') |
| 181 |
| 182 if results_with_patch[1]: |
| 183 raise RuntimeError('Patched version failed to run performance test.') |
| 184 |
| 185 bisect_utils.OutputAnnotationStepClosed() |
| 186 |
| 187 bisect_utils.OutputAnnotationStepStart('Reverting Patch') |
| 188 if bisect_utils.RunGClient(['revert']): |
| 189 raise RuntimeError('Failed to run gclient runhooks') |
| 190 bisect_utils.OutputAnnotationStepClosed() |
| 191 |
| 192 bisect_utils.OutputAnnotationStepStart('Building Without Patch') |
| 193 |
| 194 if bisect_utils.RunGClient(['runhooks']): |
| 195 raise RuntimeError('Failed to run gclient runhooks') |
| 196 |
| 197 if not b.BuildCurrentRevision('chromium'): |
| 198 raise RuntimeError('Unpatched version failed to build.') |
| 199 |
| 200 bisect_utils.OutputAnnotationStepClosed() |
| 201 bisect_utils.OutputAnnotationStepStart('Running Without Patch') |
| 202 |
| 203 results_without_patch = b.RunPerformanceTestAndParseResults( |
| 204 opts.command, opts.metric, upload_on_last_run=True, results_label='ToT') |
| 205 |
| 206 if results_without_patch[1]: |
| 207 raise RuntimeError('Unpatched version failed to run performance test.') |
| 208 |
| 209 # Find the link to the cloud stored results file. |
| 210 output = results_without_patch[2] |
| 211 cloud_file_link = [t for t in output.splitlines() |
| 212 if 'storage.googleapis.com/chromium-telemetry/html-results/' in t] |
| 213 if cloud_file_link: |
| 214 cloud_file_link = cloud_file_link[0] |
| 215 else: |
| 216 cloud_file_link = '' |
| 217 |
| 218 # Calculate the % difference in the means of the 2 runs. |
| 219 percent_diff_in_means = (results_with_patch[0]['mean'] / |
| 220 max(0.0001, results_without_patch[0]['mean'])) * 100.0 - 100.0 |
| 221 std_err = bisect.CalculatePooledStandardError( |
| 222 [results_with_patch[0]['values'], results_without_patch[0]['values']]) |
| 223 |
| 224 bisect_utils.OutputAnnotationStepClosed() |
| 225 bisect_utils.OutputAnnotationStepStart('Results - %.02f +- %0.02f delta' % |
| 226 (percent_diff_in_means, std_err)) |
| 227 print ' %s %s %s' % (''.center(10, ' '), 'Mean'.center(20, ' '), |
| 228 'Std. Error'.center(20, ' ')) |
| 229 print ' %s %s %s' % ('Patch'.center(10, ' '), |
| 230 ('%.02f' % results_with_patch[0]['mean']).center(20, ' '), |
| 231 ('%.02f' % results_with_patch[0]['std_err']).center(20, ' ')) |
| 232 print ' %s %s %s' % ('No Patch'.center(10, ' '), |
| 233 ('%.02f' % results_without_patch[0]['mean']).center(20, ' '), |
| 234 ('%.02f' % results_without_patch[0]['std_err']).center(20, ' ')) |
| 235 if cloud_file_link: |
| 236 bisect_utils.OutputAnnotationStepLink('HTML Results', cloud_file_link) |
| 237 bisect_utils.OutputAnnotationStepClosed() |
| 238 |
| 239 |
| 240 def _SetupAndRunPerformanceTest(config, path_to_file, path_to_goma): |
| 241 """Attempts to build and run the current revision with and without the |
| 242 current patch, with the parameters passed in. |
| 243 |
| 244 Args: |
| 245 config: The config read from run-perf-test.cfg. |
| 246 path_to_file: Path to the bisect-perf-regression.py script. |
| 247 path_to_goma: Path to goma directory. |
| 248 |
| 249 Returns: |
| 250 0 on success, otherwise 1. |
| 251 """ |
| 252 try: |
| 253 with Goma(path_to_goma) as goma: |
| 254 config['use_goma'] = bool(path_to_goma) |
| 255 _RunPerformanceTest(config, path_to_file) |
| 256 return 0 |
| 257 except RuntimeError, e: |
| 258 bisect_utils.OutputAnnotationStepClosed() |
| 259 _OutputFailedResults('Error: %s' % e.message) |
| 260 return 1 |
| 261 |
| 262 |
| 263 def _RunBisectionScript(config, working_directory, path_to_file, path_to_goma): |
52 """Attempts to execute src/tools/bisect-perf-regression.py with the parameters | 264 """Attempts to execute src/tools/bisect-perf-regression.py with the parameters |
53 passed in. | 265 passed in. |
54 | 266 |
55 Args: | 267 Args: |
56 config: A dict containing the parameters to pass to the script. | 268 config: A dict containing the parameters to pass to the script. |
57 working_directory: A working directory to provide to the | 269 working_directory: A working directory to provide to the |
58 bisect-perf-regression.py script, where it will store it's own copy of | 270 bisect-perf-regression.py script, where it will store it's own copy of |
59 the depot. | 271 the depot. |
60 path_to_file: Path to the bisect-perf-regression.py script. | 272 path_to_file: Path to the bisect-perf-regression.py script. |
61 path_to_goma: Path to goma directory. | 273 path_to_goma: Path to goma directory. |
(...skipping 29 matching lines...) Expand all Loading... |
91 cmd.extend(['--cros_remote_ip', os.environ[CROS_IP_ENV]]) | 303 cmd.extend(['--cros_remote_ip', os.environ[CROS_IP_ENV]]) |
92 else: | 304 else: |
93 print 'Error: Cros build selected, but BISECT_CROS_IP or'\ | 305 print 'Error: Cros build selected, but BISECT_CROS_IP or'\ |
94 'BISECT_CROS_BOARD undefined.' | 306 'BISECT_CROS_BOARD undefined.' |
95 print | 307 print |
96 return 1 | 308 return 1 |
97 | 309 |
98 if 'android' in config['command']: | 310 if 'android' in config['command']: |
99 cmd.extend(['--target_platform', 'android']) | 311 cmd.extend(['--target_platform', 'android']) |
100 | 312 |
101 goma_file = '' | |
102 if path_to_goma: | 313 if path_to_goma: |
103 path_to_goma = os.path.abspath(path_to_goma) | |
104 | |
105 if os.name == 'nt': | |
106 os.environ['CC'] = os.path.join(path_to_goma, 'gomacc.exe') + ' cl.exe' | |
107 os.environ['CXX'] = os.path.join(path_to_goma, 'gomacc.exe') + ' cl.exe' | |
108 goma_file = os.path.join(path_to_goma, 'goma_ctl.bat') | |
109 else: | |
110 os.environ['PATH'] = os.pathsep.join([path_to_goma, os.environ['PATH']]) | |
111 goma_file = os.path.join(path_to_goma, 'goma_ctl.sh') | |
112 | |
113 cmd.append('--use_goma') | 314 cmd.append('--use_goma') |
114 | 315 |
115 # Sometimes goma is lingering around if something went bad on a previous | |
116 # run. Stop it before starting a new process. Can ignore the return code | |
117 # since it will return an error if it wasn't running. | |
118 subprocess.call([goma_file, 'stop']) | |
119 | |
120 return_code = subprocess.call([goma_file, 'start']) | |
121 if return_code: | |
122 print 'Error: goma failed to start.' | |
123 print | |
124 return return_code | |
125 | |
126 cmd = [str(c) for c in cmd] | 316 cmd = [str(c) for c in cmd] |
127 | 317 |
128 return_code = subprocess.call(cmd) | 318 with Goma(path_to_goma) as goma: |
129 | 319 return_code = subprocess.call(cmd) |
130 if path_to_goma: | |
131 subprocess.call([goma_file, 'stop']) | |
132 | 320 |
133 if return_code: | 321 if return_code: |
134 print 'Error: bisect-perf-regression.py returned with error %d' %\ | 322 print 'Error: bisect-perf-regression.py returned with error %d' %\ |
135 return_code | 323 return_code |
136 print | 324 print |
137 | 325 |
138 return return_code | 326 return return_code |
139 | 327 |
140 | 328 |
141 def main(): | 329 def main(): |
142 | 330 |
143 usage = ('%prog [options] [-- chromium-options]\n' | 331 usage = ('%prog [options] [-- chromium-options]\n' |
144 'Used by a trybot to run the bisection script using the parameters' | 332 'Used by a trybot to run the bisection script using the parameters' |
145 ' provided in the run-bisect-perf-regression.cfg file.') | 333 ' provided in the run-bisect-perf-regression.cfg file.') |
146 | 334 |
147 parser = optparse.OptionParser(usage=usage) | 335 parser = optparse.OptionParser(usage=usage) |
148 parser.add_option('-w', '--working_directory', | 336 parser.add_option('-w', '--working_directory', |
149 type='str', | 337 type='str', |
150 help='A working directory to supply to the bisection ' | 338 help='A working directory to supply to the bisection ' |
151 'script, which will use it as the location to checkout ' | 339 'script, which will use it as the location to checkout ' |
152 'a copy of the chromium depot.') | 340 'a copy of the chromium depot.') |
153 parser.add_option('-p', '--path_to_goma', | 341 parser.add_option('-p', '--path_to_goma', |
154 type='str', | 342 type='str', |
155 help='Path to goma directory. If this is supplied, goma ' | 343 help='Path to goma directory. If this is supplied, goma ' |
156 'builds will be enabled.') | 344 'builds will be enabled.') |
157 (opts, args) = parser.parse_args() | 345 (opts, args) = parser.parse_args() |
158 | 346 |
159 if not opts.working_directory: | 347 path_to_current_directory = os.path.abspath(os.path.dirname(sys.argv[0])) |
160 print 'Error: missing required parameter: --working_directory' | 348 path_to_bisect_cfg = os.path.join(path_to_current_directory, |
161 print | 349 'run-bisect-perf-regression.cfg') |
162 parser.print_help() | |
163 return 1 | |
164 | 350 |
165 path_to_file = os.path.abspath(os.path.dirname(sys.argv[0])) | 351 config = _LoadConfigFile(path_to_bisect_cfg) |
166 | 352 |
167 config = LoadConfigFile(path_to_file) | 353 # Check if the config is empty |
168 if not config: | 354 config_has_values = [v for v in config.values() if v] |
169 print 'Error: Could not load config file. Double check your changes to '\ | |
170 'run-bisect-perf-regression.cfg for syntax errors.' | |
171 print | |
172 return 1 | |
173 | 355 |
174 return RunBisectionScript(config, opts.working_directory, path_to_file, | 356 if config and config_has_values: |
175 opts.path_to_goma) | 357 if not opts.working_directory: |
| 358 print 'Error: missing required parameter: --working_directory' |
| 359 print |
| 360 parser.print_help() |
| 361 return 1 |
| 362 |
| 363 return _RunBisectionScript(config, opts.working_directory, |
| 364 path_to_current_directory, opts.path_to_goma) |
| 365 else: |
| 366 path_to_perf_cfg = os.path.join( |
| 367 os.path.abspath(os.path.dirname(sys.argv[0])), 'run-perf-test.cfg') |
| 368 |
| 369 config = _LoadConfigFile(path_to_perf_cfg) |
| 370 |
| 371 if config: |
| 372 return _SetupAndRunPerformanceTest(config, path_to_current_directory, |
| 373 opts.path_to_goma) |
| 374 else: |
| 375 print 'Error: Could not load config file. Double check your changes to '\ |
| 376 'run-bisect-perf-regression.cfg for syntax errors.' |
| 377 print |
| 378 return 1 |
176 | 379 |
177 | 380 |
178 if __name__ == '__main__': | 381 if __name__ == '__main__': |
179 sys.exit(main()) | 382 sys.exit(main()) |
OLD | NEW |