Add python coverage module to third_party
[chromium-blink-merge.git] / tools / run-bisect-perf-regression.py
blob243601527481003d80e40a95b8cc9e7eb326265f
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 _ParseCloudLinksFromOutput(output, bucket):
240 cloud_file_links = [t for t in output.splitlines()
241 if 'storage.googleapis.com/chromium-telemetry/%s/' % bucket in t]
243 # What we're getting here is basically "View online at http://..." so parse
244 # out just the URL portion.
245 for i in xrange(len(cloud_file_links)):
246 cloud_file_link = cloud_file_links[i]
247 cloud_file_link = [t for t in cloud_file_link.split(' ')
248 if 'storage.googleapis.com/chromium-telemetry/%s/' % bucket in t]
249 assert cloud_file_link, 'Couldn\'t parse URL from output.'
250 cloud_file_links[i] = cloud_file_link[0]
251 return cloud_file_links
254 def _RunPerformanceTest(config):
255 """Runs a performance test with and without the current patch.
257 Args:
258 config: Contents of the config file, a dictionary.
260 Attempts to build and run the current revision with and without the
261 current patch, with the parameters passed in.
263 # Bisect script expects to be run from the src directory
264 os.chdir(SRC_DIR)
266 bisect_utils.OutputAnnotationStepStart('Building With Patch')
268 opts = _CreateBisectOptionsFromConfig(config)
269 b = bisect_perf_regression.BisectPerformanceMetrics(opts, os.getcwd())
271 if bisect_utils.RunGClient(['runhooks']):
272 raise RuntimeError('Failed to run gclient runhooks')
274 if not b.ObtainBuild('chromium'):
275 raise RuntimeError('Patched version failed to build.')
277 bisect_utils.OutputAnnotationStepClosed()
278 bisect_utils.OutputAnnotationStepStart('Running With Patch')
280 results_with_patch = b.RunPerformanceTestAndParseResults(
281 opts.command,
282 opts.metric,
283 reset_on_first_run=True,
284 upload_on_last_run=True,
285 results_label='Patch')
287 if results_with_patch[1]:
288 raise RuntimeError('Patched version failed to run performance test.')
290 bisect_utils.OutputAnnotationStepClosed()
292 bisect_utils.OutputAnnotationStepStart('Reverting Patch')
293 # TODO: When this is re-written to recipes, this should use bot_update's
294 # revert mechanism to fully revert the client. But for now, since we know that
295 # the perf try bot currently only supports src/ and src/third_party/WebKit, we
296 # simply reset those two directories.
297 bisect_utils.CheckRunGit(['reset', '--hard'])
298 bisect_utils.CheckRunGit(['reset', '--hard'],
299 os.path.join('third_party', 'WebKit'))
300 bisect_utils.OutputAnnotationStepClosed()
302 bisect_utils.OutputAnnotationStepStart('Building Without Patch')
304 if bisect_utils.RunGClient(['runhooks']):
305 raise RuntimeError('Failed to run gclient runhooks')
307 if not b.ObtainBuild('chromium'):
308 raise RuntimeError('Unpatched version failed to build.')
310 bisect_utils.OutputAnnotationStepClosed()
311 bisect_utils.OutputAnnotationStepStart('Running Without Patch')
313 results_without_patch = b.RunPerformanceTestAndParseResults(
314 opts.command, opts.metric, upload_on_last_run=True, results_label='ToT')
316 if results_without_patch[1]:
317 raise RuntimeError('Unpatched version failed to run performance test.')
319 # Find the link to the cloud stored results file.
320 cloud_file_link = _ParseCloudLinksFromOutput(
321 results_without_patch[2], 'html-results')
323 cloud_file_link = cloud_file_link[0] if cloud_file_link else ''
325 profiler_file_links_with_patch = _ParseCloudLinksFromOutput(
326 results_with_patch[2], 'profiling-results')
328 profiler_file_links_without_patch = _ParseCloudLinksFromOutput(
329 results_without_patch[2], 'profiling-results')
331 # Calculate the % difference in the means of the 2 runs.
332 percent_diff_in_means = None
333 std_err = None
334 if (results_with_patch[0].has_key('mean') and
335 results_with_patch[0].has_key('values')):
336 percent_diff_in_means = (results_with_patch[0]['mean'] /
337 max(0.0001, results_without_patch[0]['mean'])) * 100.0 - 100.0
338 std_err = math_utils.PooledStandardError(
339 [results_with_patch[0]['values'], results_without_patch[0]['values']])
341 bisect_utils.OutputAnnotationStepClosed()
342 if percent_diff_in_means is not None and std_err is not None:
343 bisect_utils.OutputAnnotationStepStart('Results - %.02f +- %0.02f delta' %
344 (percent_diff_in_means, std_err))
345 print ' %s %s %s' % (''.center(10, ' '), 'Mean'.center(20, ' '),
346 'Std. Error'.center(20, ' '))
347 print ' %s %s %s' % ('Patch'.center(10, ' '),
348 ('%.02f' % results_with_patch[0]['mean']).center(20, ' '),
349 ('%.02f' % results_with_patch[0]['std_err']).center(20, ' '))
350 print ' %s %s %s' % ('No Patch'.center(10, ' '),
351 ('%.02f' % results_without_patch[0]['mean']).center(20, ' '),
352 ('%.02f' % results_without_patch[0]['std_err']).center(20, ' '))
353 if cloud_file_link:
354 bisect_utils.OutputAnnotationStepLink('HTML Results', cloud_file_link)
355 bisect_utils.OutputAnnotationStepClosed()
356 elif cloud_file_link:
357 bisect_utils.OutputAnnotationStepLink('HTML Results', cloud_file_link)
359 if profiler_file_links_with_patch and profiler_file_links_without_patch:
360 for i in xrange(len(profiler_file_links_with_patch)):
361 bisect_utils.OutputAnnotationStepLink(
362 'With Patch - Profiler Data[%d]' % i,
363 profiler_file_links_with_patch[i])
364 for i in xrange(len(profiler_file_links_without_patch)):
365 bisect_utils.OutputAnnotationStepLink(
366 'Without Patch - Profiler Data[%d]' % i,
367 profiler_file_links_without_patch[i])
370 def _SetupAndRunPerformanceTest(config, path_to_goma):
371 """Attempts to build and run the current revision with and without the
372 current patch, with the parameters passed in.
374 Args:
375 config: The config read from run-perf-test.cfg.
376 path_to_goma: Path to goma directory.
378 Returns:
379 An exit code: 0 on success, otherwise 1.
381 if platform.release() == 'XP':
382 print 'Windows XP is not supported for perf try jobs because it lacks '
383 print 'goma support. Please refer to crbug.com/330900.'
384 return 1
385 try:
386 with Goma(path_to_goma) as _:
387 config['use_goma'] = bool(path_to_goma)
388 if config['use_goma']:
389 config['goma_dir'] = os.path.abspath(path_to_goma)
390 _RunPerformanceTest(config)
391 return 0
392 except RuntimeError, e:
393 bisect_utils.OutputAnnotationStepClosed()
394 _OutputFailedResults('Error: %s' % e.message)
395 return 1
398 def _RunBisectionScript(
399 config, working_directory, path_to_goma, path_to_extra_src, dry_run):
400 """Attempts to execute the bisect script with the given parameters.
402 Args:
403 config: A dict containing the parameters to pass to the script.
404 working_directory: A working directory to provide to the bisect script,
405 where it will store it's own copy of the depot.
406 path_to_goma: Path to goma directory.
407 path_to_extra_src: Path to extra source file.
408 dry_run: Do a dry run, skipping sync, build, and performance testing steps.
410 Returns:
411 An exit status code: 0 on success, otherwise 1.
413 _PrintConfigStep(config)
415 # Construct the basic command with all necessary arguments.
416 cmd = [
417 'python',
418 os.path.join(BISECT_SCRIPT_DIR, 'bisect_perf_regression.py'),
419 '--command', config['command'],
420 '--good_revision', config['good_revision'],
421 '--bad_revision', config['bad_revision'],
422 '--metric', config['metric'],
423 '--working_directory', working_directory,
424 '--output_buildbot_annotations'
427 # Add flags for any optional config parameters if given in the config.
428 options = [
429 ('repeat_count', '--repeat_test_count'),
430 ('truncate_percent', '--truncate_percent'),
431 ('max_time_minutes', '--max_time_minutes'),
432 ('bisect_mode', '--bisect_mode'),
433 ('improvement_direction', '--improvement_direction'),
434 ('bug_id', '--bug_id'),
435 ('builder_host', '--builder_host'),
436 ('builder_port', '--builder_port'),
438 for config_key, flag in options:
439 if config.has_key(config_key):
440 cmd.extend([flag, config[config_key]])
442 cmd.extend(['--build_preference', 'ninja'])
444 # Possibly set the target platform name based on the browser name in a
445 # Telemetry command.
446 if 'android-chrome-shell' in config['command']:
447 cmd.extend(['--target_platform', 'android'])
448 elif 'android-chrome' in config['command']:
449 cmd.extend(['--target_platform', 'android-chrome'])
450 elif 'android' in config['command']:
451 cmd.extend(['--target_platform', 'android'])
453 if path_to_goma:
454 # For Windows XP platforms, goma service is not supported.
455 # Moreover we don't compile chrome when gs_bucket flag is set instead
456 # use builds archives, therefore ignore goma service for Windows XP.
457 # See http://crbug.com/330900.
458 if platform.release() == 'XP':
459 print ('Goma doesn\'t have a win32 binary, therefore it is not supported '
460 'on Windows XP platform. Please refer to crbug.com/330900.')
461 path_to_goma = None
462 cmd.append('--use_goma')
464 if path_to_extra_src:
465 cmd.extend(['--extra_src', path_to_extra_src])
467 if dry_run:
468 cmd.extend([
469 '--debug_ignore_build',
470 '--debug_ignore_sync',
471 '--debug_ignore_perf_test'
474 cmd = [str(c) for c in cmd]
476 with Goma(path_to_goma) as _:
477 return_code = subprocess.call(cmd)
479 if return_code:
480 print ('Error: bisect_perf_regression.py returned with error %d\n'
481 % return_code)
483 return return_code
486 def _PrintConfigStep(config):
487 """Prints out the given config, along with Buildbot annotations."""
488 bisect_utils.OutputAnnotationStepStart('Config')
489 print
490 for k, v in config.iteritems():
491 print ' %s : %s' % (k, v)
492 print
493 bisect_utils.OutputAnnotationStepClosed()
496 def _OptionParser():
497 """Returns the options parser for run-bisect-perf-regression.py."""
498 usage = ('%prog [options] [-- chromium-options]\n'
499 'Used by a try bot to run the bisection script using the parameters'
500 ' provided in the auto_bisect/bisect.cfg file.')
501 parser = optparse.OptionParser(usage=usage)
502 parser.add_option('-w', '--working_directory',
503 type='str',
504 help='A working directory to supply to the bisection '
505 'script, which will use it as the location to checkout '
506 'a copy of the chromium depot.')
507 parser.add_option('-p', '--path_to_goma',
508 type='str',
509 help='Path to goma directory. If this is supplied, goma '
510 'builds will be enabled.')
511 parser.add_option('--path_to_config',
512 type='str',
513 help='Path to the config file to use. If this is supplied, '
514 'the bisect script will use this to override the default '
515 'config file path. The script will attempt to load it '
516 'as a bisect config first, then a perf config.')
517 parser.add_option('--extra_src',
518 type='str',
519 help='Path to extra source file. If this is supplied, '
520 'bisect script will use this to override default behavior.')
521 parser.add_option('--dry_run',
522 action="store_true",
523 help='The script will perform the full bisect, but '
524 'without syncing, building, or running the performance '
525 'tests.')
526 return parser
529 def main():
530 """Entry point for run-bisect-perf-regression.py.
532 Reads the config file, and then tries to either bisect a regression or
533 just run a performance test, depending on the particular config parameters
534 specified in the config file.
536 parser = _OptionParser()
537 opts, _ = parser.parse_args()
539 # Use the default config file path unless one was specified.
540 config_path = BISECT_CONFIG_PATH
541 if opts.path_to_config:
542 config_path = opts.path_to_config
543 config = _LoadConfigFile(config_path)
545 # Check if the config is valid for running bisect job.
546 config_is_valid = _ValidateBisectConfigFile(config)
548 if config and config_is_valid:
549 if not opts.working_directory:
550 print 'Error: missing required parameter: --working_directory\n'
551 parser.print_help()
552 return 1
554 return _RunBisectionScript(
555 config, opts.working_directory, opts.path_to_goma, opts.extra_src,
556 opts.dry_run)
558 # If it wasn't valid for running a bisect, then maybe the user wanted
559 # to run a perf test instead of a bisect job. Try reading any possible
560 # perf test config files.
561 perf_cfg_files = [RUN_TEST_CONFIG_PATH, WEBKIT_RUN_TEST_CONFIG_PATH]
562 for current_perf_cfg_file in perf_cfg_files:
563 if opts.path_to_config:
564 path_to_perf_cfg = opts.path_to_config
565 else:
566 path_to_perf_cfg = os.path.join(
567 os.path.abspath(os.path.dirname(sys.argv[0])),
568 current_perf_cfg_file)
570 config = _LoadConfigFile(path_to_perf_cfg)
571 config_is_valid = _ValidatePerfConfigFile(config)
573 if config and config_is_valid:
574 return _SetupAndRunPerformanceTest(config, opts.path_to_goma)
576 print ('Error: Could not load config file. Double check your changes to '
577 'auto_bisect/bisect.cfg or run-perf-test.cfg for syntax errors.\n')
578 return 1
581 if __name__ == '__main__':
582 sys.exit(main())