Add window.gc back to Smoothness.WillNavigateToPageHook
[chromium-blink-merge.git] / tools / auto_bisect / bisect_perf_regression.py
blob7fc16db5121878abaf02287f5edb2e3e45b66f13
1 #!/usr/bin/env python
2 # Copyright 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 """Chromium auto-bisect tool
8 This script bisects a range of commits using binary search. It starts by getting
9 reference values for the specified "good" and "bad" commits. Then, for revisions
10 in between, it will get builds, run tests and classify intermediate revisions as
11 "good" or "bad" until an adjacent "good" and "bad" revision is found; this is
12 the culprit.
14 If the culprit is a roll of a depedency repository (e.g. v8), it will then
15 expand the revision range and continue the bisect until a culprit revision in
16 the dependency repository is found.
18 Example usage using git commit hashes, bisecting a performance test based on
19 the mean value of a particular metric:
21 ./tools/auto_bisect/bisect_perf_regression.py
22 --command "out/Release/performance_ui_tests \
23 --gtest_filter=ShutdownTest.SimpleUserQuit"\
24 --metric shutdown/simple-user-quit
25 --good_revision 1f6e67861535121c5c819c16a666f2436c207e7b\
26 --bad-revision b732f23b4f81c382db0b23b9035f3dadc7d925bb\
28 Example usage using git commit positions, bisecting a functional test based on
29 whether it passes or fails.
31 ./tools/auto_bisect/bisect_perf_regression.py\
32 --command "out/Release/content_unittests -single-process-tests \
33 --gtest_filter=GpuMemoryBufferImplTests"\
34 --good_revision 408222\
35 --bad_revision 408232\
36 --bisect_mode return_code\
37 --builder_type full
39 In practice, the auto-bisect tool is usually run on tryserver.chromium.perf
40 try bots, and is started by tools/run-bisect-perf-regression.py using
41 config parameters from tools/auto_bisect/bisect.cfg.
42 """
44 import copy
45 import errno
46 import hashlib
47 import logging
48 import argparse
49 import os
50 import re
51 import shlex
52 import shutil
53 import StringIO
54 import sys
55 import time
57 sys.path.append(os.path.join(
58 os.path.dirname(__file__), os.path.pardir, 'telemetry'))
60 from bisect_printer import BisectPrinter
61 from bisect_results import BisectResults
62 from bisect_state import BisectState
63 import bisect_utils
64 import builder
65 import fetch_build
66 import math_utils
67 import query_crbug
68 import request_build
69 import source_control
71 # The script is in chromium/src/tools/auto_bisect. Throughout this script,
72 # we use paths to other things in the chromium/src repository.
74 # Possible return values from BisectPerformanceMetrics.RunTest.
75 BUILD_RESULT_SUCCEED = 0
76 BUILD_RESULT_FAIL = 1
77 BUILD_RESULT_SKIPPED = 2
79 # The confidence percentage we require to consider the initial range a
80 # regression based on the test results of the initial good and bad revisions.
81 REGRESSION_CONFIDENCE = 80
82 # How many times to repeat the test on the last known good and first known bad
83 # revisions in order to assess a more accurate confidence score in the
84 # regression culprit.
85 BORDER_REVISIONS_EXTRA_RUNS = 2
87 # Patch template to add a new file, DEPS.sha under src folder.
88 # This file contains SHA1 value of the DEPS changes made while bisecting
89 # dependency repositories. This patch send along with DEPS patch to try server.
90 # When a build requested is posted with a patch, bisect builders on try server,
91 # once build is produced, it reads SHA value from this file and appends it
92 # to build archive filename.
93 DEPS_SHA_PATCH = """diff --git DEPS.sha DEPS.sha
94 new file mode 100644
95 --- /dev/null
96 +++ DEPS.sha
97 @@ -0,0 +1 @@
98 +%(deps_sha)s
99 """
101 REGRESSION_NOT_REPRODUCED_MESSAGE_TEMPLATE = """
102 Bisect did not clearly reproduce a regression between the given "good"
103 and "bad" revisions.
105 Results:
106 "Good" revision: {good_rev}
107 \tMean: {good_mean}
108 \tStandard error: {good_std_err}
109 \tSample size: {good_sample_size}
111 "Bad" revision: {bad_rev}
112 \tMean: {bad_mean}
113 \tStandard error: {bad_std_err}
114 \tSample size: {bad_sample_size}
116 You may want to try bisecting on a different platform or metric.
119 # Git branch name used to run bisect try jobs.
120 BISECT_TRYJOB_BRANCH = 'bisect-tryjob'
121 # Git master branch name.
122 BISECT_MASTER_BRANCH = 'master'
123 # File to store 'git diff' content.
124 BISECT_PATCH_FILE = 'deps_patch.txt'
125 # SVN repo where the bisect try jobs are submitted.
126 PERF_SVN_REPO_URL = 'svn://svn.chromium.org/chrome-try/try-perf'
127 FULL_SVN_REPO_URL = 'svn://svn.chromium.org/chrome-try/try'
128 ANDROID_CHROME_SVN_REPO_URL = ('svn://svn.chromium.org/chrome-try-internal/'
129 'try-perf')
132 class RunGitError(Exception):
134 def __str__(self):
135 return '%s\nError executing git command.' % self.args[0]
138 def GetSHA1HexDigest(contents):
139 """Returns SHA1 hex digest of the given string."""
140 return hashlib.sha1(contents).hexdigest()
143 def WriteStringToFile(text, file_name):
144 """Writes text to a file, raising an RuntimeError on failure."""
145 try:
146 with open(file_name, 'wb') as f:
147 f.write(text)
148 except IOError:
149 raise RuntimeError('Error writing to file [%s]' % file_name)
152 def ReadStringFromFile(file_name):
153 """Writes text to a file, raising an RuntimeError on failure."""
154 try:
155 with open(file_name) as f:
156 return f.read()
157 except IOError:
158 raise RuntimeError('Error reading file [%s]' % file_name)
161 def ChangeBackslashToSlashInPatch(diff_text):
162 """Formats file paths in the given patch text to Unix-style paths."""
163 if not diff_text:
164 return None
165 diff_lines = diff_text.split('\n')
166 for i in range(len(diff_lines)):
167 line = diff_lines[i]
168 if line.startswith('--- ') or line.startswith('+++ '):
169 diff_lines[i] = line.replace('\\', '/')
170 return '\n'.join(diff_lines)
173 def _ParseRevisionsFromDEPSFileManually(deps_file_contents):
174 """Parses the vars section of the DEPS file using regular expressions.
176 Args:
177 deps_file_contents: The DEPS file contents as a string.
179 Returns:
180 A dictionary in the format {depot: revision} if successful, otherwise None.
182 # We'll parse the "vars" section of the DEPS file.
183 rxp = re.compile('vars = {(?P<vars_body>[^}]+)', re.MULTILINE)
184 re_results = rxp.search(deps_file_contents)
186 if not re_results:
187 return None
189 # We should be left with a series of entries in the vars component of
190 # the DEPS file with the following format:
191 # 'depot_name': 'revision',
192 vars_body = re_results.group('vars_body')
193 rxp = re.compile(r"'(?P<depot_body>[\w_-]+)':[\s]+'(?P<rev_body>[\w@]+)'",
194 re.MULTILINE)
195 re_results = rxp.findall(vars_body)
197 return dict(re_results)
200 def _WaitUntilBuildIsReady(fetch_build_func, builder_name, build_request_id,
201 max_timeout, buildbot_server_url):
202 """Waits until build is produced by bisect builder on try server.
204 Args:
205 fetch_build_func: Function to check and download build from cloud storage.
206 builder_name: Builder bot name on try server.
207 build_request_id: A unique ID of the build request posted to try server.
208 max_timeout: Maximum time to wait for the build.
209 buildbot_server_url: Buildbot url to check build status.
211 Returns:
212 Downloaded archive file path if exists, otherwise None.
214 # Build number on the try server.
215 build_num = None
216 # Interval to check build on cloud storage.
217 poll_interval = 60
218 # Interval to check build status on try server in seconds.
219 status_check_interval = 600
220 last_status_check = time.time()
221 start_time = time.time()
223 while True:
224 # Checks for build on gs://chrome-perf and download if exists.
225 res = fetch_build_func()
226 if res:
227 return (res, 'Build successfully found')
228 elapsed_status_check = time.time() - last_status_check
229 # To avoid overloading try server with status check requests, we check
230 # build status for every 10 minutes.
231 if elapsed_status_check > status_check_interval:
232 last_status_check = time.time()
233 if not build_num:
234 # Get the build number on try server for the current build.
235 build_num = request_build.GetBuildNumFromBuilder(
236 build_request_id, builder_name, buildbot_server_url)
237 # Check the status of build using the build number.
238 # Note: Build is treated as PENDING if build number is not found
239 # on the the try server.
240 build_status, status_link = request_build.GetBuildStatus(
241 build_num, builder_name, buildbot_server_url)
242 if build_status == request_build.FAILED:
243 return (None, 'Failed to produce build, log: %s' % status_link)
244 elapsed_time = time.time() - start_time
245 if elapsed_time > max_timeout:
246 return (None, 'Timed out: %ss without build' % max_timeout)
248 logging.info('Time elapsed: %ss without build.', elapsed_time)
249 time.sleep(poll_interval)
250 # For some reason, mac bisect bots were not flushing stdout periodically.
251 # As a result buildbot command is timed-out. Flush stdout on all platforms
252 # while waiting for build.
253 sys.stdout.flush()
256 def _UpdateV8Branch(deps_content):
257 """Updates V8 branch in DEPS file to process v8_bleeding_edge.
259 Check for "v8_branch" in DEPS file if exists update its value
260 with v8_bleeding_edge branch. Note: "v8_branch" is added to DEPS
261 variable from DEPS revision 254916, therefore check for "src/v8":
262 <v8 source path> in DEPS in order to support prior DEPS revisions
263 and update it.
265 Args:
266 deps_content: DEPS file contents to be modified.
268 Returns:
269 Modified DEPS file contents as a string.
271 new_branch = r'branches/bleeding_edge'
272 v8_branch_pattern = re.compile(r'(?<="v8_branch": ")(.*)(?=")')
273 if re.search(v8_branch_pattern, deps_content):
274 deps_content = re.sub(v8_branch_pattern, new_branch, deps_content)
275 else:
276 # Replaces the branch assigned to "src/v8" key in DEPS file.
277 # Format of "src/v8" in DEPS:
278 # "src/v8":
279 # (Var("googlecode_url") % "v8") + "/trunk@" + Var("v8_revision"),
280 # So, "/trunk@" is replace with "/branches/bleeding_edge@"
281 v8_src_pattern = re.compile(
282 r'(?<="v8"\) \+ "/)(.*)(?=@" \+ Var\("v8_revision"\))', re.MULTILINE)
283 if re.search(v8_src_pattern, deps_content):
284 deps_content = re.sub(v8_src_pattern, new_branch, deps_content)
285 return deps_content
288 def _UpdateDEPSForAngle(revision, depot, deps_file):
289 """Updates DEPS file with new revision for Angle repository.
291 This is a hack for Angle depot case because, in DEPS file "vars" dictionary
292 variable contains "angle_revision" key that holds git hash instead of
293 SVN revision.
295 And sometimes "angle_revision" key is not specified in "vars" variable,
296 in such cases check "deps" dictionary variable that matches
297 angle.git@[a-fA-F0-9]{40}$ and replace git hash.
299 deps_var = bisect_utils.DEPOT_DEPS_NAME[depot]['deps_var']
300 try:
301 deps_contents = ReadStringFromFile(deps_file)
302 # Check whether the depot and revision pattern in DEPS file vars variable
303 # e.g. "angle_revision": "fa63e947cb3eccf463648d21a05d5002c9b8adfa".
304 angle_rev_pattern = re.compile(r'(?<="%s": ")([a-fA-F0-9]{40})(?=")' %
305 deps_var, re.MULTILINE)
306 match = re.search(angle_rev_pattern, deps_contents)
307 if match:
308 # Update the revision information for the given depot
309 new_data = re.sub(angle_rev_pattern, revision, deps_contents)
310 else:
311 # Check whether the depot and revision pattern in DEPS file deps
312 # variable. e.g.,
313 # "src/third_party/angle": Var("chromium_git") +
314 # "/angle/angle.git@fa63e947cb3eccf463648d21a05d5002c9b8adfa",.
315 angle_rev_pattern = re.compile(
316 r'(?<=angle\.git@)([a-fA-F0-9]{40})(?=")', re.MULTILINE)
317 match = re.search(angle_rev_pattern, deps_contents)
318 if not match:
319 logging.info('Could not find angle revision information in DEPS file.')
320 return False
321 new_data = re.sub(angle_rev_pattern, revision, deps_contents)
322 # Write changes to DEPS file
323 WriteStringToFile(new_data, deps_file)
324 return True
325 except IOError, e:
326 logging.warn('Something went wrong while updating DEPS file, %s', e)
327 return False
330 def _TryParseHistogramValuesFromOutput(metric, text):
331 """Attempts to parse a metric in the format HISTOGRAM <graph: <trace>.
333 Args:
334 metric: The metric as a list of [<trace>, <value>] strings.
335 text: The text to parse the metric values from.
337 Returns:
338 A list of floating point numbers found, [] if none were found.
340 metric_formatted = 'HISTOGRAM %s: %s= ' % (metric[0], metric[1])
342 text_lines = text.split('\n')
343 values_list = []
345 for current_line in text_lines:
346 if metric_formatted in current_line:
347 current_line = current_line[len(metric_formatted):]
349 try:
350 histogram_values = eval(current_line)
352 for b in histogram_values['buckets']:
353 average_for_bucket = float(b['high'] + b['low']) * 0.5
354 # Extends the list with N-elements with the average for that bucket.
355 values_list.extend([average_for_bucket] * b['count'])
356 except Exception:
357 pass
359 return values_list
362 def _TryParseResultValuesFromOutput(metric, text):
363 """Attempts to parse a metric in the format RESULT <graph>: <trace>= ...
365 Args:
366 metric: The metric as a list of [<trace>, <value>] string pairs.
367 text: The text to parse the metric values from.
369 Returns:
370 A list of floating point numbers found.
372 # Format is: RESULT <graph>: <trace>= <value> <units>
373 metric_re = re.escape('RESULT %s: %s=' % (metric[0], metric[1]))
375 # The log will be parsed looking for format:
376 # <*>RESULT <graph_name>: <trace_name>= <value>
377 single_result_re = re.compile(
378 metric_re + r'\s*(?P<VALUE>[-]?\d*(\.\d*)?)')
380 # The log will be parsed looking for format:
381 # <*>RESULT <graph_name>: <trace_name>= [<value>,value,value,...]
382 multi_results_re = re.compile(
383 metric_re + r'\s*\[\s*(?P<VALUES>[-]?[\d\., ]+)\s*\]')
385 # The log will be parsed looking for format:
386 # <*>RESULT <graph_name>: <trace_name>= {<mean>, <std deviation>}
387 mean_stddev_re = re.compile(
388 metric_re +
389 r'\s*\{\s*(?P<MEAN>[-]?\d*(\.\d*)?),\s*(?P<STDDEV>\d+(\.\d*)?)\s*\}')
391 text_lines = text.split('\n')
392 values_list = []
393 for current_line in text_lines:
394 # Parse the output from the performance test for the metric we're
395 # interested in.
396 single_result_match = single_result_re.search(current_line)
397 multi_results_match = multi_results_re.search(current_line)
398 mean_stddev_match = mean_stddev_re.search(current_line)
399 if (not single_result_match is None and
400 single_result_match.group('VALUE')):
401 values_list += [single_result_match.group('VALUE')]
402 elif (not multi_results_match is None and
403 multi_results_match.group('VALUES')):
404 metric_values = multi_results_match.group('VALUES')
405 values_list += metric_values.split(',')
406 elif (not mean_stddev_match is None and
407 mean_stddev_match.group('MEAN')):
408 values_list += [mean_stddev_match.group('MEAN')]
410 values_list = [float(v) for v in values_list
411 if bisect_utils.IsStringFloat(v)]
413 return values_list
416 def _ParseMetricValuesFromOutput(metric, text):
417 """Parses output from performance_ui_tests and retrieves the results for
418 a given metric.
420 Args:
421 metric: The metric as a list of [<trace>, <value>] strings.
422 text: The text to parse the metric values from.
424 Returns:
425 A list of floating point numbers found.
427 metric_values = _TryParseResultValuesFromOutput(metric, text)
429 if not metric_values:
430 metric_values = _TryParseHistogramValuesFromOutput(metric, text)
432 return metric_values
435 def _GenerateProfileIfNecessary(command_args):
436 """Checks the command line of the performance test for dependencies on
437 profile generation, and runs tools/perf/generate_profile as necessary.
439 Args:
440 command_args: Command line being passed to performance test, as a list.
442 Returns:
443 False if profile generation was necessary and failed, otherwise True.
445 if '--profile-dir' in ' '.join(command_args):
446 # If we were using python 2.7+, we could just use the argparse
447 # module's parse_known_args to grab --profile-dir. Since some of the
448 # bots still run 2.6, have to grab the arguments manually.
449 arg_dict = {}
450 args_to_parse = ['--profile-dir', '--browser']
452 for arg_to_parse in args_to_parse:
453 for i, current_arg in enumerate(command_args):
454 if arg_to_parse in current_arg:
455 current_arg_split = current_arg.split('=')
457 # Check 2 cases, --arg=<val> and --arg <val>
458 if len(current_arg_split) == 2:
459 arg_dict[arg_to_parse] = current_arg_split[1]
460 elif i + 1 < len(command_args):
461 arg_dict[arg_to_parse] = command_args[i+1]
463 path_to_generate = os.path.join('tools', 'perf', 'generate_profile')
465 if '--profile-dir' in arg_dict and '--browser' in arg_dict:
466 profile_path, profile_type = os.path.split(arg_dict['--profile-dir'])
467 return not bisect_utils.RunProcess(
469 'python', path_to_generate,
470 '--profile-type-to-generate', profile_type,
471 '--browser', arg_dict['--browser'],
472 '--output-dir', profile_path
474 return False
475 return True
478 def _IsRegressionReproduced(known_good_result, known_bad_result):
479 """Checks whether the regression was reproduced based on the initial values.
481 Args:
482 known_good_result: A dict with the keys "values", "mean" and "std_err".
483 known_bad_result: Same as above.
485 Returns:
486 True if there is a clear change between the result values for the given
487 good and bad revisions, False otherwise.
489 def PossiblyFlatten(values):
490 """Flattens if needed, by averaging the values in each nested list."""
491 if isinstance(values, list) and all(isinstance(x, list) for x in values):
492 return map(math_utils.Mean, values)
493 return values
495 p_value = BisectResults.ConfidenceScore(
496 PossiblyFlatten(known_bad_result['values']),
497 PossiblyFlatten(known_good_result['values']),
498 accept_single_bad_or_good=True)
500 return p_value > REGRESSION_CONFIDENCE
503 def _RegressionNotReproducedWarningMessage(
504 good_revision, bad_revision, known_good_value, known_bad_value):
505 return REGRESSION_NOT_REPRODUCED_MESSAGE_TEMPLATE.format(
506 good_rev=good_revision,
507 good_mean=known_good_value['mean'],
508 good_std_err=known_good_value['std_err'],
509 good_sample_size=len(known_good_value['values']),
510 bad_rev=bad_revision,
511 bad_mean=known_bad_value['mean'],
512 bad_std_err=known_bad_value['std_err'],
513 bad_sample_size=len(known_bad_value['values']))
516 class DepotDirectoryRegistry(object):
518 def __init__(self, src_cwd):
519 self.depot_cwd = {}
520 for depot in bisect_utils.DEPOT_NAMES:
521 # The working directory of each depot is just the path to the depot, but
522 # since we're already in 'src', we can skip that part.
523 path_in_src = bisect_utils.DEPOT_DEPS_NAME[depot]['src'][4:]
524 self.SetDepotDir(depot, os.path.join(src_cwd, path_in_src))
526 self.SetDepotDir('chromium', src_cwd)
528 def SetDepotDir(self, depot_name, depot_dir):
529 self.depot_cwd[depot_name] = depot_dir
531 def GetDepotDir(self, depot_name):
532 if depot_name in self.depot_cwd:
533 return self.depot_cwd[depot_name]
534 else:
535 assert False, ('Unknown depot [ %s ] encountered. Possibly a new one '
536 'was added without proper support?' % depot_name)
538 def ChangeToDepotDir(self, depot_name):
539 """Given a depot, changes to the appropriate working directory.
541 Args:
542 depot_name: The name of the depot (see DEPOT_NAMES).
544 os.chdir(self.GetDepotDir(depot_name))
547 def _PrepareBisectBranch(parent_branch, new_branch):
548 """Creates a new branch to submit bisect try job.
550 Args:
551 parent_branch: Parent branch to be used to create new branch.
552 new_branch: New branch name.
554 current_branch, returncode = bisect_utils.RunGit(
555 ['rev-parse', '--abbrev-ref', 'HEAD'])
556 if returncode:
557 raise RunGitError('Must be in a git repository to send changes to trybots.')
559 current_branch = current_branch.strip()
560 # Make sure current branch is master.
561 if current_branch != parent_branch:
562 output, returncode = bisect_utils.RunGit(['checkout', '-f', parent_branch])
563 if returncode:
564 raise RunGitError('Failed to checkout branch: %s.' % output)
566 # Delete new branch if exists.
567 output, returncode = bisect_utils.RunGit(['branch', '--list'])
568 if new_branch in output:
569 output, returncode = bisect_utils.RunGit(['branch', '-D', new_branch])
570 if returncode:
571 raise RunGitError('Deleting branch failed, %s', output)
573 # Check if the tree is dirty: make sure the index is up to date and then
574 # run diff-index.
575 bisect_utils.RunGit(['update-index', '--refresh', '-q'])
576 output, returncode = bisect_utils.RunGit(['diff-index', 'HEAD'])
577 if output:
578 raise RunGitError('Cannot send a try job with a dirty tree.')
580 # Create and check out the telemetry-tryjob branch, and edit the configs
581 # for the try job there.
582 output, returncode = bisect_utils.RunGit(['checkout', '-b', new_branch])
583 if returncode:
584 raise RunGitError('Failed to checkout branch: %s.' % output)
586 output, returncode = bisect_utils.RunGit(
587 ['branch', '--set-upstream-to', parent_branch])
588 if returncode:
589 raise RunGitError('Error in git branch --set-upstream-to')
592 def _StartBuilderTryJob(
593 builder_type, git_revision, builder_name, job_name, patch=None):
594 """Attempts to run a try job from the current directory.
596 Args:
597 builder_type: One of the builder types in fetch_build, e.g. "perf".
598 git_revision: A git commit hash.
599 builder_name: Name of the bisect bot to be used for try job.
600 bisect_job_name: Try job name, used to identify which bisect
601 job was responsible for requesting a build.
602 patch: A DEPS patch (used while bisecting dependency repositories),
603 or None if we're bisecting the top-level repository.
605 # TODO(prasadv, qyearsley): Make this a method of BuildArchive
606 # (which may be renamed to BuilderTryBot or Builder).
607 try:
608 # Temporary branch for running a try job.
609 _PrepareBisectBranch(BISECT_MASTER_BRANCH, BISECT_TRYJOB_BRANCH)
610 patch_content = '/dev/null'
611 # Create a temporary patch file.
612 if patch:
613 WriteStringToFile(patch, BISECT_PATCH_FILE)
614 patch_content = BISECT_PATCH_FILE
616 try_command = [
617 'try',
618 '--bot=%s' % builder_name,
619 '--revision=%s' % git_revision,
620 '--name=%s' % job_name,
621 '--svn_repo=%s' % _TryJobSvnRepo(builder_type),
622 '--diff=%s' % patch_content,
624 # Execute try job to build revision.
625 print try_command
626 output, return_code = bisect_utils.RunGit(try_command)
628 command_string = ' '.join(['git'] + try_command)
629 if return_code:
630 raise RunGitError('Could not execute try job: %s.\n'
631 'Error: %s' % (command_string, output))
632 logging.info('Try job successfully submitted.\n TryJob Details: %s\n%s',
633 command_string, output)
634 finally:
635 # Delete patch file if exists.
636 try:
637 os.remove(BISECT_PATCH_FILE)
638 except OSError as e:
639 if e.errno != errno.ENOENT:
640 raise
641 # Checkout master branch and delete bisect-tryjob branch.
642 bisect_utils.RunGit(['checkout', '-f', BISECT_MASTER_BRANCH])
643 bisect_utils.RunGit(['branch', '-D', BISECT_TRYJOB_BRANCH])
646 def _TryJobSvnRepo(builder_type):
647 """Returns an SVN repo to use for try jobs based on the builder type."""
648 if builder_type == fetch_build.PERF_BUILDER:
649 return PERF_SVN_REPO_URL
650 if builder_type == fetch_build.FULL_BUILDER:
651 return FULL_SVN_REPO_URL
652 if builder_type == fetch_build.ANDROID_CHROME_PERF_BUILDER:
653 return ANDROID_CHROME_SVN_REPO_URL
654 raise NotImplementedError('Unknown builder type "%s".' % builder_type)
657 class BisectPerformanceMetrics(object):
658 """This class contains functionality to perform a bisection of a range of
659 revisions to narrow down where performance regressions may have occurred.
661 The main entry-point is the Run method.
664 def __init__(self, opts, src_cwd):
665 """Constructs a BisectPerformancesMetrics object.
667 Args:
668 opts: BisectOptions object containing parsed options.
669 src_cwd: Root src/ directory of the test repository (inside bisect/ dir).
671 super(BisectPerformanceMetrics, self).__init__()
673 self.opts = opts
674 self.src_cwd = src_cwd
675 self.depot_registry = DepotDirectoryRegistry(self.src_cwd)
676 self.printer = BisectPrinter(self.opts, self.depot_registry)
677 self.cleanup_commands = []
678 self.warnings = []
679 self.builder = builder.Builder.FromOpts(opts)
681 def PerformCleanup(self):
682 """Performs cleanup when script is finished."""
683 os.chdir(self.src_cwd)
684 for c in self.cleanup_commands:
685 if c[0] == 'mv':
686 shutil.move(c[1], c[2])
687 else:
688 assert False, 'Invalid cleanup command.'
690 def GetRevisionList(self, depot, bad_revision, good_revision):
691 """Retrieves a list of all the commits between the bad revision and
692 last known good revision."""
694 cwd = self.depot_registry.GetDepotDir(depot)
695 return source_control.GetRevisionList(bad_revision, good_revision, cwd=cwd)
697 def _ParseRevisionsFromDEPSFile(self, depot):
698 """Parses the local DEPS file to determine blink/skia/v8 revisions which may
699 be needed if the bisect recurses into those depots later.
701 Args:
702 depot: Name of depot being bisected.
704 Returns:
705 A dict in the format {depot:revision} if successful, otherwise None.
707 try:
708 deps_data = {
709 'Var': lambda _: deps_data["vars"][_],
710 'From': lambda *args: None,
713 deps_file = bisect_utils.FILE_DEPS_GIT
714 if not os.path.exists(deps_file):
715 deps_file = bisect_utils.FILE_DEPS
716 execfile(deps_file, {}, deps_data)
717 deps_data = deps_data['deps']
719 rxp = re.compile(".git@(?P<revision>[a-fA-F0-9]+)")
720 results = {}
721 for depot_name, depot_data in bisect_utils.DEPOT_DEPS_NAME.iteritems():
722 if (depot_data.get('platform') and
723 depot_data.get('platform') != os.name):
724 continue
726 if depot_data.get('recurse') and depot in depot_data.get('from'):
727 depot_data_src = depot_data.get('src') or depot_data.get('src_old')
728 src_dir = deps_data.get(depot_data_src)
729 if src_dir:
730 self.depot_registry.SetDepotDir(depot_name, os.path.join(
731 self.src_cwd, depot_data_src[4:]))
732 re_results = rxp.search(src_dir)
733 if re_results:
734 results[depot_name] = re_results.group('revision')
735 else:
736 warning_text = ('Could not parse revision for %s while bisecting '
737 '%s' % (depot_name, depot))
738 if not warning_text in self.warnings:
739 self.warnings.append(warning_text)
740 else:
741 results[depot_name] = None
742 return results
743 except ImportError:
744 deps_file_contents = ReadStringFromFile(deps_file)
745 parse_results = _ParseRevisionsFromDEPSFileManually(deps_file_contents)
746 results = {}
747 for depot_name, depot_revision in parse_results.iteritems():
748 depot_revision = depot_revision.strip('@')
749 logging.warn(depot_name, depot_revision)
750 for cur_name, cur_data in bisect_utils.DEPOT_DEPS_NAME.iteritems():
751 if cur_data.get('deps_var') == depot_name:
752 src_name = cur_name
753 results[src_name] = depot_revision
754 break
755 return results
757 def _Get3rdPartyRevisions(self, depot):
758 """Parses the DEPS file to determine WebKit/v8/etc... versions.
760 Args:
761 depot: A depot name. Should be in the DEPOT_NAMES list.
763 Returns:
764 A dict in the format {depot: revision} if successful, otherwise None.
766 cwd = os.getcwd()
767 self.depot_registry.ChangeToDepotDir(depot)
769 results = {}
771 if depot == 'chromium' or depot == 'android-chrome':
772 results = self._ParseRevisionsFromDEPSFile(depot)
773 os.chdir(cwd)
775 if depot == 'v8':
776 # We can't try to map the trunk revision to bleeding edge yet, because
777 # we don't know which direction to try to search in. Have to wait until
778 # the bisect has narrowed the results down to 2 v8 rolls.
779 results['v8_bleeding_edge'] = None
781 return results
783 def BackupOrRestoreOutputDirectory(self, restore=False, build_type='Release'):
784 """Backs up or restores build output directory based on restore argument.
786 Args:
787 restore: Indicates whether to restore or backup. Default is False(Backup)
788 build_type: Target build type ('Release', 'Debug', 'Release_x64' etc.)
790 Returns:
791 Path to backup or restored location as string. otherwise None if it fails.
793 build_dir = os.path.abspath(
794 builder.GetBuildOutputDirectory(self.opts, self.src_cwd))
795 source_dir = os.path.join(build_dir, build_type)
796 destination_dir = os.path.join(build_dir, '%s.bak' % build_type)
797 if restore:
798 source_dir, destination_dir = destination_dir, source_dir
799 if os.path.exists(source_dir):
800 RemoveDirectoryTree(destination_dir)
801 shutil.move(source_dir, destination_dir)
802 return destination_dir
803 return None
805 def _DownloadAndUnzipBuild(self, revision, depot, build_type='Release',
806 create_patch=False):
807 """Downloads the build archive for the given revision.
809 Args:
810 revision: The git revision to download.
811 depot: The name of a dependency repository. Should be in DEPOT_NAMES.
812 build_type: Target build type, e.g. Release', 'Debug', 'Release_x64' etc.
813 create_patch: Create a patch with any locally modified files.
815 Returns:
816 True if download succeeds, otherwise False.
818 patch = None
819 patch_sha = None
820 if depot not in ('chromium', 'android-chrome'):
821 # Create a DEPS patch with new revision for dependency repository.
822 self._CreateDEPSPatch(depot, revision)
823 create_patch = True
825 if create_patch:
826 revision, patch = self._CreatePatch(revision)
828 if patch:
829 # Get the SHA of the DEPS changes patch.
830 patch_sha = GetSHA1HexDigest(patch)
832 # Update the DEPS changes patch with a patch to create a new file named
833 # 'DEPS.sha' and add patch_sha evaluated above to it.
834 patch = '%s\n%s' % (patch, DEPS_SHA_PATCH % {'deps_sha': patch_sha})
836 build_dir = builder.GetBuildOutputDirectory(self.opts, self.src_cwd)
837 downloaded_file = self._WaitForBuildDownload(
838 revision, build_dir, deps_patch=patch, deps_patch_sha=patch_sha)
839 if not downloaded_file:
840 return False
841 return self._UnzipAndMoveBuildProducts(downloaded_file, build_dir,
842 build_type=build_type)
844 def _WaitForBuildDownload(self, revision, build_dir, deps_patch=None,
845 deps_patch_sha=None):
846 """Tries to download a zip archive for a build.
848 This involves seeing whether the archive is already available, and if not,
849 then requesting a build and waiting before downloading.
851 Args:
852 revision: A git commit hash.
853 build_dir: The directory to download the build into.
854 deps_patch: A patch which changes a dependency repository revision in
855 the DEPS, if applicable.
856 deps_patch_sha: The SHA1 hex digest of the above patch.
858 Returns:
859 File path of the downloaded file if successful, otherwise None.
861 bucket_name, remote_path = fetch_build.GetBucketAndRemotePath(
862 revision, builder_type=self.opts.builder_type,
863 target_arch=self.opts.target_arch,
864 target_platform=self.opts.target_platform,
865 deps_patch_sha=deps_patch_sha,
866 extra_src=self.opts.extra_src)
867 output_dir = os.path.abspath(build_dir)
868 fetch_build_func = lambda: fetch_build.FetchFromCloudStorage(
869 bucket_name, remote_path, output_dir)
871 is_available = fetch_build.BuildIsAvailable(bucket_name, remote_path)
872 if is_available:
873 return fetch_build_func()
875 # When build archive doesn't exist, make a request and wait.
876 return self._RequestBuildAndWait(
877 revision, fetch_build_func, deps_patch=deps_patch)
879 def _RequestBuildAndWait(self, git_revision, fetch_build_func,
880 deps_patch=None):
881 """Triggers a try job for a build job.
883 This function prepares and starts a try job for a builder, and waits for
884 the archive to be produced and archived. Once the build is ready it is
885 downloaded.
887 For performance tests, builders on the tryserver.chromium.perf are used.
889 TODO(qyearsley): Make this function take "builder_type" as a parameter
890 and make requests to different bot names based on that parameter.
892 Args:
893 git_revision: A git commit hash.
894 fetch_build_func: Function to check and download build from cloud storage.
895 deps_patch: DEPS patch string, used when bisecting dependency repos.
897 Returns:
898 Downloaded archive file path when requested build exists and download is
899 successful, otherwise None.
901 if not fetch_build_func:
902 return None
904 # Create a unique ID for each build request posted to try server builders.
905 # This ID is added to "Reason" property of the build.
906 build_request_id = GetSHA1HexDigest(
907 '%s-%s-%s' % (git_revision, deps_patch, time.time()))
909 # Revert any changes to DEPS file.
910 bisect_utils.CheckRunGit(['reset', '--hard', 'HEAD'], cwd=self.src_cwd)
912 builder_name, build_timeout = fetch_build.GetBuilderNameAndBuildTime(
913 builder_type=self.opts.builder_type,
914 target_arch=self.opts.target_arch,
915 target_platform=self.opts.target_platform,
916 extra_src=self.opts.extra_src)
918 try:
919 _StartBuilderTryJob(self.opts.builder_type, git_revision, builder_name,
920 job_name=build_request_id, patch=deps_patch)
921 except RunGitError as e:
922 logging.warn('Failed to post builder try job for revision: [%s].\n'
923 'Error: %s', git_revision, e)
924 return None
926 # Get the buildbot master URL to monitor build status.
927 buildbot_server_url = fetch_build.GetBuildBotUrl(
928 builder_type=self.opts.builder_type,
929 target_arch=self.opts.target_arch,
930 target_platform=self.opts.target_platform,
931 extra_src=self.opts.extra_src)
933 archive_filename, error_msg = _WaitUntilBuildIsReady(
934 fetch_build_func, builder_name, build_request_id, build_timeout,
935 buildbot_server_url)
936 if not archive_filename:
937 logging.warn('%s [revision: %s]', error_msg, git_revision)
938 return archive_filename
940 def _UnzipAndMoveBuildProducts(self, downloaded_file, build_dir,
941 build_type='Release'):
942 """Unzips the build archive and moves it to the build output directory.
944 The build output directory is wherever the binaries are expected to
945 be in order to start Chrome and run tests.
947 TODO: Simplify and clarify this method if possible.
949 Args:
950 downloaded_file: File path of the downloaded zip file.
951 build_dir: Directory where the the zip file was downloaded to.
952 build_type: "Release" or "Debug".
954 Returns:
955 True if successful, False otherwise.
957 abs_build_dir = os.path.abspath(build_dir)
958 output_dir = os.path.join(abs_build_dir, self.GetZipFileBuildDirName())
959 logging.info('EXPERIMENTAL RUN, _UnzipAndMoveBuildProducts locals %s',
960 str(locals()))
962 try:
963 RemoveDirectoryTree(output_dir)
964 self.BackupOrRestoreOutputDirectory(restore=False)
965 # Build output directory based on target(e.g. out/Release, out/Debug).
966 target_build_output_dir = os.path.join(abs_build_dir, build_type)
968 logging.info('Extracting "%s" to "%s"', downloaded_file, abs_build_dir)
969 fetch_build.Unzip(downloaded_file, abs_build_dir)
971 if not os.path.exists(output_dir):
972 # Due to recipe changes, the builds extract folder contains
973 # out/Release instead of full-build-<platform>/Release.
974 if os.path.exists(os.path.join(abs_build_dir, 'out', build_type)):
975 output_dir = os.path.join(abs_build_dir, 'out', build_type)
976 else:
977 raise IOError('Missing extracted folder %s ' % output_dir)
979 logging.info('Moving build from %s to %s',
980 output_dir, target_build_output_dir)
981 shutil.move(output_dir, target_build_output_dir)
982 return True
983 except Exception as e:
984 logging.info('Something went wrong while extracting archive file: %s', e)
985 self.BackupOrRestoreOutputDirectory(restore=True)
986 # Cleanup any leftovers from unzipping.
987 if os.path.exists(output_dir):
988 RemoveDirectoryTree(output_dir)
989 finally:
990 # Delete downloaded archive
991 if os.path.exists(downloaded_file):
992 os.remove(downloaded_file)
993 return False
995 @staticmethod
996 def GetZipFileBuildDirName():
997 """Gets the base file name of the zip file.
999 After extracting the zip file, this is the name of the directory where
1000 the build files are expected to be. Possibly.
1002 TODO: Make sure that this returns the actual directory name where the
1003 Release or Debug directory is inside of the zip files. This probably
1004 depends on the builder recipe, and may depend on whether the builder is
1005 a perf builder or full builder.
1007 Returns:
1008 The name of the directory inside a build archive which is expected to
1009 contain a Release or Debug directory.
1011 if bisect_utils.IsWindowsHost():
1012 return 'full-build-win32'
1013 if bisect_utils.IsLinuxHost():
1014 return 'full-build-linux'
1015 if bisect_utils.IsMacHost():
1016 return 'full-build-mac'
1017 raise NotImplementedError('Unknown platform "%s".' % sys.platform)
1019 def IsDownloadable(self, depot):
1020 """Checks if build can be downloaded based on target platform and depot."""
1021 if (self.opts.target_platform in ['chromium', 'android', 'android-chrome']
1022 and self.opts.builder_type):
1023 # In case of android-chrome platform, download archives only for
1024 # android-chrome depot; for other depots such as chromium, v8, skia
1025 # etc., build the binary locally.
1026 if self.opts.target_platform == 'android-chrome':
1027 return depot == 'android-chrome'
1028 else:
1029 return (depot == 'chromium' or
1030 'chromium' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'] or
1031 'v8' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'])
1032 return False
1034 def UpdateDepsContents(self, deps_contents, depot, git_revision, deps_key):
1035 """Returns modified version of DEPS file contents.
1037 Args:
1038 deps_contents: DEPS file content.
1039 depot: Current depot being bisected.
1040 git_revision: A git hash to be updated in DEPS.
1041 deps_key: Key in vars section of DEPS file to be searched.
1043 Returns:
1044 Updated DEPS content as string if deps key is found, otherwise None.
1046 # Check whether the depot and revision pattern in DEPS file vars
1047 # e.g. for webkit the format is "webkit_revision": "12345".
1048 deps_revision = re.compile(r'(?<="%s": ")([0-9]+)(?=")' % deps_key,
1049 re.MULTILINE)
1050 new_data = None
1051 if re.search(deps_revision, deps_contents):
1052 commit_position = source_control.GetCommitPosition(
1053 git_revision, self.depot_registry.GetDepotDir(depot))
1054 if not commit_position:
1055 logging.warn('Could not determine commit position for %s', git_revision)
1056 return None
1057 # Update the revision information for the given depot
1058 new_data = re.sub(deps_revision, str(commit_position), deps_contents)
1059 else:
1060 # Check whether the depot and revision pattern in DEPS file vars
1061 # e.g. for webkit the format is "webkit_revision": "559a6d4ab7a84c539..".
1062 deps_revision = re.compile(
1063 r'(?<=["\']%s["\']: ["\'])([a-fA-F0-9]{40})(?=["\'])' % deps_key,
1064 re.MULTILINE)
1065 if re.search(deps_revision, deps_contents):
1066 new_data = re.sub(deps_revision, git_revision, deps_contents)
1067 if new_data:
1068 # For v8_bleeding_edge revisions change V8 branch in order
1069 # to fetch bleeding edge revision.
1070 if depot == 'v8_bleeding_edge':
1071 new_data = _UpdateV8Branch(new_data)
1072 if not new_data:
1073 return None
1074 return new_data
1076 def UpdateDeps(self, revision, depot, deps_file):
1077 """Updates DEPS file with new revision of dependency repository.
1079 This method search DEPS for a particular pattern in which depot revision
1080 is specified (e.g "webkit_revision": "123456"). If a match is found then
1081 it resolves the given git hash to SVN revision and replace it in DEPS file.
1083 Args:
1084 revision: A git hash revision of the dependency repository.
1085 depot: Current depot being bisected.
1086 deps_file: Path to DEPS file.
1088 Returns:
1089 True if DEPS file is modified successfully, otherwise False.
1091 if not os.path.exists(deps_file):
1092 return False
1094 deps_var = bisect_utils.DEPOT_DEPS_NAME[depot]['deps_var']
1095 # Don't update DEPS file if deps_var is not set in DEPOT_DEPS_NAME.
1096 if not deps_var:
1097 logging.warn('DEPS update not supported for Depot: %s', depot)
1098 return False
1100 # Hack for Angle repository. In the DEPS file, "vars" dictionary variable
1101 # contains "angle_revision" key that holds git hash instead of SVN revision.
1102 # And sometime "angle_revision" key is not specified in "vars" variable.
1103 # In such cases check, "deps" dictionary variable that matches
1104 # angle.git@[a-fA-F0-9]{40}$ and replace git hash.
1105 if depot == 'angle':
1106 return _UpdateDEPSForAngle(revision, depot, deps_file)
1108 try:
1109 deps_contents = ReadStringFromFile(deps_file)
1110 updated_deps_content = self.UpdateDepsContents(
1111 deps_contents, depot, revision, deps_var)
1112 # Write changes to DEPS file
1113 if updated_deps_content:
1114 WriteStringToFile(updated_deps_content, deps_file)
1115 return True
1116 except IOError, e:
1117 logging.warn('Something went wrong while updating DEPS file. [%s]', e)
1118 return False
1120 def _CreateDEPSPatch(self, depot, revision):
1121 """Checks out the DEPS file at the specified revision and modifies it.
1123 Args:
1124 depot: Current depot being bisected.
1125 revision: A git hash revision of the dependency repository.
1127 deps_file_path = os.path.join(self.src_cwd, bisect_utils.FILE_DEPS)
1128 if not os.path.exists(deps_file_path):
1129 raise RuntimeError('DEPS file does not exists.[%s]' % deps_file_path)
1130 # Get current chromium revision (git hash).
1131 cmd = ['rev-parse', 'HEAD']
1132 chromium_sha = bisect_utils.CheckRunGit(cmd).strip()
1133 if not chromium_sha:
1134 raise RuntimeError('Failed to determine Chromium revision for %s' %
1135 revision)
1136 if ('chromium' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'] or
1137 'v8' in bisect_utils.DEPOT_DEPS_NAME[depot]['from']):
1138 # Checkout DEPS file for the current chromium revision.
1139 if not source_control.CheckoutFileAtRevision(
1140 bisect_utils.FILE_DEPS, chromium_sha, cwd=self.src_cwd):
1141 raise RuntimeError(
1142 'DEPS checkout Failed for chromium revision : [%s]' % chromium_sha)
1144 if not self.UpdateDeps(revision, depot, deps_file_path):
1145 raise RuntimeError(
1146 'Failed to update DEPS file for chromium: [%s]' % chromium_sha)
1148 def _CreatePatch(self, revision):
1149 """Creates a patch from currently modified files.
1151 Args:
1152 depot: Current depot being bisected.
1153 revision: A git hash revision of the dependency repository.
1155 Returns:
1156 A tuple with git hash of chromium revision and DEPS patch text.
1158 # Get current chromium revision (git hash).
1159 chromium_sha = bisect_utils.CheckRunGit(['rev-parse', 'HEAD']).strip()
1160 if not chromium_sha:
1161 raise RuntimeError('Failed to determine Chromium revision for %s' %
1162 revision)
1163 # Checkout DEPS file for the current chromium revision.
1164 diff_command = [
1165 'diff',
1166 '--src-prefix=',
1167 '--dst-prefix=',
1168 '--no-ext-diff',
1169 'HEAD',
1171 diff_text = bisect_utils.CheckRunGit(diff_command)
1172 return (chromium_sha, ChangeBackslashToSlashInPatch(diff_text))
1174 def ObtainBuild(
1175 self, depot, revision=None, create_patch=False):
1176 """Obtains a build by either downloading or building directly.
1178 Args:
1179 depot: Dependency repository name.
1180 revision: A git commit hash. If None is given, the currently checked-out
1181 revision is built.
1182 create_patch: Create a patch with any locally modified files.
1184 Returns:
1185 True for success.
1187 if self.opts.debug_ignore_build:
1188 return True
1190 build_success = False
1191 cwd = os.getcwd()
1192 os.chdir(self.src_cwd)
1193 # Fetch build archive for the given revision from the cloud storage when
1194 # the storage bucket is passed.
1195 if self.IsDownloadable(depot) and revision:
1196 build_success = self._DownloadAndUnzipBuild(
1197 revision, depot, build_type='Release', create_patch=create_patch)
1198 else:
1199 # Print the current environment set on the machine.
1200 print 'Full Environment:'
1201 for key, value in sorted(os.environ.items()):
1202 print '%s: %s' % (key, value)
1203 # Print the environment before proceeding with compile.
1204 sys.stdout.flush()
1205 build_success = self.builder.Build(depot, self.opts)
1206 os.chdir(cwd)
1207 return build_success
1209 def RunGClientHooks(self):
1210 """Runs gclient with runhooks command.
1212 Returns:
1213 True if gclient reports no errors.
1215 if self.opts.debug_ignore_build:
1216 return True
1217 # Some "runhooks" calls create symlinks that other (older?) versions
1218 # do not handle correctly causing the build to fail. We want to avoid
1219 # clearing the entire out/ directory so that changes close together will
1220 # build faster so we just clear out all symlinks on the expectation that
1221 # the next "runhooks" call will recreate everything properly. Ignore
1222 # failures (like Windows that doesn't have "find").
1223 try:
1224 bisect_utils.RunProcess(
1225 ['find', 'out/', '-type', 'l', '-exec', 'rm', '-f', '{}', ';'],
1226 cwd=self.src_cwd, shell=False)
1227 except OSError:
1228 pass
1229 return not bisect_utils.RunGClient(['runhooks'], cwd=self.src_cwd)
1231 def _IsBisectModeUsingMetric(self):
1232 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_MEAN,
1233 bisect_utils.BISECT_MODE_STD_DEV]
1235 def _IsBisectModeReturnCode(self):
1236 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_RETURN_CODE]
1238 def _IsBisectModeStandardDeviation(self):
1239 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_STD_DEV]
1241 def RunPerformanceTestAndParseResults(
1242 self, command_to_run, metric, reset_on_first_run=False,
1243 upload_on_last_run=False, results_label=None, test_run_multiplier=1,
1244 allow_flakes=True):
1245 """Runs a performance test on the current revision and parses the results.
1247 Args:
1248 command_to_run: The command to be run to execute the performance test.
1249 metric: The metric to parse out from the results of the performance test.
1250 This is the result chart name and trace name, separated by slash.
1251 May be None for perf try jobs.
1252 reset_on_first_run: If True, pass the flag --reset-results on first run.
1253 upload_on_last_run: If True, pass the flag --upload-results on last run.
1254 results_label: A value for the option flag --results-label.
1255 The arguments reset_on_first_run, upload_on_last_run and results_label
1256 are all ignored if the test is not a Telemetry test.
1257 test_run_multiplier: Factor by which to multiply the number of test runs
1258 and the timeout period specified in self.opts.
1259 allow_flakes: Report success even if some tests fail to run.
1261 Returns:
1262 (values dict, 0) if --debug_ignore_perf_test was passed.
1263 (values dict, 0, test output) if the test was run successfully.
1264 (error message, -1) if the test couldn't be run.
1265 (error message, -1, test output) if the test ran but there was an error.
1267 success_code, failure_code = 0, -1
1269 if self.opts.debug_ignore_perf_test:
1270 fake_results = {
1271 'mean': 0.0,
1272 'std_err': 0.0,
1273 'std_dev': 0.0,
1274 'values': [0.0]
1277 # When debug_fake_test_mean is set, its value is returned as the mean
1278 # and the flag is cleared so that further calls behave as if it wasn't
1279 # set (returning the fake_results dict as defined above).
1280 if self.opts.debug_fake_first_test_mean:
1281 fake_results['mean'] = float(self.opts.debug_fake_first_test_mean)
1282 self.opts.debug_fake_first_test_mean = 0
1284 return (fake_results, success_code)
1286 # For Windows platform set posix=False, to parse windows paths correctly.
1287 # On Windows, path separators '\' or '\\' are replace by '' when posix=True,
1288 # refer to http://bugs.python.org/issue1724822. By default posix=True.
1289 args = shlex.split(command_to_run, posix=not bisect_utils.IsWindowsHost())
1291 if not _GenerateProfileIfNecessary(args):
1292 err_text = 'Failed to generate profile for performance test.'
1293 return (err_text, failure_code)
1295 is_telemetry = bisect_utils.IsTelemetryCommand(command_to_run)
1297 start_time = time.time()
1299 metric_values = []
1300 output_of_all_runs = ''
1301 repeat_count = self.opts.repeat_test_count * test_run_multiplier
1302 return_codes = []
1303 for i in xrange(repeat_count):
1304 # Can ignore the return code since if the tests fail, it won't return 0.
1305 current_args = copy.copy(args)
1306 if is_telemetry:
1307 if i == 0 and reset_on_first_run:
1308 current_args.append('--reset-results')
1309 if i == self.opts.repeat_test_count - 1 and upload_on_last_run:
1310 current_args.append('--upload-results')
1311 if results_label:
1312 current_args.append('--results-label=%s' % results_label)
1313 try:
1314 output, return_code = bisect_utils.RunProcessAndRetrieveOutput(
1315 current_args, cwd=self.src_cwd)
1316 return_codes.append(return_code)
1317 except OSError, e:
1318 if e.errno == errno.ENOENT:
1319 err_text = ('Something went wrong running the performance test. '
1320 'Please review the command line:\n\n')
1321 if 'src/' in ' '.join(args):
1322 err_text += ('Check that you haven\'t accidentally specified a '
1323 'path with src/ in the command.\n\n')
1324 err_text += ' '.join(args)
1325 err_text += '\n'
1327 return (err_text, failure_code)
1328 raise
1330 output_of_all_runs += output
1331 if self.opts.output_buildbot_annotations:
1332 print output
1334 if metric and self._IsBisectModeUsingMetric():
1335 parsed_metric = _ParseMetricValuesFromOutput(metric, output)
1336 if parsed_metric:
1337 metric_values.append(math_utils.Mean(parsed_metric))
1338 # If we're bisecting on a metric (ie, changes in the mean or
1339 # standard deviation) and no metric values are produced, bail out.
1340 if not metric_values:
1341 break
1342 elif self._IsBisectModeReturnCode():
1343 metric_values.append(return_code)
1344 # If there's a failed test, we can bail out early.
1345 if return_code:
1346 break
1348 elapsed_minutes = (time.time() - start_time) / 60.0
1349 time_limit = self.opts.max_time_minutes * test_run_multiplier
1350 if elapsed_minutes >= time_limit:
1351 break
1353 if metric and len(metric_values) == 0:
1354 err_text = 'Metric %s was not found in the test output.' % metric
1355 # TODO(qyearsley): Consider also getting and displaying a list of metrics
1356 # that were found in the output here.
1357 return (err_text, failure_code, output_of_all_runs)
1359 # If we're bisecting on return codes, we're really just looking for zero vs
1360 # non-zero.
1361 values = {}
1362 if self._IsBisectModeReturnCode():
1363 # If any of the return codes is non-zero, output 1.
1364 overall_return_code = 0 if (
1365 all(current_value == 0 for current_value in metric_values)) else 1
1367 values = {
1368 'mean': overall_return_code,
1369 'std_err': 0.0,
1370 'std_dev': 0.0,
1371 'values': metric_values,
1374 print 'Results of performance test: Command returned with %d' % (
1375 overall_return_code)
1376 print
1377 elif metric:
1378 # Need to get the average value if there were multiple values.
1379 truncated_mean = math_utils.TruncatedMean(
1380 metric_values, self.opts.truncate_percent)
1381 standard_err = math_utils.StandardError(metric_values)
1382 standard_dev = math_utils.StandardDeviation(metric_values)
1384 if self._IsBisectModeStandardDeviation():
1385 metric_values = [standard_dev]
1387 values = {
1388 'mean': truncated_mean,
1389 'std_err': standard_err,
1390 'std_dev': standard_dev,
1391 'values': metric_values,
1394 print 'Results of performance test: %12f %12f' % (
1395 truncated_mean, standard_err)
1396 print
1398 overall_success = success_code
1399 if not allow_flakes and not self._IsBisectModeReturnCode():
1400 overall_success = (
1401 success_code
1402 if (all(current_value == 0 for current_value in return_codes))
1403 else failure_code)
1405 return (values, overall_success, output_of_all_runs)
1407 def PerformPreBuildCleanup(self):
1408 """Performs cleanup between runs."""
1409 print 'Cleaning up between runs.'
1410 print
1412 # Leaving these .pyc files around between runs may disrupt some perf tests.
1413 for (path, _, files) in os.walk(self.src_cwd):
1414 for cur_file in files:
1415 if cur_file.endswith('.pyc'):
1416 path_to_file = os.path.join(path, cur_file)
1417 os.remove(path_to_file)
1419 def _RunPostSync(self, _depot):
1420 """Performs any work after syncing.
1422 Args:
1423 depot: Depot name.
1425 Returns:
1426 True if successful.
1428 if 'android' in self.opts.target_platform:
1429 if not builder.SetupAndroidBuildEnvironment(
1430 self.opts, path_to_src=self.src_cwd):
1431 return False
1433 return self.RunGClientHooks()
1435 @staticmethod
1436 def ShouldSkipRevision(depot, revision):
1437 """Checks whether a particular revision can be safely skipped.
1439 Some commits can be safely skipped (such as a DEPS roll for the repos
1440 still using .DEPS.git), since the tool is git based those changes
1441 would have no effect.
1443 Args:
1444 depot: The depot being bisected.
1445 revision: Current revision we're synced to.
1447 Returns:
1448 True if we should skip building/testing this revision.
1450 # Skips revisions with DEPS on android-chrome.
1451 if depot == 'android-chrome':
1452 cmd = ['diff-tree', '--no-commit-id', '--name-only', '-r', revision]
1453 output = bisect_utils.CheckRunGit(cmd)
1455 files = output.splitlines()
1457 if len(files) == 1 and files[0] == 'DEPS':
1458 return True
1460 return False
1462 def RunTest(self, revision, depot, command, metric, skippable=False,
1463 skip_sync=False, create_patch=False, force_build=False,
1464 test_run_multiplier=1):
1465 """Performs a full sync/build/run of the specified revision.
1467 Args:
1468 revision: The revision to sync to.
1469 depot: The depot that's being used at the moment (src, webkit, etc.)
1470 command: The command to execute the performance test.
1471 metric: The performance metric being tested.
1472 skip_sync: Skip the sync step.
1473 create_patch: Create a patch with any locally modified files.
1474 force_build: Force a local build.
1475 test_run_multiplier: Factor by which to multiply the given number of runs
1476 and the set timeout period.
1478 Returns:
1479 On success, a tuple containing the results of the performance test.
1480 Otherwise, a tuple with the error message.
1482 logging.info('Running RunTest with rev "%s", command "%s"',
1483 revision, command)
1484 # Decide which sync program to use.
1485 sync_client = None
1486 if depot == 'chromium' or depot == 'android-chrome':
1487 sync_client = 'gclient'
1489 # Do the syncing for all depots.
1490 if not (self.opts.debug_ignore_sync or skip_sync):
1491 if not self._SyncRevision(depot, revision, sync_client):
1492 return ('Failed to sync: [%s]' % str(revision), BUILD_RESULT_FAIL)
1494 # Try to do any post-sync steps. This may include "gclient runhooks".
1495 if not self._RunPostSync(depot):
1496 return ('Failed to run [gclient runhooks].', BUILD_RESULT_FAIL)
1498 # Skip this revision if it can be skipped.
1499 if skippable and self.ShouldSkipRevision(depot, revision):
1500 return ('Skipped revision: [%s]' % str(revision),
1501 BUILD_RESULT_SKIPPED)
1503 # Obtain a build for this revision. This may be done by requesting a build
1504 # from another builder, waiting for it and downloading it.
1505 start_build_time = time.time()
1506 revision_to_build = revision if not force_build else None
1507 build_success = self.ObtainBuild(
1508 depot, revision=revision_to_build, create_patch=create_patch)
1509 if not build_success:
1510 return ('Failed to build revision: [%s]' % str(revision),
1511 BUILD_RESULT_FAIL)
1512 after_build_time = time.time()
1514 # Run the command and get the results.
1515 results = self.RunPerformanceTestAndParseResults(
1516 command, metric, test_run_multiplier=test_run_multiplier)
1518 # Restore build output directory once the tests are done, to avoid
1519 # any discrepancies.
1520 if self.IsDownloadable(depot) and revision:
1521 self.BackupOrRestoreOutputDirectory(restore=True)
1523 # A value other than 0 indicates that the test couldn't be run, and results
1524 # should also include an error message.
1525 if results[1] != 0:
1526 return results
1528 external_revisions = self._Get3rdPartyRevisions(depot)
1530 if not external_revisions is None:
1531 return (results[0], results[1], external_revisions,
1532 time.time() - after_build_time, after_build_time -
1533 start_build_time)
1534 else:
1535 return ('Failed to parse DEPS file for external revisions.',
1536 BUILD_RESULT_FAIL)
1538 def _SyncRevision(self, depot, revision, sync_client):
1539 """Syncs depot to particular revision.
1541 Args:
1542 depot: The depot that's being used at the moment (src, webkit, etc.)
1543 revision: The revision to sync to.
1544 sync_client: Program used to sync, e.g. "gclient". Can be None.
1546 Returns:
1547 True if successful, False otherwise.
1549 self.depot_registry.ChangeToDepotDir(depot)
1551 if sync_client:
1552 self.PerformPreBuildCleanup()
1554 # When using gclient to sync, you need to specify the depot you
1555 # want so that all the dependencies sync properly as well.
1556 # i.e. gclient sync src@<SHA1>
1557 if sync_client == 'gclient' and revision:
1558 revision = '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME[depot]['src'],
1559 revision)
1560 if depot == 'chromium' and self.opts.target_platform == 'android-chrome':
1561 return self._SyncRevisionsForAndroidChrome(revision)
1563 return source_control.SyncToRevision(revision, sync_client)
1565 def _SyncRevisionsForAndroidChrome(self, revision):
1566 """Syncs android-chrome and chromium repos to particular revision.
1568 This is a special case for android-chrome as the gclient sync for chromium
1569 overwrites the android-chrome revision to TOT. Therefore both the repos
1570 are synced to known revisions.
1572 Args:
1573 revision: Git hash of the Chromium to sync.
1575 Returns:
1576 True if successful, False otherwise.
1578 revisions_list = [revision]
1579 current_android_rev = source_control.GetCurrentRevision(
1580 self.depot_registry.GetDepotDir('android-chrome'))
1581 revisions_list.append(
1582 '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME['android-chrome']['src'],
1583 current_android_rev))
1584 return not bisect_utils.RunGClientAndSync(revisions_list)
1586 def _CheckIfRunPassed(self, current_value, known_good_value, known_bad_value):
1587 """Given known good and bad values, decide if the current_value passed
1588 or failed.
1590 Args:
1591 current_value: The value of the metric being checked.
1592 known_bad_value: The reference value for a "failed" run.
1593 known_good_value: The reference value for a "passed" run.
1595 Returns:
1596 True if the current_value is closer to the known_good_value than the
1597 known_bad_value.
1599 if self.opts.bisect_mode == bisect_utils.BISECT_MODE_STD_DEV:
1600 dist_to_good_value = abs(current_value['std_dev'] -
1601 known_good_value['std_dev'])
1602 dist_to_bad_value = abs(current_value['std_dev'] -
1603 known_bad_value['std_dev'])
1604 else:
1605 dist_to_good_value = abs(current_value['mean'] - known_good_value['mean'])
1606 dist_to_bad_value = abs(current_value['mean'] - known_bad_value['mean'])
1608 return dist_to_good_value < dist_to_bad_value
1610 def _GetV8BleedingEdgeFromV8TrunkIfMappable(
1611 self, revision, bleeding_edge_branch):
1612 """Gets v8 bleeding edge revision mapped to v8 revision in trunk.
1614 Args:
1615 revision: A trunk V8 revision mapped to bleeding edge revision.
1616 bleeding_edge_branch: Branch used to perform lookup of bleeding edge
1617 revision.
1618 Return:
1619 A mapped bleeding edge revision if found, otherwise None.
1621 commit_position = source_control.GetCommitPosition(revision)
1623 if bisect_utils.IsStringInt(commit_position):
1624 # V8 is tricky to bisect, in that there are only a few instances when
1625 # we can dive into bleeding_edge and get back a meaningful result.
1626 # Try to detect a V8 "business as usual" case, which is when:
1627 # 1. trunk revision N has description "Version X.Y.Z"
1628 # 2. bleeding_edge revision (N-1) has description "Prepare push to
1629 # trunk. Now working on X.Y.(Z+1)."
1631 # As of 01/24/2014, V8 trunk descriptions are formatted:
1632 # "Version 3.X.Y (based on bleeding_edge revision rZ)"
1633 # So we can just try parsing that out first and fall back to the old way.
1634 v8_dir = self.depot_registry.GetDepotDir('v8')
1635 v8_bleeding_edge_dir = self.depot_registry.GetDepotDir('v8_bleeding_edge')
1637 revision_info = source_control.QueryRevisionInfo(revision, cwd=v8_dir)
1638 version_re = re.compile("Version (?P<values>[0-9,.]+)")
1639 regex_results = version_re.search(revision_info['subject'])
1640 if regex_results:
1641 git_revision = None
1642 if 'based on bleeding_edge' in revision_info['subject']:
1643 try:
1644 bleeding_edge_revision = revision_info['subject'].split(
1645 'bleeding_edge revision r')[1]
1646 bleeding_edge_revision = int(bleeding_edge_revision.split(')')[0])
1647 bleeding_edge_url = ('https://v8.googlecode.com/svn/branches/'
1648 'bleeding_edge@%s' % bleeding_edge_revision)
1649 cmd = ['log',
1650 '--format=%H',
1651 '--grep',
1652 bleeding_edge_url,
1653 '-1',
1654 bleeding_edge_branch]
1655 output = bisect_utils.CheckRunGit(cmd, cwd=v8_dir)
1656 if output:
1657 git_revision = output.strip()
1658 return git_revision
1659 except (IndexError, ValueError):
1660 pass
1661 else:
1662 # V8 rolls description changed after V8 git migration, new description
1663 # includes "Version 3.X.Y (based on <git hash>)"
1664 try:
1665 rxp = re.compile('based on (?P<git_revision>[a-fA-F0-9]+)')
1666 re_results = rxp.search(revision_info['subject'])
1667 if re_results:
1668 return re_results.group('git_revision')
1669 except (IndexError, ValueError):
1670 pass
1671 if not git_revision:
1672 # Wasn't successful, try the old way of looking for "Prepare push to"
1673 git_revision = source_control.ResolveToRevision(
1674 int(commit_position) - 1, 'v8_bleeding_edge',
1675 bisect_utils.DEPOT_DEPS_NAME, -1, cwd=v8_bleeding_edge_dir)
1677 if git_revision:
1678 revision_info = source_control.QueryRevisionInfo(
1679 git_revision, cwd=v8_bleeding_edge_dir)
1681 if 'Prepare push to trunk' in revision_info['subject']:
1682 return git_revision
1683 return None
1685 def _GetNearestV8BleedingEdgeFromTrunk(
1686 self, revision, v8_branch, bleeding_edge_branch, search_forward=True):
1687 """Gets the nearest V8 roll and maps to bleeding edge revision.
1689 V8 is a bit tricky to bisect since it isn't just rolled out like blink.
1690 Each revision on trunk might just be whatever was in bleeding edge, rolled
1691 directly out. Or it could be some mixture of previous v8 trunk versions,
1692 with bits and pieces cherry picked out from bleeding edge. In order to
1693 bisect, we need both the before/after versions on trunk v8 to be just pushes
1694 from bleeding edge. With the V8 git migration, the branches got switched.
1695 a) master (external/v8) == candidates (v8/v8)
1696 b) bleeding_edge (external/v8) == master (v8/v8)
1698 Args:
1699 revision: A V8 revision to get its nearest bleeding edge revision
1700 search_forward: Searches forward if True, otherwise search backward.
1702 Return:
1703 A mapped bleeding edge revision if found, otherwise None.
1705 cwd = self.depot_registry.GetDepotDir('v8')
1706 cmd = ['log', '--format=%ct', '-1', revision]
1707 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1708 commit_time = int(output)
1709 commits = []
1710 if search_forward:
1711 cmd = ['log',
1712 '--format=%H',
1713 '--after=%d' % commit_time,
1714 v8_branch,
1715 '--reverse']
1716 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1717 output = output.split()
1718 commits = output
1719 #Get 10 git hashes immediately after the given commit.
1720 commits = commits[:10]
1721 else:
1722 cmd = ['log',
1723 '--format=%H',
1724 '-10',
1725 '--before=%d' % commit_time,
1726 v8_branch]
1727 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1728 output = output.split()
1729 commits = output
1731 bleeding_edge_revision = None
1733 for c in commits:
1734 bleeding_edge_revision = self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1735 c, bleeding_edge_branch)
1736 if bleeding_edge_revision:
1737 break
1739 return bleeding_edge_revision
1741 def _FillInV8BleedingEdgeInfo(self, min_revision_state, max_revision_state):
1742 cwd = self.depot_registry.GetDepotDir('v8')
1743 # when "remote.origin.url" is https://chromium.googlesource.com/v8/v8.git
1744 v8_branch = 'origin/candidates'
1745 bleeding_edge_branch = 'origin/master'
1747 # Support for the chromium revisions with external V8 repo.
1748 # ie https://chromium.googlesource.com/external/v8.git
1749 cmd = ['config', '--get', 'remote.origin.url']
1750 v8_repo_url = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1752 if 'external/v8.git' in v8_repo_url:
1753 v8_branch = 'origin/master'
1754 bleeding_edge_branch = 'origin/bleeding_edge'
1756 r1 = self._GetNearestV8BleedingEdgeFromTrunk(
1757 min_revision_state.revision,
1758 v8_branch,
1759 bleeding_edge_branch,
1760 search_forward=True)
1761 r2 = self._GetNearestV8BleedingEdgeFromTrunk(
1762 max_revision_state.revision,
1763 v8_branch,
1764 bleeding_edge_branch,
1765 search_forward=False)
1766 min_revision_state.external['v8_bleeding_edge'] = r1
1767 max_revision_state.external['v8_bleeding_edge'] = r2
1769 if (not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1770 min_revision_state.revision, bleeding_edge_branch)
1771 or not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1772 max_revision_state.revision, bleeding_edge_branch)):
1773 self.warnings.append(
1774 'Trunk revisions in V8 did not map directly to bleeding_edge. '
1775 'Attempted to expand the range to find V8 rolls which did map '
1776 'directly to bleeding_edge revisions, but results might not be '
1777 'valid.')
1779 def _FindNextDepotToBisect(
1780 self, current_depot, min_revision_state, max_revision_state):
1781 """Decides which depot the script should dive into next (if any).
1783 Args:
1784 current_depot: Current depot being bisected.
1785 min_revision_state: State of the earliest revision in the bisect range.
1786 max_revision_state: State of the latest revision in the bisect range.
1788 Returns:
1789 Name of the depot to bisect next, or None.
1791 external_depot = None
1792 for next_depot in bisect_utils.DEPOT_NAMES:
1793 if ('platform' in bisect_utils.DEPOT_DEPS_NAME[next_depot] and
1794 bisect_utils.DEPOT_DEPS_NAME[next_depot]['platform'] != os.name):
1795 continue
1797 if not (bisect_utils.DEPOT_DEPS_NAME[next_depot]['recurse']
1798 and min_revision_state.depot
1799 in bisect_utils.DEPOT_DEPS_NAME[next_depot]['from']):
1800 continue
1802 if current_depot == 'v8':
1803 # We grab the bleeding_edge info here rather than earlier because we
1804 # finally have the revision range. From that we can search forwards and
1805 # backwards to try to match trunk revisions to bleeding_edge.
1806 self._FillInV8BleedingEdgeInfo(min_revision_state, max_revision_state)
1808 if (min_revision_state.external.get(next_depot) ==
1809 max_revision_state.external.get(next_depot)):
1810 continue
1812 if (min_revision_state.external.get(next_depot) and
1813 max_revision_state.external.get(next_depot)):
1814 external_depot = next_depot
1815 break
1817 return external_depot
1819 def PrepareToBisectOnDepot(
1820 self, current_depot, start_revision, end_revision, previous_revision):
1821 """Changes to the appropriate directory and gathers a list of revisions
1822 to bisect between |start_revision| and |end_revision|.
1824 Args:
1825 current_depot: The depot we want to bisect.
1826 start_revision: Start of the revision range.
1827 end_revision: End of the revision range.
1828 previous_revision: The last revision we synced to on |previous_depot|.
1830 Returns:
1831 A list containing the revisions between |start_revision| and
1832 |end_revision| inclusive.
1834 # Change into working directory of external library to run
1835 # subsequent commands.
1836 self.depot_registry.ChangeToDepotDir(current_depot)
1838 # V8 (and possibly others) is merged in periodically. Bisecting
1839 # this directory directly won't give much good info.
1840 if 'custom_deps' in bisect_utils.DEPOT_DEPS_NAME[current_depot]:
1841 config_path = os.path.join(self.src_cwd, '..')
1842 if bisect_utils.RunGClientAndCreateConfig(
1843 self.opts, bisect_utils.DEPOT_DEPS_NAME[current_depot]['custom_deps'],
1844 cwd=config_path):
1845 return []
1846 if bisect_utils.RunGClient(
1847 ['sync', '--revision', previous_revision], cwd=self.src_cwd):
1848 return []
1850 if current_depot == 'v8_bleeding_edge':
1851 self.depot_registry.ChangeToDepotDir('chromium')
1853 shutil.move('v8', 'v8.bak')
1854 shutil.move('v8_bleeding_edge', 'v8')
1856 self.cleanup_commands.append(['mv', 'v8', 'v8_bleeding_edge'])
1857 self.cleanup_commands.append(['mv', 'v8.bak', 'v8'])
1859 self.depot_registry.SetDepotDir(
1860 'v8_bleeding_edge', os.path.join(self.src_cwd, 'v8'))
1861 self.depot_registry.SetDepotDir(
1862 'v8', os.path.join(self.src_cwd, 'v8.bak'))
1864 self.depot_registry.ChangeToDepotDir(current_depot)
1866 depot_revision_list = self.GetRevisionList(current_depot,
1867 end_revision,
1868 start_revision)
1870 self.depot_registry.ChangeToDepotDir('chromium')
1872 return depot_revision_list
1874 def GatherReferenceValues(self, good_rev, bad_rev, cmd, metric, target_depot):
1875 """Gathers reference values by running the performance tests on the
1876 known good and bad revisions.
1878 Args:
1879 good_rev: The last known good revision where the performance regression
1880 has not occurred yet.
1881 bad_rev: A revision where the performance regression has already occurred.
1882 cmd: The command to execute the performance test.
1883 metric: The metric being tested for regression.
1885 Returns:
1886 A tuple with the results of building and running each revision.
1888 bad_run_results = self.RunTest(bad_rev, target_depot, cmd, metric)
1890 good_run_results = None
1892 if not bad_run_results[1]:
1893 good_run_results = self.RunTest(good_rev, target_depot, cmd, metric)
1895 return (bad_run_results, good_run_results)
1897 def PrintRevisionsToBisectMessage(self, revision_list, depot):
1898 if self.opts.output_buildbot_annotations:
1899 step_name = 'Bisection Range: [%s:%s - %s]' % (depot, revision_list[-1],
1900 revision_list[0])
1901 bisect_utils.OutputAnnotationStepStart(step_name)
1903 print
1904 print 'Revisions to bisect on [%s]:' % depot
1905 for revision_id in revision_list:
1906 print ' -> %s' % (revision_id, )
1907 print
1909 if self.opts.output_buildbot_annotations:
1910 bisect_utils.OutputAnnotationStepClosed()
1912 def NudgeRevisionsIfDEPSChange(self, bad_revision, good_revision,
1913 good_svn_revision=None):
1914 """Checks to see if changes to DEPS file occurred, and that the revision
1915 range also includes the change to .DEPS.git. If it doesn't, attempts to
1916 expand the revision range to include it.
1918 Args:
1919 bad_revision: First known bad git revision.
1920 good_revision: Last known good git revision.
1921 good_svn_revision: Last known good svn revision.
1923 Returns:
1924 A tuple with the new bad and good revisions.
1926 # DONOT perform nudge because at revision 291563 .DEPS.git was removed
1927 # and source contain only DEPS file for dependency changes.
1928 if good_svn_revision >= 291563:
1929 return (bad_revision, good_revision)
1931 if self.opts.target_platform == 'chromium':
1932 changes_to_deps = source_control.QueryFileRevisionHistory(
1933 bisect_utils.FILE_DEPS, good_revision, bad_revision)
1935 if changes_to_deps:
1936 # DEPS file was changed, search from the oldest change to DEPS file to
1937 # bad_revision to see if there are matching .DEPS.git changes.
1938 oldest_deps_change = changes_to_deps[-1]
1939 changes_to_gitdeps = source_control.QueryFileRevisionHistory(
1940 bisect_utils.FILE_DEPS_GIT, oldest_deps_change, bad_revision)
1942 if len(changes_to_deps) != len(changes_to_gitdeps):
1943 # Grab the timestamp of the last DEPS change
1944 cmd = ['log', '--format=%ct', '-1', changes_to_deps[0]]
1945 output = bisect_utils.CheckRunGit(cmd)
1946 commit_time = int(output)
1948 # Try looking for a commit that touches the .DEPS.git file in the
1949 # next 15 minutes after the DEPS file change.
1950 cmd = [
1951 'log', '--format=%H', '-1',
1952 '--before=%d' % (commit_time + 900),
1953 '--after=%d' % commit_time,
1954 'origin/master', '--', bisect_utils.FILE_DEPS_GIT
1956 output = bisect_utils.CheckRunGit(cmd)
1957 output = output.strip()
1958 if output:
1959 self.warnings.append(
1960 'Detected change to DEPS and modified '
1961 'revision range to include change to .DEPS.git')
1962 return (output, good_revision)
1963 else:
1964 self.warnings.append(
1965 'Detected change to DEPS but couldn\'t find '
1966 'matching change to .DEPS.git')
1967 return (bad_revision, good_revision)
1969 def CheckIfRevisionsInProperOrder(
1970 self, target_depot, good_revision, bad_revision):
1971 """Checks that |good_revision| is an earlier revision than |bad_revision|.
1973 Args:
1974 good_revision: Number/tag of the known good revision.
1975 bad_revision: Number/tag of the known bad revision.
1977 Returns:
1978 True if the revisions are in the proper order (good earlier than bad).
1980 cwd = self.depot_registry.GetDepotDir(target_depot)
1981 good_position = source_control.GetCommitPosition(good_revision, cwd)
1982 bad_position = source_control.GetCommitPosition(bad_revision, cwd)
1983 # Compare commit timestamp for repos that don't support commit position.
1984 if not (bad_position and good_position):
1985 logging.info('Could not get commit positions for revisions %s and %s in '
1986 'depot %s', good_position, bad_position, target_depot)
1987 good_position = source_control.GetCommitTime(good_revision, cwd=cwd)
1988 bad_position = source_control.GetCommitTime(bad_revision, cwd=cwd)
1990 return good_position <= bad_position
1992 def CanPerformBisect(self, good_revision, bad_revision):
1993 """Checks whether a given revision is bisectable.
1995 Checks for following:
1996 1. Non-bisectable revisions for android bots (refer to crbug.com/385324).
1997 2. Non-bisectable revisions for Windows bots (refer to crbug.com/405274).
1999 Args:
2000 good_revision: Known good revision.
2001 bad_revision: Known bad revision.
2003 Returns:
2004 A dictionary indicating the result. If revision is not bisectable,
2005 this will contain the field "error", otherwise None.
2007 if self.opts.target_platform == 'android':
2008 good_revision = source_control.GetCommitPosition(good_revision)
2009 if (bisect_utils.IsStringInt(good_revision)
2010 and good_revision < 265549):
2011 return {'error': (
2012 'Bisect cannot continue for the given revision range.\n'
2013 'It is impossible to bisect Android regressions '
2014 'prior to r265549, which allows the bisect bot to '
2015 'rely on Telemetry to do apk installation of the most recently '
2016 'built local ChromeShell(refer to crbug.com/385324).\n'
2017 'Please try bisecting revisions greater than or equal to r265549.')}
2019 if bisect_utils.IsWindowsHost():
2020 good_revision = source_control.GetCommitPosition(good_revision)
2021 bad_revision = source_control.GetCommitPosition(bad_revision)
2022 if (bisect_utils.IsStringInt(good_revision) and
2023 bisect_utils.IsStringInt(bad_revision)):
2024 if (289987 <= good_revision < 290716 or
2025 289987 <= bad_revision < 290716):
2026 return {'error': ('Oops! Revision between r289987 and r290716 are '
2027 'marked as dead zone for Windows due to '
2028 'crbug.com/405274. Please try another range.')}
2030 return None
2032 def _GatherResultsFromRevertedCulpritCL(
2033 self, results, target_depot, command_to_run, metric):
2034 """Gathers performance results with/without culprit CL.
2036 Attempts to revert the culprit CL against ToT and runs the
2037 performance tests again with and without the CL, adding the results to
2038 the over bisect results.
2040 Args:
2041 results: BisectResults from the bisect.
2042 target_depot: The target depot we're bisecting.
2043 command_to_run: Specify the command to execute the performance test.
2044 metric: The performance metric to monitor.
2046 run_results_tot, run_results_reverted = self._RevertCulpritCLAndRetest(
2047 results, target_depot, command_to_run, metric)
2049 results.AddRetestResults(run_results_tot, run_results_reverted)
2051 if len(results.culprit_revisions) != 1:
2052 return
2054 # Cleanup reverted files if anything is left.
2055 _, _, culprit_depot = results.culprit_revisions[0]
2056 bisect_utils.CheckRunGit(
2057 ['reset', '--hard', 'HEAD'],
2058 cwd=self.depot_registry.GetDepotDir(culprit_depot))
2060 def _RevertCL(self, culprit_revision, culprit_depot):
2061 """Reverts the specified revision in the specified depot."""
2062 if self.opts.output_buildbot_annotations:
2063 bisect_utils.OutputAnnotationStepStart(
2064 'Reverting culprit CL: %s' % culprit_revision)
2065 _, return_code = bisect_utils.RunGit(
2066 ['revert', '--no-commit', culprit_revision],
2067 cwd=self.depot_registry.GetDepotDir(culprit_depot))
2068 if return_code:
2069 bisect_utils.OutputAnnotationStepWarning()
2070 bisect_utils.OutputAnnotationStepText('Failed to revert CL cleanly.')
2071 if self.opts.output_buildbot_annotations:
2072 bisect_utils.OutputAnnotationStepClosed()
2073 return not return_code
2075 def _RevertCulpritCLAndRetest(
2076 self, results, target_depot, command_to_run, metric):
2077 """Reverts the culprit CL against ToT and runs the performance test.
2079 Attempts to revert the culprit CL against ToT and runs the
2080 performance tests again with and without the CL.
2082 Args:
2083 results: BisectResults from the bisect.
2084 target_depot: The target depot we're bisecting.
2085 command_to_run: Specify the command to execute the performance test.
2086 metric: The performance metric to monitor.
2088 Returns:
2089 A tuple with the results of running the CL at ToT/reverted.
2091 # Might want to retest ToT with a revert of the CL to confirm that
2092 # performance returns.
2093 if results.confidence < bisect_utils.HIGH_CONFIDENCE:
2094 return (None, None)
2096 # If there were multiple culprit CLs, we won't try to revert.
2097 if len(results.culprit_revisions) != 1:
2098 return (None, None)
2100 culprit_revision, _, culprit_depot = results.culprit_revisions[0]
2102 if not self._SyncRevision(target_depot, None, 'gclient'):
2103 return (None, None)
2105 head_revision = bisect_utils.CheckRunGit(['log', '--format=%H', '-1'])
2106 head_revision = head_revision.strip()
2108 if not self._RevertCL(culprit_revision, culprit_depot):
2109 return (None, None)
2111 # If the culprit CL happened to be in a depot that gets pulled in, we
2112 # can't revert the change and issue a try job to build, since that would
2113 # require modifying both the DEPS file and files in another depot.
2114 # Instead, we build locally.
2115 force_build = (culprit_depot != target_depot)
2116 if force_build:
2117 results.warnings.append(
2118 'Culprit CL is in another depot, attempting to revert and build'
2119 ' locally to retest. This may not match the performance of official'
2120 ' builds.')
2122 run_results_reverted = self._RunTestWithAnnotations(
2123 'Re-Testing ToT with reverted culprit',
2124 'Failed to run reverted CL.',
2125 head_revision, target_depot, command_to_run, metric, force_build)
2127 # Clear the reverted file(s).
2128 bisect_utils.RunGit(
2129 ['reset', '--hard', 'HEAD'],
2130 cwd=self.depot_registry.GetDepotDir(culprit_depot))
2132 # Retesting with the reverted CL failed, so bail out of retesting against
2133 # ToT.
2134 if run_results_reverted[1]:
2135 return (None, None)
2137 run_results_tot = self._RunTestWithAnnotations(
2138 'Re-Testing ToT',
2139 'Failed to run ToT.',
2140 head_revision, target_depot, command_to_run, metric, force_build)
2142 return (run_results_tot, run_results_reverted)
2144 def _RunTestWithAnnotations(
2145 self, step_text, error_text, head_revision,
2146 target_depot, command_to_run, metric, force_build):
2147 """Runs the performance test and outputs start/stop annotations.
2149 Args:
2150 results: BisectResults from the bisect.
2151 target_depot: The target depot we're bisecting.
2152 command_to_run: Specify the command to execute the performance test.
2153 metric: The performance metric to monitor.
2154 force_build: Whether to force a build locally.
2156 Returns:
2157 Results of the test.
2159 if self.opts.output_buildbot_annotations:
2160 bisect_utils.OutputAnnotationStepStart(step_text)
2162 # Build and run the test again with the reverted culprit CL against ToT.
2163 run_test_results = self.RunTest(
2164 head_revision, target_depot, command_to_run,
2165 metric, skippable=False, skip_sync=True, create_patch=True,
2166 force_build=force_build)
2168 if self.opts.output_buildbot_annotations:
2169 if run_test_results[1]:
2170 bisect_utils.OutputAnnotationStepWarning()
2171 bisect_utils.OutputAnnotationStepText(error_text)
2172 bisect_utils.OutputAnnotationStepClosed()
2174 return run_test_results
2176 def Run(self, command_to_run, bad_revision_in, good_revision_in, metric):
2177 """Given known good and bad revisions, run a binary search on all
2178 intermediate revisions to determine the CL where the performance regression
2179 occurred.
2181 Args:
2182 command_to_run: Specify the command to execute the performance test.
2183 good_revision: Number/tag of the known good revision.
2184 bad_revision: Number/tag of the known bad revision.
2185 metric: The performance metric to monitor.
2187 Returns:
2188 A BisectResults object.
2190 # Choose depot to bisect first
2191 target_depot = 'chromium'
2192 if self.opts.target_platform == 'android-chrome':
2193 target_depot = 'android-chrome'
2195 cwd = os.getcwd()
2196 self.depot_registry.ChangeToDepotDir(target_depot)
2198 # If they passed SVN revisions, we can try match them to git SHA1 hashes.
2199 bad_revision = source_control.ResolveToRevision(
2200 bad_revision_in, target_depot, bisect_utils.DEPOT_DEPS_NAME, 100)
2201 good_revision = source_control.ResolveToRevision(
2202 good_revision_in, target_depot, bisect_utils.DEPOT_DEPS_NAME, -100)
2204 os.chdir(cwd)
2205 if bad_revision is None:
2206 return BisectResults(
2207 error='Couldn\'t resolve [%s] to SHA1.' % bad_revision_in)
2209 if good_revision is None:
2210 return BisectResults(
2211 error='Couldn\'t resolve [%s] to SHA1.' % good_revision_in)
2213 # Check that they didn't accidentally swap good and bad revisions.
2214 if not self.CheckIfRevisionsInProperOrder(
2215 target_depot, good_revision, bad_revision):
2216 return BisectResults(error='Bad rev (%s) appears to be earlier than good '
2217 'rev (%s).' % (good_revision, bad_revision))
2219 bad_revision, good_revision = self.NudgeRevisionsIfDEPSChange(
2220 bad_revision, good_revision, good_revision_in)
2221 if self.opts.output_buildbot_annotations:
2222 bisect_utils.OutputAnnotationStepStart('Gathering Revisions')
2224 cannot_bisect = self.CanPerformBisect(good_revision, bad_revision)
2225 if cannot_bisect:
2226 return BisectResults(error=cannot_bisect.get('error'))
2228 print 'Gathering revision range for bisection.'
2229 # Retrieve a list of revisions to do bisection on.
2230 revision_list = self.GetRevisionList(target_depot, bad_revision,
2231 good_revision)
2233 if self.opts.output_buildbot_annotations:
2234 bisect_utils.OutputAnnotationStepClosed()
2236 if revision_list:
2237 self.PrintRevisionsToBisectMessage(revision_list, target_depot)
2239 if self.opts.output_buildbot_annotations:
2240 bisect_utils.OutputAnnotationStepStart('Gathering Reference Values')
2242 print 'Gathering reference values for bisection.'
2244 # Perform the performance tests on the good and bad revisions, to get
2245 # reference values.
2246 bad_results, good_results = self.GatherReferenceValues(good_revision,
2247 bad_revision,
2248 command_to_run,
2249 metric,
2250 target_depot)
2252 if self.opts.output_buildbot_annotations:
2253 bisect_utils.OutputAnnotationStepClosed()
2255 if bad_results[1]:
2256 error = ('An error occurred while building and running the \'bad\' '
2257 'reference value. The bisect cannot continue without '
2258 'a working \'bad\' revision to start from.\n\nError: %s' %
2259 bad_results[0])
2260 return BisectResults(error=error)
2262 if good_results[1]:
2263 error = ('An error occurred while building and running the \'good\' '
2264 'reference value. The bisect cannot continue without '
2265 'a working \'good\' revision to start from.\n\nError: %s' %
2266 good_results[0])
2267 return BisectResults(error=error)
2269 # We need these reference values to determine if later runs should be
2270 # classified as pass or fail.
2272 known_bad_value = bad_results[0]
2273 known_good_value = good_results[0]
2275 # Abort bisect early when the return codes for known good
2276 # and known bad revisions are same.
2277 if (self._IsBisectModeReturnCode() and
2278 known_bad_value['mean'] == known_good_value['mean']):
2279 return BisectResults(abort_reason=('known good and known bad revisions '
2280 'returned same return code (return code=%s). '
2281 'Continuing bisect might not yield any results.' %
2282 known_bad_value['mean']))
2283 # Check the direction of improvement only if the improvement_direction
2284 # option is set to a specific direction (1 for higher is better or -1 for
2285 # lower is better).
2286 improvement_dir = self.opts.improvement_direction
2287 if improvement_dir:
2288 higher_is_better = improvement_dir > 0
2289 if higher_is_better:
2290 message = "Expecting higher values to be better for this metric, "
2291 else:
2292 message = "Expecting lower values to be better for this metric, "
2293 metric_increased = known_bad_value['mean'] > known_good_value['mean']
2294 if metric_increased:
2295 message += "and the metric appears to have increased. "
2296 else:
2297 message += "and the metric appears to have decreased. "
2298 if ((higher_is_better and metric_increased) or
2299 (not higher_is_better and not metric_increased)):
2300 error = (message + 'Then, the test results for the ends of the given '
2301 '\'good\' - \'bad\' range of revisions represent an '
2302 'improvement (and not a regression).')
2303 return BisectResults(error=error)
2304 logging.info(message + "Therefore we continue to bisect.")
2306 bisect_state = BisectState(target_depot, revision_list)
2307 revision_states = bisect_state.GetRevisionStates()
2309 min_revision = 0
2310 max_revision = len(revision_states) - 1
2312 # Can just mark the good and bad revisions explicitly here since we
2313 # already know the results.
2314 bad_revision_state = revision_states[min_revision]
2315 bad_revision_state.external = bad_results[2]
2316 bad_revision_state.perf_time = bad_results[3]
2317 bad_revision_state.build_time = bad_results[4]
2318 bad_revision_state.passed = False
2319 bad_revision_state.value = known_bad_value
2321 good_revision_state = revision_states[max_revision]
2322 good_revision_state.external = good_results[2]
2323 good_revision_state.perf_time = good_results[3]
2324 good_revision_state.build_time = good_results[4]
2325 good_revision_state.passed = True
2326 good_revision_state.value = known_good_value
2328 # Check how likely it is that the good and bad results are different
2329 # beyond chance-induced variation.
2330 if not (self.opts.debug_ignore_regression_confidence or
2331 self._IsBisectModeReturnCode()):
2332 if not _IsRegressionReproduced(known_good_value, known_bad_value):
2333 # If there is no significant difference between "good" and "bad"
2334 # revision results, then the "bad revision" is considered "good".
2335 # TODO(qyearsley): Remove this if it is not necessary.
2336 bad_revision_state.passed = True
2337 self.warnings.append(_RegressionNotReproducedWarningMessage(
2338 good_revision, bad_revision, known_good_value, known_bad_value))
2339 return BisectResults(bisect_state, self.depot_registry, self.opts,
2340 self.warnings)
2342 while True:
2343 if not revision_states:
2344 break
2346 if max_revision - min_revision <= 1:
2347 min_revision_state = revision_states[min_revision]
2348 max_revision_state = revision_states[max_revision]
2349 current_depot = min_revision_state.depot
2350 # TODO(sergiyb): Under which conditions can first two branches be hit?
2351 if min_revision_state.passed == '?':
2352 next_revision_index = min_revision
2353 elif max_revision_state.passed == '?':
2354 next_revision_index = max_revision
2355 elif current_depot in ['android-chrome', 'chromium', 'v8']:
2356 previous_revision = revision_states[min_revision].revision
2357 # If there were changes to any of the external libraries we track,
2358 # should bisect the changes there as well.
2359 external_depot = self._FindNextDepotToBisect(
2360 current_depot, min_revision_state, max_revision_state)
2361 # If there was no change in any of the external depots, the search
2362 # is over.
2363 if not external_depot:
2364 if current_depot == 'v8':
2365 self.warnings.append(
2366 'Unfortunately, V8 bisection couldn\'t '
2367 'continue any further. The script can only bisect into '
2368 'V8\'s bleeding_edge repository if both the current and '
2369 'previous revisions in trunk map directly to revisions in '
2370 'bleeding_edge.')
2371 break
2373 earliest_revision = max_revision_state.external[external_depot]
2374 latest_revision = min_revision_state.external[external_depot]
2376 new_revision_list = self.PrepareToBisectOnDepot(
2377 external_depot, earliest_revision, latest_revision,
2378 previous_revision)
2380 if not new_revision_list:
2381 error = ('An error occurred attempting to retrieve revision '
2382 'range: [%s..%s]' % (earliest_revision, latest_revision))
2383 return BisectResults(error=error)
2385 revision_states = bisect_state.CreateRevisionStatesAfter(
2386 external_depot, new_revision_list, current_depot,
2387 previous_revision)
2389 # Reset the bisection and perform it on the newly inserted states.
2390 min_revision = 0
2391 max_revision = len(revision_states) - 1
2393 print ('Regression in metric %s appears to be the result of '
2394 'changes in [%s].' % (metric, external_depot))
2396 revision_list = [state.revision for state in revision_states]
2397 self.PrintRevisionsToBisectMessage(revision_list, external_depot)
2399 continue
2400 else:
2401 break
2402 else:
2403 next_revision_index = (int((max_revision - min_revision) / 2) +
2404 min_revision)
2406 next_revision_state = revision_states[next_revision_index]
2407 next_revision = next_revision_state.revision
2408 next_depot = next_revision_state.depot
2410 self.depot_registry.ChangeToDepotDir(next_depot)
2412 message = 'Working on [%s:%s]' % (next_depot, next_revision)
2413 print message
2414 if self.opts.output_buildbot_annotations:
2415 bisect_utils.OutputAnnotationStepStart(message)
2417 run_results = self.RunTest(next_revision, next_depot, command_to_run,
2418 metric, skippable=True)
2420 # If the build is successful, check whether or not the metric
2421 # had regressed.
2422 if not run_results[1]:
2423 if len(run_results) > 2:
2424 next_revision_state.external = run_results[2]
2425 next_revision_state.perf_time = run_results[3]
2426 next_revision_state.build_time = run_results[4]
2428 passed_regression = self._CheckIfRunPassed(run_results[0],
2429 known_good_value,
2430 known_bad_value)
2432 next_revision_state.passed = passed_regression
2433 next_revision_state.value = run_results[0]
2435 if passed_regression:
2436 max_revision = next_revision_index
2437 else:
2438 min_revision = next_revision_index
2439 else:
2440 if run_results[1] == BUILD_RESULT_SKIPPED:
2441 next_revision_state.passed = 'Skipped'
2442 elif run_results[1] == BUILD_RESULT_FAIL:
2443 next_revision_state.passed = 'Build Failed'
2445 print run_results[0]
2447 # If the build is broken, remove it and redo search.
2448 revision_states.pop(next_revision_index)
2450 max_revision -= 1
2452 if self.opts.output_buildbot_annotations:
2453 self.printer.PrintPartialResults(bisect_state)
2454 bisect_utils.OutputAnnotationStepClosed()
2456 self._ConfidenceExtraTestRuns(min_revision_state, max_revision_state,
2457 command_to_run, metric)
2458 results = BisectResults(bisect_state, self.depot_registry, self.opts,
2459 self.warnings)
2461 self._GatherResultsFromRevertedCulpritCL(
2462 results, target_depot, command_to_run, metric)
2464 return results
2465 else:
2466 # Weren't able to sync and retrieve the revision range.
2467 error = ('An error occurred attempting to retrieve revision range: '
2468 '[%s..%s]' % (good_revision, bad_revision))
2469 return BisectResults(error=error)
2471 def _ConfidenceExtraTestRuns(self, good_state, bad_state, command_to_run,
2472 metric):
2473 if (bool(good_state.passed) != bool(bad_state.passed)
2474 and good_state.passed not in ('Skipped', 'Build Failed')
2475 and bad_state.passed not in ('Skipped', 'Build Failed')):
2476 for state in (good_state, bad_state):
2477 run_results = self.RunTest(
2478 state.revision,
2479 state.depot,
2480 command_to_run,
2481 metric,
2482 test_run_multiplier=BORDER_REVISIONS_EXTRA_RUNS)
2483 # Is extend the right thing to do here?
2484 if run_results[1] != BUILD_RESULT_FAIL:
2485 state.value['values'].extend(run_results[0]['values'])
2486 else:
2487 warning_text = 'Re-test of revision %s failed with error message: %s'
2488 warning_text %= (state.revision, run_results[0])
2489 if warning_text not in self.warnings:
2490 self.warnings.append(warning_text)
2493 def _IsPlatformSupported():
2494 """Checks that this platform and build system are supported.
2496 Args:
2497 opts: The options parsed from the command line.
2499 Returns:
2500 True if the platform and build system are supported.
2502 # Haven't tested the script out on any other platforms yet.
2503 supported = ['posix', 'nt']
2504 return os.name in supported
2507 def RemoveBuildFiles(build_type):
2508 """Removes build files from previous runs."""
2509 out_dir = os.path.join('out', build_type)
2510 build_dir = os.path.join('build', build_type)
2511 logging.info('Removing build files in "%s" and "%s".',
2512 os.path.abspath(out_dir), os.path.abspath(build_dir))
2513 try:
2514 RemakeDirectoryTree(out_dir)
2515 RemakeDirectoryTree(build_dir)
2516 except Exception as e:
2517 raise RuntimeError('Got error in RemoveBuildFiles: %s' % e)
2520 def RemakeDirectoryTree(path_to_dir):
2521 """Removes a directory tree and replaces it with an empty one.
2523 Returns True if successful, False otherwise.
2525 RemoveDirectoryTree(path_to_dir)
2526 MaybeMakeDirectory(path_to_dir)
2529 def RemoveDirectoryTree(path_to_dir):
2530 """Removes a directory tree. Returns True if successful or False otherwise."""
2531 if os.path.isfile(path_to_dir):
2532 logging.info('REMOVING FILE %s' % path_to_dir)
2533 os.remove(path_to_dir)
2534 try:
2535 if os.path.exists(path_to_dir):
2536 shutil.rmtree(path_to_dir)
2537 except OSError, e:
2538 if e.errno != errno.ENOENT:
2539 raise
2542 # This is copied from build/scripts/common/chromium_utils.py.
2543 def MaybeMakeDirectory(*path):
2544 """Creates an entire path, if it doesn't already exist."""
2545 file_path = os.path.join(*path)
2546 try:
2547 os.makedirs(file_path)
2548 except OSError as e:
2549 if e.errno != errno.EEXIST:
2550 raise
2553 class BisectOptions(object):
2554 """Options to be used when running bisection."""
2555 def __init__(self):
2556 super(BisectOptions, self).__init__()
2558 self.target_platform = 'chromium'
2559 self.build_preference = None
2560 self.good_revision = None
2561 self.bad_revision = None
2562 self.use_goma = None
2563 self.goma_dir = None
2564 self.goma_threads = 64
2565 self.repeat_test_count = 20
2566 self.truncate_percent = 25
2567 self.max_time_minutes = 20
2568 self.metric = None
2569 self.command = None
2570 self.output_buildbot_annotations = None
2571 self.no_custom_deps = False
2572 self.working_directory = None
2573 self.extra_src = None
2574 self.debug_ignore_build = None
2575 self.debug_ignore_sync = None
2576 self.debug_ignore_perf_test = None
2577 self.debug_ignore_regression_confidence = None
2578 self.debug_fake_first_test_mean = 0
2579 self.target_arch = 'ia32'
2580 self.target_build_type = 'Release'
2581 self.builder_type = 'perf'
2582 self.bisect_mode = bisect_utils.BISECT_MODE_MEAN
2583 self.improvement_direction = 0
2584 self.bug_id = ''
2586 @staticmethod
2587 def _AddBisectOptionsGroup(parser):
2588 group = parser.add_argument_group('Bisect options')
2589 group.add_argument('-c', '--command', required=True,
2590 help='A command to execute your performance test at '
2591 'each point in the bisection.')
2592 group.add_argument('-b', '--bad_revision', required=True,
2593 help='A bad revision to start bisection. Must be later '
2594 'than good revision. May be either a git or svn '
2595 'revision.')
2596 group.add_argument('-g', '--good_revision', required=True,
2597 help='A revision to start bisection where performance '
2598 'test is known to pass. Must be earlier than the '
2599 'bad revision. May be either a git or a svn '
2600 'revision.')
2601 group.add_argument('-m', '--metric',
2602 help='The desired metric to bisect on. For example '
2603 '"vm_rss_final_b/vm_rss_f_b"')
2604 group.add_argument('-d', '--improvement_direction', type=int, default=0,
2605 help='An integer number representing the direction of '
2606 'improvement. 1 for higher is better, -1 for lower '
2607 'is better, 0 for ignore (default).')
2608 group.add_argument('-r', '--repeat_test_count', type=int, default=20,
2609 choices=range(1, 101),
2610 help='The number of times to repeat the performance '
2611 'test. Values will be clamped to range [1, 100]. '
2612 'Default value is 20.')
2613 group.add_argument('--max_time_minutes', type=int, default=20,
2614 choices=range(1, 61),
2615 help='The maximum time (in minutes) to take running the '
2616 'performance tests. The script will run the '
2617 'performance tests according to '
2618 '--repeat_test_count, so long as it doesn\'t exceed'
2619 ' --max_time_minutes. Values will be clamped to '
2620 'range [1, 60]. Default value is 20.')
2621 group.add_argument('-t', '--truncate_percent', type=int, default=25,
2622 help='The highest/lowest percent are discarded to form '
2623 'a truncated mean. Values will be clamped to range '
2624 '[0, 25]. Default value is 25 percent.')
2625 group.add_argument('--bisect_mode', default=bisect_utils.BISECT_MODE_MEAN,
2626 choices=[bisect_utils.BISECT_MODE_MEAN,
2627 bisect_utils.BISECT_MODE_STD_DEV,
2628 bisect_utils.BISECT_MODE_RETURN_CODE],
2629 help='The bisect mode. Choices are to bisect on the '
2630 'difference in mean, std_dev, or return_code.')
2631 group.add_argument('--bug_id', default='',
2632 help='The id for the bug associated with this bisect. ' +
2633 'If this number is given, bisect will attempt to ' +
2634 'verify that the bug is not closed before '
2635 'starting.')
2637 @staticmethod
2638 def _AddBuildOptionsGroup(parser):
2639 group = parser.add_argument_group('Build options')
2640 group.add_argument('-w', '--working_directory',
2641 help='Path to the working directory where the script '
2642 'will do an initial checkout of the chromium depot. The '
2643 'files will be placed in a subdirectory "bisect" under '
2644 'working_directory and that will be used to perform the '
2645 'bisection. This parameter is optional, if it is not '
2646 'supplied, the script will work from the current depot.')
2647 group.add_argument('--build_preference',
2648 choices=['msvs', 'ninja', 'make'],
2649 help='The preferred build system to use. On linux/mac '
2650 'the options are make/ninja. On Windows, the '
2651 'options are msvs/ninja.')
2652 group.add_argument('--target_platform', default='chromium',
2653 choices=['chromium', 'android', 'android-chrome'],
2654 help='The target platform. Choices are "chromium" '
2655 '(current platform), or "android". If you specify '
2656 'something other than "chromium", you must be '
2657 'properly set up to build that platform.')
2658 group.add_argument('--no_custom_deps', dest='no_custom_deps',
2659 action='store_true', default=False,
2660 help='Run the script with custom_deps or not.')
2661 group.add_argument('--extra_src',
2662 help='Path to a script which can be used to modify the '
2663 'bisect script\'s behavior.')
2664 group.add_argument('--use_goma', action='store_true',
2665 help='Add a bunch of extra threads for goma, and enable '
2666 'goma')
2667 group.add_argument('--goma_dir',
2668 help='Path to goma tools (or system default if not '
2669 'specified).')
2670 group.add_argument('--goma_threads', type=int, default='64',
2671 help='Number of threads for goma, only if using goma.')
2672 group.add_argument('--output_buildbot_annotations', action='store_true',
2673 help='Add extra annotation output for buildbot.')
2674 group.add_argument('--target_arch', default='ia32',
2675 dest='target_arch',
2676 choices=['ia32', 'x64', 'arm', 'arm64'],
2677 help='The target build architecture. Choices are "ia32" '
2678 '(default), "x64", "arm" or "arm64".')
2679 group.add_argument('--target_build_type', default='Release',
2680 choices=['Release', 'Debug', 'Release_x64'],
2681 help='The target build type. Choices are "Release" '
2682 '(default), Release_x64 or "Debug".')
2683 group.add_argument('--builder_type', default=fetch_build.PERF_BUILDER,
2684 choices=[fetch_build.PERF_BUILDER,
2685 fetch_build.FULL_BUILDER,
2686 fetch_build.ANDROID_CHROME_PERF_BUILDER, ''],
2687 help='Type of builder to get build from. This '
2688 'determines both the bot that builds and the '
2689 'place where archived builds are downloaded from. '
2690 'For local builds, an empty string can be passed.')
2692 @staticmethod
2693 def _AddDebugOptionsGroup(parser):
2694 group = parser.add_argument_group('Debug options')
2695 group.add_argument('--debug_ignore_build', action='store_true',
2696 help='DEBUG: Don\'t perform builds.')
2697 group.add_argument('--debug_ignore_sync', action='store_true',
2698 help='DEBUG: Don\'t perform syncs.')
2699 group.add_argument('--debug_ignore_perf_test', action='store_true',
2700 help='DEBUG: Don\'t perform performance tests.')
2701 group.add_argument('--debug_ignore_regression_confidence',
2702 action='store_true',
2703 help='DEBUG: Don\'t score the confidence of the initial '
2704 'good and bad revisions\' test results.')
2705 group.add_argument('--debug_fake_first_test_mean', type=int, default='0',
2706 help='DEBUG: When faking performance tests, return this '
2707 'value as the mean of the first performance test, '
2708 'and return a mean of 0.0 for further tests.')
2709 return group
2711 @classmethod
2712 def _CreateCommandLineParser(cls):
2713 """Creates a parser with bisect options.
2715 Returns:
2716 An instance of argparse.ArgumentParser.
2718 usage = ('%(prog)s [options] [-- chromium-options]\n'
2719 'Perform binary search on revision history to find a minimal '
2720 'range of revisions where a performance metric regressed.\n')
2722 parser = argparse.ArgumentParser(usage=usage)
2723 cls._AddBisectOptionsGroup(parser)
2724 cls._AddBuildOptionsGroup(parser)
2725 cls._AddDebugOptionsGroup(parser)
2726 return parser
2728 def ParseCommandLine(self):
2729 """Parses the command line for bisect options."""
2730 parser = self._CreateCommandLineParser()
2731 opts = parser.parse_args()
2733 try:
2734 if (not opts.metric and
2735 opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE):
2736 raise RuntimeError('missing required parameter: --metric')
2738 if opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE:
2739 metric_values = opts.metric.split('/')
2740 if len(metric_values) != 2:
2741 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
2742 opts.metric = metric_values
2744 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25) / 100.0
2746 for k, v in opts.__dict__.iteritems():
2747 assert hasattr(self, k), 'Invalid %s attribute in BisectOptions.' % k
2748 setattr(self, k, v)
2749 except RuntimeError, e:
2750 output_string = StringIO.StringIO()
2751 parser.print_help(file=output_string)
2752 error_message = '%s\n\n%s' % (e.message, output_string.getvalue())
2753 output_string.close()
2754 raise RuntimeError(error_message)
2756 @staticmethod
2757 def FromDict(values):
2758 """Creates an instance of BisectOptions from a dictionary.
2760 Args:
2761 values: a dict containing options to set.
2763 Returns:
2764 An instance of BisectOptions.
2766 opts = BisectOptions()
2767 for k, v in values.iteritems():
2768 assert hasattr(opts, k), 'Invalid %s attribute in BisectOptions.' % k
2769 setattr(opts, k, v)
2771 if opts.metric and opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE:
2772 metric_values = opts.metric.split('/')
2773 if len(metric_values) != 2:
2774 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
2775 opts.metric = metric_values
2777 if opts.target_arch == 'x64' and opts.target_build_type == 'Release':
2778 opts.target_build_type = 'Release_x64'
2779 opts.repeat_test_count = min(max(opts.repeat_test_count, 1), 100)
2780 opts.max_time_minutes = min(max(opts.max_time_minutes, 1), 60)
2781 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25)
2782 opts.truncate_percent = opts.truncate_percent / 100.0
2784 return opts
2787 def _ConfigureLogging():
2788 """Trivial logging config.
2790 Configures logging to output any messages at or above INFO to standard out,
2791 without any additional formatting.
2793 logging_format = '%(message)s'
2794 logging.basicConfig(
2795 stream=logging.sys.stdout, level=logging.INFO, format=logging_format)
2798 def main():
2799 _ConfigureLogging()
2800 try:
2801 opts = BisectOptions()
2802 opts.ParseCommandLine()
2804 if opts.bug_id:
2805 if opts.output_buildbot_annotations:
2806 bisect_utils.OutputAnnotationStepStart('Checking Issue Tracker')
2807 issue_closed = query_crbug.CheckIssueClosed(opts.bug_id)
2808 if issue_closed:
2809 print 'Aborting bisect because bug is closed'
2810 else:
2811 print 'Could not confirm bug is closed, proceeding.'
2812 if opts.output_buildbot_annotations:
2813 bisect_utils.OutputAnnotationStepClosed()
2814 if issue_closed:
2815 results = BisectResults(abort_reason='the bug is closed.')
2816 bisect_printer = BisectPrinter(opts)
2817 bisect_printer.FormatAndPrintResults(results)
2818 return 0
2820 if opts.extra_src:
2821 extra_src = bisect_utils.LoadExtraSrc(opts.extra_src)
2822 if not extra_src:
2823 raise RuntimeError('Invalid or missing --extra_src.')
2824 bisect_utils.AddAdditionalDepotInfo(extra_src.GetAdditionalDepotInfo())
2826 if opts.working_directory:
2827 custom_deps = bisect_utils.DEFAULT_GCLIENT_CUSTOM_DEPS
2828 if opts.no_custom_deps:
2829 custom_deps = None
2830 bisect_utils.CreateBisectDirectoryAndSetupDepot(opts, custom_deps)
2832 os.chdir(os.path.join(os.getcwd(), 'src'))
2833 RemoveBuildFiles(opts.target_build_type)
2835 if not _IsPlatformSupported():
2836 raise RuntimeError('Sorry, this platform isn\'t supported yet.')
2838 if not source_control.IsInGitRepository():
2839 raise RuntimeError(
2840 'Sorry, only the git workflow is supported at the moment.')
2842 # gClient sync seems to fail if you're not in master branch.
2843 if (not source_control.IsInProperBranch() and
2844 not opts.debug_ignore_sync and
2845 not opts.working_directory):
2846 raise RuntimeError('You must switch to master branch to run bisection.')
2847 bisect_test = BisectPerformanceMetrics(opts, os.getcwd())
2848 try:
2849 results = bisect_test.Run(opts.command, opts.bad_revision,
2850 opts.good_revision, opts.metric)
2851 if results.error:
2852 raise RuntimeError(results.error)
2853 bisect_test.printer.FormatAndPrintResults(results)
2854 return 0
2855 finally:
2856 bisect_test.PerformCleanup()
2857 except RuntimeError as e:
2858 if opts.output_buildbot_annotations:
2859 # The perf dashboard scrapes the "results" step in order to comment on
2860 # bugs. If you change this, please update the perf dashboard as well.
2861 bisect_utils.OutputAnnotationStepStart('Results')
2862 print 'Runtime Error: %s' % e
2863 if opts.output_buildbot_annotations:
2864 bisect_utils.OutputAnnotationStepClosed()
2865 return 1
2868 if __name__ == '__main__':
2869 sys.exit(main())