Move android_app_version_* into an inner variables dict.
[chromium-blink-merge.git] / tools / auto_bisect / bisect_perf_regression.py
blobddc0f60cf82bd09149435488b36b2c501f6ff778
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)
1368 elapsed_minutes = (time.time() - start_time) / 60.0
1369 time_limit = self.opts.max_time_minutes * test_run_multiplier
1370 if elapsed_minutes >= time_limit:
1371 break
1373 if metric and len(metric_values) == 0:
1374 err_text = 'Metric %s was not found in the test output.' % metric
1375 # TODO(qyearsley): Consider also getting and displaying a list of metrics
1376 # that were found in the output here.
1377 return (err_text, failure_code, output_of_all_runs)
1379 # If we're bisecting on return codes, we're really just looking for zero vs
1380 # non-zero.
1381 values = {}
1382 if self._IsBisectModeReturnCode():
1383 # If any of the return codes is non-zero, output 1.
1384 overall_return_code = 0 if (
1385 all(current_value == 0 for current_value in metric_values)) else 1
1387 values = {
1388 'mean': overall_return_code,
1389 'std_err': 0.0,
1390 'std_dev': 0.0,
1391 'values': metric_values,
1394 print 'Results of performance test: Command returned with %d' % (
1395 overall_return_code)
1396 print
1397 elif metric:
1398 # Need to get the average value if there were multiple values.
1399 truncated_mean = math_utils.TruncatedMean(
1400 metric_values, self.opts.truncate_percent)
1401 standard_err = math_utils.StandardError(metric_values)
1402 standard_dev = math_utils.StandardDeviation(metric_values)
1404 if self._IsBisectModeStandardDeviation():
1405 metric_values = [standard_dev]
1407 values = {
1408 'mean': truncated_mean,
1409 'std_err': standard_err,
1410 'std_dev': standard_dev,
1411 'values': metric_values,
1414 print 'Results of performance test: %12f %12f' % (
1415 truncated_mean, standard_err)
1416 print
1418 overall_success = success_code
1419 if not allow_flakes and not self._IsBisectModeReturnCode():
1420 overall_success = (
1421 success_code
1422 if (all(current_value == 0 for current_value in return_codes))
1423 else failure_code)
1425 return (values, overall_success, output_of_all_runs)
1427 def PerformPreBuildCleanup(self):
1428 """Performs cleanup between runs."""
1429 print 'Cleaning up between runs.'
1430 print
1432 # Leaving these .pyc files around between runs may disrupt some perf tests.
1433 for (path, _, files) in os.walk(self.src_cwd):
1434 for cur_file in files:
1435 if cur_file.endswith('.pyc'):
1436 path_to_file = os.path.join(path, cur_file)
1437 os.remove(path_to_file)
1439 def _RunPostSync(self, _depot):
1440 """Performs any work after syncing.
1442 Args:
1443 depot: Depot name.
1445 Returns:
1446 True if successful.
1448 if 'android' in self.opts.target_platform:
1449 if not builder.SetupAndroidBuildEnvironment(
1450 self.opts, path_to_src=self.src_cwd):
1451 return False
1453 return self.RunGClientHooks()
1455 @staticmethod
1456 def ShouldSkipRevision(depot, revision):
1457 """Checks whether a particular revision can be safely skipped.
1459 Some commits can be safely skipped (such as a DEPS roll for the repos
1460 still using .DEPS.git), since the tool is git based those changes
1461 would have no effect.
1463 Args:
1464 depot: The depot being bisected.
1465 revision: Current revision we're synced to.
1467 Returns:
1468 True if we should skip building/testing this revision.
1470 # Skips revisions with DEPS on android-chrome.
1471 if depot == 'android-chrome':
1472 cmd = ['diff-tree', '--no-commit-id', '--name-only', '-r', revision]
1473 output = bisect_utils.CheckRunGit(cmd)
1475 files = output.splitlines()
1477 if len(files) == 1 and files[0] == 'DEPS':
1478 return True
1480 return False
1482 def RunTest(self, revision, depot, command, metric, skippable=False,
1483 skip_sync=False, create_patch=False, force_build=False,
1484 test_run_multiplier=1):
1485 """Performs a full sync/build/run of the specified revision.
1487 Args:
1488 revision: The revision to sync to.
1489 depot: The depot that's being used at the moment (src, webkit, etc.)
1490 command: The command to execute the performance test.
1491 metric: The performance metric being tested.
1492 skip_sync: Skip the sync step.
1493 create_patch: Create a patch with any locally modified files.
1494 force_build: Force a local build.
1495 test_run_multiplier: Factor by which to multiply the given number of runs
1496 and the set timeout period.
1498 Returns:
1499 On success, a tuple containing the results of the performance test.
1500 Otherwise, a tuple with the error message.
1502 logging.info('Running RunTest with rev "%s", command "%s"',
1503 revision, command)
1504 # Decide which sync program to use.
1505 sync_client = None
1506 if depot == 'chromium' or depot == 'android-chrome':
1507 sync_client = 'gclient'
1509 # Do the syncing for all depots.
1510 if not (self.opts.debug_ignore_sync or skip_sync):
1511 if not self._SyncRevision(depot, revision, sync_client):
1512 return ('Failed to sync: [%s]' % str(revision), BUILD_RESULT_FAIL)
1514 # Try to do any post-sync steps. This may include "gclient runhooks".
1515 if not self._RunPostSync(depot):
1516 return ('Failed to run [gclient runhooks].', BUILD_RESULT_FAIL)
1518 # Skip this revision if it can be skipped.
1519 if skippable and self.ShouldSkipRevision(depot, revision):
1520 return ('Skipped revision: [%s]' % str(revision),
1521 BUILD_RESULT_SKIPPED)
1523 # Obtain a build for this revision. This may be done by requesting a build
1524 # from another builder, waiting for it and downloading it.
1525 start_build_time = time.time()
1526 revision_to_build = revision if not force_build else None
1527 build_success = self.ObtainBuild(
1528 depot, revision=revision_to_build, create_patch=create_patch)
1529 if not build_success:
1530 return ('Failed to build revision: [%s]' % str(revision),
1531 BUILD_RESULT_FAIL)
1532 after_build_time = time.time()
1534 # Possibly alter the command.
1535 command = self.GetCompatibleCommand(command, revision, depot)
1537 # Run the command and get the results.
1538 results = self.RunPerformanceTestAndParseResults(
1539 command, metric, test_run_multiplier=test_run_multiplier)
1541 # Restore build output directory once the tests are done, to avoid
1542 # any discrepancies.
1543 if self.IsDownloadable(depot) and revision:
1544 self.BackupOrRestoreOutputDirectory(restore=True)
1546 # A value other than 0 indicates that the test couldn't be run, and results
1547 # should also include an error message.
1548 if results[1] != 0:
1549 return results
1551 external_revisions = self._Get3rdPartyRevisions(depot)
1553 if not external_revisions is None:
1554 return (results[0], results[1], external_revisions,
1555 time.time() - after_build_time, after_build_time -
1556 start_build_time)
1557 else:
1558 return ('Failed to parse DEPS file for external revisions.',
1559 BUILD_RESULT_FAIL)
1561 def _SyncRevision(self, depot, revision, sync_client):
1562 """Syncs depot to particular revision.
1564 Args:
1565 depot: The depot that's being used at the moment (src, webkit, etc.)
1566 revision: The revision to sync to.
1567 sync_client: Program used to sync, e.g. "gclient". Can be None.
1569 Returns:
1570 True if successful, False otherwise.
1572 self.depot_registry.ChangeToDepotDir(depot)
1574 if sync_client:
1575 self.PerformPreBuildCleanup()
1577 # When using gclient to sync, you need to specify the depot you
1578 # want so that all the dependencies sync properly as well.
1579 # i.e. gclient sync src@<SHA1>
1580 if sync_client == 'gclient' and revision:
1581 revision = '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME[depot]['src'],
1582 revision)
1583 if depot == 'chromium' and self.opts.target_platform == 'android-chrome':
1584 return self._SyncRevisionsForAndroidChrome(revision)
1586 return source_control.SyncToRevision(revision, sync_client)
1588 def _SyncRevisionsForAndroidChrome(self, revision):
1589 """Syncs android-chrome and chromium repos to particular revision.
1591 This is a special case for android-chrome as the gclient sync for chromium
1592 overwrites the android-chrome revision to TOT. Therefore both the repos
1593 are synced to known revisions.
1595 Args:
1596 revision: Git hash of the Chromium to sync.
1598 Returns:
1599 True if successful, False otherwise.
1601 revisions_list = [revision]
1602 current_android_rev = source_control.GetCurrentRevision(
1603 self.depot_registry.GetDepotDir('android-chrome'))
1604 revisions_list.append(
1605 '%s@%s' % (bisect_utils.DEPOT_DEPS_NAME['android-chrome']['src'],
1606 current_android_rev))
1607 return not bisect_utils.RunGClientAndSync(revisions_list)
1609 def _CheckIfRunPassed(self, current_value, known_good_value, known_bad_value):
1610 """Given known good and bad values, decide if the current_value passed
1611 or failed.
1613 Args:
1614 current_value: The value of the metric being checked.
1615 known_bad_value: The reference value for a "failed" run.
1616 known_good_value: The reference value for a "passed" run.
1618 Returns:
1619 True if the current_value is closer to the known_good_value than the
1620 known_bad_value.
1622 if self.opts.bisect_mode == bisect_utils.BISECT_MODE_STD_DEV:
1623 dist_to_good_value = abs(current_value['std_dev'] -
1624 known_good_value['std_dev'])
1625 dist_to_bad_value = abs(current_value['std_dev'] -
1626 known_bad_value['std_dev'])
1627 else:
1628 dist_to_good_value = abs(current_value['mean'] - known_good_value['mean'])
1629 dist_to_bad_value = abs(current_value['mean'] - known_bad_value['mean'])
1631 return dist_to_good_value < dist_to_bad_value
1633 def _GetV8BleedingEdgeFromV8TrunkIfMappable(
1634 self, revision, bleeding_edge_branch):
1635 """Gets v8 bleeding edge revision mapped to v8 revision in trunk.
1637 Args:
1638 revision: A trunk V8 revision mapped to bleeding edge revision.
1639 bleeding_edge_branch: Branch used to perform lookup of bleeding edge
1640 revision.
1641 Return:
1642 A mapped bleeding edge revision if found, otherwise None.
1644 commit_position = source_control.GetCommitPosition(revision)
1646 if bisect_utils.IsStringInt(commit_position):
1647 # V8 is tricky to bisect, in that there are only a few instances when
1648 # we can dive into bleeding_edge and get back a meaningful result.
1649 # Try to detect a V8 "business as usual" case, which is when:
1650 # 1. trunk revision N has description "Version X.Y.Z"
1651 # 2. bleeding_edge revision (N-1) has description "Prepare push to
1652 # trunk. Now working on X.Y.(Z+1)."
1654 # As of 01/24/2014, V8 trunk descriptions are formatted:
1655 # "Version 3.X.Y (based on bleeding_edge revision rZ)"
1656 # So we can just try parsing that out first and fall back to the old way.
1657 v8_dir = self.depot_registry.GetDepotDir('v8')
1658 v8_bleeding_edge_dir = self.depot_registry.GetDepotDir('v8_bleeding_edge')
1660 revision_info = source_control.QueryRevisionInfo(revision, cwd=v8_dir)
1661 version_re = re.compile("Version (?P<values>[0-9,.]+)")
1662 regex_results = version_re.search(revision_info['subject'])
1663 if regex_results:
1664 git_revision = None
1665 if 'based on bleeding_edge' in revision_info['subject']:
1666 try:
1667 bleeding_edge_revision = revision_info['subject'].split(
1668 'bleeding_edge revision r')[1]
1669 bleeding_edge_revision = int(bleeding_edge_revision.split(')')[0])
1670 bleeding_edge_url = ('https://v8.googlecode.com/svn/branches/'
1671 'bleeding_edge@%s' % bleeding_edge_revision)
1672 cmd = ['log',
1673 '--format=%H',
1674 '--grep',
1675 bleeding_edge_url,
1676 '-1',
1677 bleeding_edge_branch]
1678 output = bisect_utils.CheckRunGit(cmd, cwd=v8_dir)
1679 if output:
1680 git_revision = output.strip()
1681 return git_revision
1682 except (IndexError, ValueError):
1683 pass
1684 else:
1685 # V8 rolls description changed after V8 git migration, new description
1686 # includes "Version 3.X.Y (based on <git hash>)"
1687 try:
1688 rxp = re.compile('based on (?P<git_revision>[a-fA-F0-9]+)')
1689 re_results = rxp.search(revision_info['subject'])
1690 if re_results:
1691 return re_results.group('git_revision')
1692 except (IndexError, ValueError):
1693 pass
1694 if not git_revision:
1695 # Wasn't successful, try the old way of looking for "Prepare push to"
1696 git_revision = source_control.ResolveToRevision(
1697 int(commit_position) - 1, 'v8_bleeding_edge',
1698 bisect_utils.DEPOT_DEPS_NAME, -1, cwd=v8_bleeding_edge_dir)
1700 if git_revision:
1701 revision_info = source_control.QueryRevisionInfo(
1702 git_revision, cwd=v8_bleeding_edge_dir)
1704 if 'Prepare push to trunk' in revision_info['subject']:
1705 return git_revision
1706 return None
1708 def _GetNearestV8BleedingEdgeFromTrunk(
1709 self, revision, v8_branch, bleeding_edge_branch, search_forward=True):
1710 """Gets the nearest V8 roll and maps to bleeding edge revision.
1712 V8 is a bit tricky to bisect since it isn't just rolled out like blink.
1713 Each revision on trunk might just be whatever was in bleeding edge, rolled
1714 directly out. Or it could be some mixture of previous v8 trunk versions,
1715 with bits and pieces cherry picked out from bleeding edge. In order to
1716 bisect, we need both the before/after versions on trunk v8 to be just pushes
1717 from bleeding edge. With the V8 git migration, the branches got switched.
1718 a) master (external/v8) == candidates (v8/v8)
1719 b) bleeding_edge (external/v8) == master (v8/v8)
1721 Args:
1722 revision: A V8 revision to get its nearest bleeding edge revision
1723 search_forward: Searches forward if True, otherwise search backward.
1725 Return:
1726 A mapped bleeding edge revision if found, otherwise None.
1728 cwd = self.depot_registry.GetDepotDir('v8')
1729 cmd = ['log', '--format=%ct', '-1', revision]
1730 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1731 commit_time = int(output)
1732 commits = []
1733 if search_forward:
1734 cmd = ['log',
1735 '--format=%H',
1736 '--after=%d' % commit_time,
1737 v8_branch,
1738 '--reverse']
1739 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1740 output = output.split()
1741 commits = output
1742 #Get 10 git hashes immediately after the given commit.
1743 commits = commits[:10]
1744 else:
1745 cmd = ['log',
1746 '--format=%H',
1747 '-10',
1748 '--before=%d' % commit_time,
1749 v8_branch]
1750 output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1751 output = output.split()
1752 commits = output
1754 bleeding_edge_revision = None
1756 for c in commits:
1757 bleeding_edge_revision = self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1758 c, bleeding_edge_branch)
1759 if bleeding_edge_revision:
1760 break
1762 return bleeding_edge_revision
1764 def _FillInV8BleedingEdgeInfo(self, min_revision_state, max_revision_state):
1765 cwd = self.depot_registry.GetDepotDir('v8')
1766 # when "remote.origin.url" is https://chromium.googlesource.com/v8/v8.git
1767 v8_branch = 'origin/candidates'
1768 bleeding_edge_branch = 'origin/master'
1770 # Support for the chromium revisions with external V8 repo.
1771 # ie https://chromium.googlesource.com/external/v8.git
1772 cmd = ['config', '--get', 'remote.origin.url']
1773 v8_repo_url = bisect_utils.CheckRunGit(cmd, cwd=cwd)
1775 if 'external/v8.git' in v8_repo_url:
1776 v8_branch = 'origin/master'
1777 bleeding_edge_branch = 'origin/bleeding_edge'
1779 r1 = self._GetNearestV8BleedingEdgeFromTrunk(
1780 min_revision_state.revision,
1781 v8_branch,
1782 bleeding_edge_branch,
1783 search_forward=True)
1784 r2 = self._GetNearestV8BleedingEdgeFromTrunk(
1785 max_revision_state.revision,
1786 v8_branch,
1787 bleeding_edge_branch,
1788 search_forward=False)
1789 min_revision_state.external['v8_bleeding_edge'] = r1
1790 max_revision_state.external['v8_bleeding_edge'] = r2
1792 if (not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1793 min_revision_state.revision, bleeding_edge_branch)
1794 or not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
1795 max_revision_state.revision, bleeding_edge_branch)):
1796 self.warnings.append(
1797 'Trunk revisions in V8 did not map directly to bleeding_edge. '
1798 'Attempted to expand the range to find V8 rolls which did map '
1799 'directly to bleeding_edge revisions, but results might not be '
1800 'valid.')
1802 def _FindNextDepotToBisect(
1803 self, current_depot, min_revision_state, max_revision_state):
1804 """Decides which depot the script should dive into next (if any).
1806 Args:
1807 current_depot: Current depot being bisected.
1808 min_revision_state: State of the earliest revision in the bisect range.
1809 max_revision_state: State of the latest revision in the bisect range.
1811 Returns:
1812 Name of the depot to bisect next, or None.
1814 external_depot = None
1815 for next_depot in bisect_utils.DEPOT_NAMES:
1816 if ('platform' in bisect_utils.DEPOT_DEPS_NAME[next_depot] and
1817 bisect_utils.DEPOT_DEPS_NAME[next_depot]['platform'] != os.name):
1818 continue
1820 if not (bisect_utils.DEPOT_DEPS_NAME[next_depot]['recurse']
1821 and min_revision_state.depot
1822 in bisect_utils.DEPOT_DEPS_NAME[next_depot]['from']):
1823 continue
1825 if current_depot == 'v8':
1826 # We grab the bleeding_edge info here rather than earlier because we
1827 # finally have the revision range. From that we can search forwards and
1828 # backwards to try to match trunk revisions to bleeding_edge.
1829 self._FillInV8BleedingEdgeInfo(min_revision_state, max_revision_state)
1831 if (min_revision_state.external.get(next_depot) ==
1832 max_revision_state.external.get(next_depot)):
1833 continue
1835 if (min_revision_state.external.get(next_depot) and
1836 max_revision_state.external.get(next_depot)):
1837 external_depot = next_depot
1838 break
1840 return external_depot
1842 def PrepareToBisectOnDepot(
1843 self, current_depot, start_revision, end_revision, previous_revision):
1844 """Changes to the appropriate directory and gathers a list of revisions
1845 to bisect between |start_revision| and |end_revision|.
1847 Args:
1848 current_depot: The depot we want to bisect.
1849 start_revision: Start of the revision range.
1850 end_revision: End of the revision range.
1851 previous_revision: The last revision we synced to on |previous_depot|.
1853 Returns:
1854 A list containing the revisions between |start_revision| and
1855 |end_revision| inclusive.
1857 # Change into working directory of external library to run
1858 # subsequent commands.
1859 self.depot_registry.ChangeToDepotDir(current_depot)
1861 # V8 (and possibly others) is merged in periodically. Bisecting
1862 # this directory directly won't give much good info.
1863 if 'custom_deps' in bisect_utils.DEPOT_DEPS_NAME[current_depot]:
1864 config_path = os.path.join(self.src_cwd, '..')
1865 if bisect_utils.RunGClientAndCreateConfig(
1866 self.opts, bisect_utils.DEPOT_DEPS_NAME[current_depot]['custom_deps'],
1867 cwd=config_path):
1868 return []
1869 if bisect_utils.RunGClient(
1870 ['sync', '--revision', previous_revision], cwd=self.src_cwd):
1871 return []
1873 if current_depot == 'v8_bleeding_edge':
1874 self.depot_registry.ChangeToDepotDir('chromium')
1876 shutil.move('v8', 'v8.bak')
1877 shutil.move('v8_bleeding_edge', 'v8')
1879 self.cleanup_commands.append(['mv', 'v8', 'v8_bleeding_edge'])
1880 self.cleanup_commands.append(['mv', 'v8.bak', 'v8'])
1882 self.depot_registry.SetDepotDir(
1883 'v8_bleeding_edge', os.path.join(self.src_cwd, 'v8'))
1884 self.depot_registry.SetDepotDir(
1885 'v8', os.path.join(self.src_cwd, 'v8.bak'))
1887 self.depot_registry.ChangeToDepotDir(current_depot)
1889 depot_revision_list = self.GetRevisionList(current_depot,
1890 end_revision,
1891 start_revision)
1893 self.depot_registry.ChangeToDepotDir('chromium')
1895 return depot_revision_list
1897 def GatherReferenceValues(self, good_rev, bad_rev, cmd, metric, target_depot):
1898 """Gathers reference values by running the performance tests on the
1899 known good and bad revisions.
1901 Args:
1902 good_rev: The last known good revision where the performance regression
1903 has not occurred yet.
1904 bad_rev: A revision where the performance regression has already occurred.
1905 cmd: The command to execute the performance test.
1906 metric: The metric being tested for regression.
1908 Returns:
1909 A tuple with the results of building and running each revision.
1911 bad_run_results = self.RunTest(bad_rev, target_depot, cmd, metric)
1913 good_run_results = None
1915 if not bad_run_results[1]:
1916 good_run_results = self.RunTest(good_rev, target_depot, cmd, metric)
1918 return (bad_run_results, good_run_results)
1920 def PrintRevisionsToBisectMessage(self, revision_list, depot):
1921 if self.opts.output_buildbot_annotations:
1922 step_name = 'Bisection Range: [%s:%s - %s]' % (depot, revision_list[-1],
1923 revision_list[0])
1924 bisect_utils.OutputAnnotationStepStart(step_name)
1926 print
1927 print 'Revisions to bisect on [%s]:' % depot
1928 for revision_id in revision_list:
1929 print ' -> %s' % (revision_id, )
1930 print
1932 if self.opts.output_buildbot_annotations:
1933 bisect_utils.OutputAnnotationStepClosed()
1935 def NudgeRevisionsIfDEPSChange(self, bad_revision, good_revision,
1936 good_svn_revision=None):
1937 """Checks to see if changes to DEPS file occurred, and that the revision
1938 range also includes the change to .DEPS.git. If it doesn't, attempts to
1939 expand the revision range to include it.
1941 Args:
1942 bad_revision: First known bad git revision.
1943 good_revision: Last known good git revision.
1944 good_svn_revision: Last known good svn revision.
1946 Returns:
1947 A tuple with the new bad and good revisions.
1949 # DONOT perform nudge because at revision 291563 .DEPS.git was removed
1950 # and source contain only DEPS file for dependency changes.
1951 if good_svn_revision >= 291563:
1952 return (bad_revision, good_revision)
1954 if self.opts.target_platform == 'chromium':
1955 changes_to_deps = source_control.QueryFileRevisionHistory(
1956 bisect_utils.FILE_DEPS, good_revision, bad_revision)
1958 if changes_to_deps:
1959 # DEPS file was changed, search from the oldest change to DEPS file to
1960 # bad_revision to see if there are matching .DEPS.git changes.
1961 oldest_deps_change = changes_to_deps[-1]
1962 changes_to_gitdeps = source_control.QueryFileRevisionHistory(
1963 bisect_utils.FILE_DEPS_GIT, oldest_deps_change, bad_revision)
1965 if len(changes_to_deps) != len(changes_to_gitdeps):
1966 # Grab the timestamp of the last DEPS change
1967 cmd = ['log', '--format=%ct', '-1', changes_to_deps[0]]
1968 output = bisect_utils.CheckRunGit(cmd)
1969 commit_time = int(output)
1971 # Try looking for a commit that touches the .DEPS.git file in the
1972 # next 15 minutes after the DEPS file change.
1973 cmd = [
1974 'log', '--format=%H', '-1',
1975 '--before=%d' % (commit_time + 900),
1976 '--after=%d' % commit_time,
1977 'origin/master', '--', bisect_utils.FILE_DEPS_GIT
1979 output = bisect_utils.CheckRunGit(cmd)
1980 output = output.strip()
1981 if output:
1982 self.warnings.append(
1983 'Detected change to DEPS and modified '
1984 'revision range to include change to .DEPS.git')
1985 return (output, good_revision)
1986 else:
1987 self.warnings.append(
1988 'Detected change to DEPS but couldn\'t find '
1989 'matching change to .DEPS.git')
1990 return (bad_revision, good_revision)
1992 def CheckIfRevisionsInProperOrder(
1993 self, target_depot, good_revision, bad_revision):
1994 """Checks that |good_revision| is an earlier revision than |bad_revision|.
1996 Args:
1997 good_revision: Number/tag of the known good revision.
1998 bad_revision: Number/tag of the known bad revision.
2000 Returns:
2001 True if the revisions are in the proper order (good earlier than bad).
2003 cwd = self.depot_registry.GetDepotDir(target_depot)
2004 good_position = source_control.GetCommitPosition(good_revision, cwd)
2005 bad_position = source_control.GetCommitPosition(bad_revision, cwd)
2006 # Compare commit timestamp for repos that don't support commit position.
2007 if not (bad_position and good_position):
2008 logging.info('Could not get commit positions for revisions %s and %s in '
2009 'depot %s', good_position, bad_position, target_depot)
2010 good_position = source_control.GetCommitTime(good_revision, cwd=cwd)
2011 bad_position = source_control.GetCommitTime(bad_revision, cwd=cwd)
2013 return good_position <= bad_position
2015 def CanPerformBisect(self, good_revision, bad_revision):
2016 """Checks whether a given revision is bisectable.
2018 Checks for following:
2019 1. Non-bisectable revisions for android bots (refer to crbug.com/385324).
2020 2. Non-bisectable revisions for Windows bots (refer to crbug.com/405274).
2022 Args:
2023 good_revision: Known good revision.
2024 bad_revision: Known bad revision.
2026 Returns:
2027 A dictionary indicating the result. If revision is not bisectable,
2028 this will contain the field "error", otherwise None.
2030 if self.opts.target_platform == 'android':
2031 good_revision = source_control.GetCommitPosition(good_revision)
2032 if (bisect_utils.IsStringInt(good_revision)
2033 and good_revision < 265549):
2034 return {'error': (
2035 'Bisect cannot continue for the given revision range.\n'
2036 'It is impossible to bisect Android regressions '
2037 'prior to r265549, which allows the bisect bot to '
2038 'rely on Telemetry to do apk installation of the most recently '
2039 'built local ChromeShell(refer to crbug.com/385324).\n'
2040 'Please try bisecting revisions greater than or equal to r265549.')}
2042 if bisect_utils.IsWindowsHost():
2043 good_revision = source_control.GetCommitPosition(good_revision)
2044 bad_revision = source_control.GetCommitPosition(bad_revision)
2045 if (bisect_utils.IsStringInt(good_revision) and
2046 bisect_utils.IsStringInt(bad_revision)):
2047 if (289987 <= good_revision < 290716 or
2048 289987 <= bad_revision < 290716):
2049 return {'error': ('Oops! Revision between r289987 and r290716 are '
2050 'marked as dead zone for Windows due to '
2051 'crbug.com/405274. Please try another range.')}
2053 return None
2055 def _GatherResultsFromRevertedCulpritCL(
2056 self, results, target_depot, command_to_run, metric):
2057 """Gathers performance results with/without culprit CL.
2059 Attempts to revert the culprit CL against ToT and runs the
2060 performance tests again with and without the CL, adding the results to
2061 the over bisect results.
2063 Args:
2064 results: BisectResults from the bisect.
2065 target_depot: The target depot we're bisecting.
2066 command_to_run: Specify the command to execute the performance test.
2067 metric: The performance metric to monitor.
2069 run_results_tot, run_results_reverted = self._RevertCulpritCLAndRetest(
2070 results, target_depot, command_to_run, metric)
2072 results.AddRetestResults(run_results_tot, run_results_reverted)
2074 if len(results.culprit_revisions) != 1:
2075 return
2077 # Cleanup reverted files if anything is left.
2078 _, _, culprit_depot = results.culprit_revisions[0]
2079 bisect_utils.CheckRunGit(
2080 ['reset', '--hard', 'HEAD'],
2081 cwd=self.depot_registry.GetDepotDir(culprit_depot))
2083 def _RevertCL(self, culprit_revision, culprit_depot):
2084 """Reverts the specified revision in the specified depot."""
2085 if self.opts.output_buildbot_annotations:
2086 bisect_utils.OutputAnnotationStepStart(
2087 'Reverting culprit CL: %s' % culprit_revision)
2088 _, return_code = bisect_utils.RunGit(
2089 ['revert', '--no-commit', culprit_revision],
2090 cwd=self.depot_registry.GetDepotDir(culprit_depot))
2091 if return_code:
2092 bisect_utils.OutputAnnotationStepWarning()
2093 bisect_utils.OutputAnnotationStepText('Failed to revert CL cleanly.')
2094 if self.opts.output_buildbot_annotations:
2095 bisect_utils.OutputAnnotationStepClosed()
2096 return not return_code
2098 def _RevertCulpritCLAndRetest(
2099 self, results, target_depot, command_to_run, metric):
2100 """Reverts the culprit CL against ToT and runs the performance test.
2102 Attempts to revert the culprit CL against ToT and runs the
2103 performance tests again with and without the CL.
2105 Args:
2106 results: BisectResults from the bisect.
2107 target_depot: The target depot we're bisecting.
2108 command_to_run: Specify the command to execute the performance test.
2109 metric: The performance metric to monitor.
2111 Returns:
2112 A tuple with the results of running the CL at ToT/reverted.
2114 # Might want to retest ToT with a revert of the CL to confirm that
2115 # performance returns.
2116 if results.confidence < bisect_utils.HIGH_CONFIDENCE:
2117 return (None, None)
2119 # If there were multiple culprit CLs, we won't try to revert.
2120 if len(results.culprit_revisions) != 1:
2121 return (None, None)
2123 culprit_revision, _, culprit_depot = results.culprit_revisions[0]
2125 if not self._SyncRevision(target_depot, None, 'gclient'):
2126 return (None, None)
2128 head_revision = bisect_utils.CheckRunGit(['log', '--format=%H', '-1'])
2129 head_revision = head_revision.strip()
2131 if not self._RevertCL(culprit_revision, culprit_depot):
2132 return (None, None)
2134 # If the culprit CL happened to be in a depot that gets pulled in, we
2135 # can't revert the change and issue a try job to build, since that would
2136 # require modifying both the DEPS file and files in another depot.
2137 # Instead, we build locally.
2138 force_build = (culprit_depot != target_depot)
2139 if force_build:
2140 results.warnings.append(
2141 'Culprit CL is in another depot, attempting to revert and build'
2142 ' locally to retest. This may not match the performance of official'
2143 ' builds.')
2145 run_results_reverted = self._RunTestWithAnnotations(
2146 'Re-Testing ToT with reverted culprit',
2147 'Failed to run reverted CL.',
2148 head_revision, target_depot, command_to_run, metric, force_build)
2150 # Clear the reverted file(s).
2151 bisect_utils.RunGit(
2152 ['reset', '--hard', 'HEAD'],
2153 cwd=self.depot_registry.GetDepotDir(culprit_depot))
2155 # Retesting with the reverted CL failed, so bail out of retesting against
2156 # ToT.
2157 if run_results_reverted[1]:
2158 return (None, None)
2160 run_results_tot = self._RunTestWithAnnotations(
2161 'Re-Testing ToT',
2162 'Failed to run ToT.',
2163 head_revision, target_depot, command_to_run, metric, force_build)
2165 return (run_results_tot, run_results_reverted)
2167 def _RunTestWithAnnotations(
2168 self, step_text, error_text, head_revision,
2169 target_depot, command_to_run, metric, force_build):
2170 """Runs the performance test and outputs start/stop annotations.
2172 Args:
2173 results: BisectResults from the bisect.
2174 target_depot: The target depot we're bisecting.
2175 command_to_run: Specify the command to execute the performance test.
2176 metric: The performance metric to monitor.
2177 force_build: Whether to force a build locally.
2179 Returns:
2180 Results of the test.
2182 if self.opts.output_buildbot_annotations:
2183 bisect_utils.OutputAnnotationStepStart(step_text)
2185 # Build and run the test again with the reverted culprit CL against ToT.
2186 run_test_results = self.RunTest(
2187 head_revision, target_depot, command_to_run,
2188 metric, skippable=False, skip_sync=True, create_patch=True,
2189 force_build=force_build)
2191 if self.opts.output_buildbot_annotations:
2192 if run_test_results[1]:
2193 bisect_utils.OutputAnnotationStepWarning()
2194 bisect_utils.OutputAnnotationStepText(error_text)
2195 bisect_utils.OutputAnnotationStepClosed()
2197 return run_test_results
2199 def Run(self, command_to_run, bad_revision_in, good_revision_in, metric):
2200 """Given known good and bad revisions, run a binary search on all
2201 intermediate revisions to determine the CL where the performance regression
2202 occurred.
2204 Args:
2205 command_to_run: Specify the command to execute the performance test.
2206 good_revision: Number/tag of the known good revision.
2207 bad_revision: Number/tag of the known bad revision.
2208 metric: The performance metric to monitor.
2210 Returns:
2211 A BisectResults object.
2213 # Choose depot to bisect first
2214 target_depot = 'chromium'
2215 if self.opts.target_platform == 'android-chrome':
2216 target_depot = 'android-chrome'
2218 cwd = os.getcwd()
2219 self.depot_registry.ChangeToDepotDir(target_depot)
2221 # If they passed SVN revisions, we can try match them to git SHA1 hashes.
2222 bad_revision = source_control.ResolveToRevision(
2223 bad_revision_in, target_depot, bisect_utils.DEPOT_DEPS_NAME, 100)
2224 good_revision = source_control.ResolveToRevision(
2225 good_revision_in, target_depot, bisect_utils.DEPOT_DEPS_NAME, -100)
2227 os.chdir(cwd)
2228 if bad_revision is None:
2229 return BisectResults(
2230 error='Couldn\'t resolve [%s] to SHA1.' % bad_revision_in)
2232 if good_revision is None:
2233 return BisectResults(
2234 error='Couldn\'t resolve [%s] to SHA1.' % good_revision_in)
2236 # Check that they didn't accidentally swap good and bad revisions.
2237 if not self.CheckIfRevisionsInProperOrder(
2238 target_depot, good_revision, bad_revision):
2239 return BisectResults(error='Bad rev (%s) appears to be earlier than good '
2240 'rev (%s).' % (good_revision, bad_revision))
2242 bad_revision, good_revision = self.NudgeRevisionsIfDEPSChange(
2243 bad_revision, good_revision, good_revision_in)
2244 if self.opts.output_buildbot_annotations:
2245 bisect_utils.OutputAnnotationStepStart('Gathering Revisions')
2247 cannot_bisect = self.CanPerformBisect(good_revision, bad_revision)
2248 if cannot_bisect:
2249 return BisectResults(error=cannot_bisect.get('error'))
2251 print 'Gathering revision range for bisection.'
2252 # Retrieve a list of revisions to do bisection on.
2253 revision_list = self.GetRevisionList(target_depot, bad_revision,
2254 good_revision)
2256 if self.opts.output_buildbot_annotations:
2257 bisect_utils.OutputAnnotationStepClosed()
2259 if revision_list:
2260 self.PrintRevisionsToBisectMessage(revision_list, target_depot)
2262 if self.opts.output_buildbot_annotations:
2263 bisect_utils.OutputAnnotationStepStart('Gathering Reference Values')
2265 print 'Gathering reference values for bisection.'
2267 # Perform the performance tests on the good and bad revisions, to get
2268 # reference values.
2269 bad_results, good_results = self.GatherReferenceValues(good_revision,
2270 bad_revision,
2271 command_to_run,
2272 metric,
2273 target_depot)
2275 if self.opts.output_buildbot_annotations:
2276 bisect_utils.OutputAnnotationStepClosed()
2278 if bad_results[1]:
2279 error = ('An error occurred while building and running the \'bad\' '
2280 'reference value. The bisect cannot continue without '
2281 'a working \'bad\' revision to start from.\n\nError: %s' %
2282 bad_results[0])
2283 return BisectResults(error=error)
2285 if good_results[1]:
2286 error = ('An error occurred while building and running the \'good\' '
2287 'reference value. The bisect cannot continue without '
2288 'a working \'good\' revision to start from.\n\nError: %s' %
2289 good_results[0])
2290 return BisectResults(error=error)
2292 # We need these reference values to determine if later runs should be
2293 # classified as pass or fail.
2294 known_bad_value = bad_results[0]
2295 known_good_value = good_results[0]
2297 # Check the direction of improvement only if the improvement_direction
2298 # option is set to a specific direction (1 for higher is better or -1 for
2299 # lower is better).
2300 improvement_dir = self.opts.improvement_direction
2301 if improvement_dir:
2302 higher_is_better = improvement_dir > 0
2303 if higher_is_better:
2304 message = "Expecting higher values to be better for this metric, "
2305 else:
2306 message = "Expecting lower values to be better for this metric, "
2307 metric_increased = known_bad_value['mean'] > known_good_value['mean']
2308 if metric_increased:
2309 message += "and the metric appears to have increased. "
2310 else:
2311 message += "and the metric appears to have decreased. "
2312 if ((higher_is_better and metric_increased) or
2313 (not higher_is_better and not metric_increased)):
2314 error = (message + 'Then, the test results for the ends of the given '
2315 '\'good\' - \'bad\' range of revisions represent an '
2316 'improvement (and not a regression).')
2317 return BisectResults(error=error)
2318 logging.info(message + "Therefore we continue to bisect.")
2320 bisect_state = BisectState(target_depot, revision_list)
2321 revision_states = bisect_state.GetRevisionStates()
2323 min_revision = 0
2324 max_revision = len(revision_states) - 1
2326 # Can just mark the good and bad revisions explicitly here since we
2327 # already know the results.
2328 bad_revision_state = revision_states[min_revision]
2329 bad_revision_state.external = bad_results[2]
2330 bad_revision_state.perf_time = bad_results[3]
2331 bad_revision_state.build_time = bad_results[4]
2332 bad_revision_state.passed = False
2333 bad_revision_state.value = known_bad_value
2335 good_revision_state = revision_states[max_revision]
2336 good_revision_state.external = good_results[2]
2337 good_revision_state.perf_time = good_results[3]
2338 good_revision_state.build_time = good_results[4]
2339 good_revision_state.passed = True
2340 good_revision_state.value = known_good_value
2342 # Check how likely it is that the good and bad results are different
2343 # beyond chance-induced variation.
2344 confidence_error = False
2345 if not self.opts.debug_ignore_regression_confidence:
2346 confidence_error = _CheckRegressionConfidenceError(good_revision,
2347 bad_revision,
2348 known_good_value,
2349 known_bad_value)
2350 if confidence_error:
2351 # If there is no significant difference between "good" and "bad"
2352 # revision results, then the "bad revision" is considered "good".
2353 # TODO(qyearsley): Remove this if it is not necessary.
2354 bad_revision_state.passed = True
2355 self.warnings.append(confidence_error)
2356 return BisectResults(bisect_state, self.depot_registry, self.opts,
2357 self.warnings)
2359 while True:
2360 if not revision_states:
2361 break
2363 if max_revision - min_revision <= 1:
2364 min_revision_state = revision_states[min_revision]
2365 max_revision_state = revision_states[max_revision]
2366 current_depot = min_revision_state.depot
2367 # TODO(sergiyb): Under which conditions can first two branches be hit?
2368 if min_revision_state.passed == '?':
2369 next_revision_index = min_revision
2370 elif max_revision_state.passed == '?':
2371 next_revision_index = max_revision
2372 elif current_depot in ['android-chrome', 'chromium', 'v8']:
2373 previous_revision = revision_states[min_revision].revision
2374 # If there were changes to any of the external libraries we track,
2375 # should bisect the changes there as well.
2376 external_depot = self._FindNextDepotToBisect(
2377 current_depot, min_revision_state, max_revision_state)
2378 # If there was no change in any of the external depots, the search
2379 # is over.
2380 if not external_depot:
2381 if current_depot == 'v8':
2382 self.warnings.append(
2383 'Unfortunately, V8 bisection couldn\'t '
2384 'continue any further. The script can only bisect into '
2385 'V8\'s bleeding_edge repository if both the current and '
2386 'previous revisions in trunk map directly to revisions in '
2387 'bleeding_edge.')
2388 break
2390 earliest_revision = max_revision_state.external[external_depot]
2391 latest_revision = min_revision_state.external[external_depot]
2393 new_revision_list = self.PrepareToBisectOnDepot(
2394 external_depot, earliest_revision, latest_revision,
2395 previous_revision)
2397 if not new_revision_list:
2398 error = ('An error occurred attempting to retrieve revision '
2399 'range: [%s..%s]' % (earliest_revision, latest_revision))
2400 return BisectResults(error=error)
2402 revision_states = bisect_state.CreateRevisionStatesAfter(
2403 external_depot, new_revision_list, current_depot,
2404 previous_revision)
2406 # Reset the bisection and perform it on the newly inserted states.
2407 min_revision = 0
2408 max_revision = len(revision_states) - 1
2410 print ('Regression in metric %s appears to be the result of '
2411 'changes in [%s].' % (metric, external_depot))
2413 revision_list = [state.revision for state in revision_states]
2414 self.PrintRevisionsToBisectMessage(revision_list, external_depot)
2416 continue
2417 else:
2418 break
2419 else:
2420 next_revision_index = (int((max_revision - min_revision) / 2) +
2421 min_revision)
2423 next_revision_state = revision_states[next_revision_index]
2424 next_revision = next_revision_state.revision
2425 next_depot = next_revision_state.depot
2427 self.depot_registry.ChangeToDepotDir(next_depot)
2429 message = 'Working on [%s:%s]' % (next_depot, next_revision)
2430 print message
2431 if self.opts.output_buildbot_annotations:
2432 bisect_utils.OutputAnnotationStepStart(message)
2434 run_results = self.RunTest(next_revision, next_depot, command_to_run,
2435 metric, skippable=True)
2437 # If the build is successful, check whether or not the metric
2438 # had regressed.
2439 if not run_results[1]:
2440 if len(run_results) > 2:
2441 next_revision_state.external = run_results[2]
2442 next_revision_state.perf_time = run_results[3]
2443 next_revision_state.build_time = run_results[4]
2445 passed_regression = self._CheckIfRunPassed(run_results[0],
2446 known_good_value,
2447 known_bad_value)
2449 next_revision_state.passed = passed_regression
2450 next_revision_state.value = run_results[0]
2452 if passed_regression:
2453 max_revision = next_revision_index
2454 else:
2455 min_revision = next_revision_index
2456 else:
2457 if run_results[1] == BUILD_RESULT_SKIPPED:
2458 next_revision_state.passed = 'Skipped'
2459 elif run_results[1] == BUILD_RESULT_FAIL:
2460 next_revision_state.passed = 'Build Failed'
2462 print run_results[0]
2464 # If the build is broken, remove it and redo search.
2465 revision_states.pop(next_revision_index)
2467 max_revision -= 1
2469 if self.opts.output_buildbot_annotations:
2470 self.printer.PrintPartialResults(bisect_state)
2471 bisect_utils.OutputAnnotationStepClosed()
2473 self._ConfidenceExtraTestRuns(min_revision_state, max_revision_state,
2474 command_to_run, metric)
2475 results = BisectResults(bisect_state, self.depot_registry, self.opts,
2476 self.warnings)
2478 self._GatherResultsFromRevertedCulpritCL(
2479 results, target_depot, command_to_run, metric)
2481 return results
2482 else:
2483 # Weren't able to sync and retrieve the revision range.
2484 error = ('An error occurred attempting to retrieve revision range: '
2485 '[%s..%s]' % (good_revision, bad_revision))
2486 return BisectResults(error=error)
2488 def _ConfidenceExtraTestRuns(self, good_state, bad_state, command_to_run,
2489 metric):
2490 if (bool(good_state.passed) != bool(bad_state.passed)
2491 and good_state.passed not in ('Skipped', 'Build Failed')
2492 and bad_state.passed not in ('Skipped', 'Build Failed')):
2493 for state in (good_state, bad_state):
2494 run_results = self.RunTest(
2495 state.revision,
2496 state.depot,
2497 command_to_run,
2498 metric,
2499 test_run_multiplier=BORDER_REVISIONS_EXTRA_RUNS)
2500 # Is extend the right thing to do here?
2501 if run_results[1] != BUILD_RESULT_FAIL:
2502 state.value['values'].extend(run_results[0]['values'])
2503 else:
2504 warning_text = 'Re-test of revision %s failed with error message: %s'
2505 warning_text %= (state.revision, run_results[0])
2506 if warning_text not in self.warnings:
2507 self.warnings.append(warning_text)
2510 def _IsPlatformSupported():
2511 """Checks that this platform and build system are supported.
2513 Args:
2514 opts: The options parsed from the command line.
2516 Returns:
2517 True if the platform and build system are supported.
2519 # Haven't tested the script out on any other platforms yet.
2520 supported = ['posix', 'nt']
2521 return os.name in supported
2524 def RemoveBuildFiles(build_type):
2525 """Removes build files from previous runs."""
2526 out_dir = os.path.join('out', build_type)
2527 build_dir = os.path.join('build', build_type)
2528 logging.info('Removing build files in "%s" and "%s".',
2529 os.path.abspath(out_dir), os.path.abspath(build_dir))
2530 try:
2531 RemakeDirectoryTree(out_dir)
2532 RemakeDirectoryTree(build_dir)
2533 except Exception as e:
2534 raise RuntimeError('Got error in RemoveBuildFiles: %s' % e)
2537 def RemakeDirectoryTree(path_to_dir):
2538 """Removes a directory tree and replaces it with an empty one.
2540 Returns True if successful, False otherwise.
2542 RemoveDirectoryTree(path_to_dir)
2543 MaybeMakeDirectory(path_to_dir)
2546 def RemoveDirectoryTree(path_to_dir):
2547 """Removes a directory tree. Returns True if successful or False otherwise."""
2548 if os.path.isfile(path_to_dir):
2549 logging.info('REMOVING FILE %s' % path_to_dir)
2550 os.remove(path_to_dir)
2551 try:
2552 if os.path.exists(path_to_dir):
2553 shutil.rmtree(path_to_dir)
2554 except OSError, e:
2555 if e.errno != errno.ENOENT:
2556 raise
2559 # This is copied from build/scripts/common/chromium_utils.py.
2560 def MaybeMakeDirectory(*path):
2561 """Creates an entire path, if it doesn't already exist."""
2562 file_path = os.path.join(*path)
2563 try:
2564 os.makedirs(file_path)
2565 except OSError as e:
2566 if e.errno != errno.EEXIST:
2567 raise
2570 class BisectOptions(object):
2571 """Options to be used when running bisection."""
2572 def __init__(self):
2573 super(BisectOptions, self).__init__()
2575 self.target_platform = 'chromium'
2576 self.build_preference = None
2577 self.good_revision = None
2578 self.bad_revision = None
2579 self.use_goma = None
2580 self.goma_dir = None
2581 self.goma_threads = 64
2582 self.repeat_test_count = 20
2583 self.truncate_percent = 25
2584 self.max_time_minutes = 20
2585 self.metric = None
2586 self.command = None
2587 self.output_buildbot_annotations = None
2588 self.no_custom_deps = False
2589 self.working_directory = None
2590 self.extra_src = None
2591 self.debug_ignore_build = None
2592 self.debug_ignore_sync = None
2593 self.debug_ignore_perf_test = None
2594 self.debug_ignore_regression_confidence = None
2595 self.debug_fake_first_test_mean = 0
2596 self.target_arch = 'ia32'
2597 self.target_build_type = 'Release'
2598 self.builder_type = 'perf'
2599 self.bisect_mode = bisect_utils.BISECT_MODE_MEAN
2600 self.improvement_direction = 0
2601 self.bug_id = ''
2603 @staticmethod
2604 def _AddBisectOptionsGroup(parser):
2605 group = parser.add_argument_group('Bisect options')
2606 group.add_argument('-c', '--command', required=True,
2607 help='A command to execute your performance test at '
2608 'each point in the bisection.')
2609 group.add_argument('-b', '--bad_revision', required=True,
2610 help='A bad revision to start bisection. Must be later '
2611 'than good revision. May be either a git or svn '
2612 'revision.')
2613 group.add_argument('-g', '--good_revision', required=True,
2614 help='A revision to start bisection where performance '
2615 'test is known to pass. Must be earlier than the '
2616 'bad revision. May be either a git or a svn '
2617 'revision.')
2618 group.add_argument('-m', '--metric',
2619 help='The desired metric to bisect on. For example '
2620 '"vm_rss_final_b/vm_rss_f_b"')
2621 group.add_argument('-d', '--improvement_direction', type=int, default=0,
2622 help='An integer number representing the direction of '
2623 'improvement. 1 for higher is better, -1 for lower '
2624 'is better, 0 for ignore (default).')
2625 group.add_argument('-r', '--repeat_test_count', type=int, default=20,
2626 choices=range(1, 101),
2627 help='The number of times to repeat the performance '
2628 'test. Values will be clamped to range [1, 100]. '
2629 'Default value is 20.')
2630 group.add_argument('--max_time_minutes', type=int, default=20,
2631 choices=range(1, 61),
2632 help='The maximum time (in minutes) to take running the '
2633 'performance tests. The script will run the '
2634 'performance tests according to '
2635 '--repeat_test_count, so long as it doesn\'t exceed'
2636 ' --max_time_minutes. Values will be clamped to '
2637 'range [1, 60]. Default value is 20.')
2638 group.add_argument('-t', '--truncate_percent', type=int, default=25,
2639 help='The highest/lowest percent are discarded to form '
2640 'a truncated mean. Values will be clamped to range '
2641 '[0, 25]. Default value is 25 percent.')
2642 group.add_argument('--bisect_mode', default=bisect_utils.BISECT_MODE_MEAN,
2643 choices=[bisect_utils.BISECT_MODE_MEAN,
2644 bisect_utils.BISECT_MODE_STD_DEV,
2645 bisect_utils.BISECT_MODE_RETURN_CODE],
2646 help='The bisect mode. Choices are to bisect on the '
2647 'difference in mean, std_dev, or return_code.')
2648 group.add_argument('--bug_id', default='',
2649 help='The id for the bug associated with this bisect. ' +
2650 'If this number is given, bisect will attempt to ' +
2651 'verify that the bug is not closed before '
2652 'starting.')
2654 @staticmethod
2655 def _AddBuildOptionsGroup(parser):
2656 group = parser.add_argument_group('Build options')
2657 group.add_argument('-w', '--working_directory',
2658 help='Path to the working directory where the script '
2659 'will do an initial checkout of the chromium depot. The '
2660 'files will be placed in a subdirectory "bisect" under '
2661 'working_directory and that will be used to perform the '
2662 'bisection. This parameter is optional, if it is not '
2663 'supplied, the script will work from the current depot.')
2664 group.add_argument('--build_preference',
2665 choices=['msvs', 'ninja', 'make'],
2666 help='The preferred build system to use. On linux/mac '
2667 'the options are make/ninja. On Windows, the '
2668 'options are msvs/ninja.')
2669 group.add_argument('--target_platform', default='chromium',
2670 choices=['chromium', 'android', 'android-chrome'],
2671 help='The target platform. Choices are "chromium" '
2672 '(current platform), or "android". If you specify '
2673 'something other than "chromium", you must be '
2674 'properly set up to build that platform.')
2675 group.add_argument('--no_custom_deps', dest='no_custom_deps',
2676 action='store_true', default=False,
2677 help='Run the script with custom_deps or not.')
2678 group.add_argument('--extra_src',
2679 help='Path to a script which can be used to modify the '
2680 'bisect script\'s behavior.')
2681 group.add_argument('--use_goma', action='store_true',
2682 help='Add a bunch of extra threads for goma, and enable '
2683 'goma')
2684 group.add_argument('--goma_dir',
2685 help='Path to goma tools (or system default if not '
2686 'specified).')
2687 group.add_argument('--goma_threads', type=int, default='64',
2688 help='Number of threads for goma, only if using goma.')
2689 group.add_argument('--output_buildbot_annotations', action='store_true',
2690 help='Add extra annotation output for buildbot.')
2691 group.add_argument('--target_arch', default='ia32',
2692 dest='target_arch', choices=['ia32', 'x64', 'arm'],
2693 help='The target build architecture. Choices are "ia32" '
2694 '(default), "x64" or "arm".')
2695 group.add_argument('--target_build_type', default='Release',
2696 choices=['Release', 'Debug', 'Release_x64'],
2697 help='The target build type. Choices are "Release" '
2698 '(default), Release_x64 or "Debug".')
2699 group.add_argument('--builder_type', default=fetch_build.PERF_BUILDER,
2700 choices=[fetch_build.PERF_BUILDER,
2701 fetch_build.FULL_BUILDER,
2702 fetch_build.ANDROID_CHROME_PERF_BUILDER, ''],
2703 help='Type of builder to get build from. This '
2704 'determines both the bot that builds and the '
2705 'place where archived builds are downloaded from. '
2706 'For local builds, an empty string can be passed.')
2708 @staticmethod
2709 def _AddDebugOptionsGroup(parser):
2710 group = parser.add_argument_group('Debug options')
2711 group.add_argument('--debug_ignore_build', action='store_true',
2712 help='DEBUG: Don\'t perform builds.')
2713 group.add_argument('--debug_ignore_sync', action='store_true',
2714 help='DEBUG: Don\'t perform syncs.')
2715 group.add_argument('--debug_ignore_perf_test', action='store_true',
2716 help='DEBUG: Don\'t perform performance tests.')
2717 group.add_argument('--debug_ignore_regression_confidence',
2718 action='store_true',
2719 help='DEBUG: Don\'t score the confidence of the initial '
2720 'good and bad revisions\' test results.')
2721 group.add_argument('--debug_fake_first_test_mean', type=int, default='0',
2722 help='DEBUG: When faking performance tests, return this '
2723 'value as the mean of the first performance test, '
2724 'and return a mean of 0.0 for further tests.')
2725 return group
2727 @classmethod
2728 def _CreateCommandLineParser(cls):
2729 """Creates a parser with bisect options.
2731 Returns:
2732 An instance of argparse.ArgumentParser.
2734 usage = ('%(prog)s [options] [-- chromium-options]\n'
2735 'Perform binary search on revision history to find a minimal '
2736 'range of revisions where a performance metric regressed.\n')
2738 parser = argparse.ArgumentParser(usage=usage)
2739 cls._AddBisectOptionsGroup(parser)
2740 cls._AddBuildOptionsGroup(parser)
2741 cls._AddDebugOptionsGroup(parser)
2742 return parser
2744 def ParseCommandLine(self):
2745 """Parses the command line for bisect options."""
2746 parser = self._CreateCommandLineParser()
2747 opts = parser.parse_args()
2749 try:
2750 if (not opts.metric and
2751 opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE):
2752 raise RuntimeError('missing required parameter: --metric')
2754 if opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE:
2755 metric_values = opts.metric.split('/')
2756 if len(metric_values) != 2:
2757 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
2758 opts.metric = metric_values
2760 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25) / 100.0
2762 for k, v in opts.__dict__.iteritems():
2763 assert hasattr(self, k), 'Invalid %s attribute in BisectOptions.' % k
2764 setattr(self, k, v)
2765 except RuntimeError, e:
2766 output_string = StringIO.StringIO()
2767 parser.print_help(file=output_string)
2768 error_message = '%s\n\n%s' % (e.message, output_string.getvalue())
2769 output_string.close()
2770 raise RuntimeError(error_message)
2772 @staticmethod
2773 def FromDict(values):
2774 """Creates an instance of BisectOptions from a dictionary.
2776 Args:
2777 values: a dict containing options to set.
2779 Returns:
2780 An instance of BisectOptions.
2782 opts = BisectOptions()
2783 for k, v in values.iteritems():
2784 assert hasattr(opts, k), 'Invalid %s attribute in BisectOptions.' % k
2785 setattr(opts, k, v)
2787 if opts.metric and opts.bisect_mode != bisect_utils.BISECT_MODE_RETURN_CODE:
2788 metric_values = opts.metric.split('/')
2789 if len(metric_values) != 2:
2790 raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
2791 opts.metric = metric_values
2793 if opts.target_arch == 'x64' and opts.target_build_type == 'Release':
2794 opts.target_build_type = 'Release_x64'
2795 opts.repeat_test_count = min(max(opts.repeat_test_count, 1), 100)
2796 opts.max_time_minutes = min(max(opts.max_time_minutes, 1), 60)
2797 opts.truncate_percent = min(max(opts.truncate_percent, 0), 25)
2798 opts.truncate_percent = opts.truncate_percent / 100.0
2800 return opts
2803 def _ConfigureLogging():
2804 """Trivial logging config.
2806 Configures logging to output any messages at or above INFO to standard out,
2807 without any additional formatting.
2809 logging_format = '%(message)s'
2810 logging.basicConfig(
2811 stream=logging.sys.stdout, level=logging.INFO, format=logging_format)
2814 def main():
2815 _ConfigureLogging()
2816 try:
2817 opts = BisectOptions()
2818 opts.ParseCommandLine()
2820 if opts.bug_id:
2821 if opts.output_buildbot_annotations:
2822 bisect_utils.OutputAnnotationStepStart('Checking Issue Tracker')
2823 issue_closed = query_crbug.CheckIssueClosed(opts.bug_id)
2824 if issue_closed:
2825 print 'Aborting bisect because bug is closed'
2826 else:
2827 print 'Could not confirm bug is closed, proceeding.'
2828 if opts.output_buildbot_annotations:
2829 bisect_utils.OutputAnnotationStepClosed()
2830 if issue_closed:
2831 results = BisectResults(abort_reason='the bug is closed.')
2832 bisect_test = BisectPerformanceMetrics(opts, os.getcwd())
2833 bisect_test.printer.FormatAndPrintResults(results)
2834 return 0
2836 if opts.extra_src:
2837 extra_src = bisect_utils.LoadExtraSrc(opts.extra_src)
2838 if not extra_src:
2839 raise RuntimeError('Invalid or missing --extra_src.')
2840 bisect_utils.AddAdditionalDepotInfo(extra_src.GetAdditionalDepotInfo())
2842 if opts.working_directory:
2843 custom_deps = bisect_utils.DEFAULT_GCLIENT_CUSTOM_DEPS
2844 if opts.no_custom_deps:
2845 custom_deps = None
2846 bisect_utils.CreateBisectDirectoryAndSetupDepot(opts, custom_deps)
2848 os.chdir(os.path.join(os.getcwd(), 'src'))
2849 RemoveBuildFiles(opts.target_build_type)
2851 if not _IsPlatformSupported():
2852 raise RuntimeError('Sorry, this platform isn\'t supported yet.')
2854 if not source_control.IsInGitRepository():
2855 raise RuntimeError(
2856 'Sorry, only the git workflow is supported at the moment.')
2858 # gClient sync seems to fail if you're not in master branch.
2859 if (not source_control.IsInProperBranch() and
2860 not opts.debug_ignore_sync and
2861 not opts.working_directory):
2862 raise RuntimeError('You must switch to master branch to run bisection.')
2863 bisect_test = BisectPerformanceMetrics(opts, os.getcwd())
2864 try:
2865 results = bisect_test.Run(opts.command, opts.bad_revision,
2866 opts.good_revision, opts.metric)
2867 if results.error:
2868 raise RuntimeError(results.error)
2869 bisect_test.printer.FormatAndPrintResults(results)
2870 return 0
2871 finally:
2872 bisect_test.PerformCleanup()
2873 except RuntimeError as e:
2874 if opts.output_buildbot_annotations:
2875 # The perf dashboard scrapes the "results" step in order to comment on
2876 # bugs. If you change this, please update the perf dashboard as well.
2877 bisect_utils.OutputAnnotationStepStart('Results')
2878 print 'Runtime Error: %s' % e
2879 if opts.output_buildbot_annotations:
2880 bisect_utils.OutputAnnotationStepClosed()
2881 return 1
2884 if __name__ == '__main__':
2885 sys.exit(main())