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