Snap pinch zoom gestures near the screen edge.
[chromium-blink-merge.git] / tools / auto_bisect / bisect_perf_regression.py
blob9539b68b9827c2d1f2771fe9fc532c20a430979a
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 GetCompatibleCommand(self, command_to_run, revision, depot):
1242 """Return a possibly modified test command depending on the revision.
1244 Prior to crrev.com/274857 *only* android-chromium-testshell
1245 Then until crrev.com/276628 *both* (android-chromium-testshell and
1246 android-chrome-shell) work. After that rev 276628 *only*
1247 android-chrome-shell works. The bisect_perf_regression.py script should
1248 handle these cases and set appropriate browser type based on revision.
1250 if self.opts.target_platform in ['android']:
1251 # When its a third_party depot, get the chromium revision.
1252 if depot != 'chromium':
1253 revision = bisect_utils.CheckRunGit(
1254 ['rev-parse', 'HEAD'], cwd=self.src_cwd).strip()
1255 commit_position = source_control.GetCommitPosition(revision,
1256 cwd=self.src_cwd)
1257 if not commit_position:
1258 return command_to_run
1259 cmd_re = re.compile(r'--browser=(?P<browser_type>\S+)')
1260 matches = cmd_re.search(command_to_run)
1261 if bisect_utils.IsStringInt(commit_position) and matches:
1262 cmd_browser = matches.group('browser_type')
1263 if commit_position <= 274857 and cmd_browser == 'android-chrome-shell':
1264 return command_to_run.replace(cmd_browser,
1265 'android-chromium-testshell')
1266 elif (commit_position >= 276628 and
1267 cmd_browser == 'android-chromium-testshell'):
1268 return command_to_run.replace(cmd_browser,
1269 'android-chrome-shell')
1270 return command_to_run
1272 def RunPerformanceTestAndParseResults(
1273 self, command_to_run, metric, reset_on_first_run=False,
1274 upload_on_last_run=False, results_label=None, test_run_multiplier=1,
1275 allow_flakes=True):
1276 """Runs a performance test on the current revision and parses the results.
1278 Args:
1279 command_to_run: The command to be run to execute the performance test.
1280 metric: The metric to parse out from the results of the performance test.
1281 This is the result chart name and trace name, separated by slash.
1282 May be None for perf try jobs.
1283 reset_on_first_run: If True, pass the flag --reset-results on first run.
1284 upload_on_last_run: If True, pass the flag --upload-results on last run.
1285 results_label: A value for the option flag --results-label.
1286 The arguments reset_on_first_run, upload_on_last_run and results_label
1287 are all ignored if the test is not a Telemetry test.
1288 test_run_multiplier: Factor by which to multiply the number of test runs
1289 and the timeout period specified in self.opts.
1290 allow_flakes: Report success even if some tests fail to run.
1292 Returns:
1293 (values dict, 0) if --debug_ignore_perf_test was passed.
1294 (values dict, 0, test output) if the test was run successfully.
1295 (error message, -1) if the test couldn't be run.
1296 (error message, -1, test output) if the test ran but there was an error.
1298 success_code, failure_code = 0, -1
1300 if self.opts.debug_ignore_perf_test:
1301 fake_results = {
1302 'mean': 0.0,
1303 'std_err': 0.0,
1304 'std_dev': 0.0,
1305 'values': [0.0]
1308 # When debug_fake_test_mean is set, its value is returned as the mean
1309 # and the flag is cleared so that further calls behave as if it wasn't
1310 # set (returning the fake_results dict as defined above).
1311 if self.opts.debug_fake_first_test_mean:
1312 fake_results['mean'] = float(self.opts.debug_fake_first_test_mean)
1313 self.opts.debug_fake_first_test_mean = 0
1315 return (fake_results, success_code)
1317 # For Windows platform set posix=False, to parse windows paths correctly.
1318 # On Windows, path separators '\' or '\\' are replace by '' when posix=True,
1319 # refer to http://bugs.python.org/issue1724822. By default posix=True.
1320 args = shlex.split(command_to_run, posix=not bisect_utils.IsWindowsHost())
1322 if not _GenerateProfileIfNecessary(args):
1323 err_text = 'Failed to generate profile for performance test.'
1324 return (err_text, failure_code)
1326 is_telemetry = bisect_utils.IsTelemetryCommand(command_to_run)
1328 start_time = time.time()
1330 metric_values = []
1331 output_of_all_runs = ''
1332 repeat_count = self.opts.repeat_test_count * test_run_multiplier
1333 return_codes = []
1334 for i in xrange(repeat_count):
1335 # Can ignore the return code since if the tests fail, it won't return 0.
1336 current_args = copy.copy(args)
1337 if is_telemetry:
1338 if i == 0 and reset_on_first_run:
1339 current_args.append('--reset-results')
1340 if i == self.opts.repeat_test_count - 1 and upload_on_last_run:
1341 current_args.append('--upload-results')
1342 if results_label:
1343 current_args.append('--results-label=%s' % results_label)
1344 try:
1345 output, return_code = bisect_utils.RunProcessAndRetrieveOutput(
1346 current_args, cwd=self.src_cwd)
1347 return_codes.append(return_code)
1348 except OSError, e:
1349 if e.errno == errno.ENOENT:
1350 err_text = ('Something went wrong running the performance test. '
1351 'Please review the command line:\n\n')
1352 if 'src/' in ' '.join(args):
1353 err_text += ('Check that you haven\'t accidentally specified a '
1354 'path with src/ in the command.\n\n')
1355 err_text += ' '.join(args)
1356 err_text += '\n'
1358 return (err_text, failure_code)
1359 raise
1361 output_of_all_runs += output
1362 if self.opts.output_buildbot_annotations:
1363 print output
1365 if metric and self._IsBisectModeUsingMetric():
1366 parsed_metric = _ParseMetricValuesFromOutput(metric, output)
1367 if parsed_metric:
1368 metric_values.append(math_utils.Mean(parsed_metric))
1369 # If we're bisecting on a metric (ie, changes in the mean or
1370 # standard deviation) and no metric values are produced, bail out.
1371 if not metric_values:
1372 break
1373 elif self._IsBisectModeReturnCode():
1374 metric_values.append(return_code)
1375 # If there's a failed test, we can bail out early.
1376 if return_code:
1377 break
1379 elapsed_minutes = (time.time() - start_time) / 60.0
1380 time_limit = self.opts.max_time_minutes * test_run_multiplier
1381 if elapsed_minutes >= time_limit:
1382 break
1384 if metric and len(metric_values) == 0:
1385 err_text = 'Metric %s was not found in the test output.' % metric
1386 # TODO(qyearsley): Consider also getting and displaying a list of metrics
1387 # that were found in the output here.
1388 return (err_text, failure_code, output_of_all_runs)
1390 # If we're bisecting on return codes, we're really just looking for zero vs
1391 # non-zero.
1392 values = {}
1393 if self._IsBisectModeReturnCode():
1394 # If any of the return codes is non-zero, output 1.
1395 overall_return_code = 0 if (
1396 all(current_value == 0 for current_value in metric_values)) else 1
1398 values = {
1399 'mean': overall_return_code,
1400 'std_err': 0.0,
1401 'std_dev': 0.0,
1402 'values': metric_values,
1405 print 'Results of performance test: Command returned with %d' % (
1406 overall_return_code)
1407 print
1408 elif metric:
1409 # Need to get the average value if there were multiple values.
1410 truncated_mean = math_utils.TruncatedMean(
1411 metric_values, self.opts.truncate_percent)
1412 standard_err = math_utils.StandardError(metric_values)
1413 standard_dev = math_utils.StandardDeviation(metric_values)
1415 if self._IsBisectModeStandardDeviation():
1416 metric_values = [standard_dev]
1418 values = {
1419 'mean': truncated_mean,
1420 'std_err': standard_err,
1421 'std_dev': standard_dev,
1422 'values': metric_values,
1425 print 'Results of performance test: %12f %12f' % (
1426 truncated_mean, standard_err)
1427 print
1429 overall_success = success_code
1430 if not allow_flakes and not self._IsBisectModeReturnCode():
1431 overall_success = (
1432 success_code
1433 if (all(current_value == 0 for current_value in return_codes))
1434 else failure_code)
1436 return (values, overall_success, output_of_all_runs)
1438 def PerformPreBuildCleanup(self):
1439 """Performs cleanup between runs."""
1440 print 'Cleaning up between runs.'
1441 print
1443 # Leaving these .pyc files around between runs may disrupt some perf tests.
1444 for (path, _, files) in os.walk(self.src_cwd):
1445 for cur_file in files:
1446 if cur_file.endswith('.pyc'):
1447 path_to_file = os.path.join(path, cur_file)
1448 os.remove(path_to_file)
1450 def _RunPostSync(self, _depot):
1451 """Performs any work after syncing.
1453 Args:
1454 depot: Depot name.
1456 Returns:
1457 True if successful.
1459 if 'android' in self.opts.target_platform:
1460 if not builder.SetupAndroidBuildEnvironment(
1461 self.opts, path_to_src=self.src_cwd):
1462 return False
1464 return self.RunGClientHooks()
1466 @staticmethod
1467 def ShouldSkipRevision(depot, revision):
1468 """Checks whether a particular revision can be safely skipped.
1470 Some commits can be safely skipped (such as a DEPS roll for the repos
1471 still using .DEPS.git), since the tool is git based those changes
1472 would have no effect.
1474 Args:
1475 depot: The depot being bisected.
1476 revision: Current revision we're synced to.
1478 Returns:
1479 True if we should skip building/testing this revision.
1481 # Skips revisions with DEPS on android-chrome.
1482 if depot == 'android-chrome':
1483 cmd = ['diff-tree', '--no-commit-id', '--name-only', '-r', revision]
1484 output = bisect_utils.CheckRunGit(cmd)
1486 files = output.splitlines()
1488 if len(files) == 1 and files[0] == 'DEPS':
1489 return True
1491 return False
1493 def RunTest(self, revision, depot, command, metric, skippable=False,
1494 skip_sync=False, create_patch=False, force_build=False,
1495 test_run_multiplier=1):
1496 """Performs a full sync/build/run of the specified revision.
1498 Args:
1499 revision: The revision to sync to.
1500 depot: The depot that's being used at the moment (src, webkit, etc.)
1501 command: The command to execute the performance test.
1502 metric: The performance metric being tested.
1503 skip_sync: Skip the sync step.
1504 create_patch: Create a patch with any locally modified files.
1505 force_build: Force a local build.
1506 test_run_multiplier: Factor by which to multiply the given number of runs
1507 and the set timeout period.
1509 Returns:
1510 On success, a tuple containing the results of the performance test.
1511 Otherwise, a tuple with the error message.
1513 logging.info('Running RunTest with rev "%s", command "%s"',
1514 revision, command)
1515 # Decide which sync program to use.
1516 sync_client = None
1517 if depot == 'chromium' or depot == 'android-chrome':
1518 sync_client = 'gclient'
1520 # Do the syncing for all depots.
1521 if not (self.opts.debug_ignore_sync or skip_sync):
1522 if not self._SyncRevision(depot, revision, sync_client):
1523 return ('Failed to sync: [%s]' % str(revision), BUILD_RESULT_FAIL)
1525 # Try to do any post-sync steps. This may include "gclient runhooks".
1526 if not self._RunPostSync(depot):
1527 return ('Failed to run [gclient runhooks].', BUILD_RESULT_FAIL)
1529 # Skip this revision if it can be skipped.
1530 if skippable and self.ShouldSkipRevision(depot, revision):
1531 return ('Skipped revision: [%s]' % str(revision),
1532 BUILD_RESULT_SKIPPED)
1534 # Obtain a build for this revision. This may be done by requesting a build
1535 # from another builder, waiting for it and downloading it.
1536 start_build_time = time.time()
1537 revision_to_build = revision if not force_build else None
1538 build_success = self.ObtainBuild(
1539 depot, revision=revision_to_build, create_patch=create_patch)
1540 if not build_success:
1541 return ('Failed to build revision: [%s]' % str(revision),
1542 BUILD_RESULT_FAIL)
1543 after_build_time = time.time()
1545 # Possibly alter the command.
1546 command = self.GetCompatibleCommand(command, revision, depot)
1548 # Run the command and get the results.
1549 results = self.RunPerformanceTestAndParseResults(
1550 command, metric, test_run_multiplier=test_run_multiplier)
1552 # Restore build output directory once the tests are done, to avoid
1553 # any discrepancies.
1554 if self.IsDownloadable(depot) and revision:
1555 self.BackupOrRestoreOutputDirectory(restore=True)
1557 # A value other than 0 indicates that the test couldn't be run, and results
1558 # should also include an error message.
1559 if results[1] != 0:
1560 return results
1562 external_revisions = self._Get3rdPartyRevisions(depot)
1564 if not external_revisions is None:
1565 return (results[0], results[1], external_revisions,
1566 time.time() - after_build_time, after_build_time -
1567 start_build_time)
1568 else:
1569 return ('Failed to parse DEPS file for external revisions.',
1570 BUILD_RESULT_FAIL)
1572 def _SyncRevision(self, depot, revision, sync_client):
1573 """Syncs depot to particular revision.
1575 Args:
1576 depot: The depot that's being used at the moment (src, webkit, etc.)
1577 revision: The revision to sync to.
1578 sync_client: Program used to sync, e.g. "gclient". Can be None.
1580 Returns:
1581 True if successful, False otherwise.
1583 self.depot_registry.ChangeToDepotDir(depot)
1585 if sync_client:
1586 self.PerformPreBuildCleanup()
1588 # When using gclient to sync, you need to specify the depot you
1589 # want so that all the dependencies sync properly as well.
1590 # i.e. gclient sync src@<SHA1>
1591 if sync_client == 'gclient' and revision:
1592 revision = '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME[depot]['src'],
1593 revision)
1594 if depot == 'chromium' and self.opts.target_platform == 'android-chrome':
1595 return self._SyncRevisionsForAndroidChrome(revision)
1597 return source_control.SyncToRevision(revision, sync_client)
1599 def _SyncRevisionsForAndroidChrome(self, revision):
1600 """Syncs android-chrome and chromium repos to particular revision.
1602 This is a special case for android-chrome as the gclient sync for chromium
1603 overwrites the android-chrome revision to TOT. Therefore both the repos
1604 are synced to known revisions.
1606 Args:
1607 revision: Git hash of the Chromium to sync.
1609 Returns:
1610 True if successful, False otherwise.
1612 revisions_list = [revision]
1613 current_android_rev = source_control.GetCurrentRevision(
1614 self.depot_registry.GetDepotDir('android-chrome'))
1615 revisions_list.append(
1616 '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME['android-chrome']['src'],
1617 current_android_rev))
1618 return not bisect_utils.RunGClientAndSync(revisions_list)
1620 def _CheckIfRunPassed(self, current_value, known_good_value, known_bad_value):
1621 """Given known good and bad values, decide if the current_value passed
1622 or failed.
1624 Args:
1625 current_value: The value of the metric being checked.
1626 known_bad_value: The reference value for a "failed" run.
1627 known_good_value: The reference value for a "passed" run.
1629 Returns:
1630 True if the current_value is closer to the known_good_value than the
1631 known_bad_value.
1633 if self.opts.bisect_mode == bisect_utils.BISECT_MODE_STD_DEV:
1634 dist_to_good_value = abs(current_value['std_dev'] -
1635 known_good_value['std_dev'])
1636 dist_to_bad_value = abs(current_value['std_dev'] -
1637 known_bad_value['std_dev'])
1638 else:
1639 dist_to_good_value = abs(current_value['mean'] - known_good_value['mean'])
1640 dist_to_bad_value = abs(current_value['mean'] - known_bad_value['mean'])
1642 return dist_to_good_value < dist_to_bad_value
1644 def _GetV8BleedingEdgeFromV8TrunkIfMappable(
1645 self, revision, bleeding_edge_branch):
1646 """Gets v8 bleeding edge revision mapped to v8 revision in trunk.
1648 Args:
1649 revision: A trunk V8 revision mapped to bleeding edge revision.
1650 bleeding_edge_branch: Branch used to perform lookup of bleeding edge
1651 revision.
1652 Return:
1653 A mapped bleeding edge revision if found, otherwise None.
1655 commit_position = source_control.GetCommitPosition(revision)
1657 if bisect_utils.IsStringInt(commit_position):
1658 # V8 is tricky to bisect, in that there are only a few instances when
1659 # we can dive into bleeding_edge and get back a meaningful result.
1660 # Try to detect a V8 "business as usual" case, which is when:
1661 # 1. trunk revision N has description "Version X.Y.Z"
1662 # 2. bleeding_edge revision (N-1) has description "Prepare push to
1663 # trunk. Now working on X.Y.(Z+1)."
1665 # As of 01/24/2014, V8 trunk descriptions are formatted:
1666 # "Version 3.X.Y (based on bleeding_edge revision rZ)"
1667 # So we can just try parsing that out first and fall back to the old way.
1668 v8_dir = self.depot_registry.GetDepotDir('v8')
1669 v8_bleeding_edge_dir = self.depot_registry.GetDepotDir('v8_bleeding_edge')
1671 revision_info = source_control.QueryRevisionInfo(revision, cwd=v8_dir)
1672 version_re = re.compile("Version (?P<values>[0-9,.]+)")
1673 regex_results = version_re.search(revision_info['subject'])
1674 if regex_results:
1675 git_revision = None
1676 if 'based on bleeding_edge' in revision_info['subject']:
1677 try:
1678 bleeding_edge_revision = revision_info['subject'].split(
1679 'bleeding_edge revision r')[1]
1680 bleeding_edge_revision = int(bleeding_edge_revision.split(')')[0])
1681 bleeding_edge_url = ('https://v8.googlecode.com/svn/branches/'
1682 'bleeding_edge@%s' % bleeding_edge_revision)
1683 cmd = ['log',
1684 '--format=%H',
1685 '--grep',
1686 bleeding_edge_url,
1687 '-1',
1688 bleeding_edge_branch]
1689 output = bisect_utils.CheckRunGit(cmd, cwd=v8_dir)
1690 if output:
1691 git_revision = output.strip()
1692 return git_revision
1693 except (IndexError, ValueError):
1694 pass
1695 else:
1696 # V8 rolls description changed after V8 git migration, new description
1697 # includes "Version 3.X.Y (based on <git hash>)"
1698 try:
1699 rxp = re.compile('based on (?P<git_revision>[a-fA-F0-9]+)')
1700 re_results = rxp.search(revision_info['subject'])
1701 if re_results:
1702 return re_results.group('git_revision')
1703 except (IndexError, ValueError):
1704 pass
1705 if not git_revision:
1706 # Wasn't successful, try the old way of looking for "Prepare push to"
1707 git_revision = source_control.ResolveToRevision(
1708 int(commit_position) - 1, 'v8_bleeding_edge',
1709 bisect_utils.DEPOT_DEPS_NAME, -1, cwd=v8_bleeding_edge_dir)
1711 if git_revision:
1712 revision_info = source_control.QueryRevisionInfo(
1713 git_revision, cwd=v8_bleeding_edge_dir)
1715 if 'Prepare push to trunk' in revision_info['subject']:
1716 return git_revision
1717 return None
1719 def _GetNearestV8BleedingEdgeFromTrunk(
1720 self, revision, v8_branch, bleeding_edge_branch, search_forward=True):
1721 """Gets the nearest V8 roll and maps to bleeding edge revision.
1723 V8 is a bit tricky to bisect since it isn't just rolled out like blink.
1724 Each revision on trunk might just be whatever was in bleeding edge, rolled
1725 directly out. Or it could be some mixture of previous v8 trunk versions,
1726 with bits and pieces cherry picked out from bleeding edge. In order to
1727 bisect, we need both the before/after versions on trunk v8 to be just pushes
1728 from bleeding edge. With the V8 git migration, the branches got switched.
1729 a) master (external/v8) == candidates (v8/v8)
1730 b) bleeding_edge (external/v8) == master (v8/v8)
1732 Args:
1733 revision: A V8 revision to get its nearest bleeding edge revision
1734 search_forward: Searches forward if True, otherwise search backward.
1736 Return:
1737 A mapped bleeding edge revision if found, otherwise None.
1739 cwd = self.depot_registry.GetDepotDir('v8')
1740 cmd = ['log', '--format=%ct', '-1', revision]
1741 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1742 commit_time = int(output)
1743 commits = []
1744 if search_forward:
1745 cmd = ['log',
1746 '--format=%H',
1747 '--after=%d' % commit_time,
1748 v8_branch,
1749 '--reverse']
1750 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1751 output = output.split()
1752 commits = output
1753 #Get 10 git hashes immediately after the given commit.
1754 commits = commits[:10]
1755 else:
1756 cmd = ['log',
1757 '--format=%H',
1758 '-10',
1759 '--before=%d' % commit_time,
1760 v8_branch]
1761 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1762 output = output.split()
1763 commits = output
1765 bleeding_edge_revision = None
1767 for c in commits:
1768 bleeding_edge_revision = self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1769 c, bleeding_edge_branch)
1770 if bleeding_edge_revision:
1771 break
1773 return bleeding_edge_revision
1775 def _FillInV8BleedingEdgeInfo(self, min_revision_state, max_revision_state):
1776 cwd = self.depot_registry.GetDepotDir('v8')
1777 # when "remote.origin.url" is https://chromium.googlesource.com/v8/v8.git
1778 v8_branch = 'origin/candidates'
1779 bleeding_edge_branch = 'origin/master'
1781 # Support for the chromium revisions with external V8 repo.
1782 # ie https://chromium.googlesource.com/external/v8.git
1783 cmd = ['config', '--get', 'remote.origin.url']
1784 v8_repo_url = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1786 if 'external/v8.git' in v8_repo_url:
1787 v8_branch = 'origin/master'
1788 bleeding_edge_branch = 'origin/bleeding_edge'
1790 r1 = self._GetNearestV8BleedingEdgeFromTrunk(
1791 min_revision_state.revision,
1792 v8_branch,
1793 bleeding_edge_branch,
1794 search_forward=True)
1795 r2 = self._GetNearestV8BleedingEdgeFromTrunk(
1796 max_revision_state.revision,
1797 v8_branch,
1798 bleeding_edge_branch,
1799 search_forward=False)
1800 min_revision_state.external['v8_bleeding_edge'] = r1
1801 max_revision_state.external['v8_bleeding_edge'] = r2
1803 if (not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1804 min_revision_state.revision, bleeding_edge_branch)
1805 or not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1806 max_revision_state.revision, bleeding_edge_branch)):
1807 self.warnings.append(
1808 'Trunk revisions in V8 did not map directly to bleeding_edge. '
1809 'Attempted to expand the range to find V8 rolls which did map '
1810 'directly to bleeding_edge revisions, but results might not be '
1811 'valid.')
1813 def _FindNextDepotToBisect(
1814 self, current_depot, min_revision_state, max_revision_state):
1815 """Decides which depot the script should dive into next (if any).
1817 Args:
1818 current_depot: Current depot being bisected.
1819 min_revision_state: State of the earliest revision in the bisect range.
1820 max_revision_state: State of the latest revision in the bisect range.
1822 Returns:
1823 Name of the depot to bisect next, or None.
1825 external_depot = None
1826 for next_depot in bisect_utils.DEPOT_NAMES:
1827 if ('platform' in bisect_utils.DEPOT_DEPS_NAME[next_depot] and
1828 bisect_utils.DEPOT_DEPS_NAME[next_depot]['platform'] != os.name):
1829 continue
1831 if not (bisect_utils.DEPOT_DEPS_NAME[next_depot]['recurse']
1832 and min_revision_state.depot
1833 in bisect_utils.DEPOT_DEPS_NAME[next_depot]['from']):
1834 continue
1836 if current_depot == 'v8':
1837 # We grab the bleeding_edge info here rather than earlier because we
1838 # finally have the revision range. From that we can search forwards and
1839 # backwards to try to match trunk revisions to bleeding_edge.
1840 self._FillInV8BleedingEdgeInfo(min_revision_state, max_revision_state)
1842 if (min_revision_state.external.get(next_depot) ==
1843 max_revision_state.external.get(next_depot)):
1844 continue
1846 if (min_revision_state.external.get(next_depot) and
1847 max_revision_state.external.get(next_depot)):
1848 external_depot = next_depot
1849 break
1851 return external_depot
1853 def PrepareToBisectOnDepot(
1854 self, current_depot, start_revision, end_revision, previous_revision):
1855 """Changes to the appropriate directory and gathers a list of revisions
1856 to bisect between |start_revision| and |end_revision|.
1858 Args:
1859 current_depot: The depot we want to bisect.
1860 start_revision: Start of the revision range.
1861 end_revision: End of the revision range.
1862 previous_revision: The last revision we synced to on |previous_depot|.
1864 Returns:
1865 A list containing the revisions between |start_revision| and
1866 |end_revision| inclusive.
1868 # Change into working directory of external library to run
1869 # subsequent commands.
1870 self.depot_registry.ChangeToDepotDir(current_depot)
1872 # V8 (and possibly others) is merged in periodically. Bisecting
1873 # this directory directly won't give much good info.
1874 if 'custom_deps' in bisect_utils.DEPOT_DEPS_NAME[current_depot]:
1875 config_path = os.path.join(self.src_cwd, '..')
1876 if bisect_utils.RunGClientAndCreateConfig(
1877 self.opts, bisect_utils.DEPOT_DEPS_NAME[current_depot]['custom_deps'],
1878 cwd=config_path):
1879 return []
1880 if bisect_utils.RunGClient(
1881 ['sync', '--revision', previous_revision], cwd=self.src_cwd):
1882 return []
1884 if current_depot == 'v8_bleeding_edge':
1885 self.depot_registry.ChangeToDepotDir('chromium')
1887 shutil.move('v8', 'v8.bak')
1888 shutil.move('v8_bleeding_edge', 'v8')
1890 self.cleanup_commands.append(['mv', 'v8', 'v8_bleeding_edge'])
1891 self.cleanup_commands.append(['mv', 'v8.bak', 'v8'])
1893 self.depot_registry.SetDepotDir(
1894 'v8_bleeding_edge', os.path.join(self.src_cwd, 'v8'))
1895 self.depot_registry.SetDepotDir(
1896 'v8', os.path.join(self.src_cwd, 'v8.bak'))
1898 self.depot_registry.ChangeToDepotDir(current_depot)
1900 depot_revision_list = self.GetRevisionList(current_depot,
1901 end_revision,
1902 start_revision)
1904 self.depot_registry.ChangeToDepotDir('chromium')
1906 return depot_revision_list
1908 def GatherReferenceValues(self, good_rev, bad_rev, cmd, metric, target_depot):
1909 """Gathers reference values by running the performance tests on the
1910 known good and bad revisions.
1912 Args:
1913 good_rev: The last known good revision where the performance regression
1914 has not occurred yet.
1915 bad_rev: A revision where the performance regression has already occurred.
1916 cmd: The command to execute the performance test.
1917 metric: The metric being tested for regression.
1919 Returns:
1920 A tuple with the results of building and running each revision.
1922 bad_run_results = self.RunTest(bad_rev, target_depot, cmd, metric)
1924 good_run_results = None
1926 if not bad_run_results[1]:
1927 good_run_results = self.RunTest(good_rev, target_depot, cmd, metric)
1929 return (bad_run_results, good_run_results)
1931 def PrintRevisionsToBisectMessage(self, revision_list, depot):
1932 if self.opts.output_buildbot_annotations:
1933 step_name = 'Bisection Range: [%s:%s - %s]' % (depot, revision_list[-1],
1934 revision_list[0])
1935 bisect_utils.OutputAnnotationStepStart(step_name)
1937 print
1938 print 'Revisions to bisect on [%s]:' % depot
1939 for revision_id in revision_list:
1940 print ' -> %s' % (revision_id, )
1941 print
1943 if self.opts.output_buildbot_annotations:
1944 bisect_utils.OutputAnnotationStepClosed()
1946 def NudgeRevisionsIfDEPSChange(self, bad_revision, good_revision,
1947 good_svn_revision=None):
1948 """Checks to see if changes to DEPS file occurred, and that the revision
1949 range also includes the change to .DEPS.git. If it doesn't, attempts to
1950 expand the revision range to include it.
1952 Args:
1953 bad_revision: First known bad git revision.
1954 good_revision: Last known good git revision.
1955 good_svn_revision: Last known good svn revision.
1957 Returns:
1958 A tuple with the new bad and good revisions.
1960 # DONOT perform nudge because at revision 291563 .DEPS.git was removed
1961 # and source contain only DEPS file for dependency changes.
1962 if good_svn_revision >= 291563:
1963 return (bad_revision, good_revision)
1965 if self.opts.target_platform == 'chromium':
1966 changes_to_deps = source_control.QueryFileRevisionHistory(
1967 bisect_utils.FILE_DEPS, good_revision, bad_revision)
1969 if changes_to_deps:
1970 # DEPS file was changed, search from the oldest change to DEPS file to
1971 # bad_revision to see if there are matching .DEPS.git changes.
1972 oldest_deps_change = changes_to_deps[-1]
1973 changes_to_gitdeps = source_control.QueryFileRevisionHistory(
1974 bisect_utils.FILE_DEPS_GIT, oldest_deps_change, bad_revision)
1976 if len(changes_to_deps) != len(changes_to_gitdeps):
1977 # Grab the timestamp of the last DEPS change
1978 cmd = ['log', '--format=%ct', '-1', changes_to_deps[0]]
1979 output = bisect_utils.CheckRunGit(cmd)
1980 commit_time = int(output)
1982 # Try looking for a commit that touches the .DEPS.git file in the
1983 # next 15 minutes after the DEPS file change.
1984 cmd = [
1985 'log', '--format=%H', '-1',
1986 '--before=%d' % (commit_time + 900),
1987 '--after=%d' % commit_time,
1988 'origin/master', '--', bisect_utils.FILE_DEPS_GIT
1990 output = bisect_utils.CheckRunGit(cmd)
1991 output = output.strip()
1992 if output:
1993 self.warnings.append(
1994 'Detected change to DEPS and modified '
1995 'revision range to include change to .DEPS.git')
1996 return (output, good_revision)
1997 else:
1998 self.warnings.append(
1999 'Detected change to DEPS but couldn\'t find '
2000 'matching change to .DEPS.git')
2001 return (bad_revision, good_revision)
2003 def CheckIfRevisionsInProperOrder(
2004 self, target_depot, good_revision, bad_revision):
2005 """Checks that |good_revision| is an earlier revision than |bad_revision|.
2007 Args:
2008 good_revision: Number/tag of the known good revision.
2009 bad_revision: Number/tag of the known bad revision.
2011 Returns:
2012 True if the revisions are in the proper order (good earlier than bad).
2014 cwd = self.depot_registry.GetDepotDir(target_depot)
2015 good_position = source_control.GetCommitPosition(good_revision, cwd)
2016 bad_position = source_control.GetCommitPosition(bad_revision, cwd)
2017 # Compare commit timestamp for repos that don't support commit position.
2018 if not (bad_position and good_position):
2019 logging.info('Could not get commit positions for revisions %s and %s in '
2020 'depot %s', good_position, bad_position, target_depot)
2021 good_position = source_control.GetCommitTime(good_revision, cwd=cwd)
2022 bad_position = source_control.GetCommitTime(bad_revision, cwd=cwd)
2024 return good_position <= bad_position
2026 def CanPerformBisect(self, good_revision, bad_revision):
2027 """Checks whether a given revision is bisectable.
2029 Checks for following:
2030 1. Non-bisectable revisions for android bots (refer to crbug.com/385324).
2031 2. Non-bisectable revisions for Windows bots (refer to crbug.com/405274).
2033 Args:
2034 good_revision: Known good revision.
2035 bad_revision: Known bad revision.
2037 Returns:
2038 A dictionary indicating the result. If revision is not bisectable,
2039 this will contain the field "error", otherwise None.
2041 if self.opts.target_platform == 'android':
2042 good_revision = source_control.GetCommitPosition(good_revision)
2043 if (bisect_utils.IsStringInt(good_revision)
2044 and good_revision < 265549):
2045 return {'error': (
2046 'Bisect cannot continue for the given revision range.\n'
2047 'It is impossible to bisect Android regressions '
2048 'prior to r265549, which allows the bisect bot to '
2049 'rely on Telemetry to do apk installation of the most recently '
2050 'built local ChromeShell(refer to crbug.com/385324).\n'
2051 'Please try bisecting revisions greater than or equal to r265549.')}
2053 if bisect_utils.IsWindowsHost():
2054 good_revision = source_control.GetCommitPosition(good_revision)
2055 bad_revision = source_control.GetCommitPosition(bad_revision)
2056 if (bisect_utils.IsStringInt(good_revision) and
2057 bisect_utils.IsStringInt(bad_revision)):
2058 if (289987 <= good_revision < 290716 or
2059 289987 <= bad_revision < 290716):
2060 return {'error': ('Oops! Revision between r289987 and r290716 are '
2061 'marked as dead zone for Windows due to '
2062 'crbug.com/405274. Please try another range.')}
2064 return None
2066 def _GatherResultsFromRevertedCulpritCL(
2067 self, results, target_depot, command_to_run, metric):
2068 """Gathers performance results with/without culprit CL.
2070 Attempts to revert the culprit CL against ToT and runs the
2071 performance tests again with and without the CL, adding the results to
2072 the over bisect results.
2074 Args:
2075 results: BisectResults from the bisect.
2076 target_depot: The target depot we're bisecting.
2077 command_to_run: Specify the command to execute the performance test.
2078 metric: The performance metric to monitor.
2080 run_results_tot, run_results_reverted = self._RevertCulpritCLAndRetest(
2081 results, target_depot, command_to_run, metric)
2083 results.AddRetestResults(run_results_tot, run_results_reverted)
2085 if len(results.culprit_revisions) != 1:
2086 return
2088 # Cleanup reverted files if anything is left.
2089 _, _, culprit_depot = results.culprit_revisions[0]
2090 bisect_utils.CheckRunGit(
2091 ['reset', '--hard', 'HEAD'],
2092 cwd=self.depot_registry.GetDepotDir(culprit_depot))
2094 def _RevertCL(self, culprit_revision, culprit_depot):
2095 """Reverts the specified revision in the specified depot."""
2096 if self.opts.output_buildbot_annotations:
2097 bisect_utils.OutputAnnotationStepStart(
2098 'Reverting culprit CL: %s' % culprit_revision)
2099 _, return_code = bisect_utils.RunGit(
2100 ['revert', '--no-commit', culprit_revision],
2101 cwd=self.depot_registry.GetDepotDir(culprit_depot))
2102 if return_code:
2103 bisect_utils.OutputAnnotationStepWarning()
2104 bisect_utils.OutputAnnotationStepText('Failed to revert CL cleanly.')
2105 if self.opts.output_buildbot_annotations:
2106 bisect_utils.OutputAnnotationStepClosed()
2107 return not return_code
2109 def _RevertCulpritCLAndRetest(
2110 self, results, target_depot, command_to_run, metric):
2111 """Reverts the culprit CL against ToT and runs the performance test.
2113 Attempts to revert the culprit CL against ToT and runs the
2114 performance tests again with and without the CL.
2116 Args:
2117 results: BisectResults from the bisect.
2118 target_depot: The target depot we're bisecting.
2119 command_to_run: Specify the command to execute the performance test.
2120 metric: The performance metric to monitor.
2122 Returns:
2123 A tuple with the results of running the CL at ToT/reverted.
2125 # Might want to retest ToT with a revert of the CL to confirm that
2126 # performance returns.
2127 if results.confidence < bisect_utils.HIGH_CONFIDENCE:
2128 return (None, None)
2130 # If there were multiple culprit CLs, we won't try to revert.
2131 if len(results.culprit_revisions) != 1:
2132 return (None, None)
2134 culprit_revision, _, culprit_depot = results.culprit_revisions[0]
2136 if not self._SyncRevision(target_depot, None, 'gclient'):
2137 return (None, None)
2139 head_revision = bisect_utils.CheckRunGit(['log', '--format=%H', '-1'])
2140 head_revision = head_revision.strip()
2142 if not self._RevertCL(culprit_revision, culprit_depot):
2143 return (None, None)
2145 # If the culprit CL happened to be in a depot that gets pulled in, we
2146 # can't revert the change and issue a try job to build, since that would
2147 # require modifying both the DEPS file and files in another depot.
2148 # Instead, we build locally.
2149 force_build = (culprit_depot != target_depot)
2150 if force_build:
2151 results.warnings.append(
2152 'Culprit CL is in another depot, attempting to revert and build'
2153 ' locally to retest. This may not match the performance of official'
2154 ' builds.')
2156 run_results_reverted = self._RunTestWithAnnotations(
2157 'Re-Testing ToT with reverted culprit',
2158 'Failed to run reverted CL.',
2159 head_revision, target_depot, command_to_run, metric, force_build)
2161 # Clear the reverted file(s).
2162 bisect_utils.RunGit(
2163 ['reset', '--hard', 'HEAD'],
2164 cwd=self.depot_registry.GetDepotDir(culprit_depot))
2166 # Retesting with the reverted CL failed, so bail out of retesting against
2167 # ToT.
2168 if run_results_reverted[1]:
2169 return (None, None)
2171 run_results_tot = self._RunTestWithAnnotations(
2172 'Re-Testing ToT',
2173 'Failed to run ToT.',
2174 head_revision, target_depot, command_to_run, metric, force_build)
2176 return (run_results_tot, run_results_reverted)
2178 def _RunTestWithAnnotations(
2179 self, step_text, error_text, head_revision,
2180 target_depot, command_to_run, metric, force_build):
2181 """Runs the performance test and outputs start/stop annotations.
2183 Args:
2184 results: BisectResults from the bisect.
2185 target_depot: The target depot we're bisecting.
2186 command_to_run: Specify the command to execute the performance test.
2187 metric: The performance metric to monitor.
2188 force_build: Whether to force a build locally.
2190 Returns:
2191 Results of the test.
2193 if self.opts.output_buildbot_annotations:
2194 bisect_utils.OutputAnnotationStepStart(step_text)
2196 # Build and run the test again with the reverted culprit CL against ToT.
2197 run_test_results = self.RunTest(
2198 head_revision, target_depot, command_to_run,
2199 metric, skippable=False, skip_sync=True, create_patch=True,
2200 force_build=force_build)
2202 if self.opts.output_buildbot_annotations:
2203 if run_test_results[1]:
2204 bisect_utils.OutputAnnotationStepWarning()
2205 bisect_utils.OutputAnnotationStepText(error_text)
2206 bisect_utils.OutputAnnotationStepClosed()
2208 return run_test_results
2210 def Run(self, command_to_run, bad_revision_in, good_revision_in, metric):
2211 """Given known good and bad revisions, run a binary search on all
2212 intermediate revisions to determine the CL where the performance regression
2213 occurred.
2215 Args:
2216 command_to_run: Specify the command to execute the performance test.
2217 good_revision: Number/tag of the known good revision.
2218 bad_revision: Number/tag of the known bad revision.
2219 metric: The performance metric to monitor.
2221 Returns:
2222 A BisectResults object.
2224 # Choose depot to bisect first
2225 target_depot = 'chromium'
2226 if self.opts.target_platform == 'android-chrome':
2227 target_depot = 'android-chrome'
2229 cwd = os.getcwd()
2230 self.depot_registry.ChangeToDepotDir(target_depot)
2232 # If they passed SVN revisions, we can try match them to git SHA1 hashes.
2233 bad_revision = source_control.ResolveToRevision(
2234 bad_revision_in, target_depot, bisect_utils.DEPOT_DEPS_NAME, 100)
2235 good_revision = source_control.ResolveToRevision(
2236 good_revision_in, target_depot, bisect_utils.DEPOT_DEPS_NAME, -100)
2238 os.chdir(cwd)
2239 if bad_revision is None:
2240 return BisectResults(
2241 error='Couldn\'t resolve [%s] to SHA1.' % bad_revision_in)
2243 if good_revision is None:
2244 return BisectResults(
2245 error='Couldn\'t resolve [%s] to SHA1.' % good_revision_in)
2247 # Check that they didn't accidentally swap good and bad revisions.
2248 if not self.CheckIfRevisionsInProperOrder(
2249 target_depot, good_revision, bad_revision):
2250 return BisectResults(error='Bad rev (%s) appears to be earlier than good '
2251 'rev (%s).' % (good_revision, bad_revision))
2253 bad_revision, good_revision = self.NudgeRevisionsIfDEPSChange(
2254 bad_revision, good_revision, good_revision_in)
2255 if self.opts.output_buildbot_annotations:
2256 bisect_utils.OutputAnnotationStepStart('Gathering Revisions')
2258 cannot_bisect = self.CanPerformBisect(good_revision, bad_revision)
2259 if cannot_bisect:
2260 return BisectResults(error=cannot_bisect.get('error'))
2262 print 'Gathering revision range for bisection.'
2263 # Retrieve a list of revisions to do bisection on.
2264 revision_list = self.GetRevisionList(target_depot, bad_revision,
2265 good_revision)
2267 if self.opts.output_buildbot_annotations:
2268 bisect_utils.OutputAnnotationStepClosed()
2270 if revision_list:
2271 self.PrintRevisionsToBisectMessage(revision_list, target_depot)
2273 if self.opts.output_buildbot_annotations:
2274 bisect_utils.OutputAnnotationStepStart('Gathering Reference Values')
2276 print 'Gathering reference values for bisection.'
2278 # Perform the performance tests on the good and bad revisions, to get
2279 # reference values.
2280 bad_results, good_results = self.GatherReferenceValues(good_revision,
2281 bad_revision,
2282 command_to_run,
2283 metric,
2284 target_depot)
2286 if self.opts.output_buildbot_annotations:
2287 bisect_utils.OutputAnnotationStepClosed()
2289 if bad_results[1]:
2290 error = ('An error occurred while building and running the \'bad\' '
2291 'reference value. The bisect cannot continue without '
2292 'a working \'bad\' revision to start from.\n\nError: %s' %
2293 bad_results[0])
2294 return BisectResults(error=error)
2296 if good_results[1]:
2297 error = ('An error occurred while building and running the \'good\' '
2298 'reference value. The bisect cannot continue without '
2299 'a working \'good\' revision to start from.\n\nError: %s' %
2300 good_results[0])
2301 return BisectResults(error=error)
2303 # We need these reference values to determine if later runs should be
2304 # classified as pass or fail.
2306 known_bad_value = bad_results[0]
2307 known_good_value = good_results[0]
2309 # Abort bisect early when the return codes for known good
2310 # and known bad revisions are same.
2311 if (self._IsBisectModeReturnCode() and
2312 known_bad_value['mean'] == known_good_value['mean']):
2313 return BisectResults(abort_reason=('known good and known bad revisions '
2314 'returned same return code (return code=%s). '
2315 'Continuing bisect might not yield any results.' %
2316 known_bad_value['mean']))
2317 # Check the direction of improvement only if the improvement_direction
2318 # option is set to a specific direction (1 for higher is better or -1 for
2319 # lower is better).
2320 improvement_dir = self.opts.improvement_direction
2321 if improvement_dir:
2322 higher_is_better = improvement_dir > 0
2323 if higher_is_better:
2324 message = "Expecting higher values to be better for this metric, "
2325 else:
2326 message = "Expecting lower values to be better for this metric, "
2327 metric_increased = known_bad_value['mean'] > known_good_value['mean']
2328 if metric_increased:
2329 message += "and the metric appears to have increased. "
2330 else:
2331 message += "and the metric appears to have decreased. "
2332 if ((higher_is_better and metric_increased) or
2333 (not higher_is_better and not metric_increased)):
2334 error = (message + 'Then, the test results for the ends of the given '
2335 '\'good\' - \'bad\' range of revisions represent an '
2336 'improvement (and not a regression).')
2337 return BisectResults(error=error)
2338 logging.info(message + "Therefore we continue to bisect.")
2340 bisect_state = BisectState(target_depot, revision_list)
2341 revision_states = bisect_state.GetRevisionStates()
2343 min_revision = 0
2344 max_revision = len(revision_states) - 1
2346 # Can just mark the good and bad revisions explicitly here since we
2347 # already know the results.
2348 bad_revision_state = revision_states[min_revision]
2349 bad_revision_state.external = bad_results[2]
2350 bad_revision_state.perf_time = bad_results[3]
2351 bad_revision_state.build_time = bad_results[4]
2352 bad_revision_state.passed = False
2353 bad_revision_state.value = known_bad_value
2355 good_revision_state = revision_states[max_revision]
2356 good_revision_state.external = good_results[2]
2357 good_revision_state.perf_time = good_results[3]
2358 good_revision_state.build_time = good_results[4]
2359 good_revision_state.passed = True
2360 good_revision_state.value = known_good_value
2362 # Check how likely it is that the good and bad results are different
2363 # beyond chance-induced variation.
2364 if not (self.opts.debug_ignore_regression_confidence or
2365 self._IsBisectModeReturnCode()):
2366 if not _IsRegressionReproduced(known_good_value, known_bad_value):
2367 # If there is no significant difference between "good" and "bad"
2368 # revision results, then the "bad revision" is considered "good".
2369 # TODO(qyearsley): Remove this if it is not necessary.
2370 bad_revision_state.passed = True
2371 self.warnings.append(_RegressionNotReproducedWarningMessage(
2372 good_revision, bad_revision, known_good_value, known_bad_value))
2373 return BisectResults(bisect_state, self.depot_registry, self.opts,
2374 self.warnings)
2376 while True:
2377 if not revision_states:
2378 break
2380 if max_revision - min_revision <= 1:
2381 min_revision_state = revision_states[min_revision]
2382 max_revision_state = revision_states[max_revision]
2383 current_depot = min_revision_state.depot
2384 # TODO(sergiyb): Under which conditions can first two branches be hit?
2385 if min_revision_state.passed == '?':
2386 next_revision_index = min_revision
2387 elif max_revision_state.passed == '?':
2388 next_revision_index = max_revision
2389 elif current_depot in ['android-chrome', 'chromium', 'v8']:
2390 previous_revision = revision_states[min_revision].revision
2391 # If there were changes to any of the external libraries we track,
2392 # should bisect the changes there as well.
2393 external_depot = self._FindNextDepotToBisect(
2394 current_depot, min_revision_state, max_revision_state)
2395 # If there was no change in any of the external depots, the search
2396 # is over.
2397 if not external_depot:
2398 if current_depot == 'v8':
2399 self.warnings.append(
2400 'Unfortunately, V8 bisection couldn\'t '
2401 'continue any further. The script can only bisect into '
2402 'V8\'s bleeding_edge repository if both the current and '
2403 'previous revisions in trunk map directly to revisions in '
2404 'bleeding_edge.')
2405 break
2407 earliest_revision = max_revision_state.external[external_depot]
2408 latest_revision = min_revision_state.external[external_depot]
2410 new_revision_list = self.PrepareToBisectOnDepot(
2411 external_depot, earliest_revision, latest_revision,
2412 previous_revision)
2414 if not new_revision_list:
2415 error = ('An error occurred attempting to retrieve revision '
2416 'range: [%s..%s]' % (earliest_revision, latest_revision))
2417 return BisectResults(error=error)
2419 revision_states = bisect_state.CreateRevisionStatesAfter(
2420 external_depot, new_revision_list, current_depot,
2421 previous_revision)
2423 # Reset the bisection and perform it on the newly inserted states.
2424 min_revision = 0
2425 max_revision = len(revision_states) - 1
2427 print ('Regression in metric %s appears to be the result of '
2428 'changes in [%s].' % (metric, external_depot))
2430 revision_list = [state.revision for state in revision_states]
2431 self.PrintRevisionsToBisectMessage(revision_list, external_depot)
2433 continue
2434 else:
2435 break
2436 else:
2437 next_revision_index = (int((max_revision - min_revision) / 2) +
2438 min_revision)
2440 next_revision_state = revision_states[next_revision_index]
2441 next_revision = next_revision_state.revision
2442 next_depot = next_revision_state.depot
2444 self.depot_registry.ChangeToDepotDir(next_depot)
2446 message = 'Working on [%s:%s]' % (next_depot, next_revision)
2447 print message
2448 if self.opts.output_buildbot_annotations:
2449 bisect_utils.OutputAnnotationStepStart(message)
2451 run_results = self.RunTest(next_revision, next_depot, command_to_run,
2452 metric, skippable=True)
2454 # If the build is successful, check whether or not the metric
2455 # had regressed.
2456 if not run_results[1]:
2457 if len(run_results) > 2:
2458 next_revision_state.external = run_results[2]
2459 next_revision_state.perf_time = run_results[3]
2460 next_revision_state.build_time = run_results[4]
2462 passed_regression = self._CheckIfRunPassed(run_results[0],
2463 known_good_value,
2464 known_bad_value)
2466 next_revision_state.passed = passed_regression
2467 next_revision_state.value = run_results[0]
2469 if passed_regression:
2470 max_revision = next_revision_index
2471 else:
2472 min_revision = next_revision_index
2473 else:
2474 if run_results[1] == BUILD_RESULT_SKIPPED:
2475 next_revision_state.passed = 'Skipped'
2476 elif run_results[1] == BUILD_RESULT_FAIL:
2477 next_revision_state.passed = 'Build Failed'
2479 print run_results[0]
2481 # If the build is broken, remove it and redo search.
2482 revision_states.pop(next_revision_index)
2484 max_revision -= 1
2486 if self.opts.output_buildbot_annotations:
2487 self.printer.PrintPartialResults(bisect_state)
2488 bisect_utils.OutputAnnotationStepClosed()
2490 self._ConfidenceExtraTestRuns(min_revision_state, max_revision_state,
2491 command_to_run, metric)
2492 results = BisectResults(bisect_state, self.depot_registry, self.opts,
2493 self.warnings)
2495 self._GatherResultsFromRevertedCulpritCL(
2496 results, target_depot, command_to_run, metric)
2498 return results
2499 else:
2500 # Weren't able to sync and retrieve the revision range.
2501 error = ('An error occurred attempting to retrieve revision range: '
2502 '[%s..%s]' % (good_revision, bad_revision))
2503 return BisectResults(error=error)
2505 def _ConfidenceExtraTestRuns(self, good_state, bad_state, command_to_run,
2506 metric):
2507 if (bool(good_state.passed) != bool(bad_state.passed)
2508 and good_state.passed not in ('Skipped', 'Build Failed')
2509 and bad_state.passed not in ('Skipped', 'Build Failed')):
2510 for state in (good_state, bad_state):
2511 run_results = self.RunTest(
2512 state.revision,
2513 state.depot,
2514 command_to_run,
2515 metric,
2516 test_run_multiplier=BORDER_REVISIONS_EXTRA_RUNS)
2517 # Is extend the right thing to do here?
2518 if run_results[1] != BUILD_RESULT_FAIL:
2519 state.value['values'].extend(run_results[0]['values'])
2520 else:
2521 warning_text = 'Re-test of revision %s failed with error message: %s'
2522 warning_text %= (state.revision, run_results[0])
2523 if warning_text not in self.warnings:
2524 self.warnings.append(warning_text)
2527 def _IsPlatformSupported():
2528 """Checks that this platform and build system are supported.
2530 Args:
2531 opts: The options parsed from the command line.
2533 Returns:
2534 True if the platform and build system are supported.
2536 # Haven't tested the script out on any other platforms yet.
2537 supported = ['posix', 'nt']
2538 return os.name in supported
2541 def RemoveBuildFiles(build_type):
2542 """Removes build files from previous runs."""
2543 out_dir = os.path.join('out', build_type)
2544 build_dir = os.path.join('build', build_type)
2545 logging.info('Removing build files in "%s" and "%s".',
2546 os.path.abspath(out_dir), os.path.abspath(build_dir))
2547 try:
2548 RemakeDirectoryTree(out_dir)
2549 RemakeDirectoryTree(build_dir)
2550 except Exception as e:
2551 raise RuntimeError('Got error in RemoveBuildFiles: %s' % e)
2554 def RemakeDirectoryTree(path_to_dir):
2555 """Removes a directory tree and replaces it with an empty one.
2557 Returns True if successful, False otherwise.
2559 RemoveDirectoryTree(path_to_dir)
2560 MaybeMakeDirectory(path_to_dir)
2563 def RemoveDirectoryTree(path_to_dir):
2564 """Removes a directory tree. Returns True if successful or False otherwise."""
2565 if os.path.isfile(path_to_dir):
2566 logging.info('REMOVING FILE %s' % path_to_dir)
2567 os.remove(path_to_dir)
2568 try:
2569 if os.path.exists(path_to_dir):
2570 shutil.rmtree(path_to_dir)
2571 except OSError, e:
2572 if e.errno != errno.ENOENT:
2573 raise
2576 # This is copied from build/scripts/common/chromium_utils.py.
2577 def MaybeMakeDirectory(*path):
2578 """Creates an entire path, if it doesn't already exist."""
2579 file_path = os.path.join(*path)
2580 try:
2581 os.makedirs(file_path)
2582 except OSError as e:
2583 if e.errno != errno.EEXIST:
2584 raise
2587 class BisectOptions(object):
2588 """Options to be used when running bisection."""
2589 def __init__(self):
2590 super(BisectOptions, self).__init__()
2592 self.target_platform = 'chromium'
2593 self.build_preference = None
2594 self.good_revision = None
2595 self.bad_revision = None
2596 self.use_goma = None
2597 self.goma_dir = None
2598 self.goma_threads = 64
2599 self.repeat_test_count = 20
2600 self.truncate_percent = 25
2601 self.max_time_minutes = 20
2602 self.metric = None
2603 self.command = None
2604 self.output_buildbot_annotations = None
2605 self.no_custom_deps = False
2606 self.working_directory = None
2607 self.extra_src = None
2608 self.debug_ignore_build = None
2609 self.debug_ignore_sync = None
2610 self.debug_ignore_perf_test = None
2611 self.debug_ignore_regression_confidence = None
2612 self.debug_fake_first_test_mean = 0
2613 self.target_arch = 'ia32'
2614 self.target_build_type = 'Release'
2615 self.builder_type = 'perf'
2616 self.bisect_mode = bisect_utils.BISECT_MODE_MEAN
2617 self.improvement_direction = 0
2618 self.bug_id = ''
2620 @staticmethod
2621 def _AddBisectOptionsGroup(parser):
2622 group = parser.add_argument_group('Bisect options')
2623 group.add_argument('-c', '--command', required=True,
2624 help='A command to execute your performance test at '
2625 'each point in the bisection.')
2626 group.add_argument('-b', '--bad_revision', required=True,
2627 help='A bad revision to start bisection. Must be later '
2628 'than good revision. May be either a git or svn '
2629 'revision.')
2630 group.add_argument('-g', '--good_revision', required=True,
2631 help='A revision to start bisection where performance '
2632 'test is known to pass. Must be earlier than the '
2633 'bad revision. May be either a git or a svn '
2634 'revision.')
2635 group.add_argument('-m', '--metric',
2636 help='The desired metric to bisect on. For example '
2637 '"vm_rss_final_b/vm_rss_f_b"')
2638 group.add_argument('-d', '--improvement_direction', type=int, default=0,
2639 help='An integer number representing the direction of '
2640 'improvement. 1 for higher is better, -1 for lower '
2641 'is better, 0 for ignore (default).')
2642 group.add_argument('-r', '--repeat_test_count', type=int, default=20,
2643 choices=range(1, 101),
2644 help='The number of times to repeat the performance '
2645 'test. Values will be clamped to range [1, 100]. '
2646 'Default value is 20.')
2647 group.add_argument('--max_time_minutes', type=int, default=20,
2648 choices=range(1, 61),
2649 help='The maximum time (in minutes) to take running the '
2650 'performance tests. The script will run the '
2651 'performance tests according to '
2652 '--repeat_test_count, so long as it doesn\'t exceed'
2653 ' --max_time_minutes. Values will be clamped to '
2654 'range [1, 60]. Default value is 20.')
2655 group.add_argument('-t', '--truncate_percent', type=int, default=25,
2656 help='The highest/lowest percent are discarded to form '
2657 'a truncated mean. Values will be clamped to range '
2658 '[0, 25]. Default value is 25 percent.')
2659 group.add_argument('--bisect_mode', default=bisect_utils.BISECT_MODE_MEAN,
2660 choices=[bisect_utils.BISECT_MODE_MEAN,
2661 bisect_utils.BISECT_MODE_STD_DEV,
2662 bisect_utils.BISECT_MODE_RETURN_CODE],
2663 help='The bisect mode. Choices are to bisect on the '
2664 'difference in mean, std_dev, or return_code.')
2665 group.add_argument('--bug_id', default='',
2666 help='The id for the bug associated with this bisect. ' +
2667 'If this number is given, bisect will attempt to ' +
2668 'verify that the bug is not closed before '
2669 'starting.')
2671 @staticmethod
2672 def _AddBuildOptionsGroup(parser):
2673 group = parser.add_argument_group('Build options')
2674 group.add_argument('-w', '--working_directory',
2675 help='Path to the working directory where the script '
2676 'will do an initial checkout of the chromium depot. The '
2677 'files will be placed in a subdirectory "bisect" under '
2678 'working_directory and that will be used to perform the '
2679 'bisection. This parameter is optional, if it is not '
2680 'supplied, the script will work from the current depot.')
2681 group.add_argument('--build_preference',
2682 choices=['msvs', 'ninja', 'make'],
2683 help='The preferred build system to use. On linux/mac '
2684 'the options are make/ninja. On Windows, the '
2685 'options are msvs/ninja.')
2686 group.add_argument('--target_platform', default='chromium',
2687 choices=['chromium', 'android', 'android-chrome'],
2688 help='The target platform. Choices are "chromium" '
2689 '(current platform), or "android". If you specify '
2690 'something other than "chromium", you must be '
2691 'properly set up to build that platform.')
2692 group.add_argument('--no_custom_deps', dest='no_custom_deps',
2693 action='store_true', default=False,
2694 help='Run the script with custom_deps or not.')
2695 group.add_argument('--extra_src',
2696 help='Path to a script which can be used to modify the '
2697 'bisect script\'s behavior.')
2698 group.add_argument('--use_goma', action='store_true',
2699 help='Add a bunch of extra threads for goma, and enable '
2700 'goma')
2701 group.add_argument('--goma_dir',
2702 help='Path to goma tools (or system default if not '
2703 'specified).')
2704 group.add_argument('--goma_threads', type=int, default='64',
2705 help='Number of threads for goma, only if using goma.')
2706 group.add_argument('--output_buildbot_annotations', action='store_true',
2707 help='Add extra annotation output for buildbot.')
2708 group.add_argument('--target_arch', default='ia32',
2709 dest='target_arch',
2710 choices=['ia32', 'x64', 'arm', 'arm64'],
2711 help='The target build architecture. Choices are "ia32" '
2712 '(default), "x64", "arm" or "arm64".')
2713 group.add_argument('--target_build_type', default='Release',
2714 choices=['Release', 'Debug', 'Release_x64'],
2715 help='The target build type. Choices are "Release" '
2716 '(default), Release_x64 or "Debug".')
2717 group.add_argument('--builder_type', default=fetch_build.PERF_BUILDER,
2718 choices=[fetch_build.PERF_BUILDER,
2719 fetch_build.FULL_BUILDER,
2720 fetch_build.ANDROID_CHROME_PERF_BUILDER, ''],
2721 help='Type of builder to get build from. This '
2722 'determines both the bot that builds and the '
2723 'place where archived builds are downloaded from. '
2724 'For local builds, an empty string can be passed.')
2726 @staticmethod
2727 def _AddDebugOptionsGroup(parser):
2728 group = parser.add_argument_group('Debug options')
2729 group.add_argument('--debug_ignore_build', action='store_true',
2730 help='DEBUG: Don\'t perform builds.')
2731 group.add_argument('--debug_ignore_sync', action='store_true',
2732 help='DEBUG: Don\'t perform syncs.')
2733 group.add_argument('--debug_ignore_perf_test', action='store_true',
2734 help='DEBUG: Don\'t perform performance tests.')
2735 group.add_argument('--debug_ignore_regression_confidence',
2736 action='store_true',
2737 help='DEBUG: Don\'t score the confidence of the initial '
2738 'good and bad revisions\' test results.')
2739 group.add_argument('--debug_fake_first_test_mean', type=int, default='0',
2740 help='DEBUG: When faking performance tests, return this '
2741 'value as the mean of the first performance test, '
2742 'and return a mean of 0.0 for further tests.')
2743 return group
2745 @classmethod
2746 def _CreateCommandLineParser(cls):
2747 """Creates a parser with bisect options.
2749 Returns:
2750 An instance of argparse.ArgumentParser.
2752 usage = ('%(prog)s [options] [-- chromium-options]\n'
2753 'Perform binary search on revision history to find a minimal '
2754 'range of revisions where a performance metric regressed.\n')
2756 parser = argparse.ArgumentParser(usage=usage)
2757 cls._AddBisectOptionsGroup(parser)
2758 cls._AddBuildOptionsGroup(parser)
2759 cls._AddDebugOptionsGroup(parser)
2760 return parser
2762 def ParseCommandLine(self):
2763 """Parses the command line for bisect options."""
2764 parser = self._CreateCommandLineParser()
2765 opts = parser.parse_args()
2767 try:
2768 if (not opts.metric and
2769 opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE):
2770 raise RuntimeError('missing required parameter: --metric')
2772 if opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE:
2773 metric_values = opts.metric.split('/')
2774 if len(metric_values) != 2:
2775 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
2776 opts.metric = metric_values
2778 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25) / 100.0
2780 for k, v in opts.__dict__.iteritems():
2781 assert hasattr(self, k), 'Invalid %s attribute in BisectOptions.' % k
2782 setattr(self, k, v)
2783 except RuntimeError, e:
2784 output_string = StringIO.StringIO()
2785 parser.print_help(file=output_string)
2786 error_message = '%s\n\n%s' % (e.message, output_string.getvalue())
2787 output_string.close()
2788 raise RuntimeError(error_message)
2790 @staticmethod
2791 def FromDict(values):
2792 """Creates an instance of BisectOptions from a dictionary.
2794 Args:
2795 values: a dict containing options to set.
2797 Returns:
2798 An instance of BisectOptions.
2800 opts = BisectOptions()
2801 for k, v in values.iteritems():
2802 assert hasattr(opts, k), 'Invalid %s attribute in BisectOptions.' % k
2803 setattr(opts, k, v)
2805 if opts.metric and opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE:
2806 metric_values = opts.metric.split('/')
2807 if len(metric_values) != 2:
2808 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
2809 opts.metric = metric_values
2811 if opts.target_arch == 'x64' and opts.target_build_type == 'Release':
2812 opts.target_build_type = 'Release_x64'
2813 opts.repeat_test_count = min(max(opts.repeat_test_count, 1), 100)
2814 opts.max_time_minutes = min(max(opts.max_time_minutes, 1), 60)
2815 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25)
2816 opts.truncate_percent = opts.truncate_percent / 100.0
2818 return opts
2821 def _ConfigureLogging():
2822 """Trivial logging config.
2824 Configures logging to output any messages at or above INFO to standard out,
2825 without any additional formatting.
2827 logging_format = '%(message)s'
2828 logging.basicConfig(
2829 stream=logging.sys.stdout, level=logging.INFO, format=logging_format)
2832 def main():
2833 _ConfigureLogging()
2834 try:
2835 opts = BisectOptions()
2836 opts.ParseCommandLine()
2838 if opts.bug_id:
2839 if opts.output_buildbot_annotations:
2840 bisect_utils.OutputAnnotationStepStart('Checking Issue Tracker')
2841 issue_closed = query_crbug.CheckIssueClosed(opts.bug_id)
2842 if issue_closed:
2843 print 'Aborting bisect because bug is closed'
2844 else:
2845 print 'Could not confirm bug is closed, proceeding.'
2846 if opts.output_buildbot_annotations:
2847 bisect_utils.OutputAnnotationStepClosed()
2848 if issue_closed:
2849 results = BisectResults(abort_reason='the bug is closed.')
2850 bisect_printer = BisectPrinter(opts)
2851 bisect_printer.FormatAndPrintResults(results)
2852 return 0
2854 if opts.extra_src:
2855 extra_src = bisect_utils.LoadExtraSrc(opts.extra_src)
2856 if not extra_src:
2857 raise RuntimeError('Invalid or missing --extra_src.')
2858 bisect_utils.AddAdditionalDepotInfo(extra_src.GetAdditionalDepotInfo())
2860 if opts.working_directory:
2861 custom_deps = bisect_utils.DEFAULT_GCLIENT_CUSTOM_DEPS
2862 if opts.no_custom_deps:
2863 custom_deps = None
2864 bisect_utils.CreateBisectDirectoryAndSetupDepot(opts, custom_deps)
2866 os.chdir(os.path.join(os.getcwd(), 'src'))
2867 RemoveBuildFiles(opts.target_build_type)
2869 if not _IsPlatformSupported():
2870 raise RuntimeError('Sorry, this platform isn\'t supported yet.')
2872 if not source_control.IsInGitRepository():
2873 raise RuntimeError(
2874 'Sorry, only the git workflow is supported at the moment.')
2876 # gClient sync seems to fail if you're not in master branch.
2877 if (not source_control.IsInProperBranch() and
2878 not opts.debug_ignore_sync and
2879 not opts.working_directory):
2880 raise RuntimeError('You must switch to master branch to run bisection.')
2881 bisect_test = BisectPerformanceMetrics(opts, os.getcwd())
2882 try:
2883 results = bisect_test.Run(opts.command, opts.bad_revision,
2884 opts.good_revision, opts.metric)
2885 if results.error:
2886 raise RuntimeError(results.error)
2887 bisect_test.printer.FormatAndPrintResults(results)
2888 return 0
2889 finally:
2890 bisect_test.PerformCleanup()
2891 except RuntimeError as e:
2892 if opts.output_buildbot_annotations:
2893 # The perf dashboard scrapes the "results" step in order to comment on
2894 # bugs. If you change this, please update the perf dashboard as well.
2895 bisect_utils.OutputAnnotationStepStart('Results')
2896 print 'Runtime Error: %s' % e
2897 if opts.output_buildbot_annotations:
2898 bisect_utils.OutputAnnotationStepClosed()
2899 return 1
2902 if __name__ == '__main__':
2903 sys.exit(main())