Increase sharding of browser_tests on swarming.
[chromium-blink-merge.git] / tools / run-bisect-perf-regression.py
blob4047f825f10f39cd15ecefc30bf404d499f32162
1 #!/usr/bin/env python
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
4 # found in the LICENSE file.
6 """Run Performance Test Bisect Tool
8 This script is used by a try bot to run the bisect script with the parameters
9 specified in the bisect config file. It checks out a copy of the depot in
10 a subdirectory 'bisect' of the working directory provided, annd runs the
11 bisect scrip there.
12 """
14 import optparse
15 import os
16 import platform
17 import subprocess
18 import sys
19 import traceback
21 from auto_bisect import bisect_perf_regression
22 from auto_bisect import bisect_utils
23 from auto_bisect import math_utils
25 CROS_BOARD_ENV = 'BISECT_CROS_BOARD'
26 CROS_IP_ENV = 'BISECT_CROS_IP'
28 SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
29 SRC_DIR = os.path.join(SCRIPT_DIR, os.path.pardir)
30 BISECT_CONFIG_PATH = os.path.join(SCRIPT_DIR, 'auto_bisect', 'bisect.cfg')
31 RUN_TEST_CONFIG_PATH = os.path.join(SCRIPT_DIR, 'run-perf-test.cfg')
32 WEBKIT_RUN_TEST_CONFIG_PATH = os.path.join(
33 SRC_DIR, 'third_party', 'WebKit', 'Tools', 'run-perf-test.cfg')
34 BISECT_SCRIPT_DIR = os.path.join(SCRIPT_DIR, 'auto_bisect')
37 class Goma(object):
39 def __init__(self, path_to_goma):
40 self._abs_path_to_goma = None
41 self._abs_path_to_goma_file = None
42 if not path_to_goma:
43 return
44 self._abs_path_to_goma = os.path.abspath(path_to_goma)
45 filename = 'goma_ctl.bat' if os.name == 'nt' else 'goma_ctl.sh'
46 self._abs_path_to_goma_file = os.path.join(self._abs_path_to_goma, filename)
48 def __enter__(self):
49 if self._HasGomaPath():
50 self._SetupAndStart()
51 return self
53 def __exit__(self, *_):
54 if self._HasGomaPath():
55 self._Stop()
57 def _HasGomaPath(self):
58 return bool(self._abs_path_to_goma)
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']])
70 def _SetupAndStart(self):
71 """Sets up goma and launches it.
73 Args:
74 path_to_goma: Path to goma directory.
76 Returns:
77 True if successful."""
78 self._SetupEnvVars()
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()
85 if subprocess.call([self._abs_path_to_goma_file, 'start']):
86 raise RuntimeError('Goma failed to start.')
88 def _Stop(self):
89 subprocess.call([self._abs_path_to_goma_file, 'stop'])
92 def _LoadConfigFile(config_file_path):
93 """Attempts to load the specified config file as a module
94 and grab the global config dict.
96 Args:
97 config_file_path: Path to the config file.
99 Returns:
100 If successful, returns the config dict loaded from the file. If no
101 such dictionary could be loaded, returns the empty dictionary.
103 try:
104 local_vars = {}
105 execfile(config_file_path, local_vars)
106 return local_vars['config']
107 except Exception:
108 print
109 traceback.print_exc()
110 print
111 return {}
114 def _ValidateConfigFile(config_contents, required_parameters):
115 """Validates the config file contents, checking whether all values are
116 non-empty.
118 Args:
119 config_contents: A config dictionary.
120 required_parameters: A list of parameters to check for.
122 Returns:
123 True if valid.
125 for parameter in required_parameters:
126 if parameter not in config_contents:
127 return False
128 value = config_contents[parameter]
129 if not value or type(value) is not str:
130 return False
131 return True
134 def _ValidatePerfConfigFile(config_contents):
135 """Validates the perf config file contents.
137 This is used when we're doing a perf try job, rather than a bisect.
138 The config file is called run-perf-test.cfg by default.
140 The parameters checked are the required parameters; any additional optional
141 parameters won't be checked and validation will still pass.
143 Args:
144 config_contents: A config dictionary.
146 Returns:
147 True if valid.
149 required_parameters = [
150 'command',
151 'repeat_count',
152 'truncate_percent',
153 'max_time_minutes',
155 return _ValidateConfigFile(config_contents, required_parameters)
158 def _ValidateBisectConfigFile(config_contents):
159 """Validates the bisect config file contents.
161 The parameters checked are the required parameters; any additional optional
162 parameters won't be checked and validation will still pass.
164 Args:
165 config_contents: A config dictionary.
167 Returns:
168 True if valid.
170 required_params = [
171 'command',
172 'good_revision',
173 'bad_revision',
174 'metric',
175 'repeat_count',
176 'truncate_percent',
177 'max_time_minutes',
179 return _ValidateConfigFile(config_contents, required_params)
182 def _OutputFailedResults(text_to_print):
183 bisect_utils.OutputAnnotationStepStart('Results - Failed')
184 print
185 print text_to_print
186 print
187 bisect_utils.OutputAnnotationStepClosed()
190 def _CreateBisectOptionsFromConfig(config):
191 print config['command']
192 opts_dict = {}
193 opts_dict['command'] = config['command']
194 opts_dict['metric'] = config.get('metric')
196 if config['repeat_count']:
197 opts_dict['repeat_test_count'] = int(config['repeat_count'])
199 if config['truncate_percent']:
200 opts_dict['truncate_percent'] = int(config['truncate_percent'])
202 if config['max_time_minutes']:
203 opts_dict['max_time_minutes'] = int(config['max_time_minutes'])
205 if config.has_key('use_goma'):
206 opts_dict['use_goma'] = config['use_goma']
207 if config.has_key('goma_dir'):
208 opts_dict['goma_dir'] = config['goma_dir']
210 if config.has_key('improvement_direction'):
211 opts_dict['improvement_direction'] = int(config['improvement_direction'])
213 if config.has_key('bug_id') and str(config['bug_id']).isdigit():
214 opts_dict['bug_id'] = config['bug_id']
216 opts_dict['build_preference'] = 'ninja'
217 opts_dict['output_buildbot_annotations'] = True
219 if '--browser=cros' in config['command']:
220 opts_dict['target_platform'] = 'cros'
222 if os.environ[CROS_BOARD_ENV] and os.environ[CROS_IP_ENV]:
223 opts_dict['cros_board'] = os.environ[CROS_BOARD_ENV]
224 opts_dict['cros_remote_ip'] = os.environ[CROS_IP_ENV]
225 else:
226 raise RuntimeError('CrOS build selected, but BISECT_CROS_IP or'
227 'BISECT_CROS_BOARD undefined.')
228 elif 'android' in config['command']:
229 if 'android-chrome-shell' in config['command']:
230 opts_dict['target_platform'] = 'android'
231 elif 'android-chrome' in config['command']:
232 opts_dict['target_platform'] = 'android-chrome'
233 else:
234 opts_dict['target_platform'] = 'android'
236 return bisect_perf_regression.BisectOptions.FromDict(opts_dict)
239 def _RunPerformanceTest(config):
240 """Runs a performance test with and without the current patch.
242 Args:
243 config: Contents of the config file, a dictionary.
245 Attempts to build and run the current revision with and without the
246 current patch, with the parameters passed in.
248 # Bisect script expects to be run from the src directory
249 os.chdir(SRC_DIR)
251 bisect_utils.OutputAnnotationStepStart('Building With Patch')
253 opts = _CreateBisectOptionsFromConfig(config)
254 b = bisect_perf_regression.BisectPerformanceMetrics(opts, os.getcwd())
256 if bisect_utils.RunGClient(['runhooks']):
257 raise RuntimeError('Failed to run gclient runhooks')
259 if not b.ObtainBuild('chromium'):
260 raise RuntimeError('Patched version failed to build.')
262 bisect_utils.OutputAnnotationStepClosed()
263 bisect_utils.OutputAnnotationStepStart('Running With Patch')
265 results_with_patch = b.RunPerformanceTestAndParseResults(
266 opts.command, opts.metric, reset_on_first_run=True, results_label='Patch')
268 if results_with_patch[1]:
269 raise RuntimeError('Patched version failed to run performance test.')
271 bisect_utils.OutputAnnotationStepClosed()
273 bisect_utils.OutputAnnotationStepStart('Reverting Patch')
274 # TODO: When this is re-written to recipes, this should use bot_update's
275 # revert mechanism to fully revert the client. But for now, since we know that
276 # the perf try bot currently only supports src/ and src/third_party/WebKit, we
277 # simply reset those two directories.
278 bisect_utils.CheckRunGit(['reset', '--hard'])
279 bisect_utils.CheckRunGit(['reset', '--hard'],
280 os.path.join('third_party', 'WebKit'))
281 bisect_utils.OutputAnnotationStepClosed()
283 bisect_utils.OutputAnnotationStepStart('Building Without Patch')
285 if bisect_utils.RunGClient(['runhooks']):
286 raise RuntimeError('Failed to run gclient runhooks')
288 if not b.ObtainBuild('chromium'):
289 raise RuntimeError('Unpatched version failed to build.')
291 bisect_utils.OutputAnnotationStepClosed()
292 bisect_utils.OutputAnnotationStepStart('Running Without Patch')
294 results_without_patch = b.RunPerformanceTestAndParseResults(
295 opts.command, opts.metric, upload_on_last_run=True, results_label='ToT')
297 if results_without_patch[1]:
298 raise RuntimeError('Unpatched version failed to run performance test.')
300 # Find the link to the cloud stored results file.
301 output = results_without_patch[2]
302 cloud_file_link = [t for t in output.splitlines()
303 if 'storage.googleapis.com/chromium-telemetry/html-results/' in t]
304 if cloud_file_link:
305 # What we're getting here is basically "View online at http://..." so parse
306 # out just the URL portion.
307 cloud_file_link = cloud_file_link[0]
308 cloud_file_link = [t for t in cloud_file_link.split(' ')
309 if 'storage.googleapis.com/chromium-telemetry/html-results/' in t]
310 assert cloud_file_link, 'Couldn\'t parse URL from output.'
311 cloud_file_link = cloud_file_link[0]
312 else:
313 cloud_file_link = ''
315 # Calculate the % difference in the means of the 2 runs.
316 percent_diff_in_means = None
317 std_err = None
318 if (results_with_patch[0].has_key('mean') and
319 results_with_patch[0].has_key('values')):
320 percent_diff_in_means = (results_with_patch[0]['mean'] /
321 max(0.0001, results_without_patch[0]['mean'])) * 100.0 - 100.0
322 std_err = math_utils.PooledStandardError(
323 [results_with_patch[0]['values'], results_without_patch[0]['values']])
325 bisect_utils.OutputAnnotationStepClosed()
326 if percent_diff_in_means is not None and std_err is not None:
327 bisect_utils.OutputAnnotationStepStart('Results - %.02f +- %0.02f delta' %
328 (percent_diff_in_means, std_err))
329 print ' %s %s %s' % (''.center(10, ' '), 'Mean'.center(20, ' '),
330 'Std. Error'.center(20, ' '))
331 print ' %s %s %s' % ('Patch'.center(10, ' '),
332 ('%.02f' % results_with_patch[0]['mean']).center(20, ' '),
333 ('%.02f' % results_with_patch[0]['std_err']).center(20, ' '))
334 print ' %s %s %s' % ('No Patch'.center(10, ' '),
335 ('%.02f' % results_without_patch[0]['mean']).center(20, ' '),
336 ('%.02f' % results_without_patch[0]['std_err']).center(20, ' '))
337 if cloud_file_link:
338 bisect_utils.OutputAnnotationStepLink('HTML Results', cloud_file_link)
339 bisect_utils.OutputAnnotationStepClosed()
340 elif cloud_file_link:
341 bisect_utils.OutputAnnotationStepLink('HTML Results', cloud_file_link)
344 def _SetupAndRunPerformanceTest(config, path_to_goma):
345 """Attempts to build and run the current revision with and without the
346 current patch, with the parameters passed in.
348 Args:
349 config: The config read from run-perf-test.cfg.
350 path_to_goma: Path to goma directory.
352 Returns:
353 An exit code: 0 on success, otherwise 1.
355 if platform.release() == 'XP':
356 print 'Windows XP is not supported for perf try jobs because it lacks '
357 print 'goma support. Please refer to crbug.com/330900.'
358 return 1
359 try:
360 with Goma(path_to_goma) as _:
361 config['use_goma'] = bool(path_to_goma)
362 if config['use_goma']:
363 config['goma_dir'] = os.path.abspath(path_to_goma)
364 _RunPerformanceTest(config)
365 return 0
366 except RuntimeError, e:
367 bisect_utils.OutputAnnotationStepClosed()
368 _OutputFailedResults('Error: %s' % e.message)
369 return 1
372 def _RunBisectionScript(
373 config, working_directory, path_to_goma, path_to_extra_src, dry_run):
374 """Attempts to execute the bisect script with the given parameters.
376 Args:
377 config: A dict containing the parameters to pass to the script.
378 working_directory: A working directory to provide to the bisect script,
379 where it will store it's own copy of the depot.
380 path_to_goma: Path to goma directory.
381 path_to_extra_src: Path to extra source file.
382 dry_run: Do a dry run, skipping sync, build, and performance testing steps.
384 Returns:
385 An exit status code: 0 on success, otherwise 1.
387 _PrintConfigStep(config)
389 cmd = ['python', os.path.join(BISECT_SCRIPT_DIR, 'bisect_perf_regression.py'),
390 '-c', config['command'],
391 '-g', config['good_revision'],
392 '-b', config['bad_revision'],
393 '-m', config['metric'],
394 '--working_directory', working_directory,
395 '--output_buildbot_annotations']
397 if config.get('metric'):
398 cmd.extend(['-m', config['metric']])
400 if config['repeat_count']:
401 cmd.extend(['-r', config['repeat_count']])
403 if config['truncate_percent']:
404 cmd.extend(['-t', config['truncate_percent']])
406 if config['max_time_minutes']:
407 cmd.extend(['--max_time_minutes', config['max_time_minutes']])
409 if config.has_key('bisect_mode'):
410 cmd.extend(['--bisect_mode', config['bisect_mode']])
412 if config.has_key('improvement_direction'):
413 cmd.extend(['-d', config['improvement_direction']])
415 if config.has_key('bug_id'):
416 cmd.extend(['--bug_id', config['bug_id']])
418 cmd.extend(['--build_preference', 'ninja'])
420 if '--browser=cros' in config['command']:
421 cmd.extend(['--target_platform', 'cros'])
423 if os.environ[CROS_BOARD_ENV] and os.environ[CROS_IP_ENV]:
424 cmd.extend(['--cros_board', os.environ[CROS_BOARD_ENV]])
425 cmd.extend(['--cros_remote_ip', os.environ[CROS_IP_ENV]])
426 else:
427 print ('Error: Cros build selected, but BISECT_CROS_IP or'
428 'BISECT_CROS_BOARD undefined.\n')
429 return 1
431 if 'android' in config['command']:
432 if 'android-chrome-shell' in config['command']:
433 cmd.extend(['--target_platform', 'android'])
434 elif 'android-chrome' in config['command']:
435 cmd.extend(['--target_platform', 'android-chrome'])
436 else:
437 cmd.extend(['--target_platform', 'android'])
439 if path_to_goma:
440 # For Windows XP platforms, goma service is not supported.
441 # Moreover we don't compile chrome when gs_bucket flag is set instead
442 # use builds archives, therefore ignore goma service for Windows XP.
443 # See http://crbug.com/330900.
444 if config.get('gs_bucket') and platform.release() == 'XP':
445 print ('Goma doesn\'t have a win32 binary, therefore it is not supported '
446 'on Windows XP platform. Please refer to crbug.com/330900.')
447 path_to_goma = None
448 cmd.append('--use_goma')
450 if path_to_extra_src:
451 cmd.extend(['--extra_src', path_to_extra_src])
453 # These flags are used to download build archives from cloud storage if
454 # available, otherwise will post a try_job_http request to build it on the
455 # try server.
456 if config.get('gs_bucket'):
457 if config.get('builder_host') and config.get('builder_port'):
458 cmd.extend(['--gs_bucket', config['gs_bucket'],
459 '--builder_host', config['builder_host'],
460 '--builder_port', config['builder_port']
462 else:
463 print ('Error: Specified gs_bucket, but missing builder_host or '
464 'builder_port information in config.')
465 return 1
467 if dry_run:
468 cmd.extend(['--debug_ignore_build', '--debug_ignore_sync',
469 '--debug_ignore_perf_test'])
470 cmd = [str(c) for c in cmd]
472 with Goma(path_to_goma) as _:
473 return_code = subprocess.call(cmd)
475 if return_code:
476 print ('Error: bisect_perf_regression.py returned with error %d\n'
477 % return_code)
479 return return_code
482 def _PrintConfigStep(config):
483 """Prints out the given config, along with Buildbot annotations."""
484 bisect_utils.OutputAnnotationStepStart('Config')
485 print
486 for k, v in config.iteritems():
487 print ' %s : %s' % (k, v)
488 print
489 bisect_utils.OutputAnnotationStepClosed()
492 def _OptionParser():
493 """Returns the options parser for run-bisect-perf-regression.py."""
494 usage = ('%prog [options] [-- chromium-options]\n'
495 'Used by a try bot to run the bisection script using the parameters'
496 ' provided in the auto_bisect/bisect.cfg file.')
497 parser = optparse.OptionParser(usage=usage)
498 parser.add_option('-w', '--working_directory',
499 type='str',
500 help='A working directory to supply to the bisection '
501 'script, which will use it as the location to checkout '
502 'a copy of the chromium depot.')
503 parser.add_option('-p', '--path_to_goma',
504 type='str',
505 help='Path to goma directory. If this is supplied, goma '
506 'builds will be enabled.')
507 parser.add_option('--path_to_config',
508 type='str',
509 help='Path to the config file to use. If this is supplied, '
510 'the bisect script will use this to override the default '
511 'config file path. The script will attempt to load it '
512 'as a bisect config first, then a perf config.')
513 parser.add_option('--extra_src',
514 type='str',
515 help='Path to extra source file. If this is supplied, '
516 'bisect script will use this to override default behavior.')
517 parser.add_option('--dry_run',
518 action="store_true",
519 help='The script will perform the full bisect, but '
520 'without syncing, building, or running the performance '
521 'tests.')
522 return parser
525 def main():
526 """Entry point for run-bisect-perf-regression.py.
528 Reads the config file, and then tries to either bisect a regression or
529 just run a performance test, depending on the particular config parameters
530 specified in the config file.
532 parser = _OptionParser()
533 opts, _ = parser.parse_args()
535 # Use the default config file path unless one was specified.
536 config_path = BISECT_CONFIG_PATH
537 if opts.path_to_config:
538 config_path = opts.path_to_config
539 config = _LoadConfigFile(config_path)
541 # Check if the config is valid for running bisect job.
542 config_is_valid = _ValidateBisectConfigFile(config)
544 if config and config_is_valid:
545 if not opts.working_directory:
546 print 'Error: missing required parameter: --working_directory\n'
547 parser.print_help()
548 return 1
550 return _RunBisectionScript(
551 config, opts.working_directory, opts.path_to_goma, opts.extra_src,
552 opts.dry_run)
554 # If it wasn't valid for running a bisect, then maybe the user wanted
555 # to run a perf test instead of a bisect job. Try reading any possible
556 # perf test config files.
557 perf_cfg_files = [RUN_TEST_CONFIG_PATH, WEBKIT_RUN_TEST_CONFIG_PATH]
558 for current_perf_cfg_file in perf_cfg_files:
559 if opts.path_to_config:
560 path_to_perf_cfg = opts.path_to_config
561 else:
562 path_to_perf_cfg = os.path.join(
563 os.path.abspath(os.path.dirname(sys.argv[0])),
564 current_perf_cfg_file)
566 config = _LoadConfigFile(path_to_perf_cfg)
567 config_is_valid = _ValidatePerfConfigFile(config)
569 if config and config_is_valid:
570 return _SetupAndRunPerformanceTest(config, opts.path_to_goma)
572 print ('Error: Could not load config file. Double check your changes to '
573 'auto_bisect/bisect.cfg or run-perf-test.cfg for syntax errors.\n')
574 return 1
577 if __name__ == '__main__':
578 sys.exit(main())