Upstream ViewUtils
[chromium-blink-merge.git] / tools / run-bisect-perf-regression.py
blobac6f14e39490e420fe07ddd71ce26443590604b6
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 re
18 import subprocess
19 import sys
20 import traceback
22 from auto_bisect import bisect_perf_regression
23 from auto_bisect import bisect_utils
24 from auto_bisect import math_utils
25 from auto_bisect import source_control
27 CROS_BOARD_ENV = 'BISECT_CROS_BOARD'
28 CROS_IP_ENV = 'BISECT_CROS_IP'
30 SCRIPT_DIR = os.path.abspath(os.path.dirname(__file__))
31 SRC_DIR = os.path.join(SCRIPT_DIR, os.path.pardir)
32 BISECT_CONFIG_PATH = os.path.join(SCRIPT_DIR, 'auto_bisect', 'bisect.cfg')
33 RUN_TEST_CONFIG_PATH = os.path.join(SCRIPT_DIR, 'run-perf-test.cfg')
34 WEBKIT_RUN_TEST_CONFIG_PATH = os.path.join(
35 SRC_DIR, 'third_party', 'WebKit', 'Tools', 'run-perf-test.cfg')
36 BISECT_SCRIPT_DIR = os.path.join(SCRIPT_DIR, 'auto_bisect')
39 class Goma(object):
41 def __init__(self, path_to_goma):
42 self._abs_path_to_goma = None
43 self._abs_path_to_goma_file = None
44 if not path_to_goma:
45 return
46 self._abs_path_to_goma = os.path.abspath(path_to_goma)
47 filename = 'goma_ctl.bat' if os.name == 'nt' else 'goma_ctl.sh'
48 self._abs_path_to_goma_file = os.path.join(self._abs_path_to_goma, filename)
50 def __enter__(self):
51 if self._HasGomaPath():
52 self._SetupAndStart()
53 return self
55 def __exit__(self, *_):
56 if self._HasGomaPath():
57 self._Stop()
59 def _HasGomaPath(self):
60 return bool(self._abs_path_to_goma)
62 def _SetupEnvVars(self):
63 if os.name == 'nt':
64 os.environ['CC'] = (os.path.join(self._abs_path_to_goma, 'gomacc.exe') +
65 ' cl.exe')
66 os.environ['CXX'] = (os.path.join(self._abs_path_to_goma, 'gomacc.exe') +
67 ' cl.exe')
68 else:
69 os.environ['PATH'] = os.pathsep.join([self._abs_path_to_goma,
70 os.environ['PATH']])
72 def _SetupAndStart(self):
73 """Sets up goma and launches it.
75 Args:
76 path_to_goma: Path to goma directory.
78 Returns:
79 True if successful."""
80 self._SetupEnvVars()
82 # Sometimes goma is lingering around if something went bad on a previous
83 # run. Stop it before starting a new process. Can ignore the return code
84 # since it will return an error if it wasn't running.
85 self._Stop()
87 if subprocess.call([self._abs_path_to_goma_file, 'start']):
88 raise RuntimeError('Goma failed to start.')
90 def _Stop(self):
91 subprocess.call([self._abs_path_to_goma_file, 'stop'])
94 def _LoadConfigFile(config_file_path):
95 """Attempts to load the specified config file as a module
96 and grab the global config dict.
98 Args:
99 config_file_path: Path to the config file.
101 Returns:
102 If successful, returns the config dict loaded from the file. If no
103 such dictionary could be loaded, returns the empty dictionary.
105 try:
106 local_vars = {}
107 execfile(config_file_path, local_vars)
108 return local_vars['config']
109 except Exception:
110 print
111 traceback.print_exc()
112 print
113 return {}
116 def _ValidateConfigFile(config_contents, required_parameters):
117 """Validates the config file contents, checking whether all values are
118 non-empty.
120 Args:
121 config_contents: A config dictionary.
122 required_parameters: A list of parameters to check for.
124 Returns:
125 True if valid.
127 for parameter in required_parameters:
128 if parameter not in config_contents:
129 return False
130 value = config_contents[parameter]
131 if not value or type(value) is not str:
132 return False
133 return True
136 def _ValidatePerfConfigFile(config_contents):
137 """Validates the perf config file contents.
139 This is used when we're doing a perf try job, rather than a bisect.
140 The config file is called run-perf-test.cfg by default.
142 The parameters checked are the required parameters; any additional optional
143 parameters won't be checked and validation will still pass.
145 Args:
146 config_contents: A config dictionary.
148 Returns:
149 True if valid.
151 return _ValidateConfigFile(config_contents, required_parameters=['command'])
154 def _ValidateBisectConfigFile(config_contents):
155 """Validates the bisect config file contents.
157 The parameters checked are the required parameters; any additional optional
158 parameters won't be checked and validation will still pass.
160 Args:
161 config_contents: A config dictionary.
163 Returns:
164 True if valid.
166 return _ValidateConfigFile(
167 config_contents,
168 required_parameters=['command', 'good_revision', 'bad_revision'])
171 def _OutputFailedResults(text_to_print):
172 bisect_utils.OutputAnnotationStepStart('Results - Failed')
173 print
174 print text_to_print
175 print
176 bisect_utils.OutputAnnotationStepClosed()
179 def _CreateBisectOptionsFromConfig(config):
180 print config['command']
181 opts_dict = {}
182 opts_dict['command'] = config['command']
183 opts_dict['metric'] = config.get('metric')
185 if config['repeat_count']:
186 opts_dict['repeat_test_count'] = int(config['repeat_count'])
188 if config['truncate_percent']:
189 opts_dict['truncate_percent'] = int(config['truncate_percent'])
191 if config['max_time_minutes']:
192 opts_dict['max_time_minutes'] = int(config['max_time_minutes'])
194 if config.has_key('use_goma'):
195 opts_dict['use_goma'] = config['use_goma']
196 if config.has_key('goma_dir'):
197 opts_dict['goma_dir'] = config['goma_dir']
199 if config.has_key('improvement_direction'):
200 opts_dict['improvement_direction'] = int(config['improvement_direction'])
202 if config.has_key('bug_id') and str(config['bug_id']).isdigit():
203 opts_dict['bug_id'] = config['bug_id']
205 opts_dict['build_preference'] = 'ninja'
206 opts_dict['output_buildbot_annotations'] = True
208 if '--browser=cros' in config['command']:
209 opts_dict['target_platform'] = 'cros'
211 if os.environ[CROS_BOARD_ENV] and os.environ[CROS_IP_ENV]:
212 opts_dict['cros_board'] = os.environ[CROS_BOARD_ENV]
213 opts_dict['cros_remote_ip'] = os.environ[CROS_IP_ENV]
214 else:
215 raise RuntimeError('CrOS build selected, but BISECT_CROS_IP or'
216 'BISECT_CROS_BOARD undefined.')
217 elif 'android' in config['command']:
218 if 'android-chrome-shell' in config['command']:
219 opts_dict['target_platform'] = 'android'
220 elif 'android-chrome' in config['command']:
221 opts_dict['target_platform'] = 'android-chrome'
222 else:
223 opts_dict['target_platform'] = 'android'
225 return bisect_perf_regression.BisectOptions.FromDict(opts_dict)
228 def _ParseCloudLinksFromOutput(output):
229 html_results_pattern = re.compile(
230 r'\s(?P<VALUES>http://storage.googleapis.com/' +
231 'chromium-telemetry/html-results/results-[a-z0-9-_]+)\s',
232 re.MULTILINE)
233 profiler_pattern = re.compile(
234 r'\s(?P<VALUES>https://console.developers.google.com/' +
235 'm/cloudstorage/b/[a-z-]+/o/profiler-[a-z0-9-_.]+)\s',
236 re.MULTILINE)
238 results = {
239 'html-results': html_results_pattern.findall(output),
240 'profiler': profiler_pattern.findall(output),
243 return results
246 def _ParseAndOutputCloudLinks(
247 results_without_patch, results_with_patch, annotations_dict):
248 cloud_links_without_patch = _ParseCloudLinksFromOutput(
249 results_without_patch[2])
250 cloud_links_with_patch = _ParseCloudLinksFromOutput(
251 results_with_patch[2])
253 cloud_file_link = (cloud_links_without_patch['html-results'][0]
254 if cloud_links_without_patch['html-results'] else '')
256 profiler_file_links_with_patch = cloud_links_with_patch['profiler']
257 profiler_file_links_without_patch = cloud_links_without_patch['profiler']
259 # Calculate the % difference in the means of the 2 runs.
260 percent_diff_in_means = None
261 std_err = None
262 if (results_with_patch[0].has_key('mean') and
263 results_with_patch[0].has_key('values')):
264 percent_diff_in_means = (results_with_patch[0]['mean'] /
265 max(0.0001, results_without_patch[0]['mean'])) * 100.0 - 100.0
266 std_err = math_utils.PooledStandardError(
267 [results_with_patch[0]['values'], results_without_patch[0]['values']])
269 if percent_diff_in_means is not None and std_err is not None:
270 bisect_utils.OutputAnnotationStepStart('Results - %.02f +- %0.02f delta' %
271 (percent_diff_in_means, std_err))
272 print ' %s %s %s' % (''.center(10, ' '), 'Mean'.center(20, ' '),
273 'Std. Error'.center(20, ' '))
274 print ' %s %s %s' % ('Patch'.center(10, ' '),
275 ('%.02f' % results_with_patch[0]['mean']).center(20, ' '),
276 ('%.02f' % results_with_patch[0]['std_err']).center(20, ' '))
277 print ' %s %s %s' % ('No Patch'.center(10, ' '),
278 ('%.02f' % results_without_patch[0]['mean']).center(20, ' '),
279 ('%.02f' % results_without_patch[0]['std_err']).center(20, ' '))
280 if cloud_file_link:
281 bisect_utils.OutputAnnotationStepLink('HTML Results', cloud_file_link)
282 bisect_utils.OutputAnnotationStepClosed()
283 elif cloud_file_link:
284 bisect_utils.OutputAnnotationStepLink('HTML Results', cloud_file_link)
286 if profiler_file_links_with_patch and profiler_file_links_without_patch:
287 for i in xrange(len(profiler_file_links_with_patch)):
288 bisect_utils.OutputAnnotationStepLink(
289 '%s[%d]' % (annotations_dict.get('profiler_link1'), i),
290 profiler_file_links_with_patch[i])
291 for i in xrange(len(profiler_file_links_without_patch)):
292 bisect_utils.OutputAnnotationStepLink(
293 '%s[%d]' % (annotations_dict.get('profiler_link2'), i),
294 profiler_file_links_without_patch[i])
297 def _ResolveRevisionsFromConfig(config):
298 if not 'good_revision' in config and not 'bad_revision' in config:
299 return (None, None)
301 bad_revision = source_control.ResolveToRevision(
302 config['bad_revision'], 'chromium', bisect_utils.DEPOT_DEPS_NAME, 100)
303 if not bad_revision:
304 raise RuntimeError('Failed to resolve [%s] to git hash.',
305 config['bad_revision'])
306 good_revision = source_control.ResolveToRevision(
307 config['good_revision'], 'chromium', bisect_utils.DEPOT_DEPS_NAME, -100)
308 if not good_revision:
309 raise RuntimeError('Failed to resolve [%s] to git hash.',
310 config['good_revision'])
312 return (good_revision, bad_revision)
315 def _GetStepAnnotationStringsDict(config):
316 if 'good_revision' in config and 'bad_revision' in config:
317 return {
318 'build1': 'Building [%s]' % config['good_revision'],
319 'build2': 'Building [%s]' % config['bad_revision'],
320 'run1': 'Running [%s]' % config['good_revision'],
321 'run2': 'Running [%s]' % config['bad_revision'],
322 'sync1': 'Syncing [%s]' % config['good_revision'],
323 'sync2': 'Syncing [%s]' % config['bad_revision'],
324 'results_label1': config['good_revision'],
325 'results_label2': config['bad_revision'],
326 'profiler_link1': 'Profiler Data - %s' % config['good_revision'],
327 'profiler_link2': 'Profiler Data - %s' % config['bad_revision'],
329 else:
330 return {
331 'build1': 'Building With Patch',
332 'build2': 'Building Without Patch',
333 'run1': 'Running With Patch',
334 'run2': 'Running Without Patch',
335 'results_label1': 'Patch',
336 'results_label2': 'ToT',
337 'profiler_link1': 'With Patch - Profiler Data',
338 'profiler_link2': 'Without Patch - Profiler Data',
342 def _RunBuildStepForPerformanceTest(bisect_instance,
343 build_string,
344 sync_string,
345 revision):
346 if revision:
347 bisect_utils.OutputAnnotationStepStart(sync_string)
348 if not source_control.SyncToRevision(revision, 'gclient'):
349 raise RuntimeError('Failed [%s].' % sync_string)
350 bisect_utils.OutputAnnotationStepClosed()
352 bisect_utils.OutputAnnotationStepStart(build_string)
354 if bisect_utils.RunGClient(['runhooks']):
355 raise RuntimeError('Failed to run gclient runhooks')
357 if not bisect_instance.ObtainBuild('chromium'):
358 raise RuntimeError('Patched version failed to build.')
360 bisect_utils.OutputAnnotationStepClosed()
363 def _RunCommandStepForPerformanceTest(bisect_instance,
364 opts,
365 reset_on_first_run,
366 upload_on_last_run,
367 results_label,
368 run_string):
369 bisect_utils.OutputAnnotationStepStart(run_string)
371 results = bisect_instance.RunPerformanceTestAndParseResults(
372 opts.command,
373 opts.metric,
374 reset_on_first_run=reset_on_first_run,
375 upload_on_last_run=upload_on_last_run,
376 results_label=results_label)
378 if results[1]:
379 raise RuntimeError('Patched version failed to run performance test.')
381 bisect_utils.OutputAnnotationStepClosed()
383 return results
386 def _RunPerformanceTest(config):
387 """Runs a performance test with and without the current patch.
389 Args:
390 config: Contents of the config file, a dictionary.
392 Attempts to build and run the current revision with and without the
393 current patch, with the parameters passed in.
395 # Bisect script expects to be run from the src directory
396 os.chdir(SRC_DIR)
398 opts = _CreateBisectOptionsFromConfig(config)
399 revisions = _ResolveRevisionsFromConfig(config)
400 annotations_dict = _GetStepAnnotationStringsDict(config)
401 b = bisect_perf_regression.BisectPerformanceMetrics(opts, os.getcwd())
403 _RunBuildStepForPerformanceTest(b,
404 annotations_dict.get('build1'),
405 annotations_dict.get('sync1'),
406 revisions[0])
408 results_with_patch = _RunCommandStepForPerformanceTest(
409 b, opts, True, True, annotations_dict['results_label1'],
410 annotations_dict['run1'])
412 bisect_utils.OutputAnnotationStepStart('Reverting Patch')
413 # TODO: When this is re-written to recipes, this should use bot_update's
414 # revert mechanism to fully revert the client. But for now, since we know that
415 # the perf try bot currently only supports src/ and src/third_party/WebKit, we
416 # simply reset those two directories.
417 bisect_utils.CheckRunGit(['reset', '--hard'])
418 bisect_utils.CheckRunGit(['reset', '--hard'],
419 os.path.join('third_party', 'WebKit'))
420 bisect_utils.OutputAnnotationStepClosed()
422 _RunBuildStepForPerformanceTest(b,
423 annotations_dict.get('build2'),
424 annotations_dict.get('sync2'),
425 revisions[1])
427 results_without_patch = _RunCommandStepForPerformanceTest(
428 b, opts, False, True, annotations_dict['results_label2'],
429 annotations_dict['run2'])
431 # Find the link to the cloud stored results file.
432 _ParseAndOutputCloudLinks(
433 results_without_patch, results_with_patch, annotations_dict)
436 def _SetupAndRunPerformanceTest(config, path_to_goma):
437 """Attempts to build and run the current revision with and without the
438 current patch, with the parameters passed in.
440 Args:
441 config: The config read from run-perf-test.cfg.
442 path_to_goma: Path to goma directory.
444 Returns:
445 An exit code: 0 on success, otherwise 1.
447 if platform.release() == 'XP':
448 print 'Windows XP is not supported for perf try jobs because it lacks '
449 print 'goma support. Please refer to crbug.com/330900.'
450 return 1
451 try:
452 with Goma(path_to_goma) as _:
453 config['use_goma'] = bool(path_to_goma)
454 if config['use_goma']:
455 config['goma_dir'] = os.path.abspath(path_to_goma)
456 _RunPerformanceTest(config)
457 return 0
458 except RuntimeError, e:
459 bisect_utils.OutputAnnotationStepClosed()
460 _OutputFailedResults('Error: %s' % e.message)
461 return 1
464 def _RunBisectionScript(
465 config, working_directory, path_to_goma, path_to_extra_src, dry_run):
466 """Attempts to execute the bisect script with the given parameters.
468 Args:
469 config: A dict containing the parameters to pass to the script.
470 working_directory: A working directory to provide to the bisect script,
471 where it will store it's own copy of the depot.
472 path_to_goma: Path to goma directory.
473 path_to_extra_src: Path to extra source file.
474 dry_run: Do a dry run, skipping sync, build, and performance testing steps.
476 Returns:
477 An exit status code: 0 on success, otherwise 1.
479 _PrintConfigStep(config)
481 # Construct the basic command with all necessary arguments.
482 cmd = [
483 'python',
484 os.path.join(BISECT_SCRIPT_DIR, 'bisect_perf_regression.py'),
485 '--command', config['command'],
486 '--good_revision', config['good_revision'],
487 '--bad_revision', config['bad_revision'],
488 '--working_directory', working_directory,
489 '--output_buildbot_annotations'
492 # Add flags for any optional config parameters if given in the config.
493 options = [
494 ('metric', '--metric'),
495 ('repeat_count', '--repeat_test_count'),
496 ('truncate_percent', '--truncate_percent'),
497 ('max_time_minutes', '--max_time_minutes'),
498 ('bisect_mode', '--bisect_mode'),
499 ('improvement_direction', '--improvement_direction'),
500 ('bug_id', '--bug_id'),
501 ('builder_type', '--builder_type'),
503 for config_key, flag in options:
504 if config.has_key(config_key):
505 cmd.extend([flag, config[config_key]])
507 cmd.extend(['--build_preference', 'ninja'])
509 # Possibly set the target platform name based on the browser name in a
510 # Telemetry command.
511 if 'android-chrome-shell' in config['command']:
512 cmd.extend(['--target_platform', 'android'])
513 elif 'android-chrome' in config['command']:
514 cmd.extend(['--target_platform', 'android-chrome'])
515 elif 'android' in config['command']:
516 cmd.extend(['--target_platform', 'android'])
518 if path_to_goma:
519 # For Windows XP platforms, goma service is not supported.
520 # Moreover we don't compile chrome when gs_bucket flag is set instead
521 # use builds archives, therefore ignore goma service for Windows XP.
522 # See http://crbug.com/330900.
523 if platform.release() == 'XP':
524 print ('Goma doesn\'t have a win32 binary, therefore it is not supported '
525 'on Windows XP platform. Please refer to crbug.com/330900.')
526 path_to_goma = None
527 cmd.append('--use_goma')
528 cmd.append('--goma_dir')
529 cmd.append(os.path.abspath(path_to_goma))
531 if path_to_extra_src:
532 cmd.extend(['--extra_src', path_to_extra_src])
534 if dry_run:
535 cmd.extend([
536 '--debug_ignore_build',
537 '--debug_ignore_sync',
538 '--debug_ignore_perf_test'
541 cmd = [str(c) for c in cmd]
543 with Goma(path_to_goma) as _:
544 return_code = subprocess.call(cmd)
546 if return_code:
547 print ('Error: bisect_perf_regression.py returned with error %d\n'
548 % return_code)
550 return return_code
553 def _PrintConfigStep(config):
554 """Prints out the given config, along with Buildbot annotations."""
555 bisect_utils.OutputAnnotationStepStart('Config')
556 print
557 for k, v in config.iteritems():
558 print ' %s : %s' % (k, v)
559 print
560 bisect_utils.OutputAnnotationStepClosed()
563 def _OptionParser():
564 """Returns the options parser for run-bisect-perf-regression.py."""
565 usage = ('%prog [options] [-- chromium-options]\n'
566 'Used by a try bot to run the bisection script using the parameters'
567 ' provided in the auto_bisect/bisect.cfg file.')
568 parser = optparse.OptionParser(usage=usage)
569 parser.add_option('-w', '--working_directory',
570 type='str',
571 help='A working directory to supply to the bisection '
572 'script, which will use it as the location to checkout '
573 'a copy of the chromium depot.')
574 parser.add_option('-p', '--path_to_goma',
575 type='str',
576 help='Path to goma directory. If this is supplied, goma '
577 'builds will be enabled.')
578 parser.add_option('--path_to_config',
579 type='str',
580 help='Path to the config file to use. If this is supplied, '
581 'the bisect script will use this to override the default '
582 'config file path. The script will attempt to load it '
583 'as a bisect config first, then a perf config.')
584 parser.add_option('--extra_src',
585 type='str',
586 help='Path to extra source file. If this is supplied, '
587 'bisect script will use this to override default behavior.')
588 parser.add_option('--dry_run',
589 action="store_true",
590 help='The script will perform the full bisect, but '
591 'without syncing, building, or running the performance '
592 'tests.')
593 return parser
596 def main():
597 """Entry point for run-bisect-perf-regression.py.
599 Reads the config file, and then tries to either bisect a regression or
600 just run a performance test, depending on the particular config parameters
601 specified in the config file.
603 parser = _OptionParser()
604 opts, _ = parser.parse_args()
606 # Use the default config file path unless one was specified.
607 config_path = BISECT_CONFIG_PATH
608 if opts.path_to_config:
609 config_path = opts.path_to_config
610 config = _LoadConfigFile(config_path)
612 # Check if the config is valid for running bisect job.
613 config_is_valid = _ValidateBisectConfigFile(config)
615 if config and config_is_valid:
616 if not opts.working_directory:
617 print 'Error: missing required parameter: --working_directory\n'
618 parser.print_help()
619 return 1
621 return _RunBisectionScript(
622 config, opts.working_directory, opts.path_to_goma, opts.extra_src,
623 opts.dry_run)
625 # If it wasn't valid for running a bisect, then maybe the user wanted
626 # to run a perf test instead of a bisect job. Try reading any possible
627 # perf test config files.
628 perf_cfg_files = [RUN_TEST_CONFIG_PATH, WEBKIT_RUN_TEST_CONFIG_PATH]
629 for current_perf_cfg_file in perf_cfg_files:
630 if opts.path_to_config:
631 path_to_perf_cfg = opts.path_to_config
632 else:
633 path_to_perf_cfg = os.path.join(
634 os.path.abspath(os.path.dirname(sys.argv[0])),
635 current_perf_cfg_file)
637 config = _LoadConfigFile(path_to_perf_cfg)
638 config_is_valid = _ValidatePerfConfigFile(config)
640 if config and config_is_valid:
641 return _SetupAndRunPerformanceTest(config, opts.path_to_goma)
643 print ('Error: Could not load config file. Double check your changes to '
644 'auto_bisect/bisect.cfg or run-perf-test.cfg for syntax errors.\n')
645 return 1
648 if __name__ == '__main__':
649 sys.exit(main())