Put screenshot.py back to work
[chromium-blink-merge.git] / tools / auto_bisect / bisect_perf_regression.py
blob9ad918924306f85c55c02a278a99ee0733ada635
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 """Performance Test Bisect Tool
8 This script bisects a series of changelists using binary search. It starts at
9 a bad revision where a performance metric has regressed, and asks for a last
10 known-good revision. It will then binary search across this revision range by
11 syncing, building, and running a performance test. If the change is
12 suspected to occur as a result of WebKit/V8 changes, the script will
13 further bisect changes to those depots and attempt to narrow down the revision
14 range.
16 Example usage using SVN revisions:
18 ./tools/bisect_perf_regression.py -c\
19 "out/Release/performance_ui_tests --gtest_filter=ShutdownTest.SimpleUserQuit"\
20 -g 168222 -b 168232 -m shutdown/simple-user-quit
22 Be aware that if you're using the git workflow and specify an SVN revision,
23 the script will attempt to find the git SHA1 where SVN changes up to that
24 revision were merged in.
26 Example usage using git hashes:
28 ./tools/bisect_perf_regression.py -c\
29 "out/Release/performance_ui_tests --gtest_filter=ShutdownTest.SimpleUserQuit"\
30 -g 1f6e67861535121c5c819c16a666f2436c207e7b\
31 -b b732f23b4f81c382db0b23b9035f3dadc7d925bb\
32 -m shutdown/simple-user-quit
33 """
35 import copy
36 import errno
37 import hashlib
38 import logging
39 import argparse
40 import os
41 import re
42 import shlex
43 import shutil
44 import StringIO
45 import sys
46 import time
48 sys.path.append(os.path.join(
49 os.path.dirname(__file__), os.path.pardir, 'telemetry'))
51 from bisect_printer import BisectPrinter
52 from bisect_results import BisectResults
53 from bisect_state import BisectState
54 import bisect_utils
55 import builder
56 import fetch_build
57 import math_utils
58 import query_crbug
59 import request_build
60 import source_control
62 # The script is in chromium/src/tools/auto_bisect. Throughout this script,
63 # we use paths to other things in the chromium/src repository.
65 # Possible return values from BisectPerformanceMetrics.RunTest.
66 BUILD_RESULT_SUCCEED = 0
67 BUILD_RESULT_FAIL = 1
68 BUILD_RESULT_SKIPPED = 2
70 # Maximum time in seconds to wait after posting build request to the try server.
71 # TODO: Change these values based on the actual time taken by buildbots on
72 # the try server.
73 MAX_MAC_BUILD_TIME = 14400
74 MAX_WIN_BUILD_TIME = 14400
75 MAX_LINUX_BUILD_TIME = 14400
77 # The confidence percentage we require to consider the initial range a
78 # regression based on the test results of the inital good and bad revisions.
79 REGRESSION_CONFIDENCE = 80
81 # Patch template to add a new file, DEPS.sha under src folder.
82 # This file contains SHA1 value of the DEPS changes made while bisecting
83 # dependency repositories. This patch send along with DEPS patch to try server.
84 # When a build requested is posted with a patch, bisect builders on try server,
85 # once build is produced, it reads SHA value from this file and appends it
86 # to build archive filename.
87 DEPS_SHA_PATCH = """diff --git DEPS.sha DEPS.sha
88 new file mode 100644
89 --- /dev/null
90 +++ DEPS.sha
91 @@ -0,0 +1 @@
92 +%(deps_sha)s
93 """
95 REGRESSION_CONFIDENCE_ERROR_TEMPLATE = """
96 We could not reproduce the regression with this test/metric/platform combination
97 with enough confidence.
99 Here are the results for the given "good" and "bad" revisions:
100 "Good" revision: {good_rev}
101 \tMean: {good_mean}
102 \tStandard error: {good_std_err}
103 \tSample size: {good_sample_size}
105 "Bad" revision: {bad_rev}
106 \tMean: {bad_mean}
107 \tStandard error: {bad_std_err}
108 \tSample size: {bad_sample_size}
110 NOTE: There's still a chance that this is actually a regression, but you may
111 need to bisect a different platform."""
113 # Git branch name used to run bisect try jobs.
114 BISECT_TRYJOB_BRANCH = 'bisect-tryjob'
115 # Git master branch name.
116 BISECT_MASTER_BRANCH = 'master'
117 # File to store 'git diff' content.
118 BISECT_PATCH_FILE = 'deps_patch.txt'
119 # SVN repo where the bisect try jobs are submitted.
120 SVN_REPO_URL = 'svn://svn.chromium.org/chrome-try/try-perf'
123 class RunGitError(Exception):
125 def __str__(self):
126 return '%s\nError executing git command.' % self.args[0]
129 def GetSHA1HexDigest(contents):
130 """Returns SHA1 hex digest of the given string."""
131 return hashlib.sha1(contents).hexdigest()
134 def WriteStringToFile(text, file_name):
135 """Writes text to a file, raising an RuntimeError on failure."""
136 try:
137 with open(file_name, 'wb') as f:
138 f.write(text)
139 except IOError:
140 raise RuntimeError('Error writing to file [%s]' % file_name)
143 def ReadStringFromFile(file_name):
144 """Writes text to a file, raising an RuntimeError on failure."""
145 try:
146 with open(file_name) as f:
147 return f.read()
148 except IOError:
149 raise RuntimeError('Error reading file [%s]' % file_name)
152 def ChangeBackslashToSlashInPatch(diff_text):
153 """Formats file paths in the given patch text to Unix-style paths."""
154 if not diff_text:
155 return None
156 diff_lines = diff_text.split('\n')
157 for i in range(len(diff_lines)):
158 line = diff_lines[i]
159 if line.startswith('--- ') or line.startswith('+++ '):
160 diff_lines[i] = line.replace('\\', '/')
161 return '\n'.join(diff_lines)
164 def _ParseRevisionsFromDEPSFileManually(deps_file_contents):
165 """Parses the vars section of the DEPS file using regular expressions.
167 Args:
168 deps_file_contents: The DEPS file contents as a string.
170 Returns:
171 A dictionary in the format {depot: revision} if successful, otherwise None.
173 # We'll parse the "vars" section of the DEPS file.
174 rxp = re.compile('vars = {(?P<vars_body>[^}]+)', re.MULTILINE)
175 re_results = rxp.search(deps_file_contents)
177 if not re_results:
178 return None
180 # We should be left with a series of entries in the vars component of
181 # the DEPS file with the following format:
182 # 'depot_name': 'revision',
183 vars_body = re_results.group('vars_body')
184 rxp = re.compile(r"'(?P<depot_body>[\w_-]+)':[\s]+'(?P<rev_body>[\w@]+)'",
185 re.MULTILINE)
186 re_results = rxp.findall(vars_body)
188 return dict(re_results)
191 def _WaitUntilBuildIsReady(fetch_build_func, bot_name, builder_host,
192 builder_port, build_request_id, max_timeout):
193 """Waits until build is produced by bisect builder on try server.
195 Args:
196 fetch_build_func: Function to check and download build from cloud storage.
197 bot_name: Builder bot name on try server.
198 builder_host Try server host name.
199 builder_port: Try server port.
200 build_request_id: A unique ID of the build request posted to try server.
201 max_timeout: Maximum time to wait for the build.
203 Returns:
204 Downloaded archive file path if exists, otherwise None.
206 # Build number on the try server.
207 build_num = None
208 # Interval to check build on cloud storage.
209 poll_interval = 60
210 # Interval to check build status on try server in seconds.
211 status_check_interval = 600
212 last_status_check = time.time()
213 start_time = time.time()
214 while True:
215 # Checks for build on gs://chrome-perf and download if exists.
216 res = fetch_build_func()
217 if res:
218 return (res, 'Build successfully found')
219 elapsed_status_check = time.time() - last_status_check
220 # To avoid overloading try server with status check requests, we check
221 # build status for every 10 minutes.
222 if elapsed_status_check > status_check_interval:
223 last_status_check = time.time()
224 if not build_num:
225 # Get the build number on try server for the current build.
226 build_num = request_build.GetBuildNumFromBuilder(
227 build_request_id, bot_name, builder_host, builder_port)
228 # Check the status of build using the build number.
229 # Note: Build is treated as PENDING if build number is not found
230 # on the the try server.
231 build_status, status_link = request_build.GetBuildStatus(
232 build_num, bot_name, builder_host, builder_port)
233 if build_status == request_build.FAILED:
234 return (None, 'Failed to produce build, log: %s' % status_link)
235 elapsed_time = time.time() - start_time
236 if elapsed_time > max_timeout:
237 return (None, 'Timed out: %ss without build' % max_timeout)
239 logging.info('Time elapsed: %ss without build.', elapsed_time)
240 time.sleep(poll_interval)
241 # For some reason, mac bisect bots were not flushing stdout periodically.
242 # As a result buildbot command is timed-out. Flush stdout on all platforms
243 # while waiting for build.
244 sys.stdout.flush()
247 def _UpdateV8Branch(deps_content):
248 """Updates V8 branch in DEPS file to process v8_bleeding_edge.
250 Check for "v8_branch" in DEPS file if exists update its value
251 with v8_bleeding_edge branch. Note: "v8_branch" is added to DEPS
252 variable from DEPS revision 254916, therefore check for "src/v8":
253 <v8 source path> in DEPS in order to support prior DEPS revisions
254 and update it.
256 Args:
257 deps_content: DEPS file contents to be modified.
259 Returns:
260 Modified DEPS file contents as a string.
262 new_branch = r'branches/bleeding_edge'
263 v8_branch_pattern = re.compile(r'(?<="v8_branch": ")(.*)(?=")')
264 if re.search(v8_branch_pattern, deps_content):
265 deps_content = re.sub(v8_branch_pattern, new_branch, deps_content)
266 else:
267 # Replaces the branch assigned to "src/v8" key in DEPS file.
268 # Format of "src/v8" in DEPS:
269 # "src/v8":
270 # (Var("googlecode_url") % "v8") + "/trunk@" + Var("v8_revision"),
271 # So, "/trunk@" is replace with "/branches/bleeding_edge@"
272 v8_src_pattern = re.compile(
273 r'(?<="v8"\) \+ "/)(.*)(?=@" \+ Var\("v8_revision"\))', re.MULTILINE)
274 if re.search(v8_src_pattern, deps_content):
275 deps_content = re.sub(v8_src_pattern, new_branch, deps_content)
276 return deps_content
279 def _UpdateDEPSForAngle(revision, depot, deps_file):
280 """Updates DEPS file with new revision for Angle repository.
282 This is a hack for Angle depot case because, in DEPS file "vars" dictionary
283 variable contains "angle_revision" key that holds git hash instead of
284 SVN revision.
286 And sometimes "angle_revision" key is not specified in "vars" variable,
287 in such cases check "deps" dictionary variable that matches
288 angle.git@[a-fA-F0-9]{40}$ and replace git hash.
290 deps_var = bisect_utils.DEPOT_DEPS_NAME[depot]['deps_var']
291 try:
292 deps_contents = ReadStringFromFile(deps_file)
293 # Check whether the depot and revision pattern in DEPS file vars variable
294 # e.g. "angle_revision": "fa63e947cb3eccf463648d21a05d5002c9b8adfa".
295 angle_rev_pattern = re.compile(r'(?<="%s": ")([a-fA-F0-9]{40})(?=")' %
296 deps_var, re.MULTILINE)
297 match = re.search(angle_rev_pattern, deps_contents)
298 if match:
299 # Update the revision information for the given depot
300 new_data = re.sub(angle_rev_pattern, revision, deps_contents)
301 else:
302 # Check whether the depot and revision pattern in DEPS file deps
303 # variable. e.g.,
304 # "src/third_party/angle": Var("chromium_git") +
305 # "/angle/angle.git@fa63e947cb3eccf463648d21a05d5002c9b8adfa",.
306 angle_rev_pattern = re.compile(
307 r'(?<=angle\.git@)([a-fA-F0-9]{40})(?=")', re.MULTILINE)
308 match = re.search(angle_rev_pattern, deps_contents)
309 if not match:
310 logging.info('Could not find angle revision information in DEPS file.')
311 return False
312 new_data = re.sub(angle_rev_pattern, revision, deps_contents)
313 # Write changes to DEPS file
314 WriteStringToFile(new_data, deps_file)
315 return True
316 except IOError, e:
317 logging.warn('Something went wrong while updating DEPS file, %s', e)
318 return False
321 def _TryParseHistogramValuesFromOutput(metric, text):
322 """Attempts to parse a metric in the format HISTOGRAM <graph: <trace>.
324 Args:
325 metric: The metric as a list of [<trace>, <value>] strings.
326 text: The text to parse the metric values from.
328 Returns:
329 A list of floating point numbers found, [] if none were found.
331 metric_formatted = 'HISTOGRAM %s: %s= ' % (metric[0], metric[1])
333 text_lines = text.split('\n')
334 values_list = []
336 for current_line in text_lines:
337 if metric_formatted in current_line:
338 current_line = current_line[len(metric_formatted):]
340 try:
341 histogram_values = eval(current_line)
343 for b in histogram_values['buckets']:
344 average_for_bucket = float(b['high'] + b['low']) * 0.5
345 # Extends the list with N-elements with the average for that bucket.
346 values_list.extend([average_for_bucket] * b['count'])
347 except Exception:
348 pass
350 return values_list
353 def _TryParseResultValuesFromOutput(metric, text):
354 """Attempts to parse a metric in the format RESULT <graph>: <trace>= ...
356 Args:
357 metric: The metric as a list of [<trace>, <value>] string pairs.
358 text: The text to parse the metric values from.
360 Returns:
361 A list of floating point numbers found.
363 # Format is: RESULT <graph>: <trace>= <value> <units>
364 metric_re = re.escape('RESULT %s: %s=' % (metric[0], metric[1]))
366 # The log will be parsed looking for format:
367 # <*>RESULT <graph_name>: <trace_name>= <value>
368 single_result_re = re.compile(
369 metric_re + r'\s*(?P<VALUE>[-]?\d*(\.\d*)?)')
371 # The log will be parsed looking for format:
372 # <*>RESULT <graph_name>: <trace_name>= [<value>,value,value,...]
373 multi_results_re = re.compile(
374 metric_re + r'\s*\[\s*(?P<VALUES>[-]?[\d\., ]+)\s*\]')
376 # The log will be parsed looking for format:
377 # <*>RESULT <graph_name>: <trace_name>= {<mean>, <std deviation>}
378 mean_stddev_re = re.compile(
379 metric_re +
380 r'\s*\{\s*(?P<MEAN>[-]?\d*(\.\d*)?),\s*(?P<STDDEV>\d+(\.\d*)?)\s*\}')
382 text_lines = text.split('\n')
383 values_list = []
384 for current_line in text_lines:
385 # Parse the output from the performance test for the metric we're
386 # interested in.
387 single_result_match = single_result_re.search(current_line)
388 multi_results_match = multi_results_re.search(current_line)
389 mean_stddev_match = mean_stddev_re.search(current_line)
390 if (not single_result_match is None and
391 single_result_match.group('VALUE')):
392 values_list += [single_result_match.group('VALUE')]
393 elif (not multi_results_match is None and
394 multi_results_match.group('VALUES')):
395 metric_values = multi_results_match.group('VALUES')
396 values_list += metric_values.split(',')
397 elif (not mean_stddev_match is None and
398 mean_stddev_match.group('MEAN')):
399 values_list += [mean_stddev_match.group('MEAN')]
401 values_list = [float(v) for v in values_list
402 if bisect_utils.IsStringFloat(v)]
404 # If the metric is times/t, we need to sum the timings in order to get
405 # similar regression results as the try-bots.
406 metrics_to_sum = [
407 ['times', 't'],
408 ['times', 'page_load_time'],
409 ['cold_times', 'page_load_time'],
410 ['warm_times', 'page_load_time'],
413 if metric in metrics_to_sum:
414 if values_list:
415 values_list = [reduce(lambda x, y: float(x) + float(y), values_list)]
417 return values_list
420 def _ParseMetricValuesFromOutput(metric, text):
421 """Parses output from performance_ui_tests and retrieves the results for
422 a given metric.
424 Args:
425 metric: The metric as a list of [<trace>, <value>] strings.
426 text: The text to parse the metric values from.
428 Returns:
429 A list of floating point numbers found.
431 metric_values = _TryParseResultValuesFromOutput(metric, text)
433 if not metric_values:
434 metric_values = _TryParseHistogramValuesFromOutput(metric, text)
436 return metric_values
439 def _GenerateProfileIfNecessary(command_args):
440 """Checks the command line of the performance test for dependencies on
441 profile generation, and runs tools/perf/generate_profile as necessary.
443 Args:
444 command_args: Command line being passed to performance test, as a list.
446 Returns:
447 False if profile generation was necessary and failed, otherwise True.
449 if '--profile-dir' in ' '.join(command_args):
450 # If we were using python 2.7+, we could just use the argparse
451 # module's parse_known_args to grab --profile-dir. Since some of the
452 # bots still run 2.6, have to grab the arguments manually.
453 arg_dict = {}
454 args_to_parse = ['--profile-dir', '--browser']
456 for arg_to_parse in args_to_parse:
457 for i, current_arg in enumerate(command_args):
458 if arg_to_parse in current_arg:
459 current_arg_split = current_arg.split('=')
461 # Check 2 cases, --arg=<val> and --arg <val>
462 if len(current_arg_split) == 2:
463 arg_dict[arg_to_parse] = current_arg_split[1]
464 elif i + 1 < len(command_args):
465 arg_dict[arg_to_parse] = command_args[i+1]
467 path_to_generate = os.path.join('tools', 'perf', 'generate_profile')
469 if arg_dict.has_key('--profile-dir') and arg_dict.has_key('--browser'):
470 profile_path, profile_type = os.path.split(arg_dict['--profile-dir'])
471 return not bisect_utils.RunProcess(['python', path_to_generate,
472 '--profile-type-to-generate', profile_type,
473 '--browser', arg_dict['--browser'], '--output-dir', profile_path])
474 return False
475 return True
478 def _CheckRegressionConfidenceError(
479 good_revision,
480 bad_revision,
481 known_good_value,
482 known_bad_value):
483 """Checks whether we can be confident beyond a certain degree that the given
484 metrics represent a regression.
486 Args:
487 good_revision: string representing the commit considered 'good'
488 bad_revision: Same as above for 'bad'.
489 known_good_value: A dict with at least: 'values', 'mean' and 'std_err'
490 known_bad_value: Same as above.
492 Returns:
493 False if there is no error (i.e. we can be confident there's a regressioni),
494 a string containing the details of the lack of confidence otherwise.
496 error = False
497 # Adding good and bad values to a parameter list.
498 confidence_params = []
499 for l in [known_bad_value['values'], known_good_value['values']]:
500 # Flatten if needed, by averaging the values in each nested list
501 if isinstance(l, list) and all([isinstance(x, list) for x in l]):
502 averages = map(math_utils.Mean, l)
503 confidence_params.append(averages)
504 else:
505 confidence_params.append(l)
506 regression_confidence = BisectResults.ConfidenceScore(*confidence_params)
507 if regression_confidence < REGRESSION_CONFIDENCE:
508 error = REGRESSION_CONFIDENCE_ERROR_TEMPLATE.format(
509 good_rev=good_revision,
510 good_mean=known_good_value['mean'],
511 good_std_err=known_good_value['std_err'],
512 good_sample_size=len(known_good_value['values']),
513 bad_rev=bad_revision,
514 bad_mean=known_bad_value['mean'],
515 bad_std_err=known_bad_value['std_err'],
516 bad_sample_size=len(known_bad_value['values']))
517 return error
520 class DepotDirectoryRegistry(object):
522 def __init__(self, src_cwd):
523 self.depot_cwd = {}
524 for depot in bisect_utils.DEPOT_NAMES:
525 # The working directory of each depot is just the path to the depot, but
526 # since we're already in 'src', we can skip that part.
527 path_in_src = bisect_utils.DEPOT_DEPS_NAME[depot]['src'][4:]
528 self.SetDepotDir(depot, os.path.join(src_cwd, path_in_src))
530 self.SetDepotDir('chromium', src_cwd)
532 def SetDepotDir(self, depot_name, depot_dir):
533 self.depot_cwd[depot_name] = depot_dir
535 def GetDepotDir(self, depot_name):
536 if depot_name in self.depot_cwd:
537 return self.depot_cwd[depot_name]
538 else:
539 assert False, ('Unknown depot [ %s ] encountered. Possibly a new one '
540 'was added without proper support?' % depot_name)
542 def ChangeToDepotDir(self, depot_name):
543 """Given a depot, changes to the appropriate working directory.
545 Args:
546 depot_name: The name of the depot (see DEPOT_NAMES).
548 os.chdir(self.GetDepotDir(depot_name))
551 def _PrepareBisectBranch(parent_branch, new_branch):
552 """Creates a new branch to submit bisect try job.
554 Args:
555 parent_branch: Parent branch to be used to create new branch.
556 new_branch: New branch name.
558 current_branch, returncode = bisect_utils.RunGit(
559 ['rev-parse', '--abbrev-ref', 'HEAD'])
560 if returncode:
561 raise RunGitError('Must be in a git repository to send changes to trybots.')
563 current_branch = current_branch.strip()
564 # Make sure current branch is master.
565 if current_branch != parent_branch:
566 output, returncode = bisect_utils.RunGit(['checkout', '-f', parent_branch])
567 if returncode:
568 raise RunGitError('Failed to checkout branch: %s.' % output)
570 # Delete new branch if exists.
571 output, returncode = bisect_utils.RunGit(['branch', '--list'])
572 if new_branch in output:
573 output, returncode = bisect_utils.RunGit(['branch', '-D', new_branch])
574 if returncode:
575 raise RunGitError('Deleting branch failed, %s', output)
577 # Check if the tree is dirty: make sure the index is up to date and then
578 # run diff-index.
579 bisect_utils.RunGit(['update-index', '--refresh', '-q'])
580 output, returncode = bisect_utils.RunGit(['diff-index', 'HEAD'])
581 if output:
582 raise RunGitError('Cannot send a try job with a dirty tree.')
584 # Create/check out the telemetry-tryjob branch, and edit the configs
585 # for the tryjob there.
586 output, returncode = bisect_utils.RunGit(['checkout', '-b', new_branch])
587 if returncode:
588 raise RunGitError('Failed to checkout branch: %s.' % output)
590 output, returncode = bisect_utils.RunGit(
591 ['branch', '--set-upstream-to', parent_branch])
592 if returncode:
593 raise RunGitError('Error in git branch --set-upstream-to')
596 def _BuilderTryjob(git_revision, bot_name, bisect_job_name, patch=None):
597 """Attempts to run a tryjob from the current directory.
599 Args:
600 git_revision: A Git hash revision.
601 bot_name: Name of the bisect bot to be used for try job.
602 bisect_job_name: Bisect try job name.
603 patch: A DEPS patch (used while bisecting 3rd party repositories).
605 try:
606 # Temporary branch for running tryjob.
607 _PrepareBisectBranch(BISECT_MASTER_BRANCH, BISECT_TRYJOB_BRANCH)
608 patch_content = '/dev/null'
609 # Create a temporary patch file, if it fails raise an exception.
610 if patch:
611 WriteStringToFile(patch, BISECT_PATCH_FILE)
612 patch_content = BISECT_PATCH_FILE
614 try_cmd = ['try',
615 '-b', bot_name,
616 '-r', git_revision,
617 '-n', bisect_job_name,
618 '--svn_repo=%s' % SVN_REPO_URL,
619 '--diff=%s' % patch_content
621 # Execute try job to build revision.
622 output, returncode = bisect_utils.RunGit(try_cmd)
624 if returncode:
625 raise RunGitError('Could not execute tryjob: %s.\n Error: %s' % (
626 'git %s' % ' '.join(try_cmd), output))
627 logging.info('Try job successfully submitted.\n TryJob Details: %s\n%s',
628 'git %s' % ' '.join(try_cmd), output)
629 finally:
630 # Delete patch file if exists
631 try:
632 os.remove(BISECT_PATCH_FILE)
633 except OSError as e:
634 if e.errno != errno.ENOENT:
635 raise
636 # Checkout master branch and delete bisect-tryjob branch.
637 bisect_utils.RunGit(['checkout', '-f', BISECT_MASTER_BRANCH])
638 bisect_utils.RunGit(['branch', '-D', BISECT_TRYJOB_BRANCH])
641 class BisectPerformanceMetrics(object):
642 """This class contains functionality to perform a bisection of a range of
643 revisions to narrow down where performance regressions may have occurred.
645 The main entry-point is the Run method.
648 def __init__(self, opts, src_cwd):
649 """Constructs a BisectPerformancesMetrics object.
651 Args:
652 opts: BisectOptions object containing parsed options.
653 src_cwd: Root src/ directory of the test repository (inside bisect/ dir).
655 super(BisectPerformanceMetrics, self).__init__()
657 self.opts = opts
658 self.src_cwd = src_cwd
659 self.depot_registry = DepotDirectoryRegistry(self.src_cwd)
660 self.printer = BisectPrinter(self.opts, self.depot_registry)
661 self.cleanup_commands = []
662 self.warnings = []
663 self.builder = builder.Builder.FromOpts(opts)
665 def PerformCleanup(self):
666 """Performs cleanup when script is finished."""
667 os.chdir(self.src_cwd)
668 for c in self.cleanup_commands:
669 if c[0] == 'mv':
670 shutil.move(c[1], c[2])
671 else:
672 assert False, 'Invalid cleanup command.'
674 def GetRevisionList(self, depot, bad_revision, good_revision):
675 """Retrieves a list of all the commits between the bad revision and
676 last known good revision."""
678 cwd = self.depot_registry.GetDepotDir(depot)
679 return source_control.GetRevisionList(bad_revision, good_revision, cwd=cwd)
681 def _ParseRevisionsFromDEPSFile(self, depot):
682 """Parses the local DEPS file to determine blink/skia/v8 revisions which may
683 be needed if the bisect recurses into those depots later.
685 Args:
686 depot: Name of depot being bisected.
688 Returns:
689 A dict in the format {depot:revision} if successful, otherwise None.
691 try:
692 deps_data = {
693 'Var': lambda _: deps_data["vars"][_],
694 'From': lambda *args: None,
697 deps_file = bisect_utils.FILE_DEPS_GIT
698 if not os.path.exists(deps_file):
699 deps_file = bisect_utils.FILE_DEPS
700 execfile(deps_file, {}, deps_data)
701 deps_data = deps_data['deps']
703 rxp = re.compile(".git@(?P<revision>[a-fA-F0-9]+)")
704 results = {}
705 for depot_name, depot_data in bisect_utils.DEPOT_DEPS_NAME.iteritems():
706 if (depot_data.get('platform') and
707 depot_data.get('platform') != os.name):
708 continue
710 if depot_data.get('recurse') and depot in depot_data.get('from'):
711 depot_data_src = depot_data.get('src') or depot_data.get('src_old')
712 src_dir = deps_data.get(depot_data_src)
713 if src_dir:
714 self.depot_registry.SetDepotDir(depot_name, os.path.join(
715 self.src_cwd, depot_data_src[4:]))
716 re_results = rxp.search(src_dir)
717 if re_results:
718 results[depot_name] = re_results.group('revision')
719 else:
720 warning_text = ('Could not parse revision for %s while bisecting '
721 '%s' % (depot_name, depot))
722 if not warning_text in self.warnings:
723 self.warnings.append(warning_text)
724 else:
725 results[depot_name] = None
726 return results
727 except ImportError:
728 deps_file_contents = ReadStringFromFile(deps_file)
729 parse_results = _ParseRevisionsFromDEPSFileManually(deps_file_contents)
730 results = {}
731 for depot_name, depot_revision in parse_results.iteritems():
732 depot_revision = depot_revision.strip('@')
733 logging.warn(depot_name, depot_revision)
734 for cur_name, cur_data in bisect_utils.DEPOT_DEPS_NAME.iteritems():
735 if (cur_data.has_key('deps_var') and
736 cur_data['deps_var'] == depot_name):
737 src_name = cur_name
738 results[src_name] = depot_revision
739 break
740 return results
742 def _Get3rdPartyRevisions(self, depot):
743 """Parses the DEPS file to determine WebKit/v8/etc... versions.
745 Args:
746 depot: A depot name. Should be in the DEPOT_NAMES list.
748 Returns:
749 A dict in the format {depot: revision} if successful, otherwise None.
751 cwd = os.getcwd()
752 self.depot_registry.ChangeToDepotDir(depot)
754 results = {}
756 if depot == 'chromium' or depot == 'android-chrome':
757 results = self._ParseRevisionsFromDEPSFile(depot)
758 os.chdir(cwd)
760 if depot == 'v8':
761 # We can't try to map the trunk revision to bleeding edge yet, because
762 # we don't know which direction to try to search in. Have to wait until
763 # the bisect has narrowed the results down to 2 v8 rolls.
764 results['v8_bleeding_edge'] = None
766 return results
768 def BackupOrRestoreOutputDirectory(self, restore=False, build_type='Release'):
769 """Backs up or restores build output directory based on restore argument.
771 Args:
772 restore: Indicates whether to restore or backup. Default is False(Backup)
773 build_type: Target build type ('Release', 'Debug', 'Release_x64' etc.)
775 Returns:
776 Path to backup or restored location as string. otherwise None if it fails.
778 build_dir = os.path.abspath(
779 builder.GetBuildOutputDirectory(self.opts, self.src_cwd))
780 source_dir = os.path.join(build_dir, build_type)
781 destination_dir = os.path.join(build_dir, '%s.bak' % build_type)
782 if restore:
783 source_dir, destination_dir = destination_dir, source_dir
784 if os.path.exists(source_dir):
785 RemoveDirectoryTree(destination_dir)
786 shutil.move(source_dir, destination_dir)
787 return destination_dir
788 return None
790 def _DownloadAndUnzipBuild(self, revision, depot, build_type='Release'):
791 """Downloads the build archive for the given revision.
793 Args:
794 revision: The git revision to download.
795 depot: The name of a dependency repository. Should be in DEPOT_NAMES.
796 build_type: Target build type, e.g. Release', 'Debug', 'Release_x64' etc.
798 Returns:
799 True if download succeeds, otherwise False.
801 patch = None
802 patch_sha = None
803 if depot != 'chromium':
804 # Create a DEPS patch with new revision for dependency repository.
805 revision, patch = self.CreateDEPSPatch(depot, revision)
807 if patch:
808 # Get the SHA of the DEPS changes patch.
809 patch_sha = GetSHA1HexDigest(patch)
811 # Update the DEPS changes patch with a patch to create a new file named
812 # 'DEPS.sha' and add patch_sha evaluated above to it.
813 patch = '%s\n%s' % (patch, DEPS_SHA_PATCH % {'deps_sha': patch_sha})
815 build_dir = builder.GetBuildOutputDirectory(self.opts, self.src_cwd)
816 downloaded_file = self._WaitForBuildDownload(
817 revision, build_dir, deps_patch=patch, deps_patch_sha=patch_sha)
818 if not downloaded_file:
819 return False
820 return self._UnzipAndMoveBuildProducts(downloaded_file, build_dir,
821 build_type=build_type)
823 def _WaitForBuildDownload(self, revision, build_dir, deps_patch=None,
824 deps_patch_sha=None):
825 """Tries to download a zip archive for a build.
827 This involves seeing whether the archive is already available, and if not,
828 then requesting a build and waiting before downloading.
830 Args:
831 revision: A git commit hash.
832 build_dir: The directory to download the build into.
833 deps_patch: A patch which changes a dependency repository revision in
834 the DEPS, if applicable.
835 deps_patch_sha: The SHA1 hex digest of the above patch.
837 Returns:
838 File path of the downloaded file if successful, otherwise None.
840 bucket_name, remote_path = fetch_build.GetBucketAndRemotePath(
841 revision, target_arch=self.opts.target_arch,
842 target_platform=self.opts.target_platform,
843 deps_patch_sha=deps_patch_sha)
844 output_dir = os.path.abspath(build_dir)
845 fetch_build_func = lambda: fetch_build.FetchFromCloudStorage(
846 bucket_name, remote_path, output_dir)
848 is_available = fetch_build.BuildIsAvailable(bucket_name, remote_path)
849 if is_available:
850 return fetch_build_func()
852 # When build archive doesn't exist, make a request and wait.
853 return self._RequestBuildAndWait(
854 revision, fetch_build_func, deps_patch=deps_patch)
856 def _RequestBuildAndWait(self, git_revision, fetch_build_func,
857 deps_patch=None):
858 """Triggers a try job for a build job.
860 This function prepares and starts a try job for a builder, and waits for
861 the archive to be produced and archived. Once the build is ready it is
862 downloaded.
864 For performance tests, builders on the tryserver.chromium.perf are used.
866 TODO(qyearsley): Make this function take "builder_type" as a parameter
867 and make requests to different bot names based on that parameter.
869 Args:
870 git_revision: A git commit hash.
871 fetch_build_func: Function to check and download build from cloud storage.
872 deps_patch: DEPS patch string, used when bisecting dependency repos.
874 Returns:
875 Downloaded archive file path when requested build exists and download is
876 successful, otherwise None.
878 if not fetch_build_func:
879 return None
881 # Create a unique ID for each build request posted to try server builders.
882 # This ID is added to "Reason" property of the build.
883 build_request_id = GetSHA1HexDigest(
884 '%s-%s-%s' % (git_revision, deps_patch, time.time()))
886 # Revert any changes to DEPS file.
887 source_control.CheckoutFileAtRevision(
888 bisect_utils.FILE_DEPS, git_revision, cwd=self.src_cwd)
890 bot_name = self._GetBuilderName(self.opts.target_platform)
891 build_timeout = self._GetBuilderBuildTime()
893 try:
894 _BuilderTryjob(git_revision, bot_name, build_request_id, deps_patch)
895 except RunGitError as e:
896 logging.warn('Failed to post builder try job for revision: [%s].\n'
897 'Error: %s', git_revision, e)
898 return None
900 archive_filename, error_msg = _WaitUntilBuildIsReady(
901 fetch_build_func, bot_name, self.opts.builder_host,
902 self.opts.builder_port, build_request_id, build_timeout)
903 if not archive_filename:
904 logging.warn('%s [revision: %s]', error_msg, git_revision)
905 return archive_filename
907 @staticmethod
908 def _GetBuilderName(target_platform, builder_type=fetch_build.PERF_BUILDER):
909 """Gets builder bot name and build time in seconds based on platform."""
910 if builder_type != fetch_build.PERF_BUILDER:
911 raise NotImplementedError('No builder names for non-perf builds yet.')
912 if bisect_utils.IsWindowsHost():
913 return 'win_perf_bisect_builder'
914 if bisect_utils.IsLinuxHost():
915 if target_platform == 'android':
916 return 'android_perf_bisect_builder'
917 return 'linux_perf_bisect_builder'
918 if bisect_utils.IsMacHost():
919 return 'mac_perf_bisect_builder'
920 raise NotImplementedError('Unsupported Platform "%s".' % sys.platform)
922 @staticmethod
923 def _GetBuilderBuildTime():
924 """Returns the time to wait for a build after requesting one."""
925 if bisect_utils.IsWindowsHost():
926 return MAX_WIN_BUILD_TIME
927 if bisect_utils.IsLinuxHost():
928 return MAX_LINUX_BUILD_TIME
929 if bisect_utils.IsMacHost():
930 return MAX_MAC_BUILD_TIME
931 raise NotImplementedError('Unsupported Platform "%s".' % sys.platform)
933 def _UnzipAndMoveBuildProducts(self, downloaded_file, build_dir,
934 build_type='Release'):
935 """Unzips the build archive and moves it to the build output directory.
937 The build output directory is whereever the binaries are expected to
938 be in order to start Chrome and run tests.
940 TODO: Simplify and clarify this method if possible.
942 Args:
943 downloaded_file: File path of the downloaded zip file.
944 build_dir: Directory where the the zip file was downloaded to.
945 build_type: "Release" or "Debug".
947 Returns:
948 True if successful, False otherwise.
950 abs_build_dir = os.path.abspath(build_dir)
951 output_dir = os.path.join(abs_build_dir, self.GetZipFileBuildDirName())
952 logging.info('EXPERIMENTAL RUN, _UnzipAndMoveBuildProducts locals %s',
953 str(locals()))
955 try:
956 RemoveDirectoryTree(output_dir)
957 self.BackupOrRestoreOutputDirectory(restore=False)
958 # Build output directory based on target(e.g. out/Release, out/Debug).
959 target_build_output_dir = os.path.join(abs_build_dir, build_type)
961 logging.info('Extracting "%s" to "%s"', downloaded_file, abs_build_dir)
962 fetch_build.Unzip(downloaded_file, abs_build_dir)
964 if not os.path.exists(output_dir):
965 # Due to recipe changes, the builds extract folder contains
966 # out/Release instead of full-build-<platform>/Release.
967 if os.path.exists(os.path.join(abs_build_dir, 'out', build_type)):
968 output_dir = os.path.join(abs_build_dir, 'out', build_type)
969 else:
970 raise IOError('Missing extracted folder %s ' % output_dir)
972 logging.info('Moving build from %s to %s',
973 output_dir, target_build_output_dir)
974 shutil.move(output_dir, target_build_output_dir)
975 return True
976 except Exception as e:
977 logging.info('Something went wrong while extracting archive file: %s', e)
978 self.BackupOrRestoreOutputDirectory(restore=True)
979 # Cleanup any leftovers from unzipping.
980 if os.path.exists(output_dir):
981 RemoveDirectoryTree(output_dir)
982 finally:
983 # Delete downloaded archive
984 if os.path.exists(downloaded_file):
985 os.remove(downloaded_file)
986 return False
988 @staticmethod
989 def GetZipFileBuildDirName():
990 """Gets the base file name of the zip file.
992 After extracting the zip file, this is the name of the directory where
993 the build files are expected to be. Possibly.
995 TODO: Make sure that this returns the actual directory name where the
996 Release or Debug directory is inside of the zip files. This probably
997 depends on the builder recipe, and may depend on whether the builder is
998 a perf builder or full builder.
1000 Returns:
1001 The name of the directory inside a build archive which is expected to
1002 contain a Release or Debug directory.
1004 if bisect_utils.IsWindowsHost():
1005 return 'full-build-win32'
1006 if bisect_utils.IsLinuxHost():
1007 return 'full-build-linux'
1008 if bisect_utils.IsMacHost():
1009 return 'full-build-mac'
1010 raise NotImplementedError('Unknown platform "%s".' % sys.platform)
1012 def IsDownloadable(self, depot):
1013 """Checks if build can be downloaded based on target platform and depot."""
1014 if self.opts.target_platform in ['chromium', 'android']:
1015 return (depot == 'chromium' or
1016 'chromium' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'] or
1017 'v8' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'])
1018 return False
1020 def UpdateDepsContents(self, deps_contents, depot, git_revision, deps_key):
1021 """Returns modified version of DEPS file contents.
1023 Args:
1024 deps_contents: DEPS file content.
1025 depot: Current depot being bisected.
1026 git_revision: A git hash to be updated in DEPS.
1027 deps_key: Key in vars section of DEPS file to be searched.
1029 Returns:
1030 Updated DEPS content as string if deps key is found, otherwise None.
1032 # Check whether the depot and revision pattern in DEPS file vars
1033 # e.g. for webkit the format is "webkit_revision": "12345".
1034 deps_revision = re.compile(r'(?<="%s": ")([0-9]+)(?=")' % deps_key,
1035 re.MULTILINE)
1036 new_data = None
1037 if re.search(deps_revision, deps_contents):
1038 commit_position = source_control.GetCommitPosition(
1039 git_revision, self.depot_registry.GetDepotDir(depot))
1040 if not commit_position:
1041 logging.warn('Could not determine commit position for %s', git_revision)
1042 return None
1043 # Update the revision information for the given depot
1044 new_data = re.sub(deps_revision, str(commit_position), deps_contents)
1045 else:
1046 # Check whether the depot and revision pattern in DEPS file vars
1047 # e.g. for webkit the format is "webkit_revision": "559a6d4ab7a84c539..".
1048 deps_revision = re.compile(
1049 r'(?<=["\']%s["\']: ["\'])([a-fA-F0-9]{40})(?=["\'])' % deps_key,
1050 re.MULTILINE)
1051 if re.search(deps_revision, deps_contents):
1052 new_data = re.sub(deps_revision, git_revision, deps_contents)
1053 if new_data:
1054 # For v8_bleeding_edge revisions change V8 branch in order
1055 # to fetch bleeding edge revision.
1056 if depot == 'v8_bleeding_edge':
1057 new_data = _UpdateV8Branch(new_data)
1058 if not new_data:
1059 return None
1060 return new_data
1062 def UpdateDeps(self, revision, depot, deps_file):
1063 """Updates DEPS file with new revision of dependency repository.
1065 This method search DEPS for a particular pattern in which depot revision
1066 is specified (e.g "webkit_revision": "123456"). If a match is found then
1067 it resolves the given git hash to SVN revision and replace it in DEPS file.
1069 Args:
1070 revision: A git hash revision of the dependency repository.
1071 depot: Current depot being bisected.
1072 deps_file: Path to DEPS file.
1074 Returns:
1075 True if DEPS file is modified successfully, otherwise False.
1077 if not os.path.exists(deps_file):
1078 return False
1080 deps_var = bisect_utils.DEPOT_DEPS_NAME[depot]['deps_var']
1081 # Don't update DEPS file if deps_var is not set in DEPOT_DEPS_NAME.
1082 if not deps_var:
1083 logging.warn('DEPS update not supported for Depot: %s', depot)
1084 return False
1086 # Hack for Angle repository. In the DEPS file, "vars" dictionary variable
1087 # contains "angle_revision" key that holds git hash instead of SVN revision.
1088 # And sometime "angle_revision" key is not specified in "vars" variable.
1089 # In such cases check, "deps" dictionary variable that matches
1090 # angle.git@[a-fA-F0-9]{40}$ and replace git hash.
1091 if depot == 'angle':
1092 return _UpdateDEPSForAngle(revision, depot, deps_file)
1094 try:
1095 deps_contents = ReadStringFromFile(deps_file)
1096 updated_deps_content = self.UpdateDepsContents(
1097 deps_contents, depot, revision, deps_var)
1098 # Write changes to DEPS file
1099 if updated_deps_content:
1100 WriteStringToFile(updated_deps_content, deps_file)
1101 return True
1102 except IOError, e:
1103 logging.warn('Something went wrong while updating DEPS file. [%s]', e)
1104 return False
1106 def CreateDEPSPatch(self, depot, revision):
1107 """Modifies DEPS and returns diff as text.
1109 Args:
1110 depot: Current depot being bisected.
1111 revision: A git hash revision of the dependency repository.
1113 Returns:
1114 A tuple with git hash of chromium revision and DEPS patch text.
1116 deps_file_path = os.path.join(self.src_cwd, bisect_utils.FILE_DEPS)
1117 if not os.path.exists(deps_file_path):
1118 raise RuntimeError('DEPS file does not exists.[%s]' % deps_file_path)
1119 # Get current chromium revision (git hash).
1120 cmd = ['rev-parse', 'HEAD']
1121 chromium_sha = bisect_utils.CheckRunGit(cmd).strip()
1122 if not chromium_sha:
1123 raise RuntimeError('Failed to determine Chromium revision for %s' %
1124 revision)
1125 if ('chromium' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'] or
1126 'v8' in bisect_utils.DEPOT_DEPS_NAME[depot]['from']):
1127 # Checkout DEPS file for the current chromium revision.
1128 if source_control.CheckoutFileAtRevision(
1129 bisect_utils.FILE_DEPS, chromium_sha, cwd=self.src_cwd):
1130 if self.UpdateDeps(revision, depot, deps_file_path):
1131 diff_command = [
1132 'diff',
1133 '--src-prefix=',
1134 '--dst-prefix=',
1135 '--no-ext-diff',
1136 bisect_utils.FILE_DEPS,
1138 diff_text = bisect_utils.CheckRunGit(diff_command, cwd=self.src_cwd)
1139 return (chromium_sha, ChangeBackslashToSlashInPatch(diff_text))
1140 else:
1141 raise RuntimeError(
1142 'Failed to update DEPS file for chromium: [%s]' % chromium_sha)
1143 else:
1144 raise RuntimeError(
1145 'DEPS checkout Failed for chromium revision : [%s]' % chromium_sha)
1146 return (None, None)
1148 def ObtainBuild(self, depot, revision=None):
1149 """Obtains a build by either downloading or building directly.
1151 Args:
1152 depot: Dependency repository name.
1153 revision: A git commit hash. If None is given, the currently checked-out
1154 revision is built.
1156 Returns:
1157 True for success.
1159 if self.opts.debug_ignore_build:
1160 return True
1162 build_success = False
1163 cwd = os.getcwd()
1164 os.chdir(self.src_cwd)
1165 # Fetch build archive for the given revision from the cloud storage when
1166 # the storage bucket is passed.
1167 if self.IsDownloadable(depot) and revision:
1168 build_success = self._DownloadAndUnzipBuild(revision, depot)
1169 else:
1170 # Build locally.
1171 build_success = self.builder.Build(depot, self.opts)
1172 os.chdir(cwd)
1173 return build_success
1175 def RunGClientHooks(self):
1176 """Runs gclient with runhooks command.
1178 Returns:
1179 True if gclient reports no errors.
1181 if self.opts.debug_ignore_build:
1182 return True
1183 return not bisect_utils.RunGClient(['runhooks'], cwd=self.src_cwd)
1185 def _IsBisectModeUsingMetric(self):
1186 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_MEAN,
1187 bisect_utils.BISECT_MODE_STD_DEV]
1189 def _IsBisectModeReturnCode(self):
1190 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_RETURN_CODE]
1192 def _IsBisectModeStandardDeviation(self):
1193 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_STD_DEV]
1195 def GetCompatibleCommand(self, command_to_run, revision, depot):
1196 """Return a possibly modified test command depending on the revision.
1198 Prior to crrev.com/274857 *only* android-chromium-testshell
1199 Then until crrev.com/276628 *both* (android-chromium-testshell and
1200 android-chrome-shell) work. After that rev 276628 *only*
1201 android-chrome-shell works. The bisect_perf_regression.py script should
1202 handle these cases and set appropriate browser type based on revision.
1204 if self.opts.target_platform in ['android']:
1205 # When its a third_party depot, get the chromium revision.
1206 if depot != 'chromium':
1207 revision = bisect_utils.CheckRunGit(
1208 ['rev-parse', 'HEAD'], cwd=self.src_cwd).strip()
1209 commit_position = source_control.GetCommitPosition(revision,
1210 cwd=self.src_cwd)
1211 if not commit_position:
1212 return command_to_run
1213 cmd_re = re.compile(r'--browser=(?P<browser_type>\S+)')
1214 matches = cmd_re.search(command_to_run)
1215 if bisect_utils.IsStringInt(commit_position) and matches:
1216 cmd_browser = matches.group('browser_type')
1217 if commit_position <= 274857 and cmd_browser == 'android-chrome-shell':
1218 return command_to_run.replace(cmd_browser,
1219 'android-chromium-testshell')
1220 elif (commit_position >= 276628 and
1221 cmd_browser == 'android-chromium-testshell'):
1222 return command_to_run.replace(cmd_browser,
1223 'android-chrome-shell')
1224 return command_to_run
1226 def RunPerformanceTestAndParseResults(
1227 self, command_to_run, metric, reset_on_first_run=False,
1228 upload_on_last_run=False, results_label=None):
1229 """Runs a performance test on the current revision and parses the results.
1231 Args:
1232 command_to_run: The command to be run to execute the performance test.
1233 metric: The metric to parse out from the results of the performance test.
1234 This is the result chart name and trace name, separated by slash.
1235 May be None for perf try jobs.
1236 reset_on_first_run: If True, pass the flag --reset-results on first run.
1237 upload_on_last_run: If True, pass the flag --upload-results on last run.
1238 results_label: A value for the option flag --results-label.
1239 The arguments reset_on_first_run, upload_on_last_run and results_label
1240 are all ignored if the test is not a Telemetry test.
1242 Returns:
1243 (values dict, 0) if --debug_ignore_perf_test was passed.
1244 (values dict, 0, test output) if the test was run successfully.
1245 (error message, -1) if the test couldn't be run.
1246 (error message, -1, test output) if the test ran but there was an error.
1248 success_code, failure_code = 0, -1
1250 if self.opts.debug_ignore_perf_test:
1251 fake_results = {
1252 'mean': 0.0,
1253 'std_err': 0.0,
1254 'std_dev': 0.0,
1255 'values': [0.0]
1258 # When debug_fake_test_mean is set, its value is returned as the mean
1259 # and the flag is cleared so that further calls behave as if it wasn't
1260 # set (returning the fake_results dict as defined above).
1261 if self.opts.debug_fake_first_test_mean:
1262 fake_results['mean'] = float(self.opts.debug_fake_first_test_mean)
1263 self.opts.debug_fake_first_test_mean = 0
1265 return (fake_results, success_code)
1267 # For Windows platform set posix=False, to parse windows paths correctly.
1268 # On Windows, path separators '\' or '\\' are replace by '' when posix=True,
1269 # refer to http://bugs.python.org/issue1724822. By default posix=True.
1270 args = shlex.split(command_to_run, posix=not bisect_utils.IsWindowsHost())
1272 if not _GenerateProfileIfNecessary(args):
1273 err_text = 'Failed to generate profile for performance test.'
1274 return (err_text, failure_code)
1276 is_telemetry = bisect_utils.IsTelemetryCommand(command_to_run)
1278 start_time = time.time()
1280 metric_values = []
1281 output_of_all_runs = ''
1282 for i in xrange(self.opts.repeat_test_count):
1283 # Can ignore the return code since if the tests fail, it won't return 0.
1284 current_args = copy.copy(args)
1285 if is_telemetry:
1286 if i == 0 and reset_on_first_run:
1287 current_args.append('--reset-results')
1288 if i == self.opts.repeat_test_count - 1 and upload_on_last_run:
1289 current_args.append('--upload-results')
1290 if results_label:
1291 current_args.append('--results-label=%s' % results_label)
1292 try:
1293 output, return_code = bisect_utils.RunProcessAndRetrieveOutput(
1294 current_args, cwd=self.src_cwd)
1295 except OSError, e:
1296 if e.errno == errno.ENOENT:
1297 err_text = ('Something went wrong running the performance test. '
1298 'Please review the command line:\n\n')
1299 if 'src/' in ' '.join(args):
1300 err_text += ('Check that you haven\'t accidentally specified a '
1301 'path with src/ in the command.\n\n')
1302 err_text += ' '.join(args)
1303 err_text += '\n'
1305 return (err_text, failure_code)
1306 raise
1308 output_of_all_runs += output
1309 if self.opts.output_buildbot_annotations:
1310 print output
1312 if metric and self._IsBisectModeUsingMetric():
1313 metric_values += _ParseMetricValuesFromOutput(metric, output)
1314 # If we're bisecting on a metric (ie, changes in the mean or
1315 # standard deviation) and no metric values are produced, bail out.
1316 if not metric_values:
1317 break
1318 elif self._IsBisectModeReturnCode():
1319 metric_values.append(return_code)
1321 elapsed_minutes = (time.time() - start_time) / 60.0
1322 if elapsed_minutes >= self.opts.max_time_minutes:
1323 break
1325 if metric and len(metric_values) == 0:
1326 err_text = 'Metric %s was not found in the test output.' % metric
1327 # TODO(qyearsley): Consider also getting and displaying a list of metrics
1328 # that were found in the output here.
1329 return (err_text, failure_code, output_of_all_runs)
1331 # If we're bisecting on return codes, we're really just looking for zero vs
1332 # non-zero.
1333 values = {}
1334 if self._IsBisectModeReturnCode():
1335 # If any of the return codes is non-zero, output 1.
1336 overall_return_code = 0 if (
1337 all(current_value == 0 for current_value in metric_values)) else 1
1339 values = {
1340 'mean': overall_return_code,
1341 'std_err': 0.0,
1342 'std_dev': 0.0,
1343 'values': metric_values,
1346 print 'Results of performance test: Command returned with %d' % (
1347 overall_return_code)
1348 print
1349 elif metric:
1350 # Need to get the average value if there were multiple values.
1351 truncated_mean = math_utils.TruncatedMean(
1352 metric_values, self.opts.truncate_percent)
1353 standard_err = math_utils.StandardError(metric_values)
1354 standard_dev = math_utils.StandardDeviation(metric_values)
1356 if self._IsBisectModeStandardDeviation():
1357 metric_values = [standard_dev]
1359 values = {
1360 'mean': truncated_mean,
1361 'std_err': standard_err,
1362 'std_dev': standard_dev,
1363 'values': metric_values,
1366 print 'Results of performance test: %12f %12f' % (
1367 truncated_mean, standard_err)
1368 print
1369 return (values, success_code, output_of_all_runs)
1371 def PerformPreBuildCleanup(self):
1372 """Performs cleanup between runs."""
1373 print 'Cleaning up between runs.'
1374 print
1376 # Leaving these .pyc files around between runs may disrupt some perf tests.
1377 for (path, _, files) in os.walk(self.src_cwd):
1378 for cur_file in files:
1379 if cur_file.endswith('.pyc'):
1380 path_to_file = os.path.join(path, cur_file)
1381 os.remove(path_to_file)
1383 def _RunPostSync(self, _depot):
1384 """Performs any work after syncing.
1386 Args:
1387 depot: Depot name.
1389 Returns:
1390 True if successful.
1392 if self.opts.target_platform == 'android':
1393 if not builder.SetupAndroidBuildEnvironment(self.opts,
1394 path_to_src=self.src_cwd):
1395 return False
1397 return self.RunGClientHooks()
1399 @staticmethod
1400 def ShouldSkipRevision(depot, revision):
1401 """Checks whether a particular revision can be safely skipped.
1403 Some commits can be safely skipped (such as a DEPS roll), since the tool
1404 is git based those changes would have no effect.
1406 Args:
1407 depot: The depot being bisected.
1408 revision: Current revision we're synced to.
1410 Returns:
1411 True if we should skip building/testing this revision.
1413 if depot == 'chromium':
1414 cmd = ['diff-tree', '--no-commit-id', '--name-only', '-r', revision]
1415 output = bisect_utils.CheckRunGit(cmd)
1417 files = output.splitlines()
1419 if len(files) == 1 and files[0] == 'DEPS':
1420 return True
1422 return False
1424 def RunTest(self, revision, depot, command, metric, skippable=False):
1425 """Performs a full sync/build/run of the specified revision.
1427 Args:
1428 revision: The revision to sync to.
1429 depot: The depot that's being used at the moment (src, webkit, etc.)
1430 command: The command to execute the performance test.
1431 metric: The performance metric being tested.
1433 Returns:
1434 On success, a tuple containing the results of the performance test.
1435 Otherwise, a tuple with the error message.
1437 logging.info('Running RunTest with rev "%s", command "%s"',
1438 revision, command)
1439 # Decide which sync program to use.
1440 sync_client = None
1441 if depot == 'chromium' or depot == 'android-chrome':
1442 sync_client = 'gclient'
1444 # Do the syncing for all depots.
1445 if not self.opts.debug_ignore_sync:
1446 if not self._SyncRevision(depot, revision, sync_client):
1447 return ('Failed to sync: [%s]' % str(revision), BUILD_RESULT_FAIL)
1449 # Try to do any post-sync steps. This may include "gclient runhooks".
1450 if not self._RunPostSync(depot):
1451 return ('Failed to run [gclient runhooks].', BUILD_RESULT_FAIL)
1453 # Skip this revision if it can be skipped.
1454 if skippable and self.ShouldSkipRevision(depot, revision):
1455 return ('Skipped revision: [%s]' % str(revision),
1456 BUILD_RESULT_SKIPPED)
1458 # Obtain a build for this revision. This may be done by requesting a build
1459 # from another builder, waiting for it and downloading it.
1460 start_build_time = time.time()
1461 build_success = self.ObtainBuild(depot, revision)
1462 if not build_success:
1463 return ('Failed to build revision: [%s]' % str(revision),
1464 BUILD_RESULT_FAIL)
1465 after_build_time = time.time()
1467 # Possibly alter the command.
1468 command = self.GetCompatibleCommand(command, revision, depot)
1470 # Run the command and get the results.
1471 results = self.RunPerformanceTestAndParseResults(command, metric)
1473 # Restore build output directory once the tests are done, to avoid
1474 # any discrepancies.
1475 if self.IsDownloadable(depot) and revision:
1476 self.BackupOrRestoreOutputDirectory(restore=True)
1478 # A value other than 0 indicates that the test couldn't be run, and results
1479 # should also include an error message.
1480 if results[1] != 0:
1481 return results
1483 external_revisions = self._Get3rdPartyRevisions(depot)
1485 if not external_revisions is None:
1486 return (results[0], results[1], external_revisions,
1487 time.time() - after_build_time, after_build_time -
1488 start_build_time)
1489 else:
1490 return ('Failed to parse DEPS file for external revisions.',
1491 BUILD_RESULT_FAIL)
1493 def _SyncRevision(self, depot, revision, sync_client):
1494 """Syncs depot to particular revision.
1496 Args:
1497 depot: The depot that's being used at the moment (src, webkit, etc.)
1498 revision: The revision to sync to.
1499 sync_client: Program used to sync, e.g. "gclient". Can be None.
1501 Returns:
1502 True if successful, False otherwise.
1504 self.depot_registry.ChangeToDepotDir(depot)
1506 if sync_client:
1507 self.PerformPreBuildCleanup()
1509 # When using gclient to sync, you need to specify the depot you
1510 # want so that all the dependencies sync properly as well.
1511 # i.e. gclient sync src@<SHA1>
1512 if sync_client == 'gclient':
1513 revision = '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME[depot]['src'],
1514 revision)
1516 return source_control.SyncToRevision(revision, sync_client)
1518 def _CheckIfRunPassed(self, current_value, known_good_value, known_bad_value):
1519 """Given known good and bad values, decide if the current_value passed
1520 or failed.
1522 Args:
1523 current_value: The value of the metric being checked.
1524 known_bad_value: The reference value for a "failed" run.
1525 known_good_value: The reference value for a "passed" run.
1527 Returns:
1528 True if the current_value is closer to the known_good_value than the
1529 known_bad_value.
1531 if self.opts.bisect_mode == bisect_utils.BISECT_MODE_STD_DEV:
1532 dist_to_good_value = abs(current_value['std_dev'] -
1533 known_good_value['std_dev'])
1534 dist_to_bad_value = abs(current_value['std_dev'] -
1535 known_bad_value['std_dev'])
1536 else:
1537 dist_to_good_value = abs(current_value['mean'] - known_good_value['mean'])
1538 dist_to_bad_value = abs(current_value['mean'] - known_bad_value['mean'])
1540 return dist_to_good_value < dist_to_bad_value
1542 def _GetV8BleedingEdgeFromV8TrunkIfMappable(
1543 self, revision, bleeding_edge_branch):
1544 """Gets v8 bleeding edge revision mapped to v8 revision in trunk.
1546 Args:
1547 revision: A trunk V8 revision mapped to bleeding edge revision.
1548 bleeding_edge_branch: Branch used to perform lookup of bleeding edge
1549 revision.
1550 Return:
1551 A mapped bleeding edge revision if found, otherwise None.
1553 commit_position = source_control.GetCommitPosition(revision)
1555 if bisect_utils.IsStringInt(commit_position):
1556 # V8 is tricky to bisect, in that there are only a few instances when
1557 # we can dive into bleeding_edge and get back a meaningful result.
1558 # Try to detect a V8 "business as usual" case, which is when:
1559 # 1. trunk revision N has description "Version X.Y.Z"
1560 # 2. bleeding_edge revision (N-1) has description "Prepare push to
1561 # trunk. Now working on X.Y.(Z+1)."
1563 # As of 01/24/2014, V8 trunk descriptions are formatted:
1564 # "Version 3.X.Y (based on bleeding_edge revision rZ)"
1565 # So we can just try parsing that out first and fall back to the old way.
1566 v8_dir = self.depot_registry.GetDepotDir('v8')
1567 v8_bleeding_edge_dir = self.depot_registry.GetDepotDir('v8_bleeding_edge')
1569 revision_info = source_control.QueryRevisionInfo(revision, cwd=v8_dir)
1570 version_re = re.compile("Version (?P<values>[0-9,.]+)")
1571 regex_results = version_re.search(revision_info['subject'])
1572 if regex_results:
1573 git_revision = None
1574 if 'based on bleeding_edge' in revision_info['subject']:
1575 try:
1576 bleeding_edge_revision = revision_info['subject'].split(
1577 'bleeding_edge revision r')[1]
1578 bleeding_edge_revision = int(bleeding_edge_revision.split(')')[0])
1579 bleeding_edge_url = ('https://v8.googlecode.com/svn/branches/'
1580 'bleeding_edge@%s' % bleeding_edge_revision)
1581 cmd = ['log',
1582 '--format=%H',
1583 '--grep',
1584 bleeding_edge_url,
1585 '-1',
1586 bleeding_edge_branch]
1587 output = bisect_utils.CheckRunGit(cmd, cwd=v8_dir)
1588 if output:
1589 git_revision = output.strip()
1590 return git_revision
1591 except (IndexError, ValueError):
1592 pass
1593 else:
1594 # V8 rolls description changed after V8 git migration, new description
1595 # includes "Version 3.X.Y (based on <git hash>)"
1596 try:
1597 rxp = re.compile('based on (?P<git_revision>[a-fA-F0-9]+)')
1598 re_results = rxp.search(revision_info['subject'])
1599 if re_results:
1600 return re_results.group('git_revision')
1601 except (IndexError, ValueError):
1602 pass
1603 if not git_revision:
1604 # Wasn't successful, try the old way of looking for "Prepare push to"
1605 git_revision = source_control.ResolveToRevision(
1606 int(commit_position) - 1, 'v8_bleeding_edge',
1607 bisect_utils.DEPOT_DEPS_NAME, -1, cwd=v8_bleeding_edge_dir)
1609 if git_revision:
1610 revision_info = source_control.QueryRevisionInfo(git_revision,
1611 cwd=v8_bleeding_edge_dir)
1613 if 'Prepare push to trunk' in revision_info['subject']:
1614 return git_revision
1615 return None
1617 def _GetNearestV8BleedingEdgeFromTrunk(
1618 self, revision, v8_branch, bleeding_edge_branch, search_forward=True):
1619 """Gets the nearest V8 roll and maps to bleeding edge revision.
1621 V8 is a bit tricky to bisect since it isn't just rolled out like blink.
1622 Each revision on trunk might just be whatever was in bleeding edge, rolled
1623 directly out. Or it could be some mixture of previous v8 trunk versions,
1624 with bits and pieces cherry picked out from bleeding edge. In order to
1625 bisect, we need both the before/after versions on trunk v8 to be just pushes
1626 from bleeding edge. With the V8 git migration, the branches got switched.
1627 a) master (external/v8) == candidates (v8/v8)
1628 b) bleeding_edge (external/v8) == master (v8/v8)
1630 Args:
1631 revision: A V8 revision to get its nearest bleeding edge revision
1632 search_forward: Searches forward if True, otherwise search backward.
1634 Return:
1635 A mapped bleeding edge revision if found, otherwise None.
1637 cwd = self.depot_registry.GetDepotDir('v8')
1638 cmd = ['log', '--format=%ct', '-1', revision]
1639 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1640 commit_time = int(output)
1641 commits = []
1642 if search_forward:
1643 cmd = ['log',
1644 '--format=%H',
1645 '--after=%d' % commit_time,
1646 v8_branch,
1647 '--reverse']
1648 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1649 output = output.split()
1650 commits = output
1651 #Get 10 git hashes immediately after the given commit.
1652 commits = commits[:10]
1653 else:
1654 cmd = ['log',
1655 '--format=%H',
1656 '-10',
1657 '--before=%d' % commit_time,
1658 v8_branch]
1659 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1660 output = output.split()
1661 commits = output
1663 bleeding_edge_revision = None
1665 for c in commits:
1666 bleeding_edge_revision = self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1667 c, bleeding_edge_branch)
1668 if bleeding_edge_revision:
1669 break
1671 return bleeding_edge_revision
1673 def _FillInV8BleedingEdgeInfo(self, min_revision_state, max_revision_state):
1674 cwd = self.depot_registry.GetDepotDir('v8')
1675 # when "remote.origin.url" is https://chromium.googlesource.com/v8/v8.git
1676 v8_branch = 'origin/candidates'
1677 bleeding_edge_branch = 'origin/master'
1679 # Support for the chromium revisions with external V8 repo.
1680 # ie https://chromium.googlesource.com/external/v8.git
1681 cmd = ['config', '--get', 'remote.origin.url']
1682 v8_repo_url = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1684 if 'external/v8.git' in v8_repo_url:
1685 v8_branch = 'origin/master'
1686 bleeding_edge_branch = 'origin/bleeding_edge'
1688 r1 = self._GetNearestV8BleedingEdgeFromTrunk(min_revision_state.revision,
1689 v8_branch, bleeding_edge_branch, search_forward=True)
1690 r2 = self._GetNearestV8BleedingEdgeFromTrunk(max_revision_state.revision,
1691 v8_branch, bleeding_edge_branch, search_forward=False)
1692 min_revision_state.external['v8_bleeding_edge'] = r1
1693 max_revision_state.external['v8_bleeding_edge'] = r2
1695 if (not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1696 min_revision_state.revision, bleeding_edge_branch)
1697 or not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1698 max_revision_state.revision, bleeding_edge_branch)):
1699 self.warnings.append(
1700 'Trunk revisions in V8 did not map directly to bleeding_edge. '
1701 'Attempted to expand the range to find V8 rolls which did map '
1702 'directly to bleeding_edge revisions, but results might not be '
1703 'valid.')
1705 def _FindNextDepotToBisect(
1706 self, current_depot, min_revision_state, max_revision_state):
1707 """Decides which depot the script should dive into next (if any).
1709 Args:
1710 current_depot: Current depot being bisected.
1711 min_revision_state: State of the earliest revision in the bisect range.
1712 max_revision_state: State of the latest revision in the bisect range.
1714 Returns:
1715 Name of the depot to bisect next, or None.
1717 external_depot = None
1718 for next_depot in bisect_utils.DEPOT_NAMES:
1719 if bisect_utils.DEPOT_DEPS_NAME[next_depot].has_key('platform'):
1720 if bisect_utils.DEPOT_DEPS_NAME[next_depot]['platform'] != os.name:
1721 continue
1723 if not (bisect_utils.DEPOT_DEPS_NAME[next_depot]['recurse']
1724 and min_revision_state.depot
1725 in bisect_utils.DEPOT_DEPS_NAME[next_depot]['from']):
1726 continue
1728 if current_depot == 'v8':
1729 # We grab the bleeding_edge info here rather than earlier because we
1730 # finally have the revision range. From that we can search forwards and
1731 # backwards to try to match trunk revisions to bleeding_edge.
1732 self._FillInV8BleedingEdgeInfo(min_revision_state, max_revision_state)
1734 if (min_revision_state.external.get(next_depot) ==
1735 max_revision_state.external.get(next_depot)):
1736 continue
1738 if (min_revision_state.external.get(next_depot) and
1739 max_revision_state.external.get(next_depot)):
1740 external_depot = next_depot
1741 break
1743 return external_depot
1745 def PrepareToBisectOnDepot(
1746 self, current_depot, start_revision, end_revision, previous_revision):
1747 """Changes to the appropriate directory and gathers a list of revisions
1748 to bisect between |start_revision| and |end_revision|.
1750 Args:
1751 current_depot: The depot we want to bisect.
1752 start_revision: Start of the revision range.
1753 end_revision: End of the revision range.
1754 previous_revision: The last revision we synced to on |previous_depot|.
1756 Returns:
1757 A list containing the revisions between |start_revision| and
1758 |end_revision| inclusive.
1760 # Change into working directory of external library to run
1761 # subsequent commands.
1762 self.depot_registry.ChangeToDepotDir(current_depot)
1764 # V8 (and possibly others) is merged in periodically. Bisecting
1765 # this directory directly won't give much good info.
1766 if bisect_utils.DEPOT_DEPS_NAME[current_depot].has_key('custom_deps'):
1767 config_path = os.path.join(self.src_cwd, '..')
1768 if bisect_utils.RunGClientAndCreateConfig(
1769 self.opts, bisect_utils.DEPOT_DEPS_NAME[current_depot]['custom_deps'],
1770 cwd=config_path):
1771 return []
1772 if bisect_utils.RunGClient(
1773 ['sync', '--revision', previous_revision], cwd=self.src_cwd):
1774 return []
1776 if current_depot == 'v8_bleeding_edge':
1777 self.depot_registry.ChangeToDepotDir('chromium')
1779 shutil.move('v8', 'v8.bak')
1780 shutil.move('v8_bleeding_edge', 'v8')
1782 self.cleanup_commands.append(['mv', 'v8', 'v8_bleeding_edge'])
1783 self.cleanup_commands.append(['mv', 'v8.bak', 'v8'])
1785 self.depot_registry.SetDepotDir('v8_bleeding_edge',
1786 os.path.join(self.src_cwd, 'v8'))
1787 self.depot_registry.SetDepotDir('v8', os.path.join(self.src_cwd,
1788 'v8.bak'))
1790 self.depot_registry.ChangeToDepotDir(current_depot)
1792 depot_revision_list = self.GetRevisionList(current_depot,
1793 end_revision,
1794 start_revision)
1796 self.depot_registry.ChangeToDepotDir('chromium')
1798 return depot_revision_list
1800 def GatherReferenceValues(self, good_rev, bad_rev, cmd, metric, target_depot):
1801 """Gathers reference values by running the performance tests on the
1802 known good and bad revisions.
1804 Args:
1805 good_rev: The last known good revision where the performance regression
1806 has not occurred yet.
1807 bad_rev: A revision where the performance regression has already occurred.
1808 cmd: The command to execute the performance test.
1809 metric: The metric being tested for regression.
1811 Returns:
1812 A tuple with the results of building and running each revision.
1814 bad_run_results = self.RunTest(bad_rev, target_depot, cmd, metric)
1816 good_run_results = None
1818 if not bad_run_results[1]:
1819 good_run_results = self.RunTest(good_rev, target_depot, cmd, metric)
1821 return (bad_run_results, good_run_results)
1823 def PrintRevisionsToBisectMessage(self, revision_list, depot):
1824 if self.opts.output_buildbot_annotations:
1825 step_name = 'Bisection Range: [%s:%s - %s]' % (depot, revision_list[-1],
1826 revision_list[0])
1827 bisect_utils.OutputAnnotationStepStart(step_name)
1829 print
1830 print 'Revisions to bisect on [%s]:' % depot
1831 for revision_id in revision_list:
1832 print ' -> %s' % (revision_id, )
1833 print
1835 if self.opts.output_buildbot_annotations:
1836 bisect_utils.OutputAnnotationStepClosed()
1838 def NudgeRevisionsIfDEPSChange(self, bad_revision, good_revision,
1839 good_svn_revision=None):
1840 """Checks to see if changes to DEPS file occurred, and that the revision
1841 range also includes the change to .DEPS.git. If it doesn't, attempts to
1842 expand the revision range to include it.
1844 Args:
1845 bad_revision: First known bad git revision.
1846 good_revision: Last known good git revision.
1847 good_svn_revision: Last known good svn revision.
1849 Returns:
1850 A tuple with the new bad and good revisions.
1852 # DONOT perform nudge because at revision 291563 .DEPS.git was removed
1853 # and source contain only DEPS file for dependency changes.
1854 if good_svn_revision >= 291563:
1855 return (bad_revision, good_revision)
1857 if self.opts.target_platform == 'chromium':
1858 changes_to_deps = source_control.QueryFileRevisionHistory(
1859 bisect_utils.FILE_DEPS, good_revision, bad_revision)
1861 if changes_to_deps:
1862 # DEPS file was changed, search from the oldest change to DEPS file to
1863 # bad_revision to see if there are matching .DEPS.git changes.
1864 oldest_deps_change = changes_to_deps[-1]
1865 changes_to_gitdeps = source_control.QueryFileRevisionHistory(
1866 bisect_utils.FILE_DEPS_GIT, oldest_deps_change, bad_revision)
1868 if len(changes_to_deps) != len(changes_to_gitdeps):
1869 # Grab the timestamp of the last DEPS change
1870 cmd = ['log', '--format=%ct', '-1', changes_to_deps[0]]
1871 output = bisect_utils.CheckRunGit(cmd)
1872 commit_time = int(output)
1874 # Try looking for a commit that touches the .DEPS.git file in the
1875 # next 15 minutes after the DEPS file change.
1876 cmd = ['log', '--format=%H', '-1',
1877 '--before=%d' % (commit_time + 900), '--after=%d' % commit_time,
1878 'origin/master', '--', bisect_utils.FILE_DEPS_GIT]
1879 output = bisect_utils.CheckRunGit(cmd)
1880 output = output.strip()
1881 if output:
1882 self.warnings.append('Detected change to DEPS and modified '
1883 'revision range to include change to .DEPS.git')
1884 return (output, good_revision)
1885 else:
1886 self.warnings.append('Detected change to DEPS but couldn\'t find '
1887 'matching change to .DEPS.git')
1888 return (bad_revision, good_revision)
1890 def CheckIfRevisionsInProperOrder(
1891 self, target_depot, good_revision, bad_revision):
1892 """Checks that |good_revision| is an earlier revision than |bad_revision|.
1894 Args:
1895 good_revision: Number/tag of the known good revision.
1896 bad_revision: Number/tag of the known bad revision.
1898 Returns:
1899 True if the revisions are in the proper order (good earlier than bad).
1901 cwd = self.depot_registry.GetDepotDir(target_depot)
1902 good_position = source_control.GetCommitPosition(good_revision, cwd)
1903 bad_position = source_control.GetCommitPosition(bad_revision, cwd)
1904 # Compare commit timestamp for repos that don't support commit position.
1905 if not (bad_position and good_position):
1906 good_position = source_control.GetCommitTime(good_revision, cwd=cwd)
1907 bad_position = source_control.GetCommitTime(bad_revision, cwd=cwd)
1909 return good_position <= bad_position
1911 def CanPerformBisect(self, good_revision, bad_revision):
1912 """Checks whether a given revision is bisectable.
1914 Checks for following:
1915 1. Non-bisectable revsions for android bots (refer to crbug.com/385324).
1916 2. Non-bisectable revsions for Windows bots (refer to crbug.com/405274).
1918 Args:
1919 good_revision: Known good revision.
1920 bad_revision: Known bad revision.
1922 Returns:
1923 A dictionary indicating the result. If revision is not bisectable,
1924 this will contain the field "error", otherwise None.
1926 if self.opts.target_platform == 'android':
1927 good_revision = source_control.GetCommitPosition(good_revision)
1928 if (bisect_utils.IsStringInt(good_revision)
1929 and good_revision < 265549):
1930 return {'error': (
1931 'Bisect cannot continue for the given revision range.\n'
1932 'It is impossible to bisect Android regressions '
1933 'prior to r265549, which allows the bisect bot to '
1934 'rely on Telemetry to do apk installation of the most recently '
1935 'built local ChromeShell(refer to crbug.com/385324).\n'
1936 'Please try bisecting revisions greater than or equal to r265549.')}
1938 if bisect_utils.IsWindowsHost():
1939 good_revision = source_control.GetCommitPosition(good_revision)
1940 bad_revision = source_control.GetCommitPosition(bad_revision)
1941 if (bisect_utils.IsStringInt(good_revision) and
1942 bisect_utils.IsStringInt(bad_revision)):
1943 if (289987 <= good_revision < 290716 or
1944 289987 <= bad_revision < 290716):
1945 return {'error': ('Oops! Revision between r289987 and r290716 are '
1946 'marked as dead zone for Windows due to '
1947 'crbug.com/405274. Please try another range.')}
1949 return None
1951 def Run(self, command_to_run, bad_revision_in, good_revision_in, metric):
1952 """Given known good and bad revisions, run a binary search on all
1953 intermediate revisions to determine the CL where the performance regression
1954 occurred.
1956 Args:
1957 command_to_run: Specify the command to execute the performance test.
1958 good_revision: Number/tag of the known good revision.
1959 bad_revision: Number/tag of the known bad revision.
1960 metric: The performance metric to monitor.
1962 Returns:
1963 A BisectResults object.
1966 # Choose depot to bisect first
1967 target_depot = 'chromium'
1968 if self.opts.target_platform == 'android-chrome':
1969 target_depot = 'android-chrome'
1971 cwd = os.getcwd()
1972 self.depot_registry.ChangeToDepotDir(target_depot)
1974 # If they passed SVN revisions, we can try match them to git SHA1 hashes.
1975 bad_revision = source_control.ResolveToRevision(
1976 bad_revision_in, target_depot, bisect_utils.DEPOT_DEPS_NAME, 100)
1977 good_revision = source_control.ResolveToRevision(
1978 good_revision_in, target_depot, bisect_utils.DEPOT_DEPS_NAME, -100)
1980 os.chdir(cwd)
1981 if bad_revision is None:
1982 return BisectResults(
1983 error='Couldn\'t resolve [%s] to SHA1.' % bad_revision_in)
1985 if good_revision is None:
1986 return BisectResults(
1987 error='Couldn\'t resolve [%s] to SHA1.' % good_revision_in)
1989 # Check that they didn't accidentally swap good and bad revisions.
1990 if not self.CheckIfRevisionsInProperOrder(
1991 target_depot, good_revision, bad_revision):
1992 return BisectResults(error='bad_revision < good_revision, did you swap '
1993 'these by mistake?')
1995 bad_revision, good_revision = self.NudgeRevisionsIfDEPSChange(
1996 bad_revision, good_revision, good_revision_in)
1997 if self.opts.output_buildbot_annotations:
1998 bisect_utils.OutputAnnotationStepStart('Gathering Revisions')
2000 cannot_bisect = self.CanPerformBisect(good_revision, bad_revision)
2001 if cannot_bisect:
2002 return BisectResults(error=cannot_bisect.get('error'))
2004 print 'Gathering revision range for bisection.'
2005 # Retrieve a list of revisions to do bisection on.
2006 revision_list = self.GetRevisionList(target_depot, bad_revision,
2007 good_revision)
2009 if self.opts.output_buildbot_annotations:
2010 bisect_utils.OutputAnnotationStepClosed()
2012 if revision_list:
2013 self.PrintRevisionsToBisectMessage(revision_list, target_depot)
2015 if self.opts.output_buildbot_annotations:
2016 bisect_utils.OutputAnnotationStepStart('Gathering Reference Values')
2018 print 'Gathering reference values for bisection.'
2020 # Perform the performance tests on the good and bad revisions, to get
2021 # reference values.
2022 bad_results, good_results = self.GatherReferenceValues(good_revision,
2023 bad_revision,
2024 command_to_run,
2025 metric,
2026 target_depot)
2028 if self.opts.output_buildbot_annotations:
2029 bisect_utils.OutputAnnotationStepClosed()
2031 if bad_results[1]:
2032 error = ('An error occurred while building and running the \'bad\' '
2033 'reference value. The bisect cannot continue without '
2034 'a working \'bad\' revision to start from.\n\nError: %s' %
2035 bad_results[0])
2036 return BisectResults(error=error)
2038 if good_results[1]:
2039 error = ('An error occurred while building and running the \'good\' '
2040 'reference value. The bisect cannot continue without '
2041 'a working \'good\' revision to start from.\n\nError: %s' %
2042 good_results[0])
2043 return BisectResults(error=error)
2045 # We need these reference values to determine if later runs should be
2046 # classified as pass or fail.
2047 known_bad_value = bad_results[0]
2048 known_good_value = good_results[0]
2050 # Check the direction of improvement only if the improvement_direction
2051 # option is set to a specific direction (1 for higher is better or -1 for
2052 # lower is better).
2053 improvement_dir = self.opts.improvement_direction
2054 if improvement_dir:
2055 higher_is_better = improvement_dir > 0
2056 if higher_is_better:
2057 message = "Expecting higher values to be better for this metric, "
2058 else:
2059 message = "Expecting lower values to be better for this metric, "
2060 metric_increased = known_bad_value['mean'] > known_good_value['mean']
2061 if metric_increased:
2062 message += "and the metric appears to have increased. "
2063 else:
2064 message += "and the metric appears to have decreased. "
2065 if ((higher_is_better and metric_increased) or
2066 (not higher_is_better and not metric_increased)):
2067 error = (message + 'Then, the test results for the ends of the given '
2068 '\'good\' - \'bad\' range of revisions represent an '
2069 'improvement (and not a regression).')
2070 return BisectResults(error=error)
2071 logging.info(message + "Therefore we continue to bisect.")
2073 bisect_state = BisectState(target_depot, revision_list)
2074 revision_states = bisect_state.GetRevisionStates()
2076 min_revision = 0
2077 max_revision = len(revision_states) - 1
2079 # Can just mark the good and bad revisions explicitly here since we
2080 # already know the results.
2081 bad_revision_state = revision_states[min_revision]
2082 bad_revision_state.external = bad_results[2]
2083 bad_revision_state.perf_time = bad_results[3]
2084 bad_revision_state.build_time = bad_results[4]
2085 bad_revision_state.passed = False
2086 bad_revision_state.value = known_bad_value
2088 good_revision_state = revision_states[max_revision]
2089 good_revision_state.external = good_results[2]
2090 good_revision_state.perf_time = good_results[3]
2091 good_revision_state.build_time = good_results[4]
2092 good_revision_state.passed = True
2093 good_revision_state.value = known_good_value
2095 # Check how likely it is that the good and bad results are different
2096 # beyond chance-induced variation.
2097 confidence_error = False
2098 if not self.opts.debug_ignore_regression_confidence:
2099 confidence_error = _CheckRegressionConfidenceError(good_revision,
2100 bad_revision,
2101 known_good_value,
2102 known_bad_value)
2103 if confidence_error:
2104 self.warnings.append(confidence_error)
2105 bad_revision_state.passed = True # Marking the 'bad' revision as good.
2106 return BisectResults(bisect_state, self.depot_registry, self.opts,
2107 self.warnings)
2109 while True:
2110 if not revision_states:
2111 break
2113 if max_revision - min_revision <= 1:
2114 min_revision_state = revision_states[min_revision]
2115 max_revision_state = revision_states[max_revision]
2116 current_depot = min_revision_state.depot
2117 # TODO(sergiyb): Under which conditions can first two branches be hit?
2118 if min_revision_state.passed == '?':
2119 next_revision_index = min_revision
2120 elif max_revision_state.passed == '?':
2121 next_revision_index = max_revision
2122 elif current_depot in ['android-chrome', 'chromium', 'v8']:
2123 previous_revision = revision_states[min_revision].revision
2124 # If there were changes to any of the external libraries we track,
2125 # should bisect the changes there as well.
2126 external_depot = self._FindNextDepotToBisect(
2127 current_depot, min_revision_state, max_revision_state)
2128 # If there was no change in any of the external depots, the search
2129 # is over.
2130 if not external_depot:
2131 if current_depot == 'v8':
2132 self.warnings.append('Unfortunately, V8 bisection couldn\'t '
2133 'continue any further. The script can only bisect into '
2134 'V8\'s bleeding_edge repository if both the current and '
2135 'previous revisions in trunk map directly to revisions in '
2136 'bleeding_edge.')
2137 break
2139 earliest_revision = max_revision_state.external[external_depot]
2140 latest_revision = min_revision_state.external[external_depot]
2142 new_revision_list = self.PrepareToBisectOnDepot(
2143 external_depot, earliest_revision, latest_revision,
2144 previous_revision)
2146 if not new_revision_list:
2147 error = ('An error occurred attempting to retrieve revision '
2148 'range: [%s..%s]' % (earliest_revision, latest_revision))
2149 return BisectResults(error=error)
2151 revision_states = bisect_state.CreateRevisionStatesAfter(
2152 external_depot, new_revision_list, current_depot,
2153 previous_revision)
2155 # Reset the bisection and perform it on the newly inserted states.
2156 min_revision = 0
2157 max_revision = len(revision_states) - 1
2159 print ('Regression in metric %s appears to be the result of '
2160 'changes in [%s].' % (metric, external_depot))
2162 revision_list = [state.revision for state in revision_states]
2163 self.PrintRevisionsToBisectMessage(revision_list, external_depot)
2165 continue
2166 else:
2167 break
2168 else:
2169 next_revision_index = (int((max_revision - min_revision) / 2) +
2170 min_revision)
2172 next_revision_state = revision_states[next_revision_index]
2173 next_revision = next_revision_state.revision
2174 next_depot = next_revision_state.depot
2176 self.depot_registry.ChangeToDepotDir(next_depot)
2178 message = 'Working on [%s:%s]' % (next_depot, next_revision)
2179 print message
2180 if self.opts.output_buildbot_annotations:
2181 bisect_utils.OutputAnnotationStepStart(message)
2183 run_results = self.RunTest(next_revision, next_depot, command_to_run,
2184 metric, skippable=True)
2186 # If the build is successful, check whether or not the metric
2187 # had regressed.
2188 if not run_results[1]:
2189 if len(run_results) > 2:
2190 next_revision_state.external = run_results[2]
2191 next_revision_state.perf_time = run_results[3]
2192 next_revision_state.build_time = run_results[4]
2194 passed_regression = self._CheckIfRunPassed(run_results[0],
2195 known_good_value,
2196 known_bad_value)
2198 next_revision_state.passed = passed_regression
2199 next_revision_state.value = run_results[0]
2201 if passed_regression:
2202 max_revision = next_revision_index
2203 else:
2204 min_revision = next_revision_index
2205 else:
2206 if run_results[1] == BUILD_RESULT_SKIPPED:
2207 next_revision_state.passed = 'Skipped'
2208 elif run_results[1] == BUILD_RESULT_FAIL:
2209 next_revision_state.passed = 'Build Failed'
2211 print run_results[0]
2213 # If the build is broken, remove it and redo search.
2214 revision_states.pop(next_revision_index)
2216 max_revision -= 1
2218 if self.opts.output_buildbot_annotations:
2219 self.printer.PrintPartialResults(bisect_state)
2220 bisect_utils.OutputAnnotationStepClosed()
2222 return BisectResults(bisect_state, self.depot_registry, self.opts,
2223 self.warnings)
2224 else:
2225 # Weren't able to sync and retrieve the revision range.
2226 error = ('An error occurred attempting to retrieve revision range: '
2227 '[%s..%s]' % (good_revision, bad_revision))
2228 return BisectResults(error=error)
2231 def _IsPlatformSupported():
2232 """Checks that this platform and build system are supported.
2234 Args:
2235 opts: The options parsed from the command line.
2237 Returns:
2238 True if the platform and build system are supported.
2240 # Haven't tested the script out on any other platforms yet.
2241 supported = ['posix', 'nt']
2242 return os.name in supported
2245 def RemoveBuildFiles(build_type):
2246 """Removes build files from previous runs."""
2247 out_dir = os.path.join('out', build_type)
2248 build_dir = os.path.join('build', build_type)
2249 logging.info('Removing build files in "%s" and "%s".',
2250 os.path.abspath(out_dir), os.path.abspath(build_dir))
2251 try:
2252 RemakeDirectoryTree(out_dir)
2253 RemakeDirectoryTree(build_dir)
2254 except Exception as e:
2255 raise RuntimeError('Got error in RemoveBuildFiles: %s' % e)
2258 def RemakeDirectoryTree(path_to_dir):
2259 """Removes a directory tree and replaces it with an empty one.
2261 Returns True if successful, False otherwise.
2263 RemoveDirectoryTree(path_to_dir)
2264 MaybeMakeDirectory(path_to_dir)
2267 def RemoveDirectoryTree(path_to_dir):
2268 """Removes a directory tree. Returns True if successful or False otherwise."""
2269 if os.path.isfile(path_to_dir):
2270 logging.info('REMOVING FILE %s' % path_to_dir)
2271 os.remove(path_to_dir)
2272 try:
2273 if os.path.exists(path_to_dir):
2274 shutil.rmtree(path_to_dir)
2275 except OSError, e:
2276 if e.errno != errno.ENOENT:
2277 raise
2280 # This is copied from build/scripts/common/chromium_utils.py.
2281 def MaybeMakeDirectory(*path):
2282 """Creates an entire path, if it doesn't already exist."""
2283 file_path = os.path.join(*path)
2284 try:
2285 os.makedirs(file_path)
2286 except OSError as e:
2287 if e.errno != errno.EEXIST:
2288 raise
2291 class BisectOptions(object):
2292 """Options to be used when running bisection."""
2293 def __init__(self):
2294 super(BisectOptions, self).__init__()
2296 self.target_platform = 'chromium'
2297 self.build_preference = None
2298 self.good_revision = None
2299 self.bad_revision = None
2300 self.use_goma = None
2301 self.goma_dir = None
2302 self.goma_threads = 64
2303 self.repeat_test_count = 20
2304 self.truncate_percent = 25
2305 self.max_time_minutes = 20
2306 self.metric = None
2307 self.command = None
2308 self.output_buildbot_annotations = None
2309 self.no_custom_deps = False
2310 self.working_directory = None
2311 self.extra_src = None
2312 self.debug_ignore_build = None
2313 self.debug_ignore_sync = None
2314 self.debug_ignore_perf_test = None
2315 self.debug_ignore_regression_confidence = None
2316 self.debug_fake_first_test_mean = 0
2317 self.target_arch = 'ia32'
2318 self.target_build_type = 'Release'
2319 self.builder_host = None
2320 self.builder_port = None
2321 self.bisect_mode = bisect_utils.BISECT_MODE_MEAN
2322 self.improvement_direction = 0
2323 self.bug_id = ''
2325 @staticmethod
2326 def _AddBisectOptionsGroup(parser):
2327 group = parser.add_argument_group('Bisect options')
2328 group.add_argument('-c', '--command', required=True,
2329 help='A command to execute your performance test at '
2330 'each point in the bisection.')
2331 group.add_argument('-b', '--bad_revision', required=True,
2332 help='A bad revision to start bisection. Must be later '
2333 'than good revision. May be either a git or svn '
2334 'revision.')
2335 group.add_argument('-g', '--good_revision', required=True,
2336 help='A revision to start bisection where performance '
2337 'test is known to pass. Must be earlier than the '
2338 'bad revision. May be either a git or a svn '
2339 'revision.')
2340 group.add_argument('-m', '--metric',
2341 help='The desired metric to bisect on. For example '
2342 '"vm_rss_final_b/vm_rss_f_b"')
2343 group.add_argument('-d', '--improvement_direction', type=int, default=0,
2344 help='An integer number representing the direction of '
2345 'improvement. 1 for higher is better, -1 for lower '
2346 'is better, 0 for ignore (default).')
2347 group.add_argument('-r', '--repeat_test_count', type=int, default=20,
2348 choices=range(1, 101),
2349 help='The number of times to repeat the performance '
2350 'test. Values will be clamped to range [1, 100]. '
2351 'Default value is 20.')
2352 group.add_argument('--max_time_minutes', type=int, default=20,
2353 choices=range(1, 61),
2354 help='The maximum time (in minutes) to take running the '
2355 'performance tests. The script will run the '
2356 'performance tests according to '
2357 '--repeat_test_count, so long as it doesn\'t exceed'
2358 ' --max_time_minutes. Values will be clamped to '
2359 'range [1, 60]. Default value is 20.')
2360 group.add_argument('-t', '--truncate_percent', type=int, default=25,
2361 help='The highest/lowest % are discarded to form a '
2362 'truncated mean. Values will be clamped to range '
2363 '[0, 25]. Default value is 25 (highest/lowest 25% '
2364 'will be discarded).')
2365 group.add_argument('--bisect_mode', default=bisect_utils.BISECT_MODE_MEAN,
2366 choices=[bisect_utils.BISECT_MODE_MEAN,
2367 bisect_utils.BISECT_MODE_STD_DEV,
2368 bisect_utils.BISECT_MODE_RETURN_CODE],
2369 help='The bisect mode. Choices are to bisect on the '
2370 'difference in mean, std_dev, or return_code.')
2371 group.add_argument('--bug_id', default='',
2372 help='The id for the bug associated with this bisect. ' +
2373 'If this number is given, bisect will attempt to ' +
2374 'verify that the bug is not closed before '
2375 'starting.')
2377 @staticmethod
2378 def _AddBuildOptionsGroup(parser):
2379 group = parser.add_argument_group('Build options')
2380 group.add_argument('-w', '--working_directory',
2381 help='Path to the working directory where the script '
2382 'will do an initial checkout of the chromium depot. The '
2383 'files will be placed in a subdirectory "bisect" under '
2384 'working_directory and that will be used to perform the '
2385 'bisection. This parameter is optional, if it is not '
2386 'supplied, the script will work from the current depot.')
2387 group.add_argument('--build_preference',
2388 choices=['msvs', 'ninja', 'make'],
2389 help='The preferred build system to use. On linux/mac '
2390 'the options are make/ninja. On Windows, the '
2391 'options are msvs/ninja.')
2392 group.add_argument('--target_platform', default='chromium',
2393 choices=['chromium', 'android', 'android-chrome'],
2394 help='The target platform. Choices are "chromium" '
2395 '(current platform), or "android". If you specify '
2396 'something other than "chromium", you must be '
2397 'properly set up to build that platform.')
2398 group.add_argument('--no_custom_deps', dest='no_custom_deps',
2399 action='store_true', default=False,
2400 help='Run the script with custom_deps or not.')
2401 group.add_argument('--extra_src',
2402 help='Path to a script which can be used to modify the '
2403 'bisect script\'s behavior.')
2404 group.add_argument('--use_goma', action='store_true',
2405 help='Add a bunch of extra threads for goma, and enable '
2406 'goma')
2407 group.add_argument('--goma_dir',
2408 help='Path to goma tools (or system default if not '
2409 'specified).')
2410 group.add_argument('--goma_threads', type=int, default='64',
2411 help='Number of threads for goma, only if using goma.')
2412 group.add_argument('--output_buildbot_annotations', action='store_true',
2413 help='Add extra annotation output for buildbot.')
2414 group.add_argument('--target_arch', default='ia32',
2415 dest='target_arch', choices=['ia32', 'x64', 'arm'],
2416 help='The target build architecture. Choices are "ia32" '
2417 '(default), "x64" or "arm".')
2418 group.add_argument('--target_build_type', default='Release',
2419 choices=['Release', 'Debug'],
2420 help='The target build type. Choices are "Release" '
2421 '(default), or "Debug".')
2422 group.add_argument('--builder_host', dest='builder_host',
2423 help='Host address of server to produce build by '
2424 'posting try job request.')
2425 group.add_argument('--builder_port', dest='builder_port', type=int,
2426 help='HTTP port of the server to produce build by '
2427 'posting try job request.')
2429 @staticmethod
2430 def _AddDebugOptionsGroup(parser):
2431 group = parser.add_argument_group('Debug options')
2432 group.add_argument('--debug_ignore_build', action='store_true',
2433 help='DEBUG: Don\'t perform builds.')
2434 group.add_argument('--debug_ignore_sync', action='store_true',
2435 help='DEBUG: Don\'t perform syncs.')
2436 group.add_argument('--debug_ignore_perf_test', action='store_true',
2437 help='DEBUG: Don\'t perform performance tests.')
2438 group.add_argument('--debug_ignore_regression_confidence',
2439 action='store_true',
2440 help='DEBUG: Don\'t score the confidence of the initial '
2441 'good and bad revisions\' test results.')
2442 group.add_argument('--debug_fake_first_test_mean', type=int, default='0',
2443 help='DEBUG: When faking performance tests, return this '
2444 'value as the mean of the first performance test, '
2445 'and return a mean of 0.0 for further tests.')
2446 return group
2448 @classmethod
2449 def _CreateCommandLineParser(cls):
2450 """Creates a parser with bisect options.
2452 Returns:
2453 An instance of optparse.OptionParser.
2455 usage = ('%prog [options] [-- chromium-options]\n'
2456 'Perform binary search on revision history to find a minimal '
2457 'range of revisions where a performance metric regressed.\n')
2459 parser = argparse.ArgumentParser(usage=usage)
2460 cls._AddBisectOptionsGroup(parser)
2461 cls._AddBuildOptionsGroup(parser)
2462 cls._AddDebugOptionsGroup(parser)
2463 return parser
2465 def ParseCommandLine(self):
2466 """Parses the command line for bisect options."""
2467 parser = self._CreateCommandLineParser()
2468 opts = parser.parse_args()
2470 try:
2471 if (not opts.metric and
2472 opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE):
2473 raise RuntimeError('missing required parameter: --metric')
2475 if opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE:
2476 metric_values = opts.metric.split('/')
2477 if len(metric_values) != 2:
2478 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
2479 opts.metric = metric_values
2481 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25) / 100.0
2483 for k, v in opts.__dict__.iteritems():
2484 assert hasattr(self, k), 'Invalid %s attribute in BisectOptions.' % k
2485 setattr(self, k, v)
2486 except RuntimeError, e:
2487 output_string = StringIO.StringIO()
2488 parser.print_help(file=output_string)
2489 error_message = '%s\n\n%s' % (e.message, output_string.getvalue())
2490 output_string.close()
2491 raise RuntimeError(error_message)
2493 @staticmethod
2494 def FromDict(values):
2495 """Creates an instance of BisectOptions from a dictionary.
2497 Args:
2498 values: a dict containing options to set.
2500 Returns:
2501 An instance of BisectOptions.
2503 opts = BisectOptions()
2504 for k, v in values.iteritems():
2505 assert hasattr(opts, k), 'Invalid %s attribute in BisectOptions.' % k
2506 setattr(opts, k, v)
2508 if opts.metric and opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE:
2509 metric_values = opts.metric.split('/')
2510 if len(metric_values) != 2:
2511 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
2512 opts.metric = metric_values
2514 opts.repeat_test_count = min(max(opts.repeat_test_count, 1), 100)
2515 opts.max_time_minutes = min(max(opts.max_time_minutes, 1), 60)
2516 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25)
2517 opts.truncate_percent = opts.truncate_percent / 100.0
2519 return opts
2522 def _ConfigureLogging():
2523 """Trivial logging config.
2525 Configures logging to output any messages at or above INFO to standard out,
2526 without any additional formatting.
2528 logging_format = '%(message)s'
2529 logging.basicConfig(
2530 stream=logging.sys.stdout, level=logging.INFO, format=logging_format)
2533 def main():
2534 _ConfigureLogging()
2535 try:
2536 opts = BisectOptions()
2537 opts.ParseCommandLine()
2539 if opts.bug_id:
2540 if opts.output_buildbot_annotations:
2541 bisect_utils.OutputAnnotationStepStart('Checking Issue Tracker')
2542 issue_closed = query_crbug.CheckIssueClosed(opts.bug_id)
2543 if issue_closed:
2544 print 'Aborting bisect because bug is closed'
2545 else:
2546 print 'Could not confirm bug is closed, proceeding.'
2547 if opts.output_buildbot_annotations:
2548 bisect_utils.OutputAnnotationStepClosed()
2549 if issue_closed:
2550 results = BisectResults(abort_reason='the bug is closed.')
2551 bisect_test = BisectPerformanceMetrics(opts, os.getcwd())
2552 bisect_test.printer.FormatAndPrintResults(results)
2553 return 0
2556 if opts.extra_src:
2557 extra_src = bisect_utils.LoadExtraSrc(opts.extra_src)
2558 if not extra_src:
2559 raise RuntimeError('Invalid or missing --extra_src.')
2560 bisect_utils.AddAdditionalDepotInfo(extra_src.GetAdditionalDepotInfo())
2562 if opts.working_directory:
2563 custom_deps = bisect_utils.DEFAULT_GCLIENT_CUSTOM_DEPS
2564 if opts.no_custom_deps:
2565 custom_deps = None
2566 bisect_utils.CreateBisectDirectoryAndSetupDepot(opts, custom_deps)
2568 os.chdir(os.path.join(os.getcwd(), 'src'))
2569 RemoveBuildFiles(opts.target_build_type)
2571 if not _IsPlatformSupported():
2572 raise RuntimeError('Sorry, this platform isn\'t supported yet.')
2574 if not source_control.IsInGitRepository():
2575 raise RuntimeError(
2576 'Sorry, only the git workflow is supported at the moment.')
2578 # gClient sync seems to fail if you're not in master branch.
2579 if (not source_control.IsInProperBranch() and
2580 not opts.debug_ignore_sync and
2581 not opts.working_directory):
2582 raise RuntimeError('You must switch to master branch to run bisection.')
2583 bisect_test = BisectPerformanceMetrics(opts, os.getcwd())
2584 try:
2585 results = bisect_test.Run(opts.command, opts.bad_revision,
2586 opts.good_revision, opts.metric)
2587 if results.error:
2588 raise RuntimeError(results.error)
2589 bisect_test.printer.FormatAndPrintResults(results)
2590 return 0
2591 finally:
2592 bisect_test.PerformCleanup()
2593 except RuntimeError as e:
2594 if opts.output_buildbot_annotations:
2595 # The perf dashboard scrapes the "results" step in order to comment on
2596 # bugs. If you change this, please update the perf dashboard as well.
2597 bisect_utils.OutputAnnotationStepStart('Results')
2598 print 'Runtime Error: %s' % e
2599 if opts.output_buildbot_annotations:
2600 bisect_utils.OutputAnnotationStepClosed()
2601 return 1
2604 if __name__ == '__main__':
2605 sys.exit(main())