Revert of Reland of crrev.com/1100173002 (Add a WinHeap dump provider to the memory...
[chromium-blink-merge.git] / tools / auto_bisect / bisect_perf_regression.py
blob233a62d7723ce28a26037ef998586fe5a573da36
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_CONFIDENCE_ERROR_TEMPLATE = """
102 We could not reproduce the regression with this test/metric/platform combination
103 with enough confidence.
105 Here are the results for the given "good" and "bad" revisions:
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 NOTE: There's still a chance that this is actually a regression, but you may
117 need to bisect a different platform."""
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 _CheckRegressionConfidenceError(
479 good_revision,
480 bad_revision,
481 known_good_value,
482 known_bad_value):
483 """Checks whether we can be confident beyond a certain degree that the given
484 metrics represent a regression.
486 Args:
487 good_revision: string representing the commit considered 'good'
488 bad_revision: Same as above for 'bad'.
489 known_good_value: A dict with at least: 'values', 'mean' and 'std_err'
490 known_bad_value: Same as above.
492 Returns:
493 False if there is no error (i.e. we can be confident there's a regression),
494 a string containing the details of the lack of confidence otherwise.
496 error = False
497 # Adding good and bad values to a parameter list.
498 confidence_params = []
499 for l in [known_bad_value['values'], known_good_value['values']]:
500 # Flatten if needed, by averaging the values in each nested list
501 if isinstance(l, list) and all([isinstance(x, list) for x in l]):
502 averages = map(math_utils.Mean, l)
503 confidence_params.append(averages)
504 else:
505 confidence_params.append(l)
506 regression_confidence = BisectResults.ConfidenceScore(*confidence_params)
507 if regression_confidence < REGRESSION_CONFIDENCE:
508 error = REGRESSION_CONFIDENCE_ERROR_TEMPLATE.format(
509 good_rev=good_revision,
510 good_mean=known_good_value['mean'],
511 good_std_err=known_good_value['std_err'],
512 good_sample_size=len(known_good_value['values']),
513 bad_rev=bad_revision,
514 bad_mean=known_bad_value['mean'],
515 bad_std_err=known_bad_value['std_err'],
516 bad_sample_size=len(known_bad_value['values']))
517 return error
520 class DepotDirectoryRegistry(object):
522 def __init__(self, src_cwd):
523 self.depot_cwd = {}
524 for depot in bisect_utils.DEPOT_NAMES:
525 # The working directory of each depot is just the path to the depot, but
526 # since we're already in 'src', we can skip that part.
527 path_in_src = bisect_utils.DEPOT_DEPS_NAME[depot]['src'][4:]
528 self.SetDepotDir(depot, os.path.join(src_cwd, path_in_src))
530 self.SetDepotDir('chromium', src_cwd)
532 def SetDepotDir(self, depot_name, depot_dir):
533 self.depot_cwd[depot_name] = depot_dir
535 def GetDepotDir(self, depot_name):
536 if depot_name in self.depot_cwd:
537 return self.depot_cwd[depot_name]
538 else:
539 assert False, ('Unknown depot [ %s ] encountered. Possibly a new one '
540 'was added without proper support?' % depot_name)
542 def ChangeToDepotDir(self, depot_name):
543 """Given a depot, changes to the appropriate working directory.
545 Args:
546 depot_name: The name of the depot (see DEPOT_NAMES).
548 os.chdir(self.GetDepotDir(depot_name))
551 def _PrepareBisectBranch(parent_branch, new_branch):
552 """Creates a new branch to submit bisect try job.
554 Args:
555 parent_branch: Parent branch to be used to create new branch.
556 new_branch: New branch name.
558 current_branch, returncode = bisect_utils.RunGit(
559 ['rev-parse', '--abbrev-ref', 'HEAD'])
560 if returncode:
561 raise RunGitError('Must be in a git repository to send changes to trybots.')
563 current_branch = current_branch.strip()
564 # Make sure current branch is master.
565 if current_branch != parent_branch:
566 output, returncode = bisect_utils.RunGit(['checkout', '-f', parent_branch])
567 if returncode:
568 raise RunGitError('Failed to checkout branch: %s.' % output)
570 # Delete new branch if exists.
571 output, returncode = bisect_utils.RunGit(['branch', '--list'])
572 if new_branch in output:
573 output, returncode = bisect_utils.RunGit(['branch', '-D', new_branch])
574 if returncode:
575 raise RunGitError('Deleting branch failed, %s', output)
577 # Check if the tree is dirty: make sure the index is up to date and then
578 # run diff-index.
579 bisect_utils.RunGit(['update-index', '--refresh', '-q'])
580 output, returncode = bisect_utils.RunGit(['diff-index', 'HEAD'])
581 if output:
582 raise RunGitError('Cannot send a try job with a dirty tree.')
584 # Create and check out the telemetry-tryjob branch, and edit the configs
585 # for the try job there.
586 output, returncode = bisect_utils.RunGit(['checkout', '-b', new_branch])
587 if returncode:
588 raise RunGitError('Failed to checkout branch: %s.' % output)
590 output, returncode = bisect_utils.RunGit(
591 ['branch', '--set-upstream-to', parent_branch])
592 if returncode:
593 raise RunGitError('Error in git branch --set-upstream-to')
596 def _StartBuilderTryJob(
597 builder_type, git_revision, builder_name, job_name, patch=None):
598 """Attempts to run a try job from the current directory.
600 Args:
601 builder_type: One of the builder types in fetch_build, e.g. "perf".
602 git_revision: A git commit hash.
603 builder_name: Name of the bisect bot to be used for try job.
604 bisect_job_name: Try job name, used to identify which bisect
605 job was responsible for requesting a build.
606 patch: A DEPS patch (used while bisecting dependency repositories),
607 or None if we're bisecting the top-level repository.
609 # TODO(prasadv, qyearsley): Make this a method of BuildArchive
610 # (which may be renamed to BuilderTryBot or Builder).
611 try:
612 # Temporary branch for running a try job.
613 _PrepareBisectBranch(BISECT_MASTER_BRANCH, BISECT_TRYJOB_BRANCH)
614 patch_content = '/dev/null'
615 # Create a temporary patch file.
616 if patch:
617 WriteStringToFile(patch, BISECT_PATCH_FILE)
618 patch_content = BISECT_PATCH_FILE
620 try_command = [
621 'try',
622 '--bot=%s' % builder_name,
623 '--revision=%s' % git_revision,
624 '--name=%s' % job_name,
625 '--svn_repo=%s' % _TryJobSvnRepo(builder_type),
626 '--diff=%s' % patch_content,
628 # Execute try job to build revision.
629 print try_command
630 output, return_code = bisect_utils.RunGit(try_command)
632 command_string = ' '.join(['git'] + try_command)
633 if return_code:
634 raise RunGitError('Could not execute try job: %s.\n'
635 'Error: %s' % (command_string, output))
636 logging.info('Try job successfully submitted.\n TryJob Details: %s\n%s',
637 command_string, output)
638 finally:
639 # Delete patch file if exists.
640 try:
641 os.remove(BISECT_PATCH_FILE)
642 except OSError as e:
643 if e.errno != errno.ENOENT:
644 raise
645 # Checkout master branch and delete bisect-tryjob branch.
646 bisect_utils.RunGit(['checkout', '-f', BISECT_MASTER_BRANCH])
647 bisect_utils.RunGit(['branch', '-D', BISECT_TRYJOB_BRANCH])
650 def _TryJobSvnRepo(builder_type):
651 """Returns an SVN repo to use for try jobs based on the builder type."""
652 if builder_type == fetch_build.PERF_BUILDER:
653 return PERF_SVN_REPO_URL
654 if builder_type == fetch_build.FULL_BUILDER:
655 return FULL_SVN_REPO_URL
656 if builder_type == fetch_build.ANDROID_CHROME_PERF_BUILDER:
657 return ANDROID_CHROME_SVN_REPO_URL
658 raise NotImplementedError('Unknown builder type "%s".' % builder_type)
661 class BisectPerformanceMetrics(object):
662 """This class contains functionality to perform a bisection of a range of
663 revisions to narrow down where performance regressions may have occurred.
665 The main entry-point is the Run method.
668 def __init__(self, opts, src_cwd):
669 """Constructs a BisectPerformancesMetrics object.
671 Args:
672 opts: BisectOptions object containing parsed options.
673 src_cwd: Root src/ directory of the test repository (inside bisect/ dir).
675 super(BisectPerformanceMetrics, self).__init__()
677 self.opts = opts
678 self.src_cwd = src_cwd
679 self.depot_registry = DepotDirectoryRegistry(self.src_cwd)
680 self.printer = BisectPrinter(self.opts, self.depot_registry)
681 self.cleanup_commands = []
682 self.warnings = []
683 self.builder = builder.Builder.FromOpts(opts)
685 def PerformCleanup(self):
686 """Performs cleanup when script is finished."""
687 os.chdir(self.src_cwd)
688 for c in self.cleanup_commands:
689 if c[0] == 'mv':
690 shutil.move(c[1], c[2])
691 else:
692 assert False, 'Invalid cleanup command.'
694 def GetRevisionList(self, depot, bad_revision, good_revision):
695 """Retrieves a list of all the commits between the bad revision and
696 last known good revision."""
698 cwd = self.depot_registry.GetDepotDir(depot)
699 return source_control.GetRevisionList(bad_revision, good_revision, cwd=cwd)
701 def _ParseRevisionsFromDEPSFile(self, depot):
702 """Parses the local DEPS file to determine blink/skia/v8 revisions which may
703 be needed if the bisect recurses into those depots later.
705 Args:
706 depot: Name of depot being bisected.
708 Returns:
709 A dict in the format {depot:revision} if successful, otherwise None.
711 try:
712 deps_data = {
713 'Var': lambda _: deps_data["vars"][_],
714 'From': lambda *args: None,
717 deps_file = bisect_utils.FILE_DEPS_GIT
718 if not os.path.exists(deps_file):
719 deps_file = bisect_utils.FILE_DEPS
720 execfile(deps_file, {}, deps_data)
721 deps_data = deps_data['deps']
723 rxp = re.compile(".git@(?P<revision>[a-fA-F0-9]+)")
724 results = {}
725 for depot_name, depot_data in bisect_utils.DEPOT_DEPS_NAME.iteritems():
726 if (depot_data.get('platform') and
727 depot_data.get('platform') != os.name):
728 continue
730 if depot_data.get('recurse') and depot in depot_data.get('from'):
731 depot_data_src = depot_data.get('src') or depot_data.get('src_old')
732 src_dir = deps_data.get(depot_data_src)
733 if src_dir:
734 self.depot_registry.SetDepotDir(depot_name, os.path.join(
735 self.src_cwd, depot_data_src[4:]))
736 re_results = rxp.search(src_dir)
737 if re_results:
738 results[depot_name] = re_results.group('revision')
739 else:
740 warning_text = ('Could not parse revision for %s while bisecting '
741 '%s' % (depot_name, depot))
742 if not warning_text in self.warnings:
743 self.warnings.append(warning_text)
744 else:
745 results[depot_name] = None
746 return results
747 except ImportError:
748 deps_file_contents = ReadStringFromFile(deps_file)
749 parse_results = _ParseRevisionsFromDEPSFileManually(deps_file_contents)
750 results = {}
751 for depot_name, depot_revision in parse_results.iteritems():
752 depot_revision = depot_revision.strip('@')
753 logging.warn(depot_name, depot_revision)
754 for cur_name, cur_data in bisect_utils.DEPOT_DEPS_NAME.iteritems():
755 if cur_data.get('deps_var') == depot_name:
756 src_name = cur_name
757 results[src_name] = depot_revision
758 break
759 return results
761 def _Get3rdPartyRevisions(self, depot):
762 """Parses the DEPS file to determine WebKit/v8/etc... versions.
764 Args:
765 depot: A depot name. Should be in the DEPOT_NAMES list.
767 Returns:
768 A dict in the format {depot: revision} if successful, otherwise None.
770 cwd = os.getcwd()
771 self.depot_registry.ChangeToDepotDir(depot)
773 results = {}
775 if depot == 'chromium' or depot == 'android-chrome':
776 results = self._ParseRevisionsFromDEPSFile(depot)
777 os.chdir(cwd)
779 if depot == 'v8':
780 # We can't try to map the trunk revision to bleeding edge yet, because
781 # we don't know which direction to try to search in. Have to wait until
782 # the bisect has narrowed the results down to 2 v8 rolls.
783 results['v8_bleeding_edge'] = None
785 return results
787 def BackupOrRestoreOutputDirectory(self, restore=False, build_type='Release'):
788 """Backs up or restores build output directory based on restore argument.
790 Args:
791 restore: Indicates whether to restore or backup. Default is False(Backup)
792 build_type: Target build type ('Release', 'Debug', 'Release_x64' etc.)
794 Returns:
795 Path to backup or restored location as string. otherwise None if it fails.
797 build_dir = os.path.abspath(
798 builder.GetBuildOutputDirectory(self.opts, self.src_cwd))
799 source_dir = os.path.join(build_dir, build_type)
800 destination_dir = os.path.join(build_dir, '%s.bak' % build_type)
801 if restore:
802 source_dir, destination_dir = destination_dir, source_dir
803 if os.path.exists(source_dir):
804 RemoveDirectoryTree(destination_dir)
805 shutil.move(source_dir, destination_dir)
806 return destination_dir
807 return None
809 def _DownloadAndUnzipBuild(self, revision, depot, build_type='Release',
810 create_patch=False):
811 """Downloads the build archive for the given revision.
813 Args:
814 revision: The git revision to download.
815 depot: The name of a dependency repository. Should be in DEPOT_NAMES.
816 build_type: Target build type, e.g. Release', 'Debug', 'Release_x64' etc.
817 create_patch: Create a patch with any locally modified files.
819 Returns:
820 True if download succeeds, otherwise False.
822 patch = None
823 patch_sha = None
824 if depot not in ('chromium', 'android-chrome'):
825 # Create a DEPS patch with new revision for dependency repository.
826 self._CreateDEPSPatch(depot, revision)
827 create_patch = True
829 if create_patch:
830 revision, patch = self._CreatePatch(revision)
832 if patch:
833 # Get the SHA of the DEPS changes patch.
834 patch_sha = GetSHA1HexDigest(patch)
836 # Update the DEPS changes patch with a patch to create a new file named
837 # 'DEPS.sha' and add patch_sha evaluated above to it.
838 patch = '%s\n%s' % (patch, DEPS_SHA_PATCH % {'deps_sha': patch_sha})
840 build_dir = builder.GetBuildOutputDirectory(self.opts, self.src_cwd)
841 downloaded_file = self._WaitForBuildDownload(
842 revision, build_dir, deps_patch=patch, deps_patch_sha=patch_sha)
843 if not downloaded_file:
844 return False
845 return self._UnzipAndMoveBuildProducts(downloaded_file, build_dir,
846 build_type=build_type)
848 def _WaitForBuildDownload(self, revision, build_dir, deps_patch=None,
849 deps_patch_sha=None):
850 """Tries to download a zip archive for a build.
852 This involves seeing whether the archive is already available, and if not,
853 then requesting a build and waiting before downloading.
855 Args:
856 revision: A git commit hash.
857 build_dir: The directory to download the build into.
858 deps_patch: A patch which changes a dependency repository revision in
859 the DEPS, if applicable.
860 deps_patch_sha: The SHA1 hex digest of the above patch.
862 Returns:
863 File path of the downloaded file if successful, otherwise None.
865 bucket_name, remote_path = fetch_build.GetBucketAndRemotePath(
866 revision, builder_type=self.opts.builder_type,
867 target_arch=self.opts.target_arch,
868 target_platform=self.opts.target_platform,
869 deps_patch_sha=deps_patch_sha,
870 extra_src=self.opts.extra_src)
871 output_dir = os.path.abspath(build_dir)
872 fetch_build_func = lambda: fetch_build.FetchFromCloudStorage(
873 bucket_name, remote_path, output_dir)
875 is_available = fetch_build.BuildIsAvailable(bucket_name, remote_path)
876 if is_available:
877 return fetch_build_func()
879 # When build archive doesn't exist, make a request and wait.
880 return self._RequestBuildAndWait(
881 revision, fetch_build_func, deps_patch=deps_patch)
883 def _RequestBuildAndWait(self, git_revision, fetch_build_func,
884 deps_patch=None):
885 """Triggers a try job for a build job.
887 This function prepares and starts a try job for a builder, and waits for
888 the archive to be produced and archived. Once the build is ready it is
889 downloaded.
891 For performance tests, builders on the tryserver.chromium.perf are used.
893 TODO(qyearsley): Make this function take "builder_type" as a parameter
894 and make requests to different bot names based on that parameter.
896 Args:
897 git_revision: A git commit hash.
898 fetch_build_func: Function to check and download build from cloud storage.
899 deps_patch: DEPS patch string, used when bisecting dependency repos.
901 Returns:
902 Downloaded archive file path when requested build exists and download is
903 successful, otherwise None.
905 if not fetch_build_func:
906 return None
908 # Create a unique ID for each build request posted to try server builders.
909 # This ID is added to "Reason" property of the build.
910 build_request_id = GetSHA1HexDigest(
911 '%s-%s-%s' % (git_revision, deps_patch, time.time()))
913 # Revert any changes to DEPS file.
914 bisect_utils.CheckRunGit(['reset', '--hard', 'HEAD'], cwd=self.src_cwd)
916 builder_name, build_timeout = fetch_build.GetBuilderNameAndBuildTime(
917 builder_type=self.opts.builder_type,
918 target_arch=self.opts.target_arch,
919 target_platform=self.opts.target_platform,
920 extra_src=self.opts.extra_src)
922 try:
923 _StartBuilderTryJob(self.opts.builder_type, git_revision, builder_name,
924 job_name=build_request_id, patch=deps_patch)
925 except RunGitError as e:
926 logging.warn('Failed to post builder try job for revision: [%s].\n'
927 'Error: %s', git_revision, e)
928 return None
930 # Get the buildbot master URL to monitor build status.
931 buildbot_server_url = fetch_build.GetBuildBotUrl(
932 builder_type=self.opts.builder_type,
933 target_arch=self.opts.target_arch,
934 target_platform=self.opts.target_platform,
935 extra_src=self.opts.extra_src)
937 archive_filename, error_msg = _WaitUntilBuildIsReady(
938 fetch_build_func, builder_name, build_request_id, build_timeout,
939 buildbot_server_url)
940 if not archive_filename:
941 logging.warn('%s [revision: %s]', error_msg, git_revision)
942 return archive_filename
944 def _UnzipAndMoveBuildProducts(self, downloaded_file, build_dir,
945 build_type='Release'):
946 """Unzips the build archive and moves it to the build output directory.
948 The build output directory is wherever the binaries are expected to
949 be in order to start Chrome and run tests.
951 TODO: Simplify and clarify this method if possible.
953 Args:
954 downloaded_file: File path of the downloaded zip file.
955 build_dir: Directory where the the zip file was downloaded to.
956 build_type: "Release" or "Debug".
958 Returns:
959 True if successful, False otherwise.
961 abs_build_dir = os.path.abspath(build_dir)
962 output_dir = os.path.join(abs_build_dir, self.GetZipFileBuildDirName())
963 logging.info('EXPERIMENTAL RUN, _UnzipAndMoveBuildProducts locals %s',
964 str(locals()))
966 try:
967 RemoveDirectoryTree(output_dir)
968 self.BackupOrRestoreOutputDirectory(restore=False)
969 # Build output directory based on target(e.g. out/Release, out/Debug).
970 target_build_output_dir = os.path.join(abs_build_dir, build_type)
972 logging.info('Extracting "%s" to "%s"', downloaded_file, abs_build_dir)
973 fetch_build.Unzip(downloaded_file, abs_build_dir)
975 if not os.path.exists(output_dir):
976 # Due to recipe changes, the builds extract folder contains
977 # out/Release instead of full-build-<platform>/Release.
978 if os.path.exists(os.path.join(abs_build_dir, 'out', build_type)):
979 output_dir = os.path.join(abs_build_dir, 'out', build_type)
980 else:
981 raise IOError('Missing extracted folder %s ' % output_dir)
983 logging.info('Moving build from %s to %s',
984 output_dir, target_build_output_dir)
985 shutil.move(output_dir, target_build_output_dir)
986 return True
987 except Exception as e:
988 logging.info('Something went wrong while extracting archive file: %s', e)
989 self.BackupOrRestoreOutputDirectory(restore=True)
990 # Cleanup any leftovers from unzipping.
991 if os.path.exists(output_dir):
992 RemoveDirectoryTree(output_dir)
993 finally:
994 # Delete downloaded archive
995 if os.path.exists(downloaded_file):
996 os.remove(downloaded_file)
997 return False
999 @staticmethod
1000 def GetZipFileBuildDirName():
1001 """Gets the base file name of the zip file.
1003 After extracting the zip file, this is the name of the directory where
1004 the build files are expected to be. Possibly.
1006 TODO: Make sure that this returns the actual directory name where the
1007 Release or Debug directory is inside of the zip files. This probably
1008 depends on the builder recipe, and may depend on whether the builder is
1009 a perf builder or full builder.
1011 Returns:
1012 The name of the directory inside a build archive which is expected to
1013 contain a Release or Debug directory.
1015 if bisect_utils.IsWindowsHost():
1016 return 'full-build-win32'
1017 if bisect_utils.IsLinuxHost():
1018 return 'full-build-linux'
1019 if bisect_utils.IsMacHost():
1020 return 'full-build-mac'
1021 raise NotImplementedError('Unknown platform "%s".' % sys.platform)
1023 def IsDownloadable(self, depot):
1024 """Checks if build can be downloaded based on target platform and depot."""
1025 if (self.opts.target_platform in ['chromium', 'android', 'android-chrome']
1026 and self.opts.builder_type):
1027 # In case of android-chrome platform, download archives only for
1028 # android-chrome depot; for other depots such as chromium, v8, skia
1029 # etc., build the binary locally.
1030 if self.opts.target_platform == 'android-chrome':
1031 return depot == 'android-chrome'
1032 else:
1033 return (depot == 'chromium' or
1034 'chromium' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'] or
1035 'v8' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'])
1036 return False
1038 def UpdateDepsContents(self, deps_contents, depot, git_revision, deps_key):
1039 """Returns modified version of DEPS file contents.
1041 Args:
1042 deps_contents: DEPS file content.
1043 depot: Current depot being bisected.
1044 git_revision: A git hash to be updated in DEPS.
1045 deps_key: Key in vars section of DEPS file to be searched.
1047 Returns:
1048 Updated DEPS content as string if deps key is found, otherwise None.
1050 # Check whether the depot and revision pattern in DEPS file vars
1051 # e.g. for webkit the format is "webkit_revision": "12345".
1052 deps_revision = re.compile(r'(?<="%s": ")([0-9]+)(?=")' % deps_key,
1053 re.MULTILINE)
1054 new_data = None
1055 if re.search(deps_revision, deps_contents):
1056 commit_position = source_control.GetCommitPosition(
1057 git_revision, self.depot_registry.GetDepotDir(depot))
1058 if not commit_position:
1059 logging.warn('Could not determine commit position for %s', git_revision)
1060 return None
1061 # Update the revision information for the given depot
1062 new_data = re.sub(deps_revision, str(commit_position), deps_contents)
1063 else:
1064 # Check whether the depot and revision pattern in DEPS file vars
1065 # e.g. for webkit the format is "webkit_revision": "559a6d4ab7a84c539..".
1066 deps_revision = re.compile(
1067 r'(?<=["\']%s["\']: ["\'])([a-fA-F0-9]{40})(?=["\'])' % deps_key,
1068 re.MULTILINE)
1069 if re.search(deps_revision, deps_contents):
1070 new_data = re.sub(deps_revision, git_revision, deps_contents)
1071 if new_data:
1072 # For v8_bleeding_edge revisions change V8 branch in order
1073 # to fetch bleeding edge revision.
1074 if depot == 'v8_bleeding_edge':
1075 new_data = _UpdateV8Branch(new_data)
1076 if not new_data:
1077 return None
1078 return new_data
1080 def UpdateDeps(self, revision, depot, deps_file):
1081 """Updates DEPS file with new revision of dependency repository.
1083 This method search DEPS for a particular pattern in which depot revision
1084 is specified (e.g "webkit_revision": "123456"). If a match is found then
1085 it resolves the given git hash to SVN revision and replace it in DEPS file.
1087 Args:
1088 revision: A git hash revision of the dependency repository.
1089 depot: Current depot being bisected.
1090 deps_file: Path to DEPS file.
1092 Returns:
1093 True if DEPS file is modified successfully, otherwise False.
1095 if not os.path.exists(deps_file):
1096 return False
1098 deps_var = bisect_utils.DEPOT_DEPS_NAME[depot]['deps_var']
1099 # Don't update DEPS file if deps_var is not set in DEPOT_DEPS_NAME.
1100 if not deps_var:
1101 logging.warn('DEPS update not supported for Depot: %s', depot)
1102 return False
1104 # Hack for Angle repository. In the DEPS file, "vars" dictionary variable
1105 # contains "angle_revision" key that holds git hash instead of SVN revision.
1106 # And sometime "angle_revision" key is not specified in "vars" variable.
1107 # In such cases check, "deps" dictionary variable that matches
1108 # angle.git@[a-fA-F0-9]{40}$ and replace git hash.
1109 if depot == 'angle':
1110 return _UpdateDEPSForAngle(revision, depot, deps_file)
1112 try:
1113 deps_contents = ReadStringFromFile(deps_file)
1114 updated_deps_content = self.UpdateDepsContents(
1115 deps_contents, depot, revision, deps_var)
1116 # Write changes to DEPS file
1117 if updated_deps_content:
1118 WriteStringToFile(updated_deps_content, deps_file)
1119 return True
1120 except IOError, e:
1121 logging.warn('Something went wrong while updating DEPS file. [%s]', e)
1122 return False
1124 def _CreateDEPSPatch(self, depot, revision):
1125 """Checks out the DEPS file at the specified revision and modifies it.
1127 Args:
1128 depot: Current depot being bisected.
1129 revision: A git hash revision of the dependency repository.
1131 deps_file_path = os.path.join(self.src_cwd, bisect_utils.FILE_DEPS)
1132 if not os.path.exists(deps_file_path):
1133 raise RuntimeError('DEPS file does not exists.[%s]' % deps_file_path)
1134 # Get current chromium revision (git hash).
1135 cmd = ['rev-parse', 'HEAD']
1136 chromium_sha = bisect_utils.CheckRunGit(cmd).strip()
1137 if not chromium_sha:
1138 raise RuntimeError('Failed to determine Chromium revision for %s' %
1139 revision)
1140 if ('chromium' in bisect_utils.DEPOT_DEPS_NAME[depot]['from'] or
1141 'v8' in bisect_utils.DEPOT_DEPS_NAME[depot]['from']):
1142 # Checkout DEPS file for the current chromium revision.
1143 if not source_control.CheckoutFileAtRevision(
1144 bisect_utils.FILE_DEPS, chromium_sha, cwd=self.src_cwd):
1145 raise RuntimeError(
1146 'DEPS checkout Failed for chromium revision : [%s]' % chromium_sha)
1148 if not self.UpdateDeps(revision, depot, deps_file_path):
1149 raise RuntimeError(
1150 'Failed to update DEPS file for chromium: [%s]' % chromium_sha)
1152 def _CreatePatch(self, revision):
1153 """Creates a patch from currently modified files.
1155 Args:
1156 depot: Current depot being bisected.
1157 revision: A git hash revision of the dependency repository.
1159 Returns:
1160 A tuple with git hash of chromium revision and DEPS patch text.
1162 # Get current chromium revision (git hash).
1163 chromium_sha = bisect_utils.CheckRunGit(['rev-parse', 'HEAD']).strip()
1164 if not chromium_sha:
1165 raise RuntimeError('Failed to determine Chromium revision for %s' %
1166 revision)
1167 # Checkout DEPS file for the current chromium revision.
1168 diff_command = [
1169 'diff',
1170 '--src-prefix=',
1171 '--dst-prefix=',
1172 '--no-ext-diff',
1173 'HEAD',
1175 diff_text = bisect_utils.CheckRunGit(diff_command)
1176 return (chromium_sha, ChangeBackslashToSlashInPatch(diff_text))
1178 def ObtainBuild(
1179 self, depot, revision=None, create_patch=False):
1180 """Obtains a build by either downloading or building directly.
1182 Args:
1183 depot: Dependency repository name.
1184 revision: A git commit hash. If None is given, the currently checked-out
1185 revision is built.
1186 create_patch: Create a patch with any locally modified files.
1188 Returns:
1189 True for success.
1191 if self.opts.debug_ignore_build:
1192 return True
1194 build_success = False
1195 cwd = os.getcwd()
1196 os.chdir(self.src_cwd)
1197 # Fetch build archive for the given revision from the cloud storage when
1198 # the storage bucket is passed.
1199 if self.IsDownloadable(depot) and revision:
1200 build_success = self._DownloadAndUnzipBuild(
1201 revision, depot, build_type='Release', create_patch=create_patch)
1202 else:
1203 # Print the current environment set on the machine.
1204 print 'Full Environment:'
1205 for key, value in sorted(os.environ.items()):
1206 print '%s: %s' % (key, value)
1207 # Print the environment before proceeding with compile.
1208 sys.stdout.flush()
1209 build_success = self.builder.Build(depot, self.opts)
1210 os.chdir(cwd)
1211 return build_success
1213 def RunGClientHooks(self):
1214 """Runs gclient with runhooks command.
1216 Returns:
1217 True if gclient reports no errors.
1219 if self.opts.debug_ignore_build:
1220 return True
1221 return not bisect_utils.RunGClient(['runhooks'], cwd=self.src_cwd)
1223 def _IsBisectModeUsingMetric(self):
1224 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_MEAN,
1225 bisect_utils.BISECT_MODE_STD_DEV]
1227 def _IsBisectModeReturnCode(self):
1228 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_RETURN_CODE]
1230 def _IsBisectModeStandardDeviation(self):
1231 return self.opts.bisect_mode in [bisect_utils.BISECT_MODE_STD_DEV]
1233 def GetCompatibleCommand(self, command_to_run, revision, depot):
1234 """Return a possibly modified test command depending on the revision.
1236 Prior to crrev.com/274857 *only* android-chromium-testshell
1237 Then until crrev.com/276628 *both* (android-chromium-testshell and
1238 android-chrome-shell) work. After that rev 276628 *only*
1239 android-chrome-shell works. The bisect_perf_regression.py script should
1240 handle these cases and set appropriate browser type based on revision.
1242 if self.opts.target_platform in ['android']:
1243 # When its a third_party depot, get the chromium revision.
1244 if depot != 'chromium':
1245 revision = bisect_utils.CheckRunGit(
1246 ['rev-parse', 'HEAD'], cwd=self.src_cwd).strip()
1247 commit_position = source_control.GetCommitPosition(revision,
1248 cwd=self.src_cwd)
1249 if not commit_position:
1250 return command_to_run
1251 cmd_re = re.compile(r'--browser=(?P<browser_type>\S+)')
1252 matches = cmd_re.search(command_to_run)
1253 if bisect_utils.IsStringInt(commit_position) and matches:
1254 cmd_browser = matches.group('browser_type')
1255 if commit_position <= 274857 and cmd_browser == 'android-chrome-shell':
1256 return command_to_run.replace(cmd_browser,
1257 'android-chromium-testshell')
1258 elif (commit_position >= 276628 and
1259 cmd_browser == 'android-chromium-testshell'):
1260 return command_to_run.replace(cmd_browser,
1261 'android-chrome-shell')
1262 return command_to_run
1264 def RunPerformanceTestAndParseResults(
1265 self, command_to_run, metric, reset_on_first_run=False,
1266 upload_on_last_run=False, results_label=None, test_run_multiplier=1,
1267 allow_flakes=True):
1268 """Runs a performance test on the current revision and parses the results.
1270 Args:
1271 command_to_run: The command to be run to execute the performance test.
1272 metric: The metric to parse out from the results of the performance test.
1273 This is the result chart name and trace name, separated by slash.
1274 May be None for perf try jobs.
1275 reset_on_first_run: If True, pass the flag --reset-results on first run.
1276 upload_on_last_run: If True, pass the flag --upload-results on last run.
1277 results_label: A value for the option flag --results-label.
1278 The arguments reset_on_first_run, upload_on_last_run and results_label
1279 are all ignored if the test is not a Telemetry test.
1280 test_run_multiplier: Factor by which to multiply the number of test runs
1281 and the timeout period specified in self.opts.
1282 allow_flakes: Report success even if some tests fail to run.
1284 Returns:
1285 (values dict, 0) if --debug_ignore_perf_test was passed.
1286 (values dict, 0, test output) if the test was run successfully.
1287 (error message, -1) if the test couldn't be run.
1288 (error message, -1, test output) if the test ran but there was an error.
1290 success_code, failure_code = 0, -1
1292 if self.opts.debug_ignore_perf_test:
1293 fake_results = {
1294 'mean': 0.0,
1295 'std_err': 0.0,
1296 'std_dev': 0.0,
1297 'values': [0.0]
1300 # When debug_fake_test_mean is set, its value is returned as the mean
1301 # and the flag is cleared so that further calls behave as if it wasn't
1302 # set (returning the fake_results dict as defined above).
1303 if self.opts.debug_fake_first_test_mean:
1304 fake_results['mean'] = float(self.opts.debug_fake_first_test_mean)
1305 self.opts.debug_fake_first_test_mean = 0
1307 return (fake_results, success_code)
1309 # For Windows platform set posix=False, to parse windows paths correctly.
1310 # On Windows, path separators '\' or '\\' are replace by '' when posix=True,
1311 # refer to http://bugs.python.org/issue1724822. By default posix=True.
1312 args = shlex.split(command_to_run, posix=not bisect_utils.IsWindowsHost())
1314 if not _GenerateProfileIfNecessary(args):
1315 err_text = 'Failed to generate profile for performance test.'
1316 return (err_text, failure_code)
1318 is_telemetry = bisect_utils.IsTelemetryCommand(command_to_run)
1320 start_time = time.time()
1322 metric_values = []
1323 output_of_all_runs = ''
1324 repeat_count = self.opts.repeat_test_count * test_run_multiplier
1325 return_codes = []
1326 for i in xrange(repeat_count):
1327 # Can ignore the return code since if the tests fail, it won't return 0.
1328 current_args = copy.copy(args)
1329 if is_telemetry:
1330 if i == 0 and reset_on_first_run:
1331 current_args.append('--reset-results')
1332 if i == self.opts.repeat_test_count - 1 and upload_on_last_run:
1333 current_args.append('--upload-results')
1334 if results_label:
1335 current_args.append('--results-label=%s' % results_label)
1336 try:
1337 output, return_code = bisect_utils.RunProcessAndRetrieveOutput(
1338 current_args, cwd=self.src_cwd)
1339 return_codes.append(return_code)
1340 except OSError, e:
1341 if e.errno == errno.ENOENT:
1342 err_text = ('Something went wrong running the performance test. '
1343 'Please review the command line:\n\n')
1344 if 'src/' in ' '.join(args):
1345 err_text += ('Check that you haven\'t accidentally specified a '
1346 'path with src/ in the command.\n\n')
1347 err_text += ' '.join(args)
1348 err_text += '\n'
1350 return (err_text, failure_code)
1351 raise
1353 output_of_all_runs += output
1354 if self.opts.output_buildbot_annotations:
1355 print output
1357 if metric and self._IsBisectModeUsingMetric():
1358 parsed_metric = _ParseMetricValuesFromOutput(metric, output)
1359 if parsed_metric:
1360 metric_values.append(math_utils.Mean(parsed_metric))
1361 # If we're bisecting on a metric (ie, changes in the mean or
1362 # standard deviation) and no metric values are produced, bail out.
1363 if not metric_values:
1364 break
1365 elif self._IsBisectModeReturnCode():
1366 metric_values.append(return_code)
1367 # If there's a failed test, we can bail out early.
1368 if return_code:
1369 break
1371 elapsed_minutes = (time.time() - start_time) / 60.0
1372 time_limit = self.opts.max_time_minutes * test_run_multiplier
1373 if elapsed_minutes >= time_limit:
1374 break
1376 if metric and len(metric_values) == 0:
1377 err_text = 'Metric %s was not found in the test output.' % metric
1378 # TODO(qyearsley): Consider also getting and displaying a list of metrics
1379 # that were found in the output here.
1380 return (err_text, failure_code, output_of_all_runs)
1382 # If we're bisecting on return codes, we're really just looking for zero vs
1383 # non-zero.
1384 values = {}
1385 if self._IsBisectModeReturnCode():
1386 # If any of the return codes is non-zero, output 1.
1387 overall_return_code = 0 if (
1388 all(current_value == 0 for current_value in metric_values)) else 1
1390 values = {
1391 'mean': overall_return_code,
1392 'std_err': 0.0,
1393 'std_dev': 0.0,
1394 'values': metric_values,
1397 print 'Results of performance test: Command returned with %d' % (
1398 overall_return_code)
1399 print
1400 elif metric:
1401 # Need to get the average value if there were multiple values.
1402 truncated_mean = math_utils.TruncatedMean(
1403 metric_values, self.opts.truncate_percent)
1404 standard_err = math_utils.StandardError(metric_values)
1405 standard_dev = math_utils.StandardDeviation(metric_values)
1407 if self._IsBisectModeStandardDeviation():
1408 metric_values = [standard_dev]
1410 values = {
1411 'mean': truncated_mean,
1412 'std_err': standard_err,
1413 'std_dev': standard_dev,
1414 'values': metric_values,
1417 print 'Results of performance test: %12f %12f' % (
1418 truncated_mean, standard_err)
1419 print
1421 overall_success = success_code
1422 if not allow_flakes and not self._IsBisectModeReturnCode():
1423 overall_success = (
1424 success_code
1425 if (all(current_value == 0 for current_value in return_codes))
1426 else failure_code)
1428 return (values, overall_success, output_of_all_runs)
1430 def PerformPreBuildCleanup(self):
1431 """Performs cleanup between runs."""
1432 print 'Cleaning up between runs.'
1433 print
1435 # Leaving these .pyc files around between runs may disrupt some perf tests.
1436 for (path, _, files) in os.walk(self.src_cwd):
1437 for cur_file in files:
1438 if cur_file.endswith('.pyc'):
1439 path_to_file = os.path.join(path, cur_file)
1440 os.remove(path_to_file)
1442 def _RunPostSync(self, _depot):
1443 """Performs any work after syncing.
1445 Args:
1446 depot: Depot name.
1448 Returns:
1449 True if successful.
1451 if 'android' in self.opts.target_platform:
1452 if not builder.SetupAndroidBuildEnvironment(
1453 self.opts, path_to_src=self.src_cwd):
1454 return False
1456 return self.RunGClientHooks()
1458 @staticmethod
1459 def ShouldSkipRevision(depot, revision):
1460 """Checks whether a particular revision can be safely skipped.
1462 Some commits can be safely skipped (such as a DEPS roll for the repos
1463 still using .DEPS.git), since the tool is git based those changes
1464 would have no effect.
1466 Args:
1467 depot: The depot being bisected.
1468 revision: Current revision we're synced to.
1470 Returns:
1471 True if we should skip building/testing this revision.
1473 # Skips revisions with DEPS on android-chrome.
1474 if depot == 'android-chrome':
1475 cmd = ['diff-tree', '--no-commit-id', '--name-only', '-r', revision]
1476 output = bisect_utils.CheckRunGit(cmd)
1478 files = output.splitlines()
1480 if len(files) == 1 and files[0] == 'DEPS':
1481 return True
1483 return False
1485 def RunTest(self, revision, depot, command, metric, skippable=False,
1486 skip_sync=False, create_patch=False, force_build=False,
1487 test_run_multiplier=1):
1488 """Performs a full sync/build/run of the specified revision.
1490 Args:
1491 revision: The revision to sync to.
1492 depot: The depot that's being used at the moment (src, webkit, etc.)
1493 command: The command to execute the performance test.
1494 metric: The performance metric being tested.
1495 skip_sync: Skip the sync step.
1496 create_patch: Create a patch with any locally modified files.
1497 force_build: Force a local build.
1498 test_run_multiplier: Factor by which to multiply the given number of runs
1499 and the set timeout period.
1501 Returns:
1502 On success, a tuple containing the results of the performance test.
1503 Otherwise, a tuple with the error message.
1505 logging.info('Running RunTest with rev "%s", command "%s"',
1506 revision, command)
1507 # Decide which sync program to use.
1508 sync_client = None
1509 if depot == 'chromium' or depot == 'android-chrome':
1510 sync_client = 'gclient'
1512 # Do the syncing for all depots.
1513 if not (self.opts.debug_ignore_sync or skip_sync):
1514 if not self._SyncRevision(depot, revision, sync_client):
1515 return ('Failed to sync: [%s]' % str(revision), BUILD_RESULT_FAIL)
1517 # Try to do any post-sync steps. This may include "gclient runhooks".
1518 if not self._RunPostSync(depot):
1519 return ('Failed to run [gclient runhooks].', BUILD_RESULT_FAIL)
1521 # Skip this revision if it can be skipped.
1522 if skippable and self.ShouldSkipRevision(depot, revision):
1523 return ('Skipped revision: [%s]' % str(revision),
1524 BUILD_RESULT_SKIPPED)
1526 # Obtain a build for this revision. This may be done by requesting a build
1527 # from another builder, waiting for it and downloading it.
1528 start_build_time = time.time()
1529 revision_to_build = revision if not force_build else None
1530 build_success = self.ObtainBuild(
1531 depot, revision=revision_to_build, create_patch=create_patch)
1532 if not build_success:
1533 return ('Failed to build revision: [%s]' % str(revision),
1534 BUILD_RESULT_FAIL)
1535 after_build_time = time.time()
1537 # Possibly alter the command.
1538 command = self.GetCompatibleCommand(command, revision, depot)
1540 # Run the command and get the results.
1541 results = self.RunPerformanceTestAndParseResults(
1542 command, metric, test_run_multiplier=test_run_multiplier)
1544 # Restore build output directory once the tests are done, to avoid
1545 # any discrepancies.
1546 if self.IsDownloadable(depot) and revision:
1547 self.BackupOrRestoreOutputDirectory(restore=True)
1549 # A value other than 0 indicates that the test couldn't be run, and results
1550 # should also include an error message.
1551 if results[1] != 0:
1552 return results
1554 external_revisions = self._Get3rdPartyRevisions(depot)
1556 if not external_revisions is None:
1557 return (results[0], results[1], external_revisions,
1558 time.time() - after_build_time, after_build_time -
1559 start_build_time)
1560 else:
1561 return ('Failed to parse DEPS file for external revisions.',
1562 BUILD_RESULT_FAIL)
1564 def _SyncRevision(self, depot, revision, sync_client):
1565 """Syncs depot to particular revision.
1567 Args:
1568 depot: The depot that's being used at the moment (src, webkit, etc.)
1569 revision: The revision to sync to.
1570 sync_client: Program used to sync, e.g. "gclient". Can be None.
1572 Returns:
1573 True if successful, False otherwise.
1575 self.depot_registry.ChangeToDepotDir(depot)
1577 if sync_client:
1578 self.PerformPreBuildCleanup()
1580 # When using gclient to sync, you need to specify the depot you
1581 # want so that all the dependencies sync properly as well.
1582 # i.e. gclient sync src@<SHA1>
1583 if sync_client == 'gclient' and revision:
1584 revision = '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME[depot]['src'],
1585 revision)
1586 if depot == 'chromium' and self.opts.target_platform == 'android-chrome':
1587 return self._SyncRevisionsForAndroidChrome(revision)
1589 return source_control.SyncToRevision(revision, sync_client)
1591 def _SyncRevisionsForAndroidChrome(self, revision):
1592 """Syncs android-chrome and chromium repos to particular revision.
1594 This is a special case for android-chrome as the gclient sync for chromium
1595 overwrites the android-chrome revision to TOT. Therefore both the repos
1596 are synced to known revisions.
1598 Args:
1599 revision: Git hash of the Chromium to sync.
1601 Returns:
1602 True if successful, False otherwise.
1604 revisions_list = [revision]
1605 current_android_rev = source_control.GetCurrentRevision(
1606 self.depot_registry.GetDepotDir('android-chrome'))
1607 revisions_list.append(
1608 '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME['android-chrome']['src'],
1609 current_android_rev))
1610 return not bisect_utils.RunGClientAndSync(revisions_list)
1612 def _CheckIfRunPassed(self, current_value, known_good_value, known_bad_value):
1613 """Given known good and bad values, decide if the current_value passed
1614 or failed.
1616 Args:
1617 current_value: The value of the metric being checked.
1618 known_bad_value: The reference value for a "failed" run.
1619 known_good_value: The reference value for a "passed" run.
1621 Returns:
1622 True if the current_value is closer to the known_good_value than the
1623 known_bad_value.
1625 if self.opts.bisect_mode == bisect_utils.BISECT_MODE_STD_DEV:
1626 dist_to_good_value = abs(current_value['std_dev'] -
1627 known_good_value['std_dev'])
1628 dist_to_bad_value = abs(current_value['std_dev'] -
1629 known_bad_value['std_dev'])
1630 else:
1631 dist_to_good_value = abs(current_value['mean'] - known_good_value['mean'])
1632 dist_to_bad_value = abs(current_value['mean'] - known_bad_value['mean'])
1634 return dist_to_good_value < dist_to_bad_value
1636 def _GetV8BleedingEdgeFromV8TrunkIfMappable(
1637 self, revision, bleeding_edge_branch):
1638 """Gets v8 bleeding edge revision mapped to v8 revision in trunk.
1640 Args:
1641 revision: A trunk V8 revision mapped to bleeding edge revision.
1642 bleeding_edge_branch: Branch used to perform lookup of bleeding edge
1643 revision.
1644 Return:
1645 A mapped bleeding edge revision if found, otherwise None.
1647 commit_position = source_control.GetCommitPosition(revision)
1649 if bisect_utils.IsStringInt(commit_position):
1650 # V8 is tricky to bisect, in that there are only a few instances when
1651 # we can dive into bleeding_edge and get back a meaningful result.
1652 # Try to detect a V8 "business as usual" case, which is when:
1653 # 1. trunk revision N has description "Version X.Y.Z"
1654 # 2. bleeding_edge revision (N-1) has description "Prepare push to
1655 # trunk. Now working on X.Y.(Z+1)."
1657 # As of 01/24/2014, V8 trunk descriptions are formatted:
1658 # "Version 3.X.Y (based on bleeding_edge revision rZ)"
1659 # So we can just try parsing that out first and fall back to the old way.
1660 v8_dir = self.depot_registry.GetDepotDir('v8')
1661 v8_bleeding_edge_dir = self.depot_registry.GetDepotDir('v8_bleeding_edge')
1663 revision_info = source_control.QueryRevisionInfo(revision, cwd=v8_dir)
1664 version_re = re.compile("Version (?P<values>[0-9,.]+)")
1665 regex_results = version_re.search(revision_info['subject'])
1666 if regex_results:
1667 git_revision = None
1668 if 'based on bleeding_edge' in revision_info['subject']:
1669 try:
1670 bleeding_edge_revision = revision_info['subject'].split(
1671 'bleeding_edge revision r')[1]
1672 bleeding_edge_revision = int(bleeding_edge_revision.split(')')[0])
1673 bleeding_edge_url = ('https://v8.googlecode.com/svn/branches/'
1674 'bleeding_edge@%s' % bleeding_edge_revision)
1675 cmd = ['log',
1676 '--format=%H',
1677 '--grep',
1678 bleeding_edge_url,
1679 '-1',
1680 bleeding_edge_branch]
1681 output = bisect_utils.CheckRunGit(cmd, cwd=v8_dir)
1682 if output:
1683 git_revision = output.strip()
1684 return git_revision
1685 except (IndexError, ValueError):
1686 pass
1687 else:
1688 # V8 rolls description changed after V8 git migration, new description
1689 # includes "Version 3.X.Y (based on <git hash>)"
1690 try:
1691 rxp = re.compile('based on (?P<git_revision>[a-fA-F0-9]+)')
1692 re_results = rxp.search(revision_info['subject'])
1693 if re_results:
1694 return re_results.group('git_revision')
1695 except (IndexError, ValueError):
1696 pass
1697 if not git_revision:
1698 # Wasn't successful, try the old way of looking for "Prepare push to"
1699 git_revision = source_control.ResolveToRevision(
1700 int(commit_position) - 1, 'v8_bleeding_edge',
1701 bisect_utils.DEPOT_DEPS_NAME, -1, cwd=v8_bleeding_edge_dir)
1703 if git_revision:
1704 revision_info = source_control.QueryRevisionInfo(
1705 git_revision, cwd=v8_bleeding_edge_dir)
1707 if 'Prepare push to trunk' in revision_info['subject']:
1708 return git_revision
1709 return None
1711 def _GetNearestV8BleedingEdgeFromTrunk(
1712 self, revision, v8_branch, bleeding_edge_branch, search_forward=True):
1713 """Gets the nearest V8 roll and maps to bleeding edge revision.
1715 V8 is a bit tricky to bisect since it isn't just rolled out like blink.
1716 Each revision on trunk might just be whatever was in bleeding edge, rolled
1717 directly out. Or it could be some mixture of previous v8 trunk versions,
1718 with bits and pieces cherry picked out from bleeding edge. In order to
1719 bisect, we need both the before/after versions on trunk v8 to be just pushes
1720 from bleeding edge. With the V8 git migration, the branches got switched.
1721 a) master (external/v8) == candidates (v8/v8)
1722 b) bleeding_edge (external/v8) == master (v8/v8)
1724 Args:
1725 revision: A V8 revision to get its nearest bleeding edge revision
1726 search_forward: Searches forward if True, otherwise search backward.
1728 Return:
1729 A mapped bleeding edge revision if found, otherwise None.
1731 cwd = self.depot_registry.GetDepotDir('v8')
1732 cmd = ['log', '--format=%ct', '-1', revision]
1733 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1734 commit_time = int(output)
1735 commits = []
1736 if search_forward:
1737 cmd = ['log',
1738 '--format=%H',
1739 '--after=%d' % commit_time,
1740 v8_branch,
1741 '--reverse']
1742 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1743 output = output.split()
1744 commits = output
1745 #Get 10 git hashes immediately after the given commit.
1746 commits = commits[:10]
1747 else:
1748 cmd = ['log',
1749 '--format=%H',
1750 '-10',
1751 '--before=%d' % commit_time,
1752 v8_branch]
1753 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1754 output = output.split()
1755 commits = output
1757 bleeding_edge_revision = None
1759 for c in commits:
1760 bleeding_edge_revision = self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1761 c, bleeding_edge_branch)
1762 if bleeding_edge_revision:
1763 break
1765 return bleeding_edge_revision
1767 def _FillInV8BleedingEdgeInfo(self, min_revision_state, max_revision_state):
1768 cwd = self.depot_registry.GetDepotDir('v8')
1769 # when "remote.origin.url" is https://chromium.googlesource.com/v8/v8.git
1770 v8_branch = 'origin/candidates'
1771 bleeding_edge_branch = 'origin/master'
1773 # Support for the chromium revisions with external V8 repo.
1774 # ie https://chromium.googlesource.com/external/v8.git
1775 cmd = ['config', '--get', 'remote.origin.url']
1776 v8_repo_url = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1778 if 'external/v8.git' in v8_repo_url:
1779 v8_branch = 'origin/master'
1780 bleeding_edge_branch = 'origin/bleeding_edge'
1782 r1 = self._GetNearestV8BleedingEdgeFromTrunk(
1783 min_revision_state.revision,
1784 v8_branch,
1785 bleeding_edge_branch,
1786 search_forward=True)
1787 r2 = self._GetNearestV8BleedingEdgeFromTrunk(
1788 max_revision_state.revision,
1789 v8_branch,
1790 bleeding_edge_branch,
1791 search_forward=False)
1792 min_revision_state.external['v8_bleeding_edge'] = r1
1793 max_revision_state.external['v8_bleeding_edge'] = r2
1795 if (not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1796 min_revision_state.revision, bleeding_edge_branch)
1797 or not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1798 max_revision_state.revision, bleeding_edge_branch)):
1799 self.warnings.append(
1800 'Trunk revisions in V8 did not map directly to bleeding_edge. '
1801 'Attempted to expand the range to find V8 rolls which did map '
1802 'directly to bleeding_edge revisions, but results might not be '
1803 'valid.')
1805 def _FindNextDepotToBisect(
1806 self, current_depot, min_revision_state, max_revision_state):
1807 """Decides which depot the script should dive into next (if any).
1809 Args:
1810 current_depot: Current depot being bisected.
1811 min_revision_state: State of the earliest revision in the bisect range.
1812 max_revision_state: State of the latest revision in the bisect range.
1814 Returns:
1815 Name of the depot to bisect next, or None.
1817 external_depot = None
1818 for next_depot in bisect_utils.DEPOT_NAMES:
1819 if ('platform' in bisect_utils.DEPOT_DEPS_NAME[next_depot] and
1820 bisect_utils.DEPOT_DEPS_NAME[next_depot]['platform'] != os.name):
1821 continue
1823 if not (bisect_utils.DEPOT_DEPS_NAME[next_depot]['recurse']
1824 and min_revision_state.depot
1825 in bisect_utils.DEPOT_DEPS_NAME[next_depot]['from']):
1826 continue
1828 if current_depot == 'v8':
1829 # We grab the bleeding_edge info here rather than earlier because we
1830 # finally have the revision range. From that we can search forwards and
1831 # backwards to try to match trunk revisions to bleeding_edge.
1832 self._FillInV8BleedingEdgeInfo(min_revision_state, max_revision_state)
1834 if (min_revision_state.external.get(next_depot) ==
1835 max_revision_state.external.get(next_depot)):
1836 continue
1838 if (min_revision_state.external.get(next_depot) and
1839 max_revision_state.external.get(next_depot)):
1840 external_depot = next_depot
1841 break
1843 return external_depot
1845 def PrepareToBisectOnDepot(
1846 self, current_depot, start_revision, end_revision, previous_revision):
1847 """Changes to the appropriate directory and gathers a list of revisions
1848 to bisect between |start_revision| and |end_revision|.
1850 Args:
1851 current_depot: The depot we want to bisect.
1852 start_revision: Start of the revision range.
1853 end_revision: End of the revision range.
1854 previous_revision: The last revision we synced to on |previous_depot|.
1856 Returns:
1857 A list containing the revisions between |start_revision| and
1858 |end_revision| inclusive.
1860 # Change into working directory of external library to run
1861 # subsequent commands.
1862 self.depot_registry.ChangeToDepotDir(current_depot)
1864 # V8 (and possibly others) is merged in periodically. Bisecting
1865 # this directory directly won't give much good info.
1866 if 'custom_deps' in bisect_utils.DEPOT_DEPS_NAME[current_depot]:
1867 config_path = os.path.join(self.src_cwd, '..')
1868 if bisect_utils.RunGClientAndCreateConfig(
1869 self.opts, bisect_utils.DEPOT_DEPS_NAME[current_depot]['custom_deps'],
1870 cwd=config_path):
1871 return []
1872 if bisect_utils.RunGClient(
1873 ['sync', '--revision', previous_revision], cwd=self.src_cwd):
1874 return []
1876 if current_depot == 'v8_bleeding_edge':
1877 self.depot_registry.ChangeToDepotDir('chromium')
1879 shutil.move('v8', 'v8.bak')
1880 shutil.move('v8_bleeding_edge', 'v8')
1882 self.cleanup_commands.append(['mv', 'v8', 'v8_bleeding_edge'])
1883 self.cleanup_commands.append(['mv', 'v8.bak', 'v8'])
1885 self.depot_registry.SetDepotDir(
1886 'v8_bleeding_edge', os.path.join(self.src_cwd, 'v8'))
1887 self.depot_registry.SetDepotDir(
1888 'v8', os.path.join(self.src_cwd, 'v8.bak'))
1890 self.depot_registry.ChangeToDepotDir(current_depot)
1892 depot_revision_list = self.GetRevisionList(current_depot,
1893 end_revision,
1894 start_revision)
1896 self.depot_registry.ChangeToDepotDir('chromium')
1898 return depot_revision_list
1900 def GatherReferenceValues(self, good_rev, bad_rev, cmd, metric, target_depot):
1901 """Gathers reference values by running the performance tests on the
1902 known good and bad revisions.
1904 Args:
1905 good_rev: The last known good revision where the performance regression
1906 has not occurred yet.
1907 bad_rev: A revision where the performance regression has already occurred.
1908 cmd: The command to execute the performance test.
1909 metric: The metric being tested for regression.
1911 Returns:
1912 A tuple with the results of building and running each revision.
1914 bad_run_results = self.RunTest(bad_rev, target_depot, cmd, metric)
1916 good_run_results = None
1918 if not bad_run_results[1]:
1919 good_run_results = self.RunTest(good_rev, target_depot, cmd, metric)
1921 return (bad_run_results, good_run_results)
1923 def PrintRevisionsToBisectMessage(self, revision_list, depot):
1924 if self.opts.output_buildbot_annotations:
1925 step_name = 'Bisection Range: [%s:%s - %s]' % (depot, revision_list[-1],
1926 revision_list[0])
1927 bisect_utils.OutputAnnotationStepStart(step_name)
1929 print
1930 print 'Revisions to bisect on [%s]:' % depot
1931 for revision_id in revision_list:
1932 print ' -> %s' % (revision_id, )
1933 print
1935 if self.opts.output_buildbot_annotations:
1936 bisect_utils.OutputAnnotationStepClosed()
1938 def NudgeRevisionsIfDEPSChange(self, bad_revision, good_revision,
1939 good_svn_revision=None):
1940 """Checks to see if changes to DEPS file occurred, and that the revision
1941 range also includes the change to .DEPS.git. If it doesn't, attempts to
1942 expand the revision range to include it.
1944 Args:
1945 bad_revision: First known bad git revision.
1946 good_revision: Last known good git revision.
1947 good_svn_revision: Last known good svn revision.
1949 Returns:
1950 A tuple with the new bad and good revisions.
1952 # DONOT perform nudge because at revision 291563 .DEPS.git was removed
1953 # and source contain only DEPS file for dependency changes.
1954 if good_svn_revision >= 291563:
1955 return (bad_revision, good_revision)
1957 if self.opts.target_platform == 'chromium':
1958 changes_to_deps = source_control.QueryFileRevisionHistory(
1959 bisect_utils.FILE_DEPS, good_revision, bad_revision)
1961 if changes_to_deps:
1962 # DEPS file was changed, search from the oldest change to DEPS file to
1963 # bad_revision to see if there are matching .DEPS.git changes.
1964 oldest_deps_change = changes_to_deps[-1]
1965 changes_to_gitdeps = source_control.QueryFileRevisionHistory(
1966 bisect_utils.FILE_DEPS_GIT, oldest_deps_change, bad_revision)
1968 if len(changes_to_deps) != len(changes_to_gitdeps):
1969 # Grab the timestamp of the last DEPS change
1970 cmd = ['log', '--format=%ct', '-1', changes_to_deps[0]]
1971 output = bisect_utils.CheckRunGit(cmd)
1972 commit_time = int(output)
1974 # Try looking for a commit that touches the .DEPS.git file in the
1975 # next 15 minutes after the DEPS file change.
1976 cmd = [
1977 'log', '--format=%H', '-1',
1978 '--before=%d' % (commit_time + 900),
1979 '--after=%d' % commit_time,
1980 'origin/master', '--', bisect_utils.FILE_DEPS_GIT
1982 output = bisect_utils.CheckRunGit(cmd)
1983 output = output.strip()
1984 if output:
1985 self.warnings.append(
1986 'Detected change to DEPS and modified '
1987 'revision range to include change to .DEPS.git')
1988 return (output, good_revision)
1989 else:
1990 self.warnings.append(
1991 'Detected change to DEPS but couldn\'t find '
1992 'matching change to .DEPS.git')
1993 return (bad_revision, good_revision)
1995 def CheckIfRevisionsInProperOrder(
1996 self, target_depot, good_revision, bad_revision):
1997 """Checks that |good_revision| is an earlier revision than |bad_revision|.
1999 Args:
2000 good_revision: Number/tag of the known good revision.
2001 bad_revision: Number/tag of the known bad revision.
2003 Returns:
2004 True if the revisions are in the proper order (good earlier than bad).
2006 cwd = self.depot_registry.GetDepotDir(target_depot)
2007 good_position = source_control.GetCommitPosition(good_revision, cwd)
2008 bad_position = source_control.GetCommitPosition(bad_revision, cwd)
2009 # Compare commit timestamp for repos that don't support commit position.
2010 if not (bad_position and good_position):
2011 logging.info('Could not get commit positions for revisions %s and %s in '
2012 'depot %s', good_position, bad_position, target_depot)
2013 good_position = source_control.GetCommitTime(good_revision, cwd=cwd)
2014 bad_position = source_control.GetCommitTime(bad_revision, cwd=cwd)
2016 return good_position <= bad_position
2018 def CanPerformBisect(self, good_revision, bad_revision):
2019 """Checks whether a given revision is bisectable.
2021 Checks for following:
2022 1. Non-bisectable revisions for android bots (refer to crbug.com/385324).
2023 2. Non-bisectable revisions for Windows bots (refer to crbug.com/405274).
2025 Args:
2026 good_revision: Known good revision.
2027 bad_revision: Known bad revision.
2029 Returns:
2030 A dictionary indicating the result. If revision is not bisectable,
2031 this will contain the field "error", otherwise None.
2033 if self.opts.target_platform == 'android':
2034 good_revision = source_control.GetCommitPosition(good_revision)
2035 if (bisect_utils.IsStringInt(good_revision)
2036 and good_revision < 265549):
2037 return {'error': (
2038 'Bisect cannot continue for the given revision range.\n'
2039 'It is impossible to bisect Android regressions '
2040 'prior to r265549, which allows the bisect bot to '
2041 'rely on Telemetry to do apk installation of the most recently '
2042 'built local ChromeShell(refer to crbug.com/385324).\n'
2043 'Please try bisecting revisions greater than or equal to r265549.')}
2045 if bisect_utils.IsWindowsHost():
2046 good_revision = source_control.GetCommitPosition(good_revision)
2047 bad_revision = source_control.GetCommitPosition(bad_revision)
2048 if (bisect_utils.IsStringInt(good_revision) and
2049 bisect_utils.IsStringInt(bad_revision)):
2050 if (289987 <= good_revision < 290716 or
2051 289987 <= bad_revision < 290716):
2052 return {'error': ('Oops! Revision between r289987 and r290716 are '
2053 'marked as dead zone for Windows due to '
2054 'crbug.com/405274. Please try another range.')}
2056 return None
2058 def _GatherResultsFromRevertedCulpritCL(
2059 self, results, target_depot, command_to_run, metric):
2060 """Gathers performance results with/without culprit CL.
2062 Attempts to revert the culprit CL against ToT and runs the
2063 performance tests again with and without the CL, adding the results to
2064 the over bisect results.
2066 Args:
2067 results: BisectResults from the bisect.
2068 target_depot: The target depot we're bisecting.
2069 command_to_run: Specify the command to execute the performance test.
2070 metric: The performance metric to monitor.
2072 run_results_tot, run_results_reverted = self._RevertCulpritCLAndRetest(
2073 results, target_depot, command_to_run, metric)
2075 results.AddRetestResults(run_results_tot, run_results_reverted)
2077 if len(results.culprit_revisions) != 1:
2078 return
2080 # Cleanup reverted files if anything is left.
2081 _, _, culprit_depot = results.culprit_revisions[0]
2082 bisect_utils.CheckRunGit(
2083 ['reset', '--hard', 'HEAD'],
2084 cwd=self.depot_registry.GetDepotDir(culprit_depot))
2086 def _RevertCL(self, culprit_revision, culprit_depot):
2087 """Reverts the specified revision in the specified depot."""
2088 if self.opts.output_buildbot_annotations:
2089 bisect_utils.OutputAnnotationStepStart(
2090 'Reverting culprit CL: %s' % culprit_revision)
2091 _, return_code = bisect_utils.RunGit(
2092 ['revert', '--no-commit', culprit_revision],
2093 cwd=self.depot_registry.GetDepotDir(culprit_depot))
2094 if return_code:
2095 bisect_utils.OutputAnnotationStepWarning()
2096 bisect_utils.OutputAnnotationStepText('Failed to revert CL cleanly.')
2097 if self.opts.output_buildbot_annotations:
2098 bisect_utils.OutputAnnotationStepClosed()
2099 return not return_code
2101 def _RevertCulpritCLAndRetest(
2102 self, results, target_depot, command_to_run, metric):
2103 """Reverts the culprit CL against ToT and runs the performance test.
2105 Attempts to revert the culprit CL against ToT and runs the
2106 performance tests again with and without the CL.
2108 Args:
2109 results: BisectResults from the bisect.
2110 target_depot: The target depot we're bisecting.
2111 command_to_run: Specify the command to execute the performance test.
2112 metric: The performance metric to monitor.
2114 Returns:
2115 A tuple with the results of running the CL at ToT/reverted.
2117 # Might want to retest ToT with a revert of the CL to confirm that
2118 # performance returns.
2119 if results.confidence < bisect_utils.HIGH_CONFIDENCE:
2120 return (None, None)
2122 # If there were multiple culprit CLs, we won't try to revert.
2123 if len(results.culprit_revisions) != 1:
2124 return (None, None)
2126 culprit_revision, _, culprit_depot = results.culprit_revisions[0]
2128 if not self._SyncRevision(target_depot, None, 'gclient'):
2129 return (None, None)
2131 head_revision = bisect_utils.CheckRunGit(['log', '--format=%H', '-1'])
2132 head_revision = head_revision.strip()
2134 if not self._RevertCL(culprit_revision, culprit_depot):
2135 return (None, None)
2137 # If the culprit CL happened to be in a depot that gets pulled in, we
2138 # can't revert the change and issue a try job to build, since that would
2139 # require modifying both the DEPS file and files in another depot.
2140 # Instead, we build locally.
2141 force_build = (culprit_depot != target_depot)
2142 if force_build:
2143 results.warnings.append(
2144 'Culprit CL is in another depot, attempting to revert and build'
2145 ' locally to retest. This may not match the performance of official'
2146 ' builds.')
2148 run_results_reverted = self._RunTestWithAnnotations(
2149 'Re-Testing ToT with reverted culprit',
2150 'Failed to run reverted CL.',
2151 head_revision, target_depot, command_to_run, metric, force_build)
2153 # Clear the reverted file(s).
2154 bisect_utils.RunGit(
2155 ['reset', '--hard', 'HEAD'],
2156 cwd=self.depot_registry.GetDepotDir(culprit_depot))
2158 # Retesting with the reverted CL failed, so bail out of retesting against
2159 # ToT.
2160 if run_results_reverted[1]:
2161 return (None, None)
2163 run_results_tot = self._RunTestWithAnnotations(
2164 'Re-Testing ToT',
2165 'Failed to run ToT.',
2166 head_revision, target_depot, command_to_run, metric, force_build)
2168 return (run_results_tot, run_results_reverted)
2170 def _RunTestWithAnnotations(
2171 self, step_text, error_text, head_revision,
2172 target_depot, command_to_run, metric, force_build):
2173 """Runs the performance test and outputs start/stop annotations.
2175 Args:
2176 results: BisectResults from the bisect.
2177 target_depot: The target depot we're bisecting.
2178 command_to_run: Specify the command to execute the performance test.
2179 metric: The performance metric to monitor.
2180 force_build: Whether to force a build locally.
2182 Returns:
2183 Results of the test.
2185 if self.opts.output_buildbot_annotations:
2186 bisect_utils.OutputAnnotationStepStart(step_text)
2188 # Build and run the test again with the reverted culprit CL against ToT.
2189 run_test_results = self.RunTest(
2190 head_revision, target_depot, command_to_run,
2191 metric, skippable=False, skip_sync=True, create_patch=True,
2192 force_build=force_build)
2194 if self.opts.output_buildbot_annotations:
2195 if run_test_results[1]:
2196 bisect_utils.OutputAnnotationStepWarning()
2197 bisect_utils.OutputAnnotationStepText(error_text)
2198 bisect_utils.OutputAnnotationStepClosed()
2200 return run_test_results
2202 def Run(self, command_to_run, bad_revision_in, good_revision_in, metric):
2203 """Given known good and bad revisions, run a binary search on all
2204 intermediate revisions to determine the CL where the performance regression
2205 occurred.
2207 Args:
2208 command_to_run: Specify the command to execute the performance test.
2209 good_revision: Number/tag of the known good revision.
2210 bad_revision: Number/tag of the known bad revision.
2211 metric: The performance metric to monitor.
2213 Returns:
2214 A BisectResults object.
2216 # Choose depot to bisect first
2217 target_depot = 'chromium'
2218 if self.opts.target_platform == 'android-chrome':
2219 target_depot = 'android-chrome'
2221 cwd = os.getcwd()
2222 self.depot_registry.ChangeToDepotDir(target_depot)
2224 # If they passed SVN revisions, we can try match them to git SHA1 hashes.
2225 bad_revision = source_control.ResolveToRevision(
2226 bad_revision_in, target_depot, bisect_utils.DEPOT_DEPS_NAME, 100)
2227 good_revision = source_control.ResolveToRevision(
2228 good_revision_in, target_depot, bisect_utils.DEPOT_DEPS_NAME, -100)
2230 os.chdir(cwd)
2231 if bad_revision is None:
2232 return BisectResults(
2233 error='Couldn\'t resolve [%s] to SHA1.' % bad_revision_in)
2235 if good_revision is None:
2236 return BisectResults(
2237 error='Couldn\'t resolve [%s] to SHA1.' % good_revision_in)
2239 # Check that they didn't accidentally swap good and bad revisions.
2240 if not self.CheckIfRevisionsInProperOrder(
2241 target_depot, good_revision, bad_revision):
2242 return BisectResults(error='Bad rev (%s) appears to be earlier than good '
2243 'rev (%s).' % (good_revision, bad_revision))
2245 bad_revision, good_revision = self.NudgeRevisionsIfDEPSChange(
2246 bad_revision, good_revision, good_revision_in)
2247 if self.opts.output_buildbot_annotations:
2248 bisect_utils.OutputAnnotationStepStart('Gathering Revisions')
2250 cannot_bisect = self.CanPerformBisect(good_revision, bad_revision)
2251 if cannot_bisect:
2252 return BisectResults(error=cannot_bisect.get('error'))
2254 print 'Gathering revision range for bisection.'
2255 # Retrieve a list of revisions to do bisection on.
2256 revision_list = self.GetRevisionList(target_depot, bad_revision,
2257 good_revision)
2259 if self.opts.output_buildbot_annotations:
2260 bisect_utils.OutputAnnotationStepClosed()
2262 if revision_list:
2263 self.PrintRevisionsToBisectMessage(revision_list, target_depot)
2265 if self.opts.output_buildbot_annotations:
2266 bisect_utils.OutputAnnotationStepStart('Gathering Reference Values')
2268 print 'Gathering reference values for bisection.'
2270 # Perform the performance tests on the good and bad revisions, to get
2271 # reference values.
2272 bad_results, good_results = self.GatherReferenceValues(good_revision,
2273 bad_revision,
2274 command_to_run,
2275 metric,
2276 target_depot)
2278 if self.opts.output_buildbot_annotations:
2279 bisect_utils.OutputAnnotationStepClosed()
2281 if bad_results[1]:
2282 error = ('An error occurred while building and running the \'bad\' '
2283 'reference value. The bisect cannot continue without '
2284 'a working \'bad\' revision to start from.\n\nError: %s' %
2285 bad_results[0])
2286 return BisectResults(error=error)
2288 if good_results[1]:
2289 error = ('An error occurred while building and running the \'good\' '
2290 'reference value. The bisect cannot continue without '
2291 'a working \'good\' revision to start from.\n\nError: %s' %
2292 good_results[0])
2293 return BisectResults(error=error)
2295 # We need these reference values to determine if later runs should be
2296 # classified as pass or fail.
2297 known_bad_value = bad_results[0]
2298 known_good_value = good_results[0]
2300 # Check the direction of improvement only if the improvement_direction
2301 # option is set to a specific direction (1 for higher is better or -1 for
2302 # lower is better).
2303 improvement_dir = self.opts.improvement_direction
2304 if improvement_dir:
2305 higher_is_better = improvement_dir > 0
2306 if higher_is_better:
2307 message = "Expecting higher values to be better for this metric, "
2308 else:
2309 message = "Expecting lower values to be better for this metric, "
2310 metric_increased = known_bad_value['mean'] > known_good_value['mean']
2311 if metric_increased:
2312 message += "and the metric appears to have increased. "
2313 else:
2314 message += "and the metric appears to have decreased. "
2315 if ((higher_is_better and metric_increased) or
2316 (not higher_is_better and not metric_increased)):
2317 error = (message + 'Then, the test results for the ends of the given '
2318 '\'good\' - \'bad\' range of revisions represent an '
2319 'improvement (and not a regression).')
2320 return BisectResults(error=error)
2321 logging.info(message + "Therefore we continue to bisect.")
2323 bisect_state = BisectState(target_depot, revision_list)
2324 revision_states = bisect_state.GetRevisionStates()
2326 min_revision = 0
2327 max_revision = len(revision_states) - 1
2329 # Can just mark the good and bad revisions explicitly here since we
2330 # already know the results.
2331 bad_revision_state = revision_states[min_revision]
2332 bad_revision_state.external = bad_results[2]
2333 bad_revision_state.perf_time = bad_results[3]
2334 bad_revision_state.build_time = bad_results[4]
2335 bad_revision_state.passed = False
2336 bad_revision_state.value = known_bad_value
2338 good_revision_state = revision_states[max_revision]
2339 good_revision_state.external = good_results[2]
2340 good_revision_state.perf_time = good_results[3]
2341 good_revision_state.build_time = good_results[4]
2342 good_revision_state.passed = True
2343 good_revision_state.value = known_good_value
2345 # Check how likely it is that the good and bad results are different
2346 # beyond chance-induced variation.
2347 confidence_error = False
2348 if not self.opts.debug_ignore_regression_confidence:
2349 confidence_error = _CheckRegressionConfidenceError(good_revision,
2350 bad_revision,
2351 known_good_value,
2352 known_bad_value)
2353 if confidence_error:
2354 # If there is no significant difference between "good" and "bad"
2355 # revision results, then the "bad revision" is considered "good".
2356 # TODO(qyearsley): Remove this if it is not necessary.
2357 bad_revision_state.passed = True
2358 self.warnings.append(confidence_error)
2359 return BisectResults(bisect_state, self.depot_registry, self.opts,
2360 self.warnings)
2362 while True:
2363 if not revision_states:
2364 break
2366 if max_revision - min_revision <= 1:
2367 min_revision_state = revision_states[min_revision]
2368 max_revision_state = revision_states[max_revision]
2369 current_depot = min_revision_state.depot
2370 # TODO(sergiyb): Under which conditions can first two branches be hit?
2371 if min_revision_state.passed == '?':
2372 next_revision_index = min_revision
2373 elif max_revision_state.passed == '?':
2374 next_revision_index = max_revision
2375 elif current_depot in ['android-chrome', 'chromium', 'v8']:
2376 previous_revision = revision_states[min_revision].revision
2377 # If there were changes to any of the external libraries we track,
2378 # should bisect the changes there as well.
2379 external_depot = self._FindNextDepotToBisect(
2380 current_depot, min_revision_state, max_revision_state)
2381 # If there was no change in any of the external depots, the search
2382 # is over.
2383 if not external_depot:
2384 if current_depot == 'v8':
2385 self.warnings.append(
2386 'Unfortunately, V8 bisection couldn\'t '
2387 'continue any further. The script can only bisect into '
2388 'V8\'s bleeding_edge repository if both the current and '
2389 'previous revisions in trunk map directly to revisions in '
2390 'bleeding_edge.')
2391 break
2393 earliest_revision = max_revision_state.external[external_depot]
2394 latest_revision = min_revision_state.external[external_depot]
2396 new_revision_list = self.PrepareToBisectOnDepot(
2397 external_depot, earliest_revision, latest_revision,
2398 previous_revision)
2400 if not new_revision_list:
2401 error = ('An error occurred attempting to retrieve revision '
2402 'range: [%s..%s]' % (earliest_revision, latest_revision))
2403 return BisectResults(error=error)
2405 revision_states = bisect_state.CreateRevisionStatesAfter(
2406 external_depot, new_revision_list, current_depot,
2407 previous_revision)
2409 # Reset the bisection and perform it on the newly inserted states.
2410 min_revision = 0
2411 max_revision = len(revision_states) - 1
2413 print ('Regression in metric %s appears to be the result of '
2414 'changes in [%s].' % (metric, external_depot))
2416 revision_list = [state.revision for state in revision_states]
2417 self.PrintRevisionsToBisectMessage(revision_list, external_depot)
2419 continue
2420 else:
2421 break
2422 else:
2423 next_revision_index = (int((max_revision - min_revision) / 2) +
2424 min_revision)
2426 next_revision_state = revision_states[next_revision_index]
2427 next_revision = next_revision_state.revision
2428 next_depot = next_revision_state.depot
2430 self.depot_registry.ChangeToDepotDir(next_depot)
2432 message = 'Working on [%s:%s]' % (next_depot, next_revision)
2433 print message
2434 if self.opts.output_buildbot_annotations:
2435 bisect_utils.OutputAnnotationStepStart(message)
2437 run_results = self.RunTest(next_revision, next_depot, command_to_run,
2438 metric, skippable=True)
2440 # If the build is successful, check whether or not the metric
2441 # had regressed.
2442 if not run_results[1]:
2443 if len(run_results) > 2:
2444 next_revision_state.external = run_results[2]
2445 next_revision_state.perf_time = run_results[3]
2446 next_revision_state.build_time = run_results[4]
2448 passed_regression = self._CheckIfRunPassed(run_results[0],
2449 known_good_value,
2450 known_bad_value)
2452 next_revision_state.passed = passed_regression
2453 next_revision_state.value = run_results[0]
2455 if passed_regression:
2456 max_revision = next_revision_index
2457 else:
2458 min_revision = next_revision_index
2459 else:
2460 if run_results[1] == BUILD_RESULT_SKIPPED:
2461 next_revision_state.passed = 'Skipped'
2462 elif run_results[1] == BUILD_RESULT_FAIL:
2463 next_revision_state.passed = 'Build Failed'
2465 print run_results[0]
2467 # If the build is broken, remove it and redo search.
2468 revision_states.pop(next_revision_index)
2470 max_revision -= 1
2472 if self.opts.output_buildbot_annotations:
2473 self.printer.PrintPartialResults(bisect_state)
2474 bisect_utils.OutputAnnotationStepClosed()
2476 self._ConfidenceExtraTestRuns(min_revision_state, max_revision_state,
2477 command_to_run, metric)
2478 results = BisectResults(bisect_state, self.depot_registry, self.opts,
2479 self.warnings)
2481 self._GatherResultsFromRevertedCulpritCL(
2482 results, target_depot, command_to_run, metric)
2484 return results
2485 else:
2486 # Weren't able to sync and retrieve the revision range.
2487 error = ('An error occurred attempting to retrieve revision range: '
2488 '[%s..%s]' % (good_revision, bad_revision))
2489 return BisectResults(error=error)
2491 def _ConfidenceExtraTestRuns(self, good_state, bad_state, command_to_run,
2492 metric):
2493 if (bool(good_state.passed) != bool(bad_state.passed)
2494 and good_state.passed not in ('Skipped', 'Build Failed')
2495 and bad_state.passed not in ('Skipped', 'Build Failed')):
2496 for state in (good_state, bad_state):
2497 run_results = self.RunTest(
2498 state.revision,
2499 state.depot,
2500 command_to_run,
2501 metric,
2502 test_run_multiplier=BORDER_REVISIONS_EXTRA_RUNS)
2503 # Is extend the right thing to do here?
2504 if run_results[1] != BUILD_RESULT_FAIL:
2505 state.value['values'].extend(run_results[0]['values'])
2506 else:
2507 warning_text = 'Re-test of revision %s failed with error message: %s'
2508 warning_text %= (state.revision, run_results[0])
2509 if warning_text not in self.warnings:
2510 self.warnings.append(warning_text)
2513 def _IsPlatformSupported():
2514 """Checks that this platform and build system are supported.
2516 Args:
2517 opts: The options parsed from the command line.
2519 Returns:
2520 True if the platform and build system are supported.
2522 # Haven't tested the script out on any other platforms yet.
2523 supported = ['posix', 'nt']
2524 return os.name in supported
2527 def RemoveBuildFiles(build_type):
2528 """Removes build files from previous runs."""
2529 out_dir = os.path.join('out', build_type)
2530 build_dir = os.path.join('build', build_type)
2531 logging.info('Removing build files in "%s" and "%s".',
2532 os.path.abspath(out_dir), os.path.abspath(build_dir))
2533 try:
2534 RemakeDirectoryTree(out_dir)
2535 RemakeDirectoryTree(build_dir)
2536 except Exception as e:
2537 raise RuntimeError('Got error in RemoveBuildFiles: %s' % e)
2540 def RemakeDirectoryTree(path_to_dir):
2541 """Removes a directory tree and replaces it with an empty one.
2543 Returns True if successful, False otherwise.
2545 RemoveDirectoryTree(path_to_dir)
2546 MaybeMakeDirectory(path_to_dir)
2549 def RemoveDirectoryTree(path_to_dir):
2550 """Removes a directory tree. Returns True if successful or False otherwise."""
2551 if os.path.isfile(path_to_dir):
2552 logging.info('REMOVING FILE %s' % path_to_dir)
2553 os.remove(path_to_dir)
2554 try:
2555 if os.path.exists(path_to_dir):
2556 shutil.rmtree(path_to_dir)
2557 except OSError, e:
2558 if e.errno != errno.ENOENT:
2559 raise
2562 # This is copied from build/scripts/common/chromium_utils.py.
2563 def MaybeMakeDirectory(*path):
2564 """Creates an entire path, if it doesn't already exist."""
2565 file_path = os.path.join(*path)
2566 try:
2567 os.makedirs(file_path)
2568 except OSError as e:
2569 if e.errno != errno.EEXIST:
2570 raise
2573 class BisectOptions(object):
2574 """Options to be used when running bisection."""
2575 def __init__(self):
2576 super(BisectOptions, self).__init__()
2578 self.target_platform = 'chromium'
2579 self.build_preference = None
2580 self.good_revision = None
2581 self.bad_revision = None
2582 self.use_goma = None
2583 self.goma_dir = None
2584 self.goma_threads = 64
2585 self.repeat_test_count = 20
2586 self.truncate_percent = 25
2587 self.max_time_minutes = 20
2588 self.metric = None
2589 self.command = None
2590 self.output_buildbot_annotations = None
2591 self.no_custom_deps = False
2592 self.working_directory = None
2593 self.extra_src = None
2594 self.debug_ignore_build = None
2595 self.debug_ignore_sync = None
2596 self.debug_ignore_perf_test = None
2597 self.debug_ignore_regression_confidence = None
2598 self.debug_fake_first_test_mean = 0
2599 self.target_arch = 'ia32'
2600 self.target_build_type = 'Release'
2601 self.builder_type = 'perf'
2602 self.bisect_mode = bisect_utils.BISECT_MODE_MEAN
2603 self.improvement_direction = 0
2604 self.bug_id = ''
2606 @staticmethod
2607 def _AddBisectOptionsGroup(parser):
2608 group = parser.add_argument_group('Bisect options')
2609 group.add_argument('-c', '--command', required=True,
2610 help='A command to execute your performance test at '
2611 'each point in the bisection.')
2612 group.add_argument('-b', '--bad_revision', required=True,
2613 help='A bad revision to start bisection. Must be later '
2614 'than good revision. May be either a git or svn '
2615 'revision.')
2616 group.add_argument('-g', '--good_revision', required=True,
2617 help='A revision to start bisection where performance '
2618 'test is known to pass. Must be earlier than the '
2619 'bad revision. May be either a git or a svn '
2620 'revision.')
2621 group.add_argument('-m', '--metric',
2622 help='The desired metric to bisect on. For example '
2623 '"vm_rss_final_b/vm_rss_f_b"')
2624 group.add_argument('-d', '--improvement_direction', type=int, default=0,
2625 help='An integer number representing the direction of '
2626 'improvement. 1 for higher is better, -1 for lower '
2627 'is better, 0 for ignore (default).')
2628 group.add_argument('-r', '--repeat_test_count', type=int, default=20,
2629 choices=range(1, 101),
2630 help='The number of times to repeat the performance '
2631 'test. Values will be clamped to range [1, 100]. '
2632 'Default value is 20.')
2633 group.add_argument('--max_time_minutes', type=int, default=20,
2634 choices=range(1, 61),
2635 help='The maximum time (in minutes) to take running the '
2636 'performance tests. The script will run the '
2637 'performance tests according to '
2638 '--repeat_test_count, so long as it doesn\'t exceed'
2639 ' --max_time_minutes. Values will be clamped to '
2640 'range [1, 60]. Default value is 20.')
2641 group.add_argument('-t', '--truncate_percent', type=int, default=25,
2642 help='The highest/lowest percent are discarded to form '
2643 'a truncated mean. Values will be clamped to range '
2644 '[0, 25]. Default value is 25 percent.')
2645 group.add_argument('--bisect_mode', default=bisect_utils.BISECT_MODE_MEAN,
2646 choices=[bisect_utils.BISECT_MODE_MEAN,
2647 bisect_utils.BISECT_MODE_STD_DEV,
2648 bisect_utils.BISECT_MODE_RETURN_CODE],
2649 help='The bisect mode. Choices are to bisect on the '
2650 'difference in mean, std_dev, or return_code.')
2651 group.add_argument('--bug_id', default='',
2652 help='The id for the bug associated with this bisect. ' +
2653 'If this number is given, bisect will attempt to ' +
2654 'verify that the bug is not closed before '
2655 'starting.')
2657 @staticmethod
2658 def _AddBuildOptionsGroup(parser):
2659 group = parser.add_argument_group('Build options')
2660 group.add_argument('-w', '--working_directory',
2661 help='Path to the working directory where the script '
2662 'will do an initial checkout of the chromium depot. The '
2663 'files will be placed in a subdirectory "bisect" under '
2664 'working_directory and that will be used to perform the '
2665 'bisection. This parameter is optional, if it is not '
2666 'supplied, the script will work from the current depot.')
2667 group.add_argument('--build_preference',
2668 choices=['msvs', 'ninja', 'make'],
2669 help='The preferred build system to use. On linux/mac '
2670 'the options are make/ninja. On Windows, the '
2671 'options are msvs/ninja.')
2672 group.add_argument('--target_platform', default='chromium',
2673 choices=['chromium', 'android', 'android-chrome'],
2674 help='The target platform. Choices are "chromium" '
2675 '(current platform), or "android". If you specify '
2676 'something other than "chromium", you must be '
2677 'properly set up to build that platform.')
2678 group.add_argument('--no_custom_deps', dest='no_custom_deps',
2679 action='store_true', default=False,
2680 help='Run the script with custom_deps or not.')
2681 group.add_argument('--extra_src',
2682 help='Path to a script which can be used to modify the '
2683 'bisect script\'s behavior.')
2684 group.add_argument('--use_goma', action='store_true',
2685 help='Add a bunch of extra threads for goma, and enable '
2686 'goma')
2687 group.add_argument('--goma_dir',
2688 help='Path to goma tools (or system default if not '
2689 'specified).')
2690 group.add_argument('--goma_threads', type=int, default='64',
2691 help='Number of threads for goma, only if using goma.')
2692 group.add_argument('--output_buildbot_annotations', action='store_true',
2693 help='Add extra annotation output for buildbot.')
2694 group.add_argument('--target_arch', default='ia32',
2695 dest='target_arch', choices=['ia32', 'x64', 'arm'],
2696 help='The target build architecture. Choices are "ia32" '
2697 '(default), "x64" or "arm".')
2698 group.add_argument('--target_build_type', default='Release',
2699 choices=['Release', 'Debug', 'Release_x64'],
2700 help='The target build type. Choices are "Release" '
2701 '(default), Release_x64 or "Debug".')
2702 group.add_argument('--builder_type', default=fetch_build.PERF_BUILDER,
2703 choices=[fetch_build.PERF_BUILDER,
2704 fetch_build.FULL_BUILDER,
2705 fetch_build.ANDROID_CHROME_PERF_BUILDER, ''],
2706 help='Type of builder to get build from. This '
2707 'determines both the bot that builds and the '
2708 'place where archived builds are downloaded from. '
2709 'For local builds, an empty string can be passed.')
2711 @staticmethod
2712 def _AddDebugOptionsGroup(parser):
2713 group = parser.add_argument_group('Debug options')
2714 group.add_argument('--debug_ignore_build', action='store_true',
2715 help='DEBUG: Don\'t perform builds.')
2716 group.add_argument('--debug_ignore_sync', action='store_true',
2717 help='DEBUG: Don\'t perform syncs.')
2718 group.add_argument('--debug_ignore_perf_test', action='store_true',
2719 help='DEBUG: Don\'t perform performance tests.')
2720 group.add_argument('--debug_ignore_regression_confidence',
2721 action='store_true',
2722 help='DEBUG: Don\'t score the confidence of the initial '
2723 'good and bad revisions\' test results.')
2724 group.add_argument('--debug_fake_first_test_mean', type=int, default='0',
2725 help='DEBUG: When faking performance tests, return this '
2726 'value as the mean of the first performance test, '
2727 'and return a mean of 0.0 for further tests.')
2728 return group
2730 @classmethod
2731 def _CreateCommandLineParser(cls):
2732 """Creates a parser with bisect options.
2734 Returns:
2735 An instance of argparse.ArgumentParser.
2737 usage = ('%(prog)s [options] [-- chromium-options]\n'
2738 'Perform binary search on revision history to find a minimal '
2739 'range of revisions where a performance metric regressed.\n')
2741 parser = argparse.ArgumentParser(usage=usage)
2742 cls._AddBisectOptionsGroup(parser)
2743 cls._AddBuildOptionsGroup(parser)
2744 cls._AddDebugOptionsGroup(parser)
2745 return parser
2747 def ParseCommandLine(self):
2748 """Parses the command line for bisect options."""
2749 parser = self._CreateCommandLineParser()
2750 opts = parser.parse_args()
2752 try:
2753 if (not opts.metric and
2754 opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE):
2755 raise RuntimeError('missing required parameter: --metric')
2757 if opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE:
2758 metric_values = opts.metric.split('/')
2759 if len(metric_values) != 2:
2760 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
2761 opts.metric = metric_values
2763 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25) / 100.0
2765 for k, v in opts.__dict__.iteritems():
2766 assert hasattr(self, k), 'Invalid %s attribute in BisectOptions.' % k
2767 setattr(self, k, v)
2768 except RuntimeError, e:
2769 output_string = StringIO.StringIO()
2770 parser.print_help(file=output_string)
2771 error_message = '%s\n\n%s' % (e.message, output_string.getvalue())
2772 output_string.close()
2773 raise RuntimeError(error_message)
2775 @staticmethod
2776 def FromDict(values):
2777 """Creates an instance of BisectOptions from a dictionary.
2779 Args:
2780 values: a dict containing options to set.
2782 Returns:
2783 An instance of BisectOptions.
2785 opts = BisectOptions()
2786 for k, v in values.iteritems():
2787 assert hasattr(opts, k), 'Invalid %s attribute in BisectOptions.' % k
2788 setattr(opts, k, v)
2790 if opts.metric and opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE:
2791 metric_values = opts.metric.split('/')
2792 if len(metric_values) != 2:
2793 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
2794 opts.metric = metric_values
2796 if opts.target_arch == 'x64' and opts.target_build_type == 'Release':
2797 opts.target_build_type = 'Release_x64'
2798 opts.repeat_test_count = min(max(opts.repeat_test_count, 1), 100)
2799 opts.max_time_minutes = min(max(opts.max_time_minutes, 1), 60)
2800 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25)
2801 opts.truncate_percent = opts.truncate_percent / 100.0
2803 return opts
2806 def _ConfigureLogging():
2807 """Trivial logging config.
2809 Configures logging to output any messages at or above INFO to standard out,
2810 without any additional formatting.
2812 logging_format = '%(message)s'
2813 logging.basicConfig(
2814 stream=logging.sys.stdout, level=logging.INFO, format=logging_format)
2817 def main():
2818 _ConfigureLogging()
2819 try:
2820 opts = BisectOptions()
2821 opts.ParseCommandLine()
2823 if opts.bug_id:
2824 if opts.output_buildbot_annotations:
2825 bisect_utils.OutputAnnotationStepStart('Checking Issue Tracker')
2826 issue_closed = query_crbug.CheckIssueClosed(opts.bug_id)
2827 if issue_closed:
2828 print 'Aborting bisect because bug is closed'
2829 else:
2830 print 'Could not confirm bug is closed, proceeding.'
2831 if opts.output_buildbot_annotations:
2832 bisect_utils.OutputAnnotationStepClosed()
2833 if issue_closed:
2834 results = BisectResults(abort_reason='the bug is closed.')
2835 bisect_test = BisectPerformanceMetrics(opts, os.getcwd())
2836 bisect_test.printer.FormatAndPrintResults(results)
2837 return 0
2839 if opts.extra_src:
2840 extra_src = bisect_utils.LoadExtraSrc(opts.extra_src)
2841 if not extra_src:
2842 raise RuntimeError('Invalid or missing --extra_src.')
2843 bisect_utils.AddAdditionalDepotInfo(extra_src.GetAdditionalDepotInfo())
2845 if opts.working_directory:
2846 custom_deps = bisect_utils.DEFAULT_GCLIENT_CUSTOM_DEPS
2847 if opts.no_custom_deps:
2848 custom_deps = None
2849 bisect_utils.CreateBisectDirectoryAndSetupDepot(opts, custom_deps)
2851 os.chdir(os.path.join(os.getcwd(), 'src'))
2852 RemoveBuildFiles(opts.target_build_type)
2854 if not _IsPlatformSupported():
2855 raise RuntimeError('Sorry, this platform isn\'t supported yet.')
2857 if not source_control.IsInGitRepository():
2858 raise RuntimeError(
2859 'Sorry, only the git workflow is supported at the moment.')
2861 # gClient sync seems to fail if you're not in master branch.
2862 if (not source_control.IsInProperBranch() and
2863 not opts.debug_ignore_sync and
2864 not opts.working_directory):
2865 raise RuntimeError('You must switch to master branch to run bisection.')
2866 bisect_test = BisectPerformanceMetrics(opts, os.getcwd())
2867 try:
2868 results = bisect_test.Run(opts.command, opts.bad_revision,
2869 opts.good_revision, opts.metric)
2870 if results.error:
2871 raise RuntimeError(results.error)
2872 bisect_test.printer.FormatAndPrintResults(results)
2873 return 0
2874 finally:
2875 bisect_test.PerformCleanup()
2876 except RuntimeError as e:
2877 if opts.output_buildbot_annotations:
2878 # The perf dashboard scrapes the "results" step in order to comment on
2879 # bugs. If you change this, please update the perf dashboard as well.
2880 bisect_utils.OutputAnnotationStepStart('Results')
2881 print 'Runtime Error: %s' % e
2882 if opts.output_buildbot_annotations:
2883 bisect_utils.OutputAnnotationStepClosed()
2884 return 1
2887 if __name__ == '__main__':
2888 sys.exit(main())