Home | History | Annotate | Download | only in auto_bisect
      1 #!/usr/bin/env python
      2 # Copyright (c) 2013 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 """Performance Test Bisect Tool
      7 
      8 This script bisects a series of changelists using binary search. It starts at
      9 a bad revision where a performance metric has regressed, and asks for a last
     10 known-good revision. It will then binary search across this revision range by
     11 syncing, building, and running a performance test. If the change is
     12 suspected to occur as a result of WebKit/V8 changes, the script will
     13 further bisect changes to those depots and attempt to narrow down the revision
     14 range.
     15 
     16 Example usage using SVN revisions:
     17 
     18 ./tools/bisect_perf_regression.py -c\
     19   "out/Release/performance_ui_tests --gtest_filter=ShutdownTest.SimpleUserQuit"\
     20   -g 168222 -b 168232 -m shutdown/simple-user-quit
     21 
     22 Be aware that if you're using the git workflow and specify an SVN revision,
     23 the script will attempt to find the git SHA1 where SVN changes up to that
     24 revision were merged in.
     25 
     26 Example usage using git hashes:
     27 
     28 ./tools/bisect_perf_regression.py -c\
     29   "out/Release/performance_ui_tests --gtest_filter=ShutdownTest.SimpleUserQuit"\
     30   -g 1f6e67861535121c5c819c16a666f2436c207e7b\
     31   -b b732f23b4f81c382db0b23b9035f3dadc7d925bb\
     32   -m shutdown/simple-user-quit
     33 """
     34 
     35 import copy
     36 import datetime
     37 import errno
     38 import hashlib
     39 import optparse
     40 import os
     41 import re
     42 import shlex
     43 import shutil
     44 import StringIO
     45 import sys
     46 import time
     47 import zipfile
     48 
     49 sys.path.append(os.path.join(
     50     os.path.dirname(__file__), os.path.pardir, 'telemetry'))
     51 
     52 from bisect_results import BisectResults
     53 import bisect_utils
     54 import builder
     55 import math_utils
     56 import request_build
     57 import source_control as source_control_module
     58 from telemetry.util import cloud_storage
     59 
     60 # Below is the map of "depot" names to information about each depot. Each depot
     61 # is a repository, and in the process of bisecting, revision ranges in these
     62 # repositories may also be bisected.
     63 #
     64 # Each depot information dictionary may contain:
     65 #   src: Path to the working directory.
     66 #   recurse: True if this repository will get bisected.
     67 #   depends: A list of other repositories that are actually part of the same
     68 #       repository in svn. If the repository has any dependent repositories
     69 #       (e.g. skia/src needs skia/include and skia/gyp to be updated), then
     70 #       they are specified here.
     71 #   svn: URL of SVN repository. Needed for git workflow to resolve hashes to
     72 #       SVN revisions.
     73 #   from: Parent depot that must be bisected before this is bisected.
     74 #   deps_var: Key name in vars variable in DEPS file that has revision
     75 #       information.
     76 DEPOT_DEPS_NAME = {
     77     'chromium': {
     78         'src': 'src',
     79         'recurse': True,
     80         'depends': None,
     81         'from': ['cros', 'android-chrome'],
     82         'viewvc':
     83             'http://src.chromium.org/viewvc/chrome?view=revision&revision=',
     84         'deps_var': 'chromium_rev'
     85     },
     86     'webkit': {
     87         'src': 'src/third_party/WebKit',
     88         'recurse': True,
     89         'depends': None,
     90         'from': ['chromium'],
     91         'viewvc':
     92             'http://src.chromium.org/viewvc/blink?view=revision&revision=',
     93         'deps_var': 'webkit_revision'
     94     },
     95     'angle': {
     96         'src': 'src/third_party/angle',
     97         'src_old': 'src/third_party/angle_dx11',
     98         'recurse': True,
     99         'depends': None,
    100         'from': ['chromium'],
    101         'platform': 'nt',
    102         'deps_var': 'angle_revision'
    103     },
    104     'v8': {
    105         'src': 'src/v8',
    106         'recurse': True,
    107         'depends': None,
    108         'from': ['chromium'],
    109         'custom_deps': bisect_utils.GCLIENT_CUSTOM_DEPS_V8,
    110         'viewvc': 'https://code.google.com/p/v8/source/detail?r=',
    111         'deps_var': 'v8_revision'
    112     },
    113     'v8_bleeding_edge': {
    114         'src': 'src/v8_bleeding_edge',
    115         'recurse': True,
    116         'depends': None,
    117         'svn': 'https://v8.googlecode.com/svn/branches/bleeding_edge',
    118         'from': ['v8'],
    119         'viewvc': 'https://code.google.com/p/v8/source/detail?r=',
    120         'deps_var': 'v8_revision'
    121     },
    122     'skia/src': {
    123         'src': 'src/third_party/skia/src',
    124         'recurse': True,
    125         'svn': 'http://skia.googlecode.com/svn/trunk/src',
    126         'depends': ['skia/include', 'skia/gyp'],
    127         'from': ['chromium'],
    128         'viewvc': 'https://code.google.com/p/skia/source/detail?r=',
    129         'deps_var': 'skia_revision'
    130     },
    131     'skia/include': {
    132         'src': 'src/third_party/skia/include',
    133         'recurse': False,
    134         'svn': 'http://skia.googlecode.com/svn/trunk/include',
    135         'depends': None,
    136         'from': ['chromium'],
    137         'viewvc': 'https://code.google.com/p/skia/source/detail?r=',
    138         'deps_var': 'None'
    139     },
    140     'skia/gyp': {
    141         'src': 'src/third_party/skia/gyp',
    142         'recurse': False,
    143         'svn': 'http://skia.googlecode.com/svn/trunk/gyp',
    144         'depends': None,
    145         'from': ['chromium'],
    146         'viewvc': 'https://code.google.com/p/skia/source/detail?r=',
    147         'deps_var': 'None'
    148     }
    149 }
    150 
    151 DEPOT_NAMES = DEPOT_DEPS_NAME.keys()
    152 
    153 # The script is in chromium/src/tools/auto_bisect. Throughout this script,
    154 # we use paths to other things in the chromium/src repository.
    155 
    156 CROS_CHROMEOS_PATTERN = 'chromeos-base/chromeos-chrome'
    157 
    158 # Possible return values from BisectPerformanceMetrics.RunTest.
    159 BUILD_RESULT_SUCCEED = 0
    160 BUILD_RESULT_FAIL = 1
    161 BUILD_RESULT_SKIPPED = 2
    162 
    163 # Maximum time in seconds to wait after posting build request to the try server.
    164 # TODO: Change these values based on the actual time taken by buildbots on
    165 # the try server.
    166 MAX_MAC_BUILD_TIME = 14400
    167 MAX_WIN_BUILD_TIME = 14400
    168 MAX_LINUX_BUILD_TIME = 14400
    169 
    170 # The confidence percentage at which confidence can be consider "high".
    171 HIGH_CONFIDENCE = 95
    172 
    173 # Patch template to add a new file, DEPS.sha under src folder.
    174 # This file contains SHA1 value of the DEPS changes made while bisecting
    175 # dependency repositories. This patch send along with DEPS patch to try server.
    176 # When a build requested is posted with a patch, bisect builders on try server,
    177 # once build is produced, it reads SHA value from this file and appends it
    178 # to build archive filename.
    179 DEPS_SHA_PATCH = """diff --git src/DEPS.sha src/DEPS.sha
    180 new file mode 100644
    181 --- /dev/null
    182 +++ src/DEPS.sha
    183 @@ -0,0 +1 @@
    184 +%(deps_sha)s
    185 """
    186 
    187 # The possible values of the --bisect_mode flag, which determines what to
    188 # use when classifying a revision as "good" or "bad".
    189 BISECT_MODE_MEAN = 'mean'
    190 BISECT_MODE_STD_DEV = 'std_dev'
    191 BISECT_MODE_RETURN_CODE = 'return_code'
    192 
    193 # The perf dashboard looks for a string like "Estimated Confidence: 95%"
    194 # to decide whether or not to cc the author(s). If you change this, please
    195 # update the perf dashboard as well.
    196 RESULTS_BANNER = """
    197 ===== BISECT JOB RESULTS =====
    198 Status: %(status)s
    199 
    200 Test Command: %(command)s
    201 Test Metric: %(metrics)s
    202 Relative Change: %(change)s
    203 Estimated Confidence: %(confidence).02f%%"""
    204 
    205 # The perf dashboard specifically looks for the string
    206 # "Author  : " to parse out who to cc on a bug. If you change the
    207 # formatting here, please update the perf dashboard as well.
    208 RESULTS_REVISION_INFO = """
    209 ===== SUSPECTED CL(s) =====
    210 Subject : %(subject)s
    211 Author  : %(author)s%(email_info)s%(commit_info)s
    212 Commit  : %(cl)s
    213 Date    : %(cl_date)s"""
    214 
    215 REPRO_STEPS_LOCAL = """
    216 ==== INSTRUCTIONS TO REPRODUCE ====
    217 To run locally:
    218  - Use the test command given under 'BISECT JOB RESULTS' above.
    219  - Consider using a profiler. Pass --profiler=list to list available profilers.
    220 """
    221 
    222 REPRO_STEPS_TRYJOB = """
    223 To reproduce on a performance try bot:
    224  1. Edit run-perf-test.cfg
    225  2. Upload your patch with: $ git cl upload --bypass-hooks
    226  3. Send to the try server: $ git cl try -m tryserver.chromium.perf -b <bot>
    227 
    228 Notes:
    229  a) Follow the in-file instructions in run-perf-test.cfg.
    230  b) run-perf-test.cfg is under tools/ or under third_party/WebKit/Tools.
    231  c) Do your edits preferably under a new git branch.
    232  d) --browser=release and --browser=android-chromium-testshell are supported
    233     depending on the platform (desktop|android).
    234  e) Strip any src/ directories from the head of relative path names.
    235  f) Make sure to use the appropriate bot on step 3.
    236 
    237 For more details please visit
    238 https://sites.google.com/a/chromium.org/dev/developers/performance-try-bots"""
    239 
    240 REPRO_STEPS_TRYJOB_TELEMETRY = """
    241 To reproduce on a performance try bot:
    242 %(command)s
    243 (Where <bot-name> comes from tools/perf/run_benchmark --browser=list)
    244 
    245 For more details please visit
    246 https://sites.google.com/a/chromium.org/dev/developers/performance-try-bots
    247 """
    248 
    249 RESULTS_THANKYOU = """
    250 ===== THANK YOU FOR CHOOSING BISECT AIRLINES =====
    251 Visit http://www.chromium.org/developers/core-principles for Chrome's policy
    252 on perf regressions.
    253 Contact chrome-perf-dashboard-team with any questions or suggestions about
    254 bisecting.
    255 .                   .-----.
    256 .     .---.         \      \==)
    257 .     |PERF\         \       \\
    258 .     |     ---------'-------'-----------.
    259 .     .     0 0 0 0 0 0 0 0 0 0 0 0 0 0 |_`-.
    260 .      \_____________.-------._______________)
    261 .                   /       /
    262 .                  /      /
    263 .                 /     /==)
    264 .                ._____."""
    265 
    266 
    267 def _AddAdditionalDepotInfo(depot_info):
    268   """Adds additional depot info to the global depot variables."""
    269   global DEPOT_DEPS_NAME
    270   global DEPOT_NAMES
    271   DEPOT_DEPS_NAME = dict(DEPOT_DEPS_NAME.items() + depot_info.items())
    272   DEPOT_NAMES = DEPOT_DEPS_NAME.keys()
    273 
    274 
    275 def GetSHA1HexDigest(contents):
    276   """Returns SHA1 hex digest of the given string."""
    277   return hashlib.sha1(contents).hexdigest()
    278 
    279 
    280 def GetZipFileName(build_revision=None, target_arch='ia32', patch_sha=None):
    281   """Gets the archive file name for the given revision."""
    282   def PlatformName():
    283     """Return a string to be used in paths for the platform."""
    284     if bisect_utils.IsWindowsHost():
    285       # Build archive for x64 is still stored with the "win32" suffix.
    286       # See chromium_utils.PlatformName().
    287       if bisect_utils.Is64BitWindows() and target_arch == 'x64':
    288         return 'win32'
    289       return 'win32'
    290     if bisect_utils.IsLinuxHost():
    291       # Android builds are also archived with the "full-build-linux prefix.
    292       return 'linux'
    293     if bisect_utils.IsMacHost():
    294       return 'mac'
    295     raise NotImplementedError('Unknown platform "%s".' % sys.platform)
    296 
    297   base_name = 'full-build-%s' % PlatformName()
    298   if not build_revision:
    299     return base_name
    300   if patch_sha:
    301     build_revision = '%s_%s' % (build_revision , patch_sha)
    302   return '%s_%s.zip' % (base_name, build_revision)
    303 
    304 
    305 def GetRemoteBuildPath(build_revision, target_platform='chromium',
    306                        target_arch='ia32', patch_sha=None):
    307   """Returns the URL to download the build from."""
    308   def GetGSRootFolderName(target_platform):
    309     """Returns the Google Cloud Storage root folder name."""
    310     if bisect_utils.IsWindowsHost():
    311       if bisect_utils.Is64BitWindows() and target_arch == 'x64':
    312         return 'Win x64 Builder'
    313       return 'Win Builder'
    314     if bisect_utils.IsLinuxHost():
    315       if target_platform == 'android':
    316         return 'android_perf_rel'
    317       return 'Linux Builder'
    318     if bisect_utils.IsMacHost():
    319       return 'Mac Builder'
    320     raise NotImplementedError('Unsupported Platform "%s".' % sys.platform)
    321 
    322   base_filename = GetZipFileName(
    323       build_revision, target_arch, patch_sha)
    324   builder_folder = GetGSRootFolderName(target_platform)
    325   return '%s/%s' % (builder_folder, base_filename)
    326 
    327 
    328 def FetchFromCloudStorage(bucket_name, source_path, destination_path):
    329   """Fetches file(s) from the Google Cloud Storage.
    330 
    331   Args:
    332     bucket_name: Google Storage bucket name.
    333     source_path: Source file path.
    334     destination_path: Destination file path.
    335 
    336   Returns:
    337     Downloaded file path if exists, otherwise None.
    338   """
    339   target_file = os.path.join(destination_path, os.path.basename(source_path))
    340   try:
    341     if cloud_storage.Exists(bucket_name, source_path):
    342       print 'Fetching file from gs//%s/%s ...' % (bucket_name, source_path)
    343       cloud_storage.Get(bucket_name, source_path, destination_path)
    344       if os.path.exists(target_file):
    345         return target_file
    346     else:
    347       print ('File gs://%s/%s not found in cloud storage.' % (
    348           bucket_name, source_path))
    349   except Exception as e:
    350     print 'Something went wrong while fetching file from cloud: %s' % e
    351     if os.path.exists(target_file):
    352       os.remove(target_file)
    353   return None
    354 
    355 
    356 # This is copied from build/scripts/common/chromium_utils.py.
    357 def MaybeMakeDirectory(*path):
    358   """Creates an entire path, if it doesn't already exist."""
    359   file_path = os.path.join(*path)
    360   try:
    361     os.makedirs(file_path)
    362   except OSError as e:
    363     if e.errno != errno.EEXIST:
    364       return False
    365   return True
    366 
    367 
    368 # This was copied from build/scripts/common/chromium_utils.py.
    369 def ExtractZip(filename, output_dir, verbose=True):
    370   """ Extract the zip archive in the output directory."""
    371   MaybeMakeDirectory(output_dir)
    372 
    373   # On Linux and Mac, we use the unzip command as it will
    374   # handle links and file bits (executable), which is much
    375   # easier then trying to do that with ZipInfo options.
    376   #
    377   # The Mac Version of unzip unfortunately does not support Zip64, whereas
    378   # the python module does, so we have to fall back to the python zip module
    379   # on Mac if the file size is greater than 4GB.
    380   #
    381   # On Windows, try to use 7z if it is installed, otherwise fall back to python
    382   # zip module and pray we don't have files larger than 512MB to unzip.
    383   unzip_cmd = None
    384   if ((bisect_utils.IsMacHost()
    385        and os.path.getsize(filename) < 4 * 1024 * 1024 * 1024)
    386       or bisect_utils.IsLinuxHost()):
    387     unzip_cmd = ['unzip', '-o']
    388   elif (bisect_utils.IsWindowsHost()
    389         and os.path.exists('C:\\Program Files\\7-Zip\\7z.exe')):
    390     unzip_cmd = ['C:\\Program Files\\7-Zip\\7z.exe', 'x', '-y']
    391 
    392   if unzip_cmd:
    393     # Make sure path is absolute before changing directories.
    394     filepath = os.path.abspath(filename)
    395     saved_dir = os.getcwd()
    396     os.chdir(output_dir)
    397     command = unzip_cmd + [filepath]
    398     result = bisect_utils.RunProcess(command)
    399     os.chdir(saved_dir)
    400     if result:
    401       raise IOError('unzip failed: %s => %s' % (str(command), result))
    402   else:
    403     assert bisect_utils.IsWindowsHost() or bisect_utils.IsMacHost()
    404     zf = zipfile.ZipFile(filename)
    405     for name in zf.namelist():
    406       if verbose:
    407         print 'Extracting %s' % name
    408       zf.extract(name, output_dir)
    409       if bisect_utils.IsMacHost():
    410         # Restore permission bits.
    411         os.chmod(os.path.join(output_dir, name),
    412                  zf.getinfo(name).external_attr >> 16L)
    413 
    414 
    415 def WriteStringToFile(text, file_name):
    416   """Writes text to a file, raising an RuntimeError on failure."""
    417   try:
    418     with open(file_name, 'wb') as f:
    419       f.write(text)
    420   except IOError:
    421     raise RuntimeError('Error writing to file [%s]' % file_name )
    422 
    423 
    424 def ReadStringFromFile(file_name):
    425   """Writes text to a file, raising an RuntimeError on failure."""
    426   try:
    427     with open(file_name) as f:
    428       return f.read()
    429   except IOError:
    430     raise RuntimeError('Error reading file [%s]' % file_name )
    431 
    432 
    433 def ChangeBackslashToSlashInPatch(diff_text):
    434   """Formats file paths in the given patch text to Unix-style paths."""
    435   if not diff_text:
    436     return None
    437   diff_lines = diff_text.split('\n')
    438   for i in range(len(diff_lines)):
    439     line = diff_lines[i]
    440     if line.startswith('--- ') or line.startswith('+++ '):
    441       diff_lines[i] = line.replace('\\', '/')
    442   return '\n'.join(diff_lines)
    443 
    444 
    445 def _ParseRevisionsFromDEPSFileManually(deps_file_contents):
    446   """Parses the vars section of the DEPS file using regular expressions.
    447 
    448   Args:
    449     deps_file_contents: The DEPS file contents as a string.
    450 
    451   Returns:
    452     A dictionary in the format {depot: revision} if successful, otherwise None.
    453   """
    454   # We'll parse the "vars" section of the DEPS file.
    455   rxp = re.compile('vars = {(?P<vars_body>[^}]+)', re.MULTILINE)
    456   re_results = rxp.search(deps_file_contents)
    457 
    458   if not re_results:
    459     return None
    460 
    461   # We should be left with a series of entries in the vars component of
    462   # the DEPS file with the following format:
    463   # 'depot_name': 'revision',
    464   vars_body = re_results.group('vars_body')
    465   rxp = re.compile("'(?P<depot_body>[\w_-]+)':[\s]+'(?P<rev_body>[\w@]+)'",
    466                    re.MULTILINE)
    467   re_results = rxp.findall(vars_body)
    468 
    469   return dict(re_results)
    470 
    471 
    472 def _WaitUntilBuildIsReady(
    473     fetch_build, bot_name, builder_host, builder_port, build_request_id,
    474     max_timeout):
    475   """Waits until build is produced by bisect builder on try server.
    476 
    477   Args:
    478     fetch_build: Function to check and download build from cloud storage.
    479     bot_name: Builder bot name on try server.
    480     builder_host Try server host name.
    481     builder_port: Try server port.
    482     build_request_id: A unique ID of the build request posted to try server.
    483     max_timeout: Maximum time to wait for the build.
    484 
    485   Returns:
    486      Downloaded archive file path if exists, otherwise None.
    487   """
    488   # Build number on the try server.
    489   build_num = None
    490   # Interval to check build on cloud storage.
    491   poll_interval = 60
    492   # Interval to check build status on try server in seconds.
    493   status_check_interval = 600
    494   last_status_check = time.time()
    495   start_time = time.time()
    496   while True:
    497     # Checks for build on gs://chrome-perf and download if exists.
    498     res = fetch_build()
    499     if res:
    500       return (res, 'Build successfully found')
    501     elapsed_status_check = time.time() - last_status_check
    502     # To avoid overloading try server with status check requests, we check
    503     # build status for every 10 minutes.
    504     if elapsed_status_check > status_check_interval:
    505       last_status_check = time.time()
    506       if not build_num:
    507         # Get the build number on try server for the current build.
    508         build_num = request_build.GetBuildNumFromBuilder(
    509             build_request_id, bot_name, builder_host, builder_port)
    510       # Check the status of build using the build number.
    511       # Note: Build is treated as PENDING if build number is not found
    512       # on the the try server.
    513       build_status, status_link = request_build.GetBuildStatus(
    514           build_num, bot_name, builder_host, builder_port)
    515       if build_status == request_build.FAILED:
    516         return (None, 'Failed to produce build, log: %s' % status_link)
    517     elapsed_time = time.time() - start_time
    518     if elapsed_time > max_timeout:
    519       return (None, 'Timed out: %ss without build' % max_timeout)
    520 
    521     print 'Time elapsed: %ss without build.' % elapsed_time
    522     time.sleep(poll_interval)
    523     # For some reason, mac bisect bots were not flushing stdout periodically.
    524     # As a result buildbot command is timed-out. Flush stdout on all platforms
    525     # while waiting for build.
    526     sys.stdout.flush()
    527 
    528 
    529 def _UpdateV8Branch(deps_content):
    530   """Updates V8 branch in DEPS file to process v8_bleeding_edge.
    531 
    532   Check for "v8_branch" in DEPS file if exists update its value
    533   with v8_bleeding_edge branch. Note: "v8_branch" is added to DEPS
    534   variable from DEPS revision 254916, therefore check for "src/v8":
    535   <v8 source path> in DEPS in order to support prior DEPS revisions
    536   and update it.
    537 
    538   Args:
    539     deps_content: DEPS file contents to be modified.
    540 
    541   Returns:
    542     Modified DEPS file contents as a string.
    543   """
    544   new_branch = r'branches/bleeding_edge'
    545   v8_branch_pattern = re.compile(r'(?<="v8_branch": ")(.*)(?=")')
    546   if re.search(v8_branch_pattern, deps_content):
    547     deps_content = re.sub(v8_branch_pattern, new_branch, deps_content)
    548   else:
    549     # Replaces the branch assigned to "src/v8" key in DEPS file.
    550     # Format of "src/v8" in DEPS:
    551     # "src/v8":
    552     #    (Var("googlecode_url") % "v8") + "/trunk@" + Var("v8_revision"),
    553     # So, "/trunk@" is replace with "/branches/bleeding_edge@"
    554     v8_src_pattern = re.compile(
    555         r'(?<="v8"\) \+ "/)(.*)(?=@" \+ Var\("v8_revision"\))', re.MULTILINE)
    556     if re.search(v8_src_pattern, deps_content):
    557       deps_content = re.sub(v8_src_pattern, new_branch, deps_content)
    558   return deps_content
    559 
    560 
    561 def _UpdateDEPSForAngle(revision, depot, deps_file):
    562   """Updates DEPS file with new revision for Angle repository.
    563 
    564   This is a hack for Angle depot case because, in DEPS file "vars" dictionary
    565   variable contains "angle_revision" key that holds git hash instead of
    566   SVN revision.
    567 
    568   And sometimes "angle_revision" key is not specified in "vars" variable,
    569   in such cases check "deps" dictionary variable that matches
    570   angle.git@[a-fA-F0-9]{40}$ and replace git hash.
    571   """
    572   deps_var = DEPOT_DEPS_NAME[depot]['deps_var']
    573   try:
    574     deps_contents = ReadStringFromFile(deps_file)
    575     # Check whether the depot and revision pattern in DEPS file vars variable
    576     # e.g. "angle_revision": "fa63e947cb3eccf463648d21a05d5002c9b8adfa".
    577     angle_rev_pattern = re.compile(r'(?<="%s": ")([a-fA-F0-9]{40})(?=")' %
    578                                    deps_var, re.MULTILINE)
    579     match = re.search(angle_rev_pattern % deps_var, deps_contents)
    580     if match:
    581       # Update the revision information for the given depot
    582       new_data = re.sub(angle_rev_pattern, revision, deps_contents)
    583     else:
    584       # Check whether the depot and revision pattern in DEPS file deps
    585       # variable. e.g.,
    586       # "src/third_party/angle": Var("chromium_git") +
    587       # "/angle/angle.git@fa63e947cb3eccf463648d21a05d5002c9b8adfa",.
    588       angle_rev_pattern = re.compile(
    589           r'(?<=angle\.git@)([a-fA-F0-9]{40})(?=")', re.MULTILINE)
    590       match = re.search(angle_rev_pattern, deps_contents)
    591       if not match:
    592         print 'Could not find angle revision information in DEPS file.'
    593         return False
    594       new_data = re.sub(angle_rev_pattern, revision, deps_contents)
    595     # Write changes to DEPS file
    596     WriteStringToFile(new_data, deps_file)
    597     return True
    598   except IOError, e:
    599     print 'Something went wrong while updating DEPS file, %s' % e
    600   return False
    601 
    602 
    603 def _TryParseHistogramValuesFromOutput(metric, text):
    604   """Attempts to parse a metric in the format HISTOGRAM <graph: <trace>.
    605 
    606   Args:
    607     metric: The metric as a list of [<trace>, <value>] strings.
    608     text: The text to parse the metric values from.
    609 
    610   Returns:
    611     A list of floating point numbers found, [] if none were found.
    612   """
    613   metric_formatted = 'HISTOGRAM %s: %s= ' % (metric[0], metric[1])
    614 
    615   text_lines = text.split('\n')
    616   values_list = []
    617 
    618   for current_line in text_lines:
    619     if metric_formatted in current_line:
    620       current_line = current_line[len(metric_formatted):]
    621 
    622       try:
    623         histogram_values = eval(current_line)
    624 
    625         for b in histogram_values['buckets']:
    626           average_for_bucket = float(b['high'] + b['low']) * 0.5
    627           # Extends the list with N-elements with the average for that bucket.
    628           values_list.extend([average_for_bucket] * b['count'])
    629       except Exception:
    630         pass
    631 
    632   return values_list
    633 
    634 
    635 def _TryParseResultValuesFromOutput(metric, text):
    636   """Attempts to parse a metric in the format RESULT <graph>: <trace>= ...
    637 
    638   Args:
    639     metric: The metric as a list of [<trace>, <value>] string pairs.
    640     text: The text to parse the metric values from.
    641 
    642   Returns:
    643     A list of floating point numbers found.
    644   """
    645   # Format is: RESULT <graph>: <trace>= <value> <units>
    646   metric_re = re.escape('RESULT %s: %s=' % (metric[0], metric[1]))
    647 
    648   # The log will be parsed looking for format:
    649   # <*>RESULT <graph_name>: <trace_name>= <value>
    650   single_result_re = re.compile(
    651       metric_re + '\s*(?P<VALUE>[-]?\d*(\.\d*)?)')
    652 
    653   # The log will be parsed looking for format:
    654   # <*>RESULT <graph_name>: <trace_name>= [<value>,value,value,...]
    655   multi_results_re = re.compile(
    656       metric_re + '\s*\[\s*(?P<VALUES>[-]?[\d\., ]+)\s*\]')
    657 
    658   # The log will be parsed looking for format:
    659   # <*>RESULT <graph_name>: <trace_name>= {<mean>, <std deviation>}
    660   mean_stddev_re = re.compile(
    661       metric_re +
    662       '\s*\{\s*(?P<MEAN>[-]?\d*(\.\d*)?),\s*(?P<STDDEV>\d+(\.\d*)?)\s*\}')
    663 
    664   text_lines = text.split('\n')
    665   values_list = []
    666   for current_line in text_lines:
    667     # Parse the output from the performance test for the metric we're
    668     # interested in.
    669     single_result_match = single_result_re.search(current_line)
    670     multi_results_match = multi_results_re.search(current_line)
    671     mean_stddev_match = mean_stddev_re.search(current_line)
    672     if (not single_result_match is None and
    673         single_result_match.group('VALUE')):
    674       values_list += [single_result_match.group('VALUE')]
    675     elif (not multi_results_match is None and
    676           multi_results_match.group('VALUES')):
    677       metric_values = multi_results_match.group('VALUES')
    678       values_list += metric_values.split(',')
    679     elif (not mean_stddev_match is None and
    680           mean_stddev_match.group('MEAN')):
    681       values_list += [mean_stddev_match.group('MEAN')]
    682 
    683   values_list = [float(v) for v in values_list
    684                  if bisect_utils.IsStringFloat(v)]
    685 
    686   # If the metric is times/t, we need to sum the timings in order to get
    687   # similar regression results as the try-bots.
    688   metrics_to_sum = [
    689       ['times', 't'],
    690       ['times', 'page_load_time'],
    691       ['cold_times', 'page_load_time'],
    692       ['warm_times', 'page_load_time'],
    693   ]
    694 
    695   if metric in metrics_to_sum:
    696     if values_list:
    697       values_list = [reduce(lambda x, y: float(x) + float(y), values_list)]
    698 
    699   return values_list
    700 
    701 
    702 def _ParseMetricValuesFromOutput(metric, text):
    703   """Parses output from performance_ui_tests and retrieves the results for
    704   a given metric.
    705 
    706   Args:
    707     metric: The metric as a list of [<trace>, <value>] strings.
    708     text: The text to parse the metric values from.
    709 
    710   Returns:
    711     A list of floating point numbers found.
    712   """
    713   metric_values = _TryParseResultValuesFromOutput(metric, text)
    714 
    715   if not metric_values:
    716     metric_values = _TryParseHistogramValuesFromOutput(metric, text)
    717 
    718   return metric_values
    719 
    720 
    721 def _GenerateProfileIfNecessary(command_args):
    722   """Checks the command line of the performance test for dependencies on
    723   profile generation, and runs tools/perf/generate_profile as necessary.
    724 
    725   Args:
    726     command_args: Command line being passed to performance test, as a list.
    727 
    728   Returns:
    729     False if profile generation was necessary and failed, otherwise True.
    730   """
    731   if '--profile-dir' in ' '.join(command_args):
    732     # If we were using python 2.7+, we could just use the argparse
    733     # module's parse_known_args to grab --profile-dir. Since some of the
    734     # bots still run 2.6, have to grab the arguments manually.
    735     arg_dict = {}
    736     args_to_parse = ['--profile-dir', '--browser']
    737 
    738     for arg_to_parse in args_to_parse:
    739       for i, current_arg in enumerate(command_args):
    740         if arg_to_parse in current_arg:
    741           current_arg_split = current_arg.split('=')
    742 
    743           # Check 2 cases, --arg=<val> and --arg <val>
    744           if len(current_arg_split) == 2:
    745             arg_dict[arg_to_parse] = current_arg_split[1]
    746           elif i + 1 < len(command_args):
    747             arg_dict[arg_to_parse] = command_args[i+1]
    748 
    749     path_to_generate = os.path.join('tools', 'perf', 'generate_profile')
    750 
    751     if arg_dict.has_key('--profile-dir') and arg_dict.has_key('--browser'):
    752       profile_path, profile_type = os.path.split(arg_dict['--profile-dir'])
    753       return not bisect_utils.RunProcess(['python', path_to_generate,
    754           '--profile-type-to-generate', profile_type,
    755           '--browser', arg_dict['--browser'], '--output-dir', profile_path])
    756     return False
    757   return True
    758 
    759 
    760 def _AddRevisionsIntoRevisionData(revisions, depot, sort, revision_data):
    761   """Adds new revisions to the revision_data dictionary and initializes them.
    762 
    763   Args:
    764     revisions: List of revisions to add.
    765     depot: Depot that's currently in use (src, webkit, etc...)
    766     sort: Sorting key for displaying revisions.
    767     revision_data: A dictionary to add the new revisions into.
    768         Existing revisions will have their sort keys adjusted.
    769   """
    770   num_depot_revisions = len(revisions)
    771 
    772   for _, v in revision_data.iteritems():
    773     if v['sort'] > sort:
    774       v['sort'] += num_depot_revisions
    775 
    776   for i in xrange(num_depot_revisions):
    777     r = revisions[i]
    778     revision_data[r] = {
    779         'revision' : r,
    780         'depot' : depot,
    781         'value' : None,
    782         'perf_time' : 0,
    783         'build_time' : 0,
    784         'passed' : '?',
    785         'sort' : i + sort + 1,
    786     }
    787 
    788 
    789 def _PrintThankYou():
    790   print RESULTS_THANKYOU
    791 
    792 
    793 def _PrintTableRow(column_widths, row_data):
    794   """Prints out a row in a formatted table that has columns aligned.
    795 
    796   Args:
    797     column_widths: A list of column width numbers.
    798     row_data: A list of items for each column in this row.
    799   """
    800   assert len(column_widths) == len(row_data)
    801   text = ''
    802   for i in xrange(len(column_widths)):
    803     current_row_data = row_data[i].center(column_widths[i], ' ')
    804     text += ('%%%ds' % column_widths[i]) % current_row_data
    805   print text
    806 
    807 
    808 def _PrintStepTime(revision_data_sorted):
    809   """Prints information about how long various steps took.
    810 
    811   Args:
    812     revision_data_sorted: The sorted list of revision data dictionaries."""
    813   step_perf_time_avg = 0.0
    814   step_build_time_avg = 0.0
    815   step_count = 0.0
    816   for _, current_data in revision_data_sorted:
    817     if current_data['value']:
    818       step_perf_time_avg += current_data['perf_time']
    819       step_build_time_avg += current_data['build_time']
    820       step_count += 1
    821   if step_count:
    822     step_perf_time_avg = step_perf_time_avg / step_count
    823     step_build_time_avg = step_build_time_avg / step_count
    824   print
    825   print 'Average build time : %s' % datetime.timedelta(
    826       seconds=int(step_build_time_avg))
    827   print 'Average test time  : %s' % datetime.timedelta(
    828       seconds=int(step_perf_time_avg))
    829 
    830 
    831 class DepotDirectoryRegistry(object):
    832 
    833   def __init__(self, src_cwd):
    834     self.depot_cwd = {}
    835     for depot in DEPOT_NAMES:
    836       # The working directory of each depot is just the path to the depot, but
    837       # since we're already in 'src', we can skip that part.
    838       path_in_src = DEPOT_DEPS_NAME[depot]['src'][4:]
    839       self.AddDepot(depot, os.path.join(src_cwd, path_in_src))
    840 
    841     self.AddDepot('chromium', src_cwd)
    842     self.AddDepot('cros', os.path.join(src_cwd, 'tools', 'cros'))
    843 
    844   def AddDepot(self, depot_name, depot_dir):
    845     self.depot_cwd[depot_name] = depot_dir
    846 
    847   def GetDepotDir(self, depot_name):
    848     if depot_name in self.depot_cwd:
    849       return self.depot_cwd[depot_name]
    850     else:
    851       assert False, ('Unknown depot [ %s ] encountered. Possibly a new one '
    852                      'was added without proper support?' % depot_name)
    853 
    854   def ChangeToDepotDir(self, depot_name):
    855     """Given a depot, changes to the appropriate working directory.
    856 
    857     Args:
    858       depot_name: The name of the depot (see DEPOT_NAMES).
    859     """
    860     os.chdir(self.GetDepotDir(depot_name))
    861 
    862 
    863 class BisectPerformanceMetrics(object):
    864   """This class contains functionality to perform a bisection of a range of
    865   revisions to narrow down where performance regressions may have occurred.
    866 
    867   The main entry-point is the Run method.
    868   """
    869 
    870   def __init__(self, source_control, opts):
    871     super(BisectPerformanceMetrics, self).__init__()
    872 
    873     self.opts = opts
    874     self.source_control = source_control
    875 
    876     # The src directory here is NOT the src/ directory for the repository
    877     # where the bisect script is running from. Instead, it's the src/ directory
    878     # inside the bisect/ directory which is created before running.
    879     self.src_cwd = os.getcwd()
    880 
    881     self.depot_registry = DepotDirectoryRegistry(self.src_cwd)
    882     self.cleanup_commands = []
    883     self.warnings = []
    884     self.builder = builder.Builder.FromOpts(opts)
    885 
    886   def PerformCleanup(self):
    887     """Performs cleanup when script is finished."""
    888     os.chdir(self.src_cwd)
    889     for c in self.cleanup_commands:
    890       if c[0] == 'mv':
    891         shutil.move(c[1], c[2])
    892       else:
    893         assert False, 'Invalid cleanup command.'
    894 
    895   def GetRevisionList(self, depot, bad_revision, good_revision):
    896     """Retrieves a list of all the commits between the bad revision and
    897     last known good revision."""
    898 
    899     revision_work_list = []
    900 
    901     if depot == 'cros':
    902       revision_range_start = good_revision
    903       revision_range_end = bad_revision
    904 
    905       cwd = os.getcwd()
    906       self.depot_registry.ChangeToDepotDir('cros')
    907 
    908       # Print the commit timestamps for every commit in the revision time
    909       # range. We'll sort them and bisect by that. There is a remote chance that
    910       # 2 (or more) commits will share the exact same timestamp, but it's
    911       # probably safe to ignore that case.
    912       cmd = ['repo', 'forall', '-c',
    913           'git log --format=%%ct --before=%d --after=%d' % (
    914           revision_range_end, revision_range_start)]
    915       output, return_code = bisect_utils.RunProcessAndRetrieveOutput(cmd)
    916 
    917       assert not return_code, ('An error occurred while running '
    918                                '"%s"' % ' '.join(cmd))
    919 
    920       os.chdir(cwd)
    921 
    922       revision_work_list = list(set(
    923           [int(o) for o in output.split('\n') if bisect_utils.IsStringInt(o)]))
    924       revision_work_list = sorted(revision_work_list, reverse=True)
    925     else:
    926       cwd = self.depot_registry.GetDepotDir(depot)
    927       revision_work_list = self.source_control.GetRevisionList(bad_revision,
    928           good_revision, cwd=cwd)
    929 
    930     return revision_work_list
    931 
    932   def _GetV8BleedingEdgeFromV8TrunkIfMappable(self, revision):
    933     commit_position = self.source_control.GetCommitPosition(revision)
    934 
    935     if bisect_utils.IsStringInt(commit_position):
    936       # V8 is tricky to bisect, in that there are only a few instances when
    937       # we can dive into bleeding_edge and get back a meaningful result.
    938       # Try to detect a V8 "business as usual" case, which is when:
    939       #  1. trunk revision N has description "Version X.Y.Z"
    940       #  2. bleeding_edge revision (N-1) has description "Prepare push to
    941       #     trunk. Now working on X.Y.(Z+1)."
    942       #
    943       # As of 01/24/2014, V8 trunk descriptions are formatted:
    944       # "Version 3.X.Y (based on bleeding_edge revision rZ)"
    945       # So we can just try parsing that out first and fall back to the old way.
    946       v8_dir = self.depot_registry.GetDepotDir('v8')
    947       v8_bleeding_edge_dir = self.depot_registry.GetDepotDir('v8_bleeding_edge')
    948 
    949       revision_info = self.source_control.QueryRevisionInfo(revision,
    950           cwd=v8_dir)
    951 
    952       version_re = re.compile("Version (?P<values>[0-9,.]+)")
    953 
    954       regex_results = version_re.search(revision_info['subject'])
    955 
    956       if regex_results:
    957         git_revision = None
    958 
    959         # Look for "based on bleeding_edge" and parse out revision
    960         if 'based on bleeding_edge' in revision_info['subject']:
    961           try:
    962             bleeding_edge_revision = revision_info['subject'].split(
    963                 'bleeding_edge revision r')[1]
    964             bleeding_edge_revision = int(bleeding_edge_revision.split(')')[0])
    965             git_revision = self.source_control.ResolveToRevision(
    966                 bleeding_edge_revision, 'v8_bleeding_edge', DEPOT_DEPS_NAME, 1,
    967                 cwd=v8_bleeding_edge_dir)
    968             return git_revision
    969           except (IndexError, ValueError):
    970             pass
    971 
    972         if not git_revision:
    973           # Wasn't successful, try the old way of looking for "Prepare push to"
    974           git_revision = self.source_control.ResolveToRevision(
    975               int(commit_position) - 1, 'v8_bleeding_edge', DEPOT_DEPS_NAME, -1,
    976               cwd=v8_bleeding_edge_dir)
    977 
    978           if git_revision:
    979             revision_info = self.source_control.QueryRevisionInfo(git_revision,
    980                 cwd=v8_bleeding_edge_dir)
    981 
    982             if 'Prepare push to trunk' in revision_info['subject']:
    983               return git_revision
    984     return None
    985 
    986   def _GetNearestV8BleedingEdgeFromTrunk(self, revision, search_forward=True):
    987     cwd = self.depot_registry.GetDepotDir('v8')
    988     cmd = ['log', '--format=%ct', '-1', revision]
    989     output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
    990     commit_time = int(output)
    991     commits = []
    992 
    993     if search_forward:
    994       cmd = ['log', '--format=%H', '-10', '--after=%d' % commit_time,
    995           'origin/master']
    996       output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
    997       output = output.split()
    998       commits = output
    999       commits = reversed(commits)
   1000     else:
   1001       cmd = ['log', '--format=%H', '-10', '--before=%d' % commit_time,
   1002           'origin/master']
   1003       output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
   1004       output = output.split()
   1005       commits = output
   1006 
   1007     bleeding_edge_revision = None
   1008 
   1009     for c in commits:
   1010       bleeding_edge_revision = self._GetV8BleedingEdgeFromV8TrunkIfMappable(c)
   1011       if bleeding_edge_revision:
   1012         break
   1013 
   1014     return bleeding_edge_revision
   1015 
   1016   def _ParseRevisionsFromDEPSFile(self, depot):
   1017     """Parses the local DEPS file to determine blink/skia/v8 revisions which may
   1018     be needed if the bisect recurses into those depots later.
   1019 
   1020     Args:
   1021       depot: Name of depot being bisected.
   1022 
   1023     Returns:
   1024       A dict in the format {depot:revision} if successful, otherwise None.
   1025     """
   1026     try:
   1027       deps_data = {
   1028           'Var': lambda _: deps_data["vars"][_],
   1029           'From': lambda *args: None,
   1030       }
   1031 
   1032       deps_file = bisect_utils.FILE_DEPS_GIT
   1033       if not os.path.exists(deps_file):
   1034         deps_file = bisect_utils.FILE_DEPS
   1035       execfile(deps_file, {}, deps_data)
   1036       deps_data = deps_data['deps']
   1037 
   1038       rxp = re.compile(".git@(?P<revision>[a-fA-F0-9]+)")
   1039       results = {}
   1040       for depot_name, depot_data in DEPOT_DEPS_NAME.iteritems():
   1041         if (depot_data.get('platform') and
   1042             depot_data.get('platform') != os.name):
   1043           continue
   1044 
   1045         if (depot_data.get('recurse') and depot in depot_data.get('from')):
   1046           depot_data_src = depot_data.get('src') or depot_data.get('src_old')
   1047           src_dir = deps_data.get(depot_data_src)
   1048           if src_dir:
   1049             self.depot_registry.AddDepot(depot_name, os.path.join(
   1050                 self.src_cwd, depot_data_src[4:]))
   1051             re_results = rxp.search(src_dir)
   1052             if re_results:
   1053               results[depot_name] = re_results.group('revision')
   1054             else:
   1055               warning_text = ('Could not parse revision for %s while bisecting '
   1056                               '%s' % (depot_name, depot))
   1057               if not warning_text in self.warnings:
   1058                 self.warnings.append(warning_text)
   1059           else:
   1060             results[depot_name] = None
   1061       return results
   1062     except ImportError:
   1063       deps_file_contents = ReadStringFromFile(deps_file)
   1064       parse_results = _ParseRevisionsFromDEPSFileManually(deps_file_contents)
   1065       results = {}
   1066       for depot_name, depot_revision in parse_results.iteritems():
   1067         depot_revision = depot_revision.strip('@')
   1068         print depot_name, depot_revision
   1069         for current_name, current_data in DEPOT_DEPS_NAME.iteritems():
   1070           if (current_data.has_key('deps_var') and
   1071               current_data['deps_var'] == depot_name):
   1072             src_name = current_name
   1073             results[src_name] = depot_revision
   1074             break
   1075       return results
   1076 
   1077   def _Get3rdPartyRevisions(self, depot):
   1078     """Parses the DEPS file to determine WebKit/v8/etc... versions.
   1079 
   1080     Args:
   1081       depot: A depot name. Should be in the DEPOT_NAMES list.
   1082 
   1083     Returns:
   1084       A dict in the format {depot: revision} if successful, otherwise None.
   1085     """
   1086     cwd = os.getcwd()
   1087     self.depot_registry.ChangeToDepotDir(depot)
   1088 
   1089     results = {}
   1090 
   1091     if depot == 'chromium' or depot == 'android-chrome':
   1092       results = self._ParseRevisionsFromDEPSFile(depot)
   1093       os.chdir(cwd)
   1094 
   1095     if depot == 'cros':
   1096       cmd = [
   1097           bisect_utils.CROS_SDK_PATH,
   1098           '--',
   1099           'portageq-%s' % self.opts.cros_board,
   1100           'best_visible',
   1101           '/build/%s' % self.opts.cros_board,
   1102           'ebuild',
   1103           CROS_CHROMEOS_PATTERN
   1104       ]
   1105       output, return_code = bisect_utils.RunProcessAndRetrieveOutput(cmd)
   1106 
   1107       assert not return_code, ('An error occurred while running '
   1108                                '"%s"' % ' '.join(cmd))
   1109 
   1110       if len(output) > CROS_CHROMEOS_PATTERN:
   1111         output = output[len(CROS_CHROMEOS_PATTERN):]
   1112 
   1113       if len(output) > 1:
   1114         output = output.split('_')[0]
   1115 
   1116         if len(output) > 3:
   1117           contents = output.split('.')
   1118 
   1119           version = contents[2]
   1120 
   1121           if contents[3] != '0':
   1122             warningText = ('Chrome version: %s.%s but using %s.0 to bisect.' %
   1123                            (version, contents[3], version))
   1124             if not warningText in self.warnings:
   1125               self.warnings.append(warningText)
   1126 
   1127           cwd = os.getcwd()
   1128           self.depot_registry.ChangeToDepotDir('chromium')
   1129           cmd = ['log', '-1', '--format=%H',
   1130                  '--author=chrome-release (at] google.com',
   1131                  '--grep=to %s' % version, 'origin/master']
   1132           return_code = bisect_utils.CheckRunGit(cmd)
   1133           os.chdir(cwd)
   1134 
   1135           results['chromium'] = output.strip()
   1136 
   1137     if depot == 'v8':
   1138       # We can't try to map the trunk revision to bleeding edge yet, because
   1139       # we don't know which direction to try to search in. Have to wait until
   1140       # the bisect has narrowed the results down to 2 v8 rolls.
   1141       results['v8_bleeding_edge'] = None
   1142 
   1143     return results
   1144 
   1145   def BackupOrRestoreOutputDirectory(self, restore=False, build_type='Release'):
   1146     """Backs up or restores build output directory based on restore argument.
   1147 
   1148     Args:
   1149       restore: Indicates whether to restore or backup. Default is False(Backup)
   1150       build_type: Target build type ('Release', 'Debug', 'Release_x64' etc.)
   1151 
   1152     Returns:
   1153       Path to backup or restored location as string. otherwise None if it fails.
   1154     """
   1155     build_dir = os.path.abspath(
   1156         builder.GetBuildOutputDirectory(self.opts, self.src_cwd))
   1157     source_dir = os.path.join(build_dir, build_type)
   1158     destination_dir = os.path.join(build_dir, '%s.bak' % build_type)
   1159     if restore:
   1160       source_dir, destination_dir = destination_dir, source_dir
   1161     if os.path.exists(source_dir):
   1162       RmTreeAndMkDir(destination_dir, skip_makedir=True)
   1163       shutil.move(source_dir, destination_dir)
   1164       return destination_dir
   1165     return None
   1166 
   1167   def GetBuildArchiveForRevision(self, revision, gs_bucket, target_arch,
   1168                                  patch_sha, out_dir):
   1169     """Checks and downloads build archive for a given revision.
   1170 
   1171     Checks for build archive with Git hash or SVN revision. If either of the
   1172     file exists, then downloads the archive file.
   1173 
   1174     Args:
   1175       revision: A Git hash revision.
   1176       gs_bucket: Cloud storage bucket name
   1177       target_arch: 32 or 64 bit build target
   1178       patch: A DEPS patch (used while bisecting 3rd party repositories).
   1179       out_dir: Build output directory where downloaded file is stored.
   1180 
   1181     Returns:
   1182       Downloaded archive file path if exists, otherwise None.
   1183     """
   1184     # Source archive file path on cloud storage using Git revision.
   1185     source_file = GetRemoteBuildPath(
   1186         revision, self.opts.target_platform, target_arch, patch_sha)
   1187     downloaded_archive = FetchFromCloudStorage(gs_bucket, source_file, out_dir)
   1188     if not downloaded_archive:
   1189       # Get commit position for the given SHA.
   1190       commit_position = self.source_control.GetCommitPosition(revision)
   1191       if commit_position:
   1192         # Source archive file path on cloud storage using SVN revision.
   1193         source_file = GetRemoteBuildPath(
   1194             commit_position, self.opts.target_platform, target_arch, patch_sha)
   1195         return FetchFromCloudStorage(gs_bucket, source_file, out_dir)
   1196     return downloaded_archive
   1197 
   1198   def DownloadCurrentBuild(self, revision, build_type='Release', patch=None):
   1199     """Downloads the build archive for the given revision.
   1200 
   1201     Args:
   1202       revision: The Git revision to download or build.
   1203       build_type: Target build type ('Release', 'Debug', 'Release_x64' etc.)
   1204       patch: A DEPS patch (used while bisecting 3rd party repositories).
   1205 
   1206     Returns:
   1207       True if download succeeds, otherwise False.
   1208     """
   1209     patch_sha = None
   1210     if patch:
   1211       # Get the SHA of the DEPS changes patch.
   1212       patch_sha = GetSHA1HexDigest(patch)
   1213 
   1214       # Update the DEPS changes patch with a patch to create a new file named
   1215       # 'DEPS.sha' and add patch_sha evaluated above to it.
   1216       patch = '%s\n%s' % (patch, DEPS_SHA_PATCH % {'deps_sha': patch_sha})
   1217 
   1218     # Get Build output directory
   1219     abs_build_dir = os.path.abspath(
   1220         builder.GetBuildOutputDirectory(self.opts, self.src_cwd))
   1221 
   1222     fetch_build_func = lambda: self.GetBuildArchiveForRevision(
   1223       revision, self.opts.gs_bucket, self.opts.target_arch,
   1224       patch_sha, abs_build_dir)
   1225 
   1226     # Downloaded archive file path, downloads build archive for given revision.
   1227     downloaded_file = fetch_build_func()
   1228 
   1229     # When build archive doesn't exists, post a build request to tryserver
   1230     # and wait for the build to be produced.
   1231     if not downloaded_file:
   1232       downloaded_file = self.PostBuildRequestAndWait(
   1233           revision, fetch_build=fetch_build_func, patch=patch)
   1234       if not downloaded_file:
   1235         return False
   1236 
   1237     # Generic name for the archive, created when archive file is extracted.
   1238     output_dir = os.path.join(
   1239         abs_build_dir, GetZipFileName(target_arch=self.opts.target_arch))
   1240     # Unzip build archive directory.
   1241     try:
   1242       RmTreeAndMkDir(output_dir, skip_makedir=True)
   1243       self.BackupOrRestoreOutputDirectory(restore=False)
   1244       # Build output directory based on target(e.g. out/Release, out/Debug).
   1245       target_build_output_dir = os.path.join(abs_build_dir, build_type)
   1246       ExtractZip(downloaded_file, abs_build_dir)
   1247       if not os.path.exists(output_dir):
   1248         # Due to recipe changes, the builds extract folder contains
   1249         # out/Release instead of full-build-<platform>/Release.
   1250         if os.path.exists(os.path.join(abs_build_dir, 'out', build_type)):
   1251           output_dir = os.path.join(abs_build_dir, 'out', build_type)
   1252         else:
   1253           raise IOError('Missing extracted folder %s ' % output_dir)
   1254 
   1255       print 'Moving build from %s to %s' % (
   1256           output_dir, target_build_output_dir)
   1257       shutil.move(output_dir, target_build_output_dir)
   1258       return True
   1259     except Exception as e:
   1260       print 'Something went wrong while extracting archive file: %s' % e
   1261       self.BackupOrRestoreOutputDirectory(restore=True)
   1262       # Cleanup any leftovers from unzipping.
   1263       if os.path.exists(output_dir):
   1264         RmTreeAndMkDir(output_dir, skip_makedir=True)
   1265     finally:
   1266       # Delete downloaded archive
   1267       if os.path.exists(downloaded_file):
   1268         os.remove(downloaded_file)
   1269     return False
   1270 
   1271   def PostBuildRequestAndWait(self, git_revision, fetch_build, patch=None):
   1272     """POSTs the build request job to the try server instance.
   1273 
   1274     A try job build request is posted to tryserver.chromium.perf master,
   1275     and waits for the binaries to be produced and archived on cloud storage.
   1276     Once the build is ready and stored onto cloud, build archive is downloaded
   1277     into the output folder.
   1278 
   1279     Args:
   1280       git_revision: A Git hash revision.
   1281       fetch_build: Function to check and download build from cloud storage.
   1282       patch: A DEPS patch (used while bisecting 3rd party repositories).
   1283 
   1284     Returns:
   1285       Downloaded archive file path when requested build exists and download is
   1286       successful, otherwise None.
   1287     """
   1288     def GetBuilderNameAndBuildTime(target_platform, target_arch='ia32'):
   1289       """Gets builder bot name and build time in seconds based on platform."""
   1290       # Bot names should match the one listed in tryserver.chromium's
   1291       # master.cfg which produces builds for bisect.
   1292       if bisect_utils.IsWindowsHost():
   1293         if bisect_utils.Is64BitWindows() and target_arch == 'x64':
   1294           return ('win_perf_bisect_builder', MAX_WIN_BUILD_TIME)
   1295         return ('win_perf_bisect_builder', MAX_WIN_BUILD_TIME)
   1296       if bisect_utils.IsLinuxHost():
   1297         if target_platform == 'android':
   1298           return ('android_perf_bisect_builder', MAX_LINUX_BUILD_TIME)
   1299         return ('linux_perf_bisect_builder', MAX_LINUX_BUILD_TIME)
   1300       if bisect_utils.IsMacHost():
   1301         return ('mac_perf_bisect_builder', MAX_MAC_BUILD_TIME)
   1302       raise NotImplementedError('Unsupported Platform "%s".' % sys.platform)
   1303     if not fetch_build:
   1304       return False
   1305 
   1306     bot_name, build_timeout = GetBuilderNameAndBuildTime(
   1307        self.opts.target_platform, self.opts.target_arch)
   1308     builder_host = self.opts.builder_host
   1309     builder_port = self.opts.builder_port
   1310     # Create a unique ID for each build request posted to try server builders.
   1311     # This ID is added to "Reason" property of the build.
   1312     build_request_id = GetSHA1HexDigest(
   1313         '%s-%s-%s' % (git_revision, patch, time.time()))
   1314 
   1315     # Creates a try job description.
   1316     # Always use Git hash to post build request since Commit positions are
   1317     # not supported by builders to build.
   1318     job_args = {
   1319         'revision': 'src@%s' % git_revision,
   1320         'bot': bot_name,
   1321         'name': build_request_id,
   1322     }
   1323     # Update patch information if supplied.
   1324     if patch:
   1325       job_args['patch'] = patch
   1326     # Posts job to build the revision on the server.
   1327     if request_build.PostTryJob(builder_host, builder_port, job_args):
   1328       target_file, error_msg = _WaitUntilBuildIsReady(
   1329           fetch_build, bot_name, builder_host, builder_port, build_request_id,
   1330           build_timeout)
   1331       if not target_file:
   1332         print '%s [revision: %s]' % (error_msg, git_revision)
   1333         return None
   1334       return target_file
   1335     print 'Failed to post build request for revision: [%s]' % git_revision
   1336     return None
   1337 
   1338   def IsDownloadable(self, depot):
   1339     """Checks if build can be downloaded based on target platform and depot."""
   1340     if (self.opts.target_platform in ['chromium', 'android'] and
   1341         self.opts.gs_bucket):
   1342       return (depot == 'chromium' or
   1343               'chromium' in DEPOT_DEPS_NAME[depot]['from'] or
   1344               'v8' in DEPOT_DEPS_NAME[depot]['from'])
   1345     return False
   1346 
   1347   def UpdateDepsContents(self, deps_contents, depot, git_revision, deps_key):
   1348     """Returns modified version of DEPS file contents.
   1349 
   1350     Args:
   1351       deps_contents: DEPS file content.
   1352       depot: Current depot being bisected.
   1353       git_revision: A git hash to be updated in DEPS.
   1354       deps_key: Key in vars section of DEPS file to be searched.
   1355 
   1356     Returns:
   1357       Updated DEPS content as string if deps key is found, otherwise None.
   1358     """
   1359     # Check whether the depot and revision pattern in DEPS file vars
   1360     # e.g. for webkit the format is "webkit_revision": "12345".
   1361     deps_revision = re.compile(r'(?<="%s": ")([0-9]+)(?=")' % deps_key,
   1362                                re.MULTILINE)
   1363     new_data = None
   1364     if re.search(deps_revision, deps_contents):
   1365       commit_position = self.source_control.GetCommitPosition(
   1366           git_revision, self.depot_registry.GetDepotDir(depot))
   1367       if not commit_position:
   1368         print 'Could not determine commit position for %s' % git_revision
   1369         return None
   1370       # Update the revision information for the given depot
   1371       new_data = re.sub(deps_revision, str(commit_position), deps_contents)
   1372     else:
   1373       # Check whether the depot and revision pattern in DEPS file vars
   1374       # e.g. for webkit the format is "webkit_revision": "559a6d4ab7a84c539..".
   1375       deps_revision = re.compile(
   1376           r'(?<=["\']%s["\']: ["\'])([a-fA-F0-9]{40})(?=["\'])' % deps_key,
   1377           re.MULTILINE)
   1378       if re.search(deps_revision, deps_contents):
   1379         new_data = re.sub(deps_revision, git_revision, deps_contents)
   1380     if new_data:
   1381       # For v8_bleeding_edge revisions change V8 branch in order
   1382       # to fetch bleeding edge revision.
   1383       if depot == 'v8_bleeding_edge':
   1384         new_data = _UpdateV8Branch(new_data)
   1385         if not new_data:
   1386           return None
   1387     return new_data
   1388 
   1389   def UpdateDeps(self, revision, depot, deps_file):
   1390     """Updates DEPS file with new revision of dependency repository.
   1391 
   1392     This method search DEPS for a particular pattern in which depot revision
   1393     is specified (e.g "webkit_revision": "123456"). If a match is found then
   1394     it resolves the given git hash to SVN revision and replace it in DEPS file.
   1395 
   1396     Args:
   1397       revision: A git hash revision of the dependency repository.
   1398       depot: Current depot being bisected.
   1399       deps_file: Path to DEPS file.
   1400 
   1401     Returns:
   1402       True if DEPS file is modified successfully, otherwise False.
   1403     """
   1404     if not os.path.exists(deps_file):
   1405       return False
   1406 
   1407     deps_var = DEPOT_DEPS_NAME[depot]['deps_var']
   1408     # Don't update DEPS file if deps_var is not set in DEPOT_DEPS_NAME.
   1409     if not deps_var:
   1410       print 'DEPS update not supported for Depot: %s', depot
   1411       return False
   1412 
   1413     # Hack for Angle repository. In the DEPS file, "vars" dictionary variable
   1414     # contains "angle_revision" key that holds git hash instead of SVN revision.
   1415     # And sometime "angle_revision" key is not specified in "vars" variable.
   1416     # In such cases check, "deps" dictionary variable that matches
   1417     # angle.git@[a-fA-F0-9]{40}$ and replace git hash.
   1418     if depot == 'angle':
   1419       return _UpdateDEPSForAngle(revision, depot, deps_file)
   1420 
   1421     try:
   1422       deps_contents = ReadStringFromFile(deps_file)
   1423       updated_deps_content = self.UpdateDepsContents(
   1424           deps_contents, depot, revision, deps_var)
   1425       # Write changes to DEPS file
   1426       if updated_deps_content:
   1427         WriteStringToFile(updated_deps_content, deps_file)
   1428         return True
   1429     except IOError, e:
   1430       print 'Something went wrong while updating DEPS file. [%s]' % e
   1431     return False
   1432 
   1433   def CreateDEPSPatch(self, depot, revision):
   1434     """Modifies DEPS and returns diff as text.
   1435 
   1436     Args:
   1437       depot: Current depot being bisected.
   1438       revision: A git hash revision of the dependency repository.
   1439 
   1440     Returns:
   1441       A tuple with git hash of chromium revision and DEPS patch text.
   1442     """
   1443     deps_file_path = os.path.join(self.src_cwd, bisect_utils.FILE_DEPS)
   1444     if not os.path.exists(deps_file_path):
   1445       raise RuntimeError('DEPS file does not exists.[%s]' % deps_file_path)
   1446     # Get current chromium revision (git hash).
   1447     cmd = ['rev-parse', 'HEAD']
   1448     chromium_sha = bisect_utils.CheckRunGit(cmd).strip()
   1449     if not chromium_sha:
   1450       raise RuntimeError('Failed to determine Chromium revision for %s' %
   1451                          revision)
   1452     if ('chromium' in DEPOT_DEPS_NAME[depot]['from'] or
   1453         'v8' in DEPOT_DEPS_NAME[depot]['from']):
   1454       # Checkout DEPS file for the current chromium revision.
   1455       if self.source_control.CheckoutFileAtRevision(
   1456           bisect_utils.FILE_DEPS, chromium_sha, cwd=self.src_cwd):
   1457         if self.UpdateDeps(revision, depot, deps_file_path):
   1458           diff_command = [
   1459               'diff',
   1460               '--src-prefix=src/',
   1461               '--dst-prefix=src/',
   1462               '--no-ext-diff',
   1463                bisect_utils.FILE_DEPS,
   1464           ]
   1465           diff_text = bisect_utils.CheckRunGit(diff_command, cwd=self.src_cwd)
   1466           return (chromium_sha, ChangeBackslashToSlashInPatch(diff_text))
   1467         else:
   1468           raise RuntimeError(
   1469               'Failed to update DEPS file for chromium: [%s]' % chromium_sha)
   1470       else:
   1471         raise RuntimeError(
   1472             'DEPS checkout Failed for chromium revision : [%s]' % chromium_sha)
   1473     return (None, None)
   1474 
   1475   def BuildCurrentRevision(self, depot, revision=None):
   1476     """Builds chrome and performance_ui_tests on the current revision.
   1477 
   1478     Returns:
   1479       True if the build was successful.
   1480     """
   1481     if self.opts.debug_ignore_build:
   1482       return True
   1483 
   1484     build_success = False
   1485     cwd = os.getcwd()
   1486     os.chdir(self.src_cwd)
   1487     # Fetch build archive for the given revision from the cloud storage when
   1488     # the storage bucket is passed.
   1489     if self.IsDownloadable(depot) and revision:
   1490       deps_patch = None
   1491       if depot != 'chromium':
   1492         # Create a DEPS patch with new revision for dependency repository.
   1493         revision, deps_patch = self.CreateDEPSPatch(depot, revision)
   1494       if self.DownloadCurrentBuild(revision, patch=deps_patch):
   1495         if deps_patch:
   1496           # Reverts the changes to DEPS file.
   1497           self.source_control.CheckoutFileAtRevision(
   1498               bisect_utils.FILE_DEPS, revision, cwd=self.src_cwd)
   1499         build_success = True
   1500     else:
   1501       # These codes are executed when bisect bots builds binaries locally.
   1502       build_success = self.builder.Build(depot, self.opts)
   1503     os.chdir(cwd)
   1504     return build_success
   1505 
   1506   def RunGClientHooks(self):
   1507     """Runs gclient with runhooks command.
   1508 
   1509     Returns:
   1510       True if gclient reports no errors.
   1511     """
   1512     if self.opts.debug_ignore_build:
   1513       return True
   1514     return not bisect_utils.RunGClient(['runhooks'], cwd=self.src_cwd)
   1515 
   1516   def _IsBisectModeUsingMetric(self):
   1517     return self.opts.bisect_mode in [BISECT_MODE_MEAN, BISECT_MODE_STD_DEV]
   1518 
   1519   def _IsBisectModeReturnCode(self):
   1520     return self.opts.bisect_mode in [BISECT_MODE_RETURN_CODE]
   1521 
   1522   def _IsBisectModeStandardDeviation(self):
   1523     return self.opts.bisect_mode in [BISECT_MODE_STD_DEV]
   1524 
   1525   def GetCompatibleCommand(self, command_to_run, revision, depot):
   1526     """Return a possibly modified test command depending on the revision.
   1527 
   1528     Prior to crrev.com/274857 *only* android-chromium-testshell
   1529     Then until crrev.com/276628 *both* (android-chromium-testshell and
   1530     android-chrome-shell) work. After that rev 276628 *only*
   1531     android-chrome-shell works. The bisect_perf_regression.py script should
   1532     handle these cases and set appropriate browser type based on revision.
   1533     """
   1534     if self.opts.target_platform in ['android']:
   1535       # When its a third_party depot, get the chromium revision.
   1536       if depot != 'chromium':
   1537         revision = bisect_utils.CheckRunGit(
   1538             ['rev-parse', 'HEAD'], cwd=self.src_cwd).strip()
   1539       commit_position = self.source_control.GetCommitPosition(revision,
   1540                                                               cwd=self.src_cwd)
   1541       if not commit_position:
   1542         return command_to_run
   1543       cmd_re = re.compile('--browser=(?P<browser_type>\S+)')
   1544       matches = cmd_re.search(command_to_run)
   1545       if bisect_utils.IsStringInt(commit_position) and matches:
   1546         cmd_browser = matches.group('browser_type')
   1547         if commit_position <= 274857 and cmd_browser == 'android-chrome-shell':
   1548           return command_to_run.replace(cmd_browser,
   1549                                         'android-chromium-testshell')
   1550         elif (commit_position >= 276628 and
   1551               cmd_browser == 'android-chromium-testshell'):
   1552           return command_to_run.replace(cmd_browser,
   1553                                         'android-chrome-shell')
   1554     return command_to_run
   1555 
   1556   def RunPerformanceTestAndParseResults(
   1557       self, command_to_run, metric, reset_on_first_run=False,
   1558       upload_on_last_run=False, results_label=None):
   1559     """Runs a performance test on the current revision and parses the results.
   1560 
   1561     Args:
   1562       command_to_run: The command to be run to execute the performance test.
   1563       metric: The metric to parse out from the results of the performance test.
   1564           This is the result chart name and trace name, separated by slash.
   1565           May be None for perf try jobs.
   1566       reset_on_first_run: If True, pass the flag --reset-results on first run.
   1567       upload_on_last_run: If True, pass the flag --upload-results on last run.
   1568       results_label: A value for the option flag --results-label.
   1569           The arguments reset_on_first_run, upload_on_last_run and results_label
   1570           are all ignored if the test is not a Telemetry test.
   1571 
   1572     Returns:
   1573       (values dict, 0) if --debug_ignore_perf_test was passed.
   1574       (values dict, 0, test output) if the test was run successfully.
   1575       (error message, -1) if the test couldn't be run.
   1576       (error message, -1, test output) if the test ran but there was an error.
   1577     """
   1578     success_code, failure_code = 0, -1
   1579 
   1580     if self.opts.debug_ignore_perf_test:
   1581       fake_results = {
   1582           'mean': 0.0,
   1583           'std_err': 0.0,
   1584           'std_dev': 0.0,
   1585           'values': [0.0]
   1586       }
   1587       return (fake_results, success_code)
   1588 
   1589     # For Windows platform set posix=False, to parse windows paths correctly.
   1590     # On Windows, path separators '\' or '\\' are replace by '' when posix=True,
   1591     # refer to http://bugs.python.org/issue1724822. By default posix=True.
   1592     args = shlex.split(command_to_run, posix=not bisect_utils.IsWindowsHost())
   1593 
   1594     if not _GenerateProfileIfNecessary(args):
   1595       err_text = 'Failed to generate profile for performance test.'
   1596       return (err_text, failure_code)
   1597 
   1598     # If running a Telemetry test for Chrome OS, insert the remote IP and
   1599     # identity parameters.
   1600     is_telemetry = bisect_utils.IsTelemetryCommand(command_to_run)
   1601     if self.opts.target_platform == 'cros' and is_telemetry:
   1602       args.append('--remote=%s' % self.opts.cros_remote_ip)
   1603       args.append('--identity=%s' % bisect_utils.CROS_TEST_KEY_PATH)
   1604 
   1605     start_time = time.time()
   1606 
   1607     metric_values = []
   1608     output_of_all_runs = ''
   1609     for i in xrange(self.opts.repeat_test_count):
   1610       # Can ignore the return code since if the tests fail, it won't return 0.
   1611       current_args = copy.copy(args)
   1612       if is_telemetry:
   1613         if i == 0 and reset_on_first_run:
   1614           current_args.append('--reset-results')
   1615         elif i == self.opts.repeat_test_count - 1 and upload_on_last_run:
   1616           current_args.append('--upload-results')
   1617         if results_label:
   1618           current_args.append('--results-label=%s' % results_label)
   1619       try:
   1620         output, return_code = bisect_utils.RunProcessAndRetrieveOutput(
   1621             current_args, cwd=self.src_cwd)
   1622       except OSError, e:
   1623         if e.errno == errno.ENOENT:
   1624           err_text  = ('Something went wrong running the performance test. '
   1625                        'Please review the command line:\n\n')
   1626           if 'src/' in ' '.join(args):
   1627             err_text += ('Check that you haven\'t accidentally specified a '
   1628                          'path with src/ in the command.\n\n')
   1629           err_text += ' '.join(args)
   1630           err_text += '\n'
   1631 
   1632           return (err_text, failure_code)
   1633         raise
   1634 
   1635       output_of_all_runs += output
   1636       if self.opts.output_buildbot_annotations:
   1637         print output
   1638 
   1639       if metric and self._IsBisectModeUsingMetric():
   1640         metric_values += _ParseMetricValuesFromOutput(metric, output)
   1641         # If we're bisecting on a metric (ie, changes in the mean or
   1642         # standard deviation) and no metric values are produced, bail out.
   1643         if not metric_values:
   1644           break
   1645       elif self._IsBisectModeReturnCode():
   1646         metric_values.append(return_code)
   1647 
   1648       elapsed_minutes = (time.time() - start_time) / 60.0
   1649       if elapsed_minutes >= self.opts.max_time_minutes:
   1650         break
   1651 
   1652     if metric and len(metric_values) == 0:
   1653       err_text = 'Metric %s was not found in the test output.' % metric
   1654       # TODO(qyearsley): Consider also getting and displaying a list of metrics
   1655       # that were found in the output here.
   1656       return (err_text, failure_code, output_of_all_runs)
   1657 
   1658     # If we're bisecting on return codes, we're really just looking for zero vs
   1659     # non-zero.
   1660     values = {}
   1661     if self._IsBisectModeReturnCode():
   1662       # If any of the return codes is non-zero, output 1.
   1663       overall_return_code = 0 if (
   1664           all(current_value == 0 for current_value in metric_values)) else 1
   1665 
   1666       values = {
   1667           'mean': overall_return_code,
   1668           'std_err': 0.0,
   1669           'std_dev': 0.0,
   1670           'values': metric_values,
   1671       }
   1672 
   1673       print 'Results of performance test: Command returned with %d' % (
   1674           overall_return_code)
   1675       print
   1676     elif metric:
   1677       # Need to get the average value if there were multiple values.
   1678       truncated_mean = math_utils.TruncatedMean(
   1679           metric_values, self.opts.truncate_percent)
   1680       standard_err = math_utils.StandardError(metric_values)
   1681       standard_dev = math_utils.StandardDeviation(metric_values)
   1682 
   1683       if self._IsBisectModeStandardDeviation():
   1684         metric_values = [standard_dev]
   1685 
   1686       values = {
   1687           'mean': truncated_mean,
   1688           'std_err': standard_err,
   1689           'std_dev': standard_dev,
   1690           'values': metric_values,
   1691       }
   1692 
   1693       print 'Results of performance test: %12f %12f' % (
   1694           truncated_mean, standard_err)
   1695       print
   1696     return (values, success_code, output_of_all_runs)
   1697 
   1698   def _FindAllRevisionsToSync(self, revision, depot):
   1699     """Finds all dependent revisions and depots that need to be synced.
   1700 
   1701     For example skia is broken up into 3 git mirrors over skia/src,
   1702     skia/gyp, and skia/include. To sync skia/src properly, one has to find
   1703     the proper revisions in skia/gyp and skia/include.
   1704 
   1705     This is only useful in the git workflow, as an SVN depot may be split into
   1706     multiple mirrors.
   1707 
   1708     Args:
   1709       revision: The revision to sync to.
   1710       depot: The depot in use at the moment (probably skia).
   1711 
   1712     Returns:
   1713       A list of [depot, revision] pairs that need to be synced.
   1714     """
   1715     revisions_to_sync = [[depot, revision]]
   1716 
   1717     is_base = ((depot == 'chromium') or (depot == 'cros') or
   1718         (depot == 'android-chrome'))
   1719 
   1720     # Some SVN depots were split into multiple git depots, so we need to
   1721     # figure out for each mirror which git revision to grab. There's no
   1722     # guarantee that the SVN revision will exist for each of the dependent
   1723     # depots, so we have to grep the git logs and grab the next earlier one.
   1724     if (not is_base
   1725         and DEPOT_DEPS_NAME[depot]['depends']
   1726         and self.source_control.IsGit()):
   1727       commit_position = self.source_control.GetCommitPosition(revision)
   1728 
   1729       for d in DEPOT_DEPS_NAME[depot]['depends']:
   1730         self.depot_registry.ChangeToDepotDir(d)
   1731 
   1732         dependant_rev = self.source_control.ResolveToRevision(
   1733             commit_position, d, DEPOT_DEPS_NAME, -1000)
   1734 
   1735         if dependant_rev:
   1736           revisions_to_sync.append([d, dependant_rev])
   1737 
   1738       num_resolved = len(revisions_to_sync)
   1739       num_needed = len(DEPOT_DEPS_NAME[depot]['depends'])
   1740 
   1741       self.depot_registry.ChangeToDepotDir(depot)
   1742 
   1743       if not ((num_resolved - 1) == num_needed):
   1744         return None
   1745 
   1746     return revisions_to_sync
   1747 
   1748   def PerformPreBuildCleanup(self):
   1749     """Performs cleanup between runs."""
   1750     print 'Cleaning up between runs.'
   1751     print
   1752 
   1753     # Leaving these .pyc files around between runs may disrupt some perf tests.
   1754     for (path, _, files) in os.walk(self.src_cwd):
   1755       for cur_file in files:
   1756         if cur_file.endswith('.pyc'):
   1757           path_to_file = os.path.join(path, cur_file)
   1758           os.remove(path_to_file)
   1759 
   1760   def PerformCrosChrootCleanup(self):
   1761     """Deletes the chroot.
   1762 
   1763     Returns:
   1764       True if successful.
   1765     """
   1766     cwd = os.getcwd()
   1767     self.depot_registry.ChangeToDepotDir('cros')
   1768     cmd = [bisect_utils.CROS_SDK_PATH, '--delete']
   1769     return_code = bisect_utils.RunProcess(cmd)
   1770     os.chdir(cwd)
   1771     return not return_code
   1772 
   1773   def CreateCrosChroot(self):
   1774     """Creates a new chroot.
   1775 
   1776     Returns:
   1777       True if successful.
   1778     """
   1779     cwd = os.getcwd()
   1780     self.depot_registry.ChangeToDepotDir('cros')
   1781     cmd = [bisect_utils.CROS_SDK_PATH, '--create']
   1782     return_code = bisect_utils.RunProcess(cmd)
   1783     os.chdir(cwd)
   1784     return not return_code
   1785 
   1786   def _PerformPreSyncCleanup(self, depot):
   1787     """Performs any necessary cleanup before syncing.
   1788 
   1789     Args:
   1790       depot: Depot name.
   1791 
   1792     Returns:
   1793       True if successful.
   1794     """
   1795     if depot == 'chromium' or depot == 'android-chrome':
   1796       # Removes third_party/libjingle. At some point, libjingle was causing
   1797       # issues syncing when using the git workflow (crbug.com/266324).
   1798       os.chdir(self.src_cwd)
   1799       if not bisect_utils.RemoveThirdPartyDirectory('libjingle'):
   1800         return False
   1801       # Removes third_party/skia. At some point, skia was causing
   1802       # issues syncing when using the git workflow (crbug.com/377951).
   1803       if not bisect_utils.RemoveThirdPartyDirectory('skia'):
   1804         return False
   1805     elif depot == 'cros':
   1806       return self.PerformCrosChrootCleanup()
   1807     return True
   1808 
   1809   def _RunPostSync(self, depot):
   1810     """Performs any work after syncing.
   1811 
   1812     Args:
   1813       depot: Depot name.
   1814 
   1815     Returns:
   1816       True if successful.
   1817     """
   1818     if self.opts.target_platform == 'android':
   1819       if not builder.SetupAndroidBuildEnvironment(self.opts,
   1820           path_to_src=self.src_cwd):
   1821         return False
   1822 
   1823     if depot == 'cros':
   1824       return self.CreateCrosChroot()
   1825     else:
   1826       return self.RunGClientHooks()
   1827     return True
   1828 
   1829   def ShouldSkipRevision(self, depot, revision):
   1830     """Checks whether a particular revision can be safely skipped.
   1831 
   1832     Some commits can be safely skipped (such as a DEPS roll), since the tool
   1833     is git based those changes would have no effect.
   1834 
   1835     Args:
   1836       depot: The depot being bisected.
   1837       revision: Current revision we're synced to.
   1838 
   1839     Returns:
   1840       True if we should skip building/testing this revision.
   1841     """
   1842     if depot == 'chromium':
   1843       if self.source_control.IsGit():
   1844         cmd = ['diff-tree', '--no-commit-id', '--name-only', '-r', revision]
   1845         output = bisect_utils.CheckRunGit(cmd)
   1846 
   1847         files = output.splitlines()
   1848 
   1849         if len(files) == 1 and files[0] == 'DEPS':
   1850           return True
   1851 
   1852     return False
   1853 
   1854   def RunTest(self, revision, depot, command, metric, skippable=False):
   1855     """Performs a full sync/build/run of the specified revision.
   1856 
   1857     Args:
   1858       revision: The revision to sync to.
   1859       depot: The depot that's being used at the moment (src, webkit, etc.)
   1860       command: The command to execute the performance test.
   1861       metric: The performance metric being tested.
   1862 
   1863     Returns:
   1864       On success, a tuple containing the results of the performance test.
   1865       Otherwise, a tuple with the error message.
   1866     """
   1867     # Decide which sync program to use.
   1868     sync_client = None
   1869     if depot == 'chromium' or depot == 'android-chrome':
   1870       sync_client = 'gclient'
   1871     elif depot == 'cros':
   1872       sync_client = 'repo'
   1873 
   1874     # Decide what depots will need to be synced to what revisions.
   1875     revisions_to_sync = self._FindAllRevisionsToSync(revision, depot)
   1876     if not revisions_to_sync:
   1877       return ('Failed to resolve dependent depots.', BUILD_RESULT_FAIL)
   1878 
   1879     if not self._PerformPreSyncCleanup(depot):
   1880       return ('Failed to perform pre-sync cleanup.', BUILD_RESULT_FAIL)
   1881 
   1882     # Do the syncing for all depots.
   1883     if not self.opts.debug_ignore_sync:
   1884       if not self._SyncAllRevisions(revisions_to_sync, sync_client):
   1885         return ('Failed to sync: [%s]' % str(revision), BUILD_RESULT_FAIL)
   1886 
   1887      # Try to do any post-sync steps. This may include "gclient runhooks".
   1888     if not self._RunPostSync(depot):
   1889       return ('Failed to run [gclient runhooks].', BUILD_RESULT_FAIL)
   1890 
   1891     # Skip this revision if it can be skipped.
   1892     if skippable and self.ShouldSkipRevision(depot, revision):
   1893       return ('Skipped revision: [%s]' % str(revision),
   1894               BUILD_RESULT_SKIPPED)
   1895 
   1896     # Obtain a build for this revision. This may be done by requesting a build
   1897     # from another builder, waiting for it and downloading it.
   1898     start_build_time = time.time()
   1899     build_success = self.BuildCurrentRevision(depot, revision)
   1900     if not build_success:
   1901       return ('Failed to build revision: [%s]' % str(revision),
   1902               BUILD_RESULT_FAIL)
   1903     after_build_time = time.time()
   1904 
   1905     # Possibly alter the command.
   1906     command = self.GetCompatibleCommand(command, revision, depot)
   1907 
   1908     # Run the command and get the results.
   1909     results = self.RunPerformanceTestAndParseResults(command, metric)
   1910 
   1911     # Restore build output directory once the tests are done, to avoid
   1912     # any discrepancies.
   1913     if self.IsDownloadable(depot) and revision:
   1914       self.BackupOrRestoreOutputDirectory(restore=True)
   1915 
   1916     # A value other than 0 indicates that the test couldn't be run, and results
   1917     # should also include an error message.
   1918     if results[1] != 0:
   1919       return results
   1920 
   1921     external_revisions = self._Get3rdPartyRevisions(depot)
   1922 
   1923     if not external_revisions is None:
   1924       return (results[0], results[1], external_revisions,
   1925           time.time() - after_build_time, after_build_time -
   1926           start_build_time)
   1927     else:
   1928       return ('Failed to parse DEPS file for external revisions.',
   1929                BUILD_RESULT_FAIL)
   1930 
   1931   def _SyncAllRevisions(self, revisions_to_sync, sync_client):
   1932     """Syncs multiple depots to particular revisions.
   1933 
   1934     Args:
   1935       revisions_to_sync: A list of (depot, revision) pairs to be synced.
   1936       sync_client: Program used to sync, e.g. "gclient", "repo". Can be None.
   1937 
   1938     Returns:
   1939       True if successful, False otherwise.
   1940     """
   1941     for depot, revision in revisions_to_sync:
   1942       self.depot_registry.ChangeToDepotDir(depot)
   1943 
   1944       if sync_client:
   1945         self.PerformPreBuildCleanup()
   1946 
   1947       # When using gclient to sync, you need to specify the depot you
   1948       # want so that all the dependencies sync properly as well.
   1949       # i.e. gclient sync src@<SHA1>
   1950       if sync_client == 'gclient':
   1951         revision = '%s@%s' % (DEPOT_DEPS_NAME[depot]['src'], revision)
   1952 
   1953       sync_success = self.source_control.SyncToRevision(revision, sync_client)
   1954       if not sync_success:
   1955         return False
   1956 
   1957     return True
   1958 
   1959   def _CheckIfRunPassed(self, current_value, known_good_value, known_bad_value):
   1960     """Given known good and bad values, decide if the current_value passed
   1961     or failed.
   1962 
   1963     Args:
   1964       current_value: The value of the metric being checked.
   1965       known_bad_value: The reference value for a "failed" run.
   1966       known_good_value: The reference value for a "passed" run.
   1967 
   1968     Returns:
   1969       True if the current_value is closer to the known_good_value than the
   1970       known_bad_value.
   1971     """
   1972     if self.opts.bisect_mode == BISECT_MODE_STD_DEV:
   1973       dist_to_good_value = abs(current_value['std_dev'] -
   1974           known_good_value['std_dev'])
   1975       dist_to_bad_value = abs(current_value['std_dev'] -
   1976           known_bad_value['std_dev'])
   1977     else:
   1978       dist_to_good_value = abs(current_value['mean'] - known_good_value['mean'])
   1979       dist_to_bad_value = abs(current_value['mean'] - known_bad_value['mean'])
   1980 
   1981     return dist_to_good_value < dist_to_bad_value
   1982 
   1983   def _FillInV8BleedingEdgeInfo(self, min_revision_data, max_revision_data):
   1984     r1 = self._GetNearestV8BleedingEdgeFromTrunk(min_revision_data['revision'],
   1985         search_forward=True)
   1986     r2 = self._GetNearestV8BleedingEdgeFromTrunk(max_revision_data['revision'],
   1987         search_forward=False)
   1988     min_revision_data['external']['v8_bleeding_edge'] = r1
   1989     max_revision_data['external']['v8_bleeding_edge'] = r2
   1990 
   1991     if (not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
   1992             min_revision_data['revision'])
   1993         or not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
   1994             max_revision_data['revision'])):
   1995       self.warnings.append(
   1996           'Trunk revisions in V8 did not map directly to bleeding_edge. '
   1997           'Attempted to expand the range to find V8 rolls which did map '
   1998           'directly to bleeding_edge revisions, but results might not be '
   1999           'valid.')
   2000 
   2001   def _FindNextDepotToBisect(
   2002       self, current_depot, min_revision_data, max_revision_data):
   2003     """Decides which depot the script should dive into next (if any).
   2004 
   2005     Args:
   2006       current_depot: Current depot being bisected.
   2007       min_revision_data: Data about the earliest revision in the bisect range.
   2008       max_revision_data: Data about the latest revision in the bisect range.
   2009 
   2010     Returns:
   2011       Name of the depot to bisect next, or None.
   2012     """
   2013     external_depot = None
   2014     for next_depot in DEPOT_NAMES:
   2015       if DEPOT_DEPS_NAME[next_depot].has_key('platform'):
   2016         if DEPOT_DEPS_NAME[next_depot]['platform'] != os.name:
   2017           continue
   2018 
   2019       if not (DEPOT_DEPS_NAME[next_depot]['recurse']
   2020               and min_revision_data['depot']
   2021               in DEPOT_DEPS_NAME[next_depot]['from']):
   2022         continue
   2023 
   2024       if current_depot == 'v8':
   2025         # We grab the bleeding_edge info here rather than earlier because we
   2026         # finally have the revision range. From that we can search forwards and
   2027         # backwards to try to match trunk revisions to bleeding_edge.
   2028         self._FillInV8BleedingEdgeInfo(min_revision_data, max_revision_data)
   2029 
   2030       if (min_revision_data['external'].get(next_depot) ==
   2031           max_revision_data['external'].get(next_depot)):
   2032         continue
   2033 
   2034       if (min_revision_data['external'].get(next_depot) and
   2035           max_revision_data['external'].get(next_depot)):
   2036         external_depot = next_depot
   2037         break
   2038 
   2039     return external_depot
   2040 
   2041   def PrepareToBisectOnDepot(
   2042       self, current_depot, end_revision, start_revision, previous_revision):
   2043     """Changes to the appropriate directory and gathers a list of revisions
   2044     to bisect between |start_revision| and |end_revision|.
   2045 
   2046     Args:
   2047       current_depot: The depot we want to bisect.
   2048       end_revision: End of the revision range.
   2049       start_revision: Start of the revision range.
   2050       previous_revision: The last revision we synced to on |previous_depot|.
   2051 
   2052     Returns:
   2053       A list containing the revisions between |start_revision| and
   2054       |end_revision| inclusive.
   2055     """
   2056     # Change into working directory of external library to run
   2057     # subsequent commands.
   2058     self.depot_registry.ChangeToDepotDir(current_depot)
   2059 
   2060     # V8 (and possibly others) is merged in periodically. Bisecting
   2061     # this directory directly won't give much good info.
   2062     if DEPOT_DEPS_NAME[current_depot].has_key('custom_deps'):
   2063       config_path = os.path.join(self.src_cwd, '..')
   2064       if bisect_utils.RunGClientAndCreateConfig(self.opts,
   2065           DEPOT_DEPS_NAME[current_depot]['custom_deps'], cwd=config_path):
   2066         return []
   2067       if bisect_utils.RunGClient(
   2068           ['sync', '--revision', previous_revision], cwd=self.src_cwd):
   2069         return []
   2070 
   2071     if current_depot == 'v8_bleeding_edge':
   2072       self.depot_registry.ChangeToDepotDir('chromium')
   2073 
   2074       shutil.move('v8', 'v8.bak')
   2075       shutil.move('v8_bleeding_edge', 'v8')
   2076 
   2077       self.cleanup_commands.append(['mv', 'v8', 'v8_bleeding_edge'])
   2078       self.cleanup_commands.append(['mv', 'v8.bak', 'v8'])
   2079 
   2080       self.depot_registry.AddDepot('v8_bleeding_edge',
   2081                                   os.path.join(self.src_cwd, 'v8'))
   2082       self.depot_registry.AddDepot('v8', os.path.join(self.src_cwd, 'v8.bak'))
   2083 
   2084       self.depot_registry.ChangeToDepotDir(current_depot)
   2085 
   2086     depot_revision_list = self.GetRevisionList(current_depot,
   2087                                                end_revision,
   2088                                                start_revision)
   2089 
   2090     self.depot_registry.ChangeToDepotDir('chromium')
   2091 
   2092     return depot_revision_list
   2093 
   2094   def GatherReferenceValues(self, good_rev, bad_rev, cmd, metric, target_depot):
   2095     """Gathers reference values by running the performance tests on the
   2096     known good and bad revisions.
   2097 
   2098     Args:
   2099       good_rev: The last known good revision where the performance regression
   2100         has not occurred yet.
   2101       bad_rev: A revision where the performance regression has already occurred.
   2102       cmd: The command to execute the performance test.
   2103       metric: The metric being tested for regression.
   2104 
   2105     Returns:
   2106       A tuple with the results of building and running each revision.
   2107     """
   2108     bad_run_results = self.RunTest(bad_rev, target_depot, cmd, metric)
   2109 
   2110     good_run_results = None
   2111 
   2112     if not bad_run_results[1]:
   2113       good_run_results = self.RunTest(good_rev, target_depot, cmd, metric)
   2114 
   2115     return (bad_run_results, good_run_results)
   2116 
   2117   def PrintRevisionsToBisectMessage(self, revision_list, depot):
   2118     if self.opts.output_buildbot_annotations:
   2119       step_name = 'Bisection Range: [%s - %s]' % (
   2120           revision_list[len(revision_list)-1], revision_list[0])
   2121       bisect_utils.OutputAnnotationStepStart(step_name)
   2122 
   2123     print
   2124     print 'Revisions to bisect on [%s]:' % depot
   2125     for revision_id in revision_list:
   2126       print '  -> %s' % (revision_id, )
   2127     print
   2128 
   2129     if self.opts.output_buildbot_annotations:
   2130       bisect_utils.OutputAnnotationStepClosed()
   2131 
   2132   def NudgeRevisionsIfDEPSChange(self, bad_revision, good_revision,
   2133                                  good_svn_revision=None):
   2134     """Checks to see if changes to DEPS file occurred, and that the revision
   2135     range also includes the change to .DEPS.git. If it doesn't, attempts to
   2136     expand the revision range to include it.
   2137 
   2138     Args:
   2139       bad_revision: First known bad git revision.
   2140       good_revision: Last known good git revision.
   2141       good_svn_revision: Last known good svn revision.
   2142 
   2143     Returns:
   2144       A tuple with the new bad and good revisions.
   2145     """
   2146     # DONOT perform nudge because at revision 291563 .DEPS.git was removed
   2147     # and source contain only DEPS file for dependency changes.
   2148     if good_svn_revision >= 291563:
   2149       return (bad_revision, good_revision)
   2150 
   2151     if self.source_control.IsGit() and self.opts.target_platform == 'chromium':
   2152       changes_to_deps = self.source_control.QueryFileRevisionHistory(
   2153           bisect_utils.FILE_DEPS, good_revision, bad_revision)
   2154 
   2155       if changes_to_deps:
   2156         # DEPS file was changed, search from the oldest change to DEPS file to
   2157         # bad_revision to see if there are matching .DEPS.git changes.
   2158         oldest_deps_change = changes_to_deps[-1]
   2159         changes_to_gitdeps = self.source_control.QueryFileRevisionHistory(
   2160             bisect_utils.FILE_DEPS_GIT, oldest_deps_change, bad_revision)
   2161 
   2162         if len(changes_to_deps) != len(changes_to_gitdeps):
   2163           # Grab the timestamp of the last DEPS change
   2164           cmd = ['log', '--format=%ct', '-1', changes_to_deps[0]]
   2165           output = bisect_utils.CheckRunGit(cmd)
   2166           commit_time = int(output)
   2167 
   2168           # Try looking for a commit that touches the .DEPS.git file in the
   2169           # next 15 minutes after the DEPS file change.
   2170           cmd = ['log', '--format=%H', '-1',
   2171               '--before=%d' % (commit_time + 900), '--after=%d' % commit_time,
   2172               'origin/master', '--', bisect_utils.FILE_DEPS_GIT]
   2173           output = bisect_utils.CheckRunGit(cmd)
   2174           output = output.strip()
   2175           if output:
   2176             self.warnings.append('Detected change to DEPS and modified '
   2177                 'revision range to include change to .DEPS.git')
   2178             return (output, good_revision)
   2179           else:
   2180             self.warnings.append('Detected change to DEPS but couldn\'t find '
   2181                 'matching change to .DEPS.git')
   2182     return (bad_revision, good_revision)
   2183 
   2184   def CheckIfRevisionsInProperOrder(
   2185       self, target_depot, good_revision, bad_revision):
   2186     """Checks that |good_revision| is an earlier revision than |bad_revision|.
   2187 
   2188     Args:
   2189       good_revision: Number/tag of the known good revision.
   2190       bad_revision: Number/tag of the known bad revision.
   2191 
   2192     Returns:
   2193       True if the revisions are in the proper order (good earlier than bad).
   2194     """
   2195     if self.source_control.IsGit() and target_depot != 'cros':
   2196       cwd = self.depot_registry.GetDepotDir(target_depot)
   2197 
   2198       cmd = ['log', '--format=%ct', '-1', good_revision]
   2199       output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
   2200       good_commit_time = int(output)
   2201 
   2202       cmd = ['log', '--format=%ct', '-1', bad_revision]
   2203       output = bisect_utils.CheckRunGit(cmd, cwd=cwd)
   2204       bad_commit_time = int(output)
   2205 
   2206       return good_commit_time <= bad_commit_time
   2207     else:
   2208       # CrOS and SVN use integers.
   2209       return int(good_revision) <= int(bad_revision)
   2210 
   2211   def CanPerformBisect(self, good_revision, bad_revision):
   2212     """Checks whether a given revision is bisectable.
   2213 
   2214     Checks for following:
   2215     1. Non-bisectable revsions for android bots (refer to crbug.com/385324).
   2216     2. Non-bisectable revsions for Windows bots (refer to crbug.com/405274).
   2217 
   2218     Args:
   2219       good_revision: Known good revision.
   2220       bad_revision: Known bad revision.
   2221 
   2222     Returns:
   2223       A dictionary indicating the result. If revision is not bisectable,
   2224       this will contain the field "error", otherwise None.
   2225     """
   2226     if self.opts.target_platform == 'android':
   2227       good_revision = self.source_control.GetCommitPosition(good_revision)
   2228       if (bisect_utils.IsStringInt(good_revision)
   2229           and good_revision < 265549):
   2230         return {'error': (
   2231             'Bisect cannot continue for the given revision range.\n'
   2232             'It is impossible to bisect Android regressions '
   2233             'prior to r265549, which allows the bisect bot to '
   2234             'rely on Telemetry to do apk installation of the most recently '
   2235             'built local ChromeShell(refer to crbug.com/385324).\n'
   2236             'Please try bisecting revisions greater than or equal to r265549.')}
   2237 
   2238     if bisect_utils.IsWindowsHost():
   2239       good_revision = self.source_control.GetCommitPosition(good_revision)
   2240       bad_revision = self.source_control.GetCommitPosition(bad_revision)
   2241       if (bisect_utils.IsStringInt(good_revision) and
   2242           bisect_utils.IsStringInt(bad_revision)):
   2243         if (289987 <= good_revision < 290716 or
   2244             289987 <= bad_revision < 290716):
   2245           return {'error': ('Oops! Revision between r289987 and r290716 are '
   2246                             'marked as dead zone for Windows due to '
   2247                             'crbug.com/405274. Please try another range.')}
   2248 
   2249     return None
   2250 
   2251   def Run(self, command_to_run, bad_revision_in, good_revision_in, metric):
   2252     """Given known good and bad revisions, run a binary search on all
   2253     intermediate revisions to determine the CL where the performance regression
   2254     occurred.
   2255 
   2256     Args:
   2257       command_to_run: Specify the command to execute the performance test.
   2258       good_revision: Number/tag of the known good revision.
   2259       bad_revision: Number/tag of the known bad revision.
   2260       metric: The performance metric to monitor.
   2261 
   2262     Returns:
   2263       A BisectResults object.
   2264     """
   2265     results = BisectResults(self.depot_registry, self.source_control)
   2266 
   2267     # Choose depot to bisect first
   2268     target_depot = 'chromium'
   2269     if self.opts.target_platform == 'cros':
   2270       target_depot = 'cros'
   2271     elif self.opts.target_platform == 'android-chrome':
   2272       target_depot = 'android-chrome'
   2273 
   2274     cwd = os.getcwd()
   2275     self.depot_registry.ChangeToDepotDir(target_depot)
   2276 
   2277     # If they passed SVN revisions, we can try match them to git SHA1 hashes.
   2278     bad_revision = self.source_control.ResolveToRevision(
   2279         bad_revision_in, target_depot, DEPOT_DEPS_NAME, 100)
   2280     good_revision = self.source_control.ResolveToRevision(
   2281         good_revision_in, target_depot, DEPOT_DEPS_NAME, -100)
   2282 
   2283     os.chdir(cwd)
   2284     if bad_revision is None:
   2285       results.error = 'Couldn\'t resolve [%s] to SHA1.' % bad_revision_in
   2286       return results
   2287 
   2288     if good_revision is None:
   2289       results.error = 'Couldn\'t resolve [%s] to SHA1.' % good_revision_in
   2290       return results
   2291 
   2292     # Check that they didn't accidentally swap good and bad revisions.
   2293     if not self.CheckIfRevisionsInProperOrder(
   2294         target_depot, good_revision, bad_revision):
   2295       results.error = ('bad_revision < good_revision, did you swap these '
   2296                        'by mistake?')
   2297       return results
   2298     bad_revision, good_revision = self.NudgeRevisionsIfDEPSChange(
   2299         bad_revision, good_revision, good_revision_in)
   2300     if self.opts.output_buildbot_annotations:
   2301       bisect_utils.OutputAnnotationStepStart('Gathering Revisions')
   2302 
   2303     cannot_bisect = self.CanPerformBisect(good_revision, bad_revision)
   2304     if cannot_bisect:
   2305       results.error = cannot_bisect.get('error')
   2306       return results
   2307 
   2308     print 'Gathering revision range for bisection.'
   2309     # Retrieve a list of revisions to do bisection on.
   2310     src_revision_list = self.GetRevisionList(
   2311         target_depot, bad_revision, good_revision)
   2312 
   2313     if self.opts.output_buildbot_annotations:
   2314       bisect_utils.OutputAnnotationStepClosed()
   2315 
   2316     if src_revision_list:
   2317       # revision_data will store information about a revision such as the
   2318       # depot it came from, the webkit/V8 revision at that time,
   2319       # performance timing, build state, etc...
   2320       revision_data = results.revision_data
   2321 
   2322       # revision_list is the list we're binary searching through at the moment.
   2323       revision_list = []
   2324 
   2325       sort_key_ids = 0
   2326 
   2327       for current_revision_id in src_revision_list:
   2328         sort_key_ids += 1
   2329 
   2330         revision_data[current_revision_id] = {
   2331             'value' : None,
   2332             'passed' : '?',
   2333             'depot' : target_depot,
   2334             'external' : None,
   2335             'perf_time' : 0,
   2336             'build_time' : 0,
   2337             'sort' : sort_key_ids,
   2338         }
   2339         revision_list.append(current_revision_id)
   2340 
   2341       min_revision = 0
   2342       max_revision = len(revision_list) - 1
   2343 
   2344       self.PrintRevisionsToBisectMessage(revision_list, target_depot)
   2345 
   2346       if self.opts.output_buildbot_annotations:
   2347         bisect_utils.OutputAnnotationStepStart('Gathering Reference Values')
   2348 
   2349       print 'Gathering reference values for bisection.'
   2350 
   2351       # Perform the performance tests on the good and bad revisions, to get
   2352       # reference values.
   2353       bad_results, good_results = self.GatherReferenceValues(good_revision,
   2354                                                                bad_revision,
   2355                                                                command_to_run,
   2356                                                                metric,
   2357                                                                target_depot)
   2358 
   2359       if self.opts.output_buildbot_annotations:
   2360         bisect_utils.OutputAnnotationStepClosed()
   2361 
   2362       if bad_results[1]:
   2363         results.error = ('An error occurred while building and running '
   2364             'the \'bad\' reference value. The bisect cannot continue without '
   2365             'a working \'bad\' revision to start from.\n\nError: %s' %
   2366             bad_results[0])
   2367         return results
   2368 
   2369       if good_results[1]:
   2370         results.error = ('An error occurred while building and running '
   2371             'the \'good\' reference value. The bisect cannot continue without '
   2372             'a working \'good\' revision to start from.\n\nError: %s' %
   2373             good_results[0])
   2374         return results
   2375 
   2376 
   2377       # We need these reference values to determine if later runs should be
   2378       # classified as pass or fail.
   2379       known_bad_value = bad_results[0]
   2380       known_good_value = good_results[0]
   2381 
   2382       # Can just mark the good and bad revisions explicitly here since we
   2383       # already know the results.
   2384       bad_revision_data = revision_data[revision_list[0]]
   2385       bad_revision_data['external'] = bad_results[2]
   2386       bad_revision_data['perf_time'] = bad_results[3]
   2387       bad_revision_data['build_time'] = bad_results[4]
   2388       bad_revision_data['passed'] = False
   2389       bad_revision_data['value'] = known_bad_value
   2390 
   2391       good_revision_data = revision_data[revision_list[max_revision]]
   2392       good_revision_data['external'] = good_results[2]
   2393       good_revision_data['perf_time'] = good_results[3]
   2394       good_revision_data['build_time'] = good_results[4]
   2395       good_revision_data['passed'] = True
   2396       good_revision_data['value'] = known_good_value
   2397 
   2398       next_revision_depot = target_depot
   2399 
   2400       while True:
   2401         if not revision_list:
   2402           break
   2403 
   2404         min_revision_data = revision_data[revision_list[min_revision]]
   2405         max_revision_data = revision_data[revision_list[max_revision]]
   2406 
   2407         if max_revision - min_revision <= 1:
   2408           current_depot = min_revision_data['depot']
   2409           if min_revision_data['passed'] == '?':
   2410             next_revision_index = min_revision
   2411           elif max_revision_data['passed'] == '?':
   2412             next_revision_index = max_revision
   2413           elif current_depot in ['android-chrome', 'cros', 'chromium', 'v8']:
   2414             previous_revision = revision_list[min_revision]
   2415             # If there were changes to any of the external libraries we track,
   2416             # should bisect the changes there as well.
   2417             external_depot = self._FindNextDepotToBisect(
   2418                 current_depot, min_revision_data, max_revision_data)
   2419             # If there was no change in any of the external depots, the search
   2420             # is over.
   2421             if not external_depot:
   2422               if current_depot == 'v8':
   2423                 self.warnings.append('Unfortunately, V8 bisection couldn\'t '
   2424                     'continue any further. The script can only bisect into '
   2425                     'V8\'s bleeding_edge repository if both the current and '
   2426                     'previous revisions in trunk map directly to revisions in '
   2427                     'bleeding_edge.')
   2428               break
   2429 
   2430             earliest_revision = max_revision_data['external'][external_depot]
   2431             latest_revision = min_revision_data['external'][external_depot]
   2432 
   2433             new_revision_list = self.PrepareToBisectOnDepot(
   2434                 external_depot, latest_revision, earliest_revision,
   2435                 previous_revision)
   2436 
   2437             if not new_revision_list:
   2438               results.error = ('An error occurred attempting to retrieve '
   2439                                'revision range: [%s..%s]' %
   2440                                (earliest_revision, latest_revision))
   2441               return results
   2442 
   2443             _AddRevisionsIntoRevisionData(
   2444                 new_revision_list, external_depot, min_revision_data['sort'],
   2445                 revision_data)
   2446 
   2447             # Reset the bisection and perform it on the newly inserted
   2448             # changelists.
   2449             revision_list = new_revision_list
   2450             min_revision = 0
   2451             max_revision = len(revision_list) - 1
   2452             sort_key_ids += len(revision_list)
   2453 
   2454             print ('Regression in metric %s appears to be the result of '
   2455                    'changes in [%s].' % (metric, external_depot))
   2456 
   2457             self.PrintRevisionsToBisectMessage(revision_list, external_depot)
   2458 
   2459             continue
   2460           else:
   2461             break
   2462         else:
   2463           next_revision_index = (int((max_revision - min_revision) / 2) +
   2464                                  min_revision)
   2465 
   2466         next_revision_id = revision_list[next_revision_index]
   2467         next_revision_data = revision_data[next_revision_id]
   2468         next_revision_depot = next_revision_data['depot']
   2469 
   2470         self.depot_registry.ChangeToDepotDir(next_revision_depot)
   2471 
   2472         if self.opts.output_buildbot_annotations:
   2473           step_name = 'Working on [%s]' % next_revision_id
   2474           bisect_utils.OutputAnnotationStepStart(step_name)
   2475 
   2476         print 'Working on revision: [%s]' % next_revision_id
   2477 
   2478         run_results = self.RunTest(
   2479             next_revision_id, next_revision_depot, command_to_run, metric,
   2480             skippable=True)
   2481 
   2482         # If the build is successful, check whether or not the metric
   2483         # had regressed.
   2484         if not run_results[1]:
   2485           if len(run_results) > 2:
   2486             next_revision_data['external'] = run_results[2]
   2487             next_revision_data['perf_time'] = run_results[3]
   2488             next_revision_data['build_time'] = run_results[4]
   2489 
   2490           passed_regression = self._CheckIfRunPassed(run_results[0],
   2491                                                      known_good_value,
   2492                                                      known_bad_value)
   2493 
   2494           next_revision_data['passed'] = passed_regression
   2495           next_revision_data['value'] = run_results[0]
   2496 
   2497           if passed_regression:
   2498             max_revision = next_revision_index
   2499           else:
   2500             min_revision = next_revision_index
   2501         else:
   2502           if run_results[1] == BUILD_RESULT_SKIPPED:
   2503             next_revision_data['passed'] = 'Skipped'
   2504           elif run_results[1] == BUILD_RESULT_FAIL:
   2505             next_revision_data['passed'] = 'Build Failed'
   2506 
   2507           print run_results[0]
   2508 
   2509           # If the build is broken, remove it and redo search.
   2510           revision_list.pop(next_revision_index)
   2511 
   2512           max_revision -= 1
   2513 
   2514         if self.opts.output_buildbot_annotations:
   2515           self._PrintPartialResults(results)
   2516           bisect_utils.OutputAnnotationStepClosed()
   2517     else:
   2518       # Weren't able to sync and retrieve the revision range.
   2519       results.error = ('An error occurred attempting to retrieve revision '
   2520                        'range: [%s..%s]' % (good_revision, bad_revision))
   2521 
   2522     return results
   2523 
   2524   def _PrintPartialResults(self, results):
   2525     results_dict = results.GetResultsDict()
   2526     self._PrintTestedCommitsTable(results_dict['revision_data_sorted'],
   2527                                   results_dict['first_working_revision'],
   2528                                   results_dict['last_broken_revision'],
   2529                                   100, final_step=False)
   2530 
   2531   def _ConfidenceLevelStatus(self, results_dict):
   2532     if not results_dict['confidence']:
   2533       return None
   2534     confidence_status = 'Successful with %(level)s confidence%(warning)s.'
   2535     if results_dict['confidence'] >= HIGH_CONFIDENCE:
   2536       level = 'high'
   2537     else:
   2538       level = 'low'
   2539     warning = ' and warnings'
   2540     if not self.warnings:
   2541       warning = ''
   2542     return confidence_status % {'level': level, 'warning': warning}
   2543 
   2544   def _GetViewVCLinkFromDepotAndHash(self, cl, depot):
   2545     info = self.source_control.QueryRevisionInfo(cl,
   2546         self.depot_registry.GetDepotDir(depot))
   2547     if depot and DEPOT_DEPS_NAME[depot].has_key('viewvc'):
   2548       try:
   2549         # Format is "git-svn-id: svn://....@123456 <other data>"
   2550         svn_line = [i for i in info['body'].splitlines() if 'git-svn-id:' in i]
   2551         svn_revision = svn_line[0].split('@')
   2552         svn_revision = svn_revision[1].split(' ')[0]
   2553         return DEPOT_DEPS_NAME[depot]['viewvc'] + svn_revision
   2554       except IndexError:
   2555         return ''
   2556     return ''
   2557 
   2558   def _PrintRevisionInfo(self, cl, info, depot=None):
   2559     email_info = ''
   2560     if not info['email'].startswith(info['author']):
   2561       email_info = '\nEmail   : %s' % info['email']
   2562     commit_link = self._GetViewVCLinkFromDepotAndHash(cl, depot)
   2563     if commit_link:
   2564       commit_info = '\nLink    : %s' % commit_link
   2565     else:
   2566       commit_info = ('\nFailed to parse SVN revision from body:\n%s' %
   2567                      info['body'])
   2568     print RESULTS_REVISION_INFO % {
   2569         'subject': info['subject'],
   2570         'author': info['author'],
   2571         'email_info': email_info,
   2572         'commit_info': commit_info,
   2573         'cl': cl,
   2574         'cl_date': info['date']
   2575     }
   2576 
   2577   def _PrintTestedCommitsHeader(self):
   2578     if self.opts.bisect_mode == BISECT_MODE_MEAN:
   2579       _PrintTableRow(
   2580           [20, 70, 14, 12, 13],
   2581           ['Depot', 'Commit SHA', 'Mean', 'Std. Error', 'State'])
   2582     elif self.opts.bisect_mode == BISECT_MODE_STD_DEV:
   2583       _PrintTableRow(
   2584           [20, 70, 14, 12, 13],
   2585           ['Depot', 'Commit SHA', 'Std. Error', 'Mean', 'State'])
   2586     elif self.opts.bisect_mode == BISECT_MODE_RETURN_CODE:
   2587       _PrintTableRow(
   2588           [20, 70, 14, 13],
   2589           ['Depot', 'Commit SHA', 'Return Code', 'State'])
   2590     else:
   2591       assert False, 'Invalid bisect_mode specified.'
   2592 
   2593   def _PrintTestedCommitsEntry(self, current_data, cl_link, state_str):
   2594     if self.opts.bisect_mode == BISECT_MODE_MEAN:
   2595       std_error = '+-%.02f' % current_data['value']['std_err']
   2596       mean = '%.02f' % current_data['value']['mean']
   2597       _PrintTableRow(
   2598           [20, 70, 12, 14, 13],
   2599           [current_data['depot'], cl_link, mean, std_error, state_str])
   2600     elif self.opts.bisect_mode == BISECT_MODE_STD_DEV:
   2601       std_error = '+-%.02f' % current_data['value']['std_err']
   2602       mean = '%.02f' % current_data['value']['mean']
   2603       _PrintTableRow(
   2604           [20, 70, 12, 14, 13],
   2605           [current_data['depot'], cl_link, std_error, mean, state_str])
   2606     elif self.opts.bisect_mode == BISECT_MODE_RETURN_CODE:
   2607       mean = '%d' % current_data['value']['mean']
   2608       _PrintTableRow(
   2609           [20, 70, 14, 13],
   2610           [current_data['depot'], cl_link, mean, state_str])
   2611 
   2612   def _PrintTestedCommitsTable(
   2613       self, revision_data_sorted, first_working_revision, last_broken_revision,
   2614       confidence, final_step=True):
   2615     print
   2616     if final_step:
   2617       print '===== TESTED COMMITS ====='
   2618     else:
   2619       print '===== PARTIAL RESULTS ====='
   2620     self._PrintTestedCommitsHeader()
   2621     state = 0
   2622     for current_id, current_data in revision_data_sorted:
   2623       if current_data['value']:
   2624         if (current_id == last_broken_revision or
   2625             current_id == first_working_revision):
   2626           # If confidence is too low, don't add this empty line since it's
   2627           # used to put focus on a suspected CL.
   2628           if confidence and final_step:
   2629             print
   2630           state += 1
   2631           if state == 2 and not final_step:
   2632             # Just want a separation between "bad" and "good" cl's.
   2633             print
   2634 
   2635         state_str = 'Bad'
   2636         if state == 1 and final_step:
   2637           state_str = 'Suspected CL'
   2638         elif state == 2:
   2639           state_str = 'Good'
   2640 
   2641         # If confidence is too low, don't bother outputting good/bad.
   2642         if not confidence:
   2643           state_str = ''
   2644         state_str = state_str.center(13, ' ')
   2645 
   2646         cl_link = self._GetViewVCLinkFromDepotAndHash(current_id,
   2647             current_data['depot'])
   2648         if not cl_link:
   2649           cl_link = current_id
   2650         self._PrintTestedCommitsEntry(current_data, cl_link, state_str)
   2651 
   2652   def _PrintReproSteps(self):
   2653     """Prints out a section of the results explaining how to run the test.
   2654 
   2655     This message includes the command used to run the test.
   2656     """
   2657     command = '$ ' + self.opts.command
   2658     if bisect_utils.IsTelemetryCommand(self.opts.command):
   2659       command += ('\nAlso consider passing --profiler=list to see available '
   2660                   'profilers.')
   2661     print REPRO_STEPS_LOCAL
   2662     if bisect_utils.IsTelemetryCommand(self.opts.command):
   2663       telemetry_command = re.sub(r'--browser=[^\s]+',
   2664                                  '--browser=<bot-name>',
   2665                                  command)
   2666       print REPRO_STEPS_TRYJOB_TELEMETRY % {'command': telemetry_command}
   2667     else:
   2668       print REPRO_STEPS_TRYJOB
   2669 
   2670   def _PrintOtherRegressions(self, other_regressions, revision_data):
   2671     """Prints a section of the results about other potential regressions."""
   2672     print
   2673     print 'Other regressions may have occurred:'
   2674     print '  %8s  %70s  %10s' % ('Depot'.center(8, ' '),
   2675         'Range'.center(70, ' '), 'Confidence'.center(10, ' '))
   2676     for regression in other_regressions:
   2677       current_id, previous_id, confidence = regression
   2678       current_data = revision_data[current_id]
   2679       previous_data = revision_data[previous_id]
   2680 
   2681       current_link = self._GetViewVCLinkFromDepotAndHash(current_id,
   2682           current_data['depot'])
   2683       previous_link = self._GetViewVCLinkFromDepotAndHash(previous_id,
   2684           previous_data['depot'])
   2685 
   2686       # If we can't map it to a viewable URL, at least show the original hash.
   2687       if not current_link:
   2688         current_link = current_id
   2689       if not previous_link:
   2690         previous_link = previous_id
   2691 
   2692       print '  %8s  %70s %s' % (
   2693           current_data['depot'], current_link,
   2694           ('%d%%' % confidence).center(10, ' '))
   2695       print '  %8s  %70s' % (
   2696           previous_data['depot'], previous_link)
   2697       print
   2698 
   2699   def _CheckForWarnings(self, results_dict):
   2700     if len(results_dict['culprit_revisions']) > 1:
   2701       self.warnings.append('Due to build errors, regression range could '
   2702                            'not be narrowed down to a single commit.')
   2703     if self.opts.repeat_test_count == 1:
   2704       self.warnings.append('Tests were only set to run once. This may '
   2705                            'be insufficient to get meaningful results.')
   2706     if 0 < results_dict['confidence'] < HIGH_CONFIDENCE:
   2707       self.warnings.append('Confidence is not high. Try bisecting again '
   2708                            'with increased repeat_count, larger range, or '
   2709                            'on another metric.')
   2710     if not results_dict['confidence']:
   2711       self.warnings.append('Confidence score is 0%. Try bisecting again on '
   2712                            'another platform or another metric.')
   2713 
   2714   def FormatAndPrintResults(self, bisect_results):
   2715     """Prints the results from a bisection run in a readable format.
   2716 
   2717     Args:
   2718       bisect_results: The results from a bisection test run.
   2719     """
   2720     results_dict = bisect_results.GetResultsDict()
   2721 
   2722     self._CheckForWarnings(results_dict)
   2723 
   2724     if self.opts.output_buildbot_annotations:
   2725       bisect_utils.OutputAnnotationStepStart('Build Status Per Revision')
   2726 
   2727     print
   2728     print 'Full results of bisection:'
   2729     for current_id, current_data  in results_dict['revision_data_sorted']:
   2730       build_status = current_data['passed']
   2731 
   2732       if type(build_status) is bool:
   2733         if build_status:
   2734           build_status = 'Good'
   2735         else:
   2736           build_status = 'Bad'
   2737 
   2738       print '  %20s  %40s  %s' % (current_data['depot'],
   2739                                   current_id, build_status)
   2740     print
   2741 
   2742     if self.opts.output_buildbot_annotations:
   2743       bisect_utils.OutputAnnotationStepClosed()
   2744       # The perf dashboard scrapes the "results" step in order to comment on
   2745       # bugs. If you change this, please update the perf dashboard as well.
   2746       bisect_utils.OutputAnnotationStepStart('Results')
   2747 
   2748     self._PrintBanner(results_dict)
   2749     self._PrintWarnings()
   2750 
   2751     if results_dict['culprit_revisions'] and results_dict['confidence']:
   2752       for culprit in results_dict['culprit_revisions']:
   2753         cl, info, depot = culprit
   2754         self._PrintRevisionInfo(cl, info, depot)
   2755       if results_dict['other_regressions']:
   2756         self._PrintOtherRegressions(results_dict['other_regressions'],
   2757                                     results_dict['revision_data'])
   2758     self._PrintTestedCommitsTable(results_dict['revision_data_sorted'],
   2759                                   results_dict['first_working_revision'],
   2760                                   results_dict['last_broken_revision'],
   2761                                   results_dict['confidence'])
   2762     _PrintStepTime(results_dict['revision_data_sorted'])
   2763     self._PrintReproSteps()
   2764     _PrintThankYou()
   2765     if self.opts.output_buildbot_annotations:
   2766       bisect_utils.OutputAnnotationStepClosed()
   2767 
   2768   def _PrintBanner(self, results_dict):
   2769     if self._IsBisectModeReturnCode():
   2770       metrics = 'N/A'
   2771       change = 'Yes'
   2772     else:
   2773       metrics = '/'.join(self.opts.metric)
   2774       change = '%.02f%% (+/-%.02f%%)' % (
   2775           results_dict['regression_size'], results_dict['regression_std_err'])
   2776 
   2777     if results_dict['culprit_revisions'] and results_dict['confidence']:
   2778       status = self._ConfidenceLevelStatus(results_dict)
   2779     else:
   2780       status = 'Failure, could not reproduce.'
   2781       change = 'Bisect could not reproduce a change.'
   2782 
   2783     print RESULTS_BANNER % {
   2784         'status': status,
   2785         'command': self.opts.command,
   2786         'metrics': metrics,
   2787         'change': change,
   2788         'confidence': results_dict['confidence'],
   2789     }
   2790 
   2791   def _PrintWarnings(self):
   2792     """Prints a list of warning strings if there are any."""
   2793     if not self.warnings:
   2794       return
   2795     print
   2796     print 'WARNINGS:'
   2797     for w in set(self.warnings):
   2798       print '  ! %s' % w
   2799 
   2800 
   2801 def _IsPlatformSupported():
   2802   """Checks that this platform and build system are supported.
   2803 
   2804   Args:
   2805     opts: The options parsed from the command line.
   2806 
   2807   Returns:
   2808     True if the platform and build system are supported.
   2809   """
   2810   # Haven't tested the script out on any other platforms yet.
   2811   supported = ['posix', 'nt']
   2812   return os.name in supported
   2813 
   2814 
   2815 def RmTreeAndMkDir(path_to_dir, skip_makedir=False):
   2816   """Removes the directory tree specified, and then creates an empty
   2817   directory in the same location (if not specified to skip).
   2818 
   2819   Args:
   2820     path_to_dir: Path to the directory tree.
   2821     skip_makedir: Whether to skip creating empty directory, default is False.
   2822 
   2823   Returns:
   2824     True if successful, False if an error occurred.
   2825   """
   2826   try:
   2827     if os.path.exists(path_to_dir):
   2828       shutil.rmtree(path_to_dir)
   2829   except OSError, e:
   2830     if e.errno != errno.ENOENT:
   2831       return False
   2832 
   2833   if not skip_makedir:
   2834     return MaybeMakeDirectory(path_to_dir)
   2835 
   2836   return True
   2837 
   2838 
   2839 def RemoveBuildFiles(build_type):
   2840   """Removes build files from previous runs."""
   2841   if RmTreeAndMkDir(os.path.join('out', build_type)):
   2842     if RmTreeAndMkDir(os.path.join('build', build_type)):
   2843       return True
   2844   return False
   2845 
   2846 
   2847 class BisectOptions(object):
   2848   """Options to be used when running bisection."""
   2849   def __init__(self):
   2850     super(BisectOptions, self).__init__()
   2851 
   2852     self.target_platform = 'chromium'
   2853     self.build_preference = None
   2854     self.good_revision = None
   2855     self.bad_revision = None
   2856     self.use_goma = None
   2857     self.goma_dir = None
   2858     self.cros_board = None
   2859     self.cros_remote_ip = None
   2860     self.repeat_test_count = 20
   2861     self.truncate_percent = 25
   2862     self.max_time_minutes = 20
   2863     self.metric = None
   2864     self.command = None
   2865     self.output_buildbot_annotations = None
   2866     self.no_custom_deps = False
   2867     self.working_directory = None
   2868     self.extra_src = None
   2869     self.debug_ignore_build = None
   2870     self.debug_ignore_sync = None
   2871     self.debug_ignore_perf_test = None
   2872     self.gs_bucket = None
   2873     self.target_arch = 'ia32'
   2874     self.target_build_type = 'Release'
   2875     self.builder_host = None
   2876     self.builder_port = None
   2877     self.bisect_mode = BISECT_MODE_MEAN
   2878 
   2879   @staticmethod
   2880   def _CreateCommandLineParser():
   2881     """Creates a parser with bisect options.
   2882 
   2883     Returns:
   2884       An instance of optparse.OptionParser.
   2885     """
   2886     usage = ('%prog [options] [-- chromium-options]\n'
   2887              'Perform binary search on revision history to find a minimal '
   2888              'range of revisions where a performance metric regressed.\n')
   2889 
   2890     parser = optparse.OptionParser(usage=usage)
   2891 
   2892     group = optparse.OptionGroup(parser, 'Bisect options')
   2893     group.add_option('-c', '--command',
   2894                      type='str',
   2895                      help='A command to execute your performance test at' +
   2896                      ' each point in the bisection.')
   2897     group.add_option('-b', '--bad_revision',
   2898                      type='str',
   2899                      help='A bad revision to start bisection. ' +
   2900                      'Must be later than good revision. May be either a git' +
   2901                      ' or svn revision.')
   2902     group.add_option('-g', '--good_revision',
   2903                      type='str',
   2904                      help='A revision to start bisection where performance' +
   2905                      ' test is known to pass. Must be earlier than the ' +
   2906                      'bad revision. May be either a git or svn revision.')
   2907     group.add_option('-m', '--metric',
   2908                      type='str',
   2909                      help='The desired metric to bisect on. For example ' +
   2910                      '"vm_rss_final_b/vm_rss_f_b"')
   2911     group.add_option('-r', '--repeat_test_count',
   2912                      type='int',
   2913                      default=20,
   2914                      help='The number of times to repeat the performance '
   2915                      'test. Values will be clamped to range [1, 100]. '
   2916                      'Default value is 20.')
   2917     group.add_option('--max_time_minutes',
   2918                      type='int',
   2919                      default=20,
   2920                      help='The maximum time (in minutes) to take running the '
   2921                      'performance tests. The script will run the performance '
   2922                      'tests according to --repeat_test_count, so long as it '
   2923                      'doesn\'t exceed --max_time_minutes. Values will be '
   2924                      'clamped to range [1, 60].'
   2925                      'Default value is 20.')
   2926     group.add_option('-t', '--truncate_percent',
   2927                      type='int',
   2928                      default=25,
   2929                      help='The highest/lowest % are discarded to form a '
   2930                      'truncated mean. Values will be clamped to range [0, '
   2931                      '25]. Default value is 25 (highest/lowest 25% will be '
   2932                      'discarded).')
   2933     group.add_option('--bisect_mode',
   2934                      type='choice',
   2935                      choices=[BISECT_MODE_MEAN, BISECT_MODE_STD_DEV,
   2936                         BISECT_MODE_RETURN_CODE],
   2937                      default=BISECT_MODE_MEAN,
   2938                      help='The bisect mode. Choices are to bisect on the '
   2939                      'difference in mean, std_dev, or return_code.')
   2940     parser.add_option_group(group)
   2941 
   2942     group = optparse.OptionGroup(parser, 'Build options')
   2943     group.add_option('-w', '--working_directory',
   2944                      type='str',
   2945                      help='Path to the working directory where the script '
   2946                      'will do an initial checkout of the chromium depot. The '
   2947                      'files will be placed in a subdirectory "bisect" under '
   2948                      'working_directory and that will be used to perform the '
   2949                      'bisection. This parameter is optional, if it is not '
   2950                      'supplied, the script will work from the current depot.')
   2951     group.add_option('--build_preference',
   2952                      type='choice',
   2953                      choices=['msvs', 'ninja', 'make'],
   2954                      help='The preferred build system to use. On linux/mac '
   2955                      'the options are make/ninja. On Windows, the options '
   2956                      'are msvs/ninja.')
   2957     group.add_option('--target_platform',
   2958                      type='choice',
   2959                      choices=['chromium', 'cros', 'android', 'android-chrome'],
   2960                      default='chromium',
   2961                      help='The target platform. Choices are "chromium" '
   2962                      '(current platform), "cros", or "android". If you '
   2963                      'specify something other than "chromium", you must be '
   2964                      'properly set up to build that platform.')
   2965     group.add_option('--no_custom_deps',
   2966                      dest='no_custom_deps',
   2967                      action='store_true',
   2968                      default=False,
   2969                      help='Run the script with custom_deps or not.')
   2970     group.add_option('--extra_src',
   2971                      type='str',
   2972                      help='Path to a script which can be used to modify '
   2973                      'the bisect script\'s behavior.')
   2974     group.add_option('--cros_board',
   2975                      type='str',
   2976                      help='The cros board type to build.')
   2977     group.add_option('--cros_remote_ip',
   2978                      type='str',
   2979                      help='The remote machine to image to.')
   2980     group.add_option('--use_goma',
   2981                      action='store_true',
   2982                      help='Add a bunch of extra threads for goma, and enable '
   2983                      'goma')
   2984     group.add_option('--goma_dir',
   2985                      help='Path to goma tools (or system default if not '
   2986                      'specified).')
   2987     group.add_option('--output_buildbot_annotations',
   2988                      action='store_true',
   2989                      help='Add extra annotation output for buildbot.')
   2990     group.add_option('--gs_bucket',
   2991                      default='',
   2992                      dest='gs_bucket',
   2993                      type='str',
   2994                      help=('Name of Google Storage bucket to upload or '
   2995                      'download build. e.g., chrome-perf'))
   2996     group.add_option('--target_arch',
   2997                      type='choice',
   2998                      choices=['ia32', 'x64', 'arm'],
   2999                      default='ia32',
   3000                      dest='target_arch',
   3001                      help=('The target build architecture. Choices are "ia32" '
   3002                      '(default), "x64" or "arm".'))
   3003     group.add_option('--target_build_type',
   3004                      type='choice',
   3005                      choices=['Release', 'Debug'],
   3006                      default='Release',
   3007                      help='The target build type. Choices are "Release" '
   3008                      '(default), or "Debug".')
   3009     group.add_option('--builder_host',
   3010                      dest='builder_host',
   3011                      type='str',
   3012                      help=('Host address of server to produce build by posting'
   3013                            ' try job request.'))
   3014     group.add_option('--builder_port',
   3015                      dest='builder_port',
   3016                      type='int',
   3017                      help=('HTTP port of the server to produce build by posting'
   3018                            ' try job request.'))
   3019     parser.add_option_group(group)
   3020 
   3021     group = optparse.OptionGroup(parser, 'Debug options')
   3022     group.add_option('--debug_ignore_build',
   3023                      action='store_true',
   3024                      help='DEBUG: Don\'t perform builds.')
   3025     group.add_option('--debug_ignore_sync',
   3026                      action='store_true',
   3027                      help='DEBUG: Don\'t perform syncs.')
   3028     group.add_option('--debug_ignore_perf_test',
   3029                      action='store_true',
   3030                      help='DEBUG: Don\'t perform performance tests.')
   3031     parser.add_option_group(group)
   3032     return parser
   3033 
   3034   def ParseCommandLine(self):
   3035     """Parses the command line for bisect options."""
   3036     parser = self._CreateCommandLineParser()
   3037     opts, _ = parser.parse_args()
   3038 
   3039     try:
   3040       if not opts.command:
   3041         raise RuntimeError('missing required parameter: --command')
   3042 
   3043       if not opts.good_revision:
   3044         raise RuntimeError('missing required parameter: --good_revision')
   3045 
   3046       if not opts.bad_revision:
   3047         raise RuntimeError('missing required parameter: --bad_revision')
   3048 
   3049       if not opts.metric and opts.bisect_mode != BISECT_MODE_RETURN_CODE:
   3050         raise RuntimeError('missing required parameter: --metric')
   3051 
   3052       if opts.gs_bucket:
   3053         if not cloud_storage.List(opts.gs_bucket):
   3054           raise RuntimeError('Invalid Google Storage: gs://%s' % opts.gs_bucket)
   3055         if not opts.builder_host:
   3056           raise RuntimeError('Must specify try server host name using '
   3057                              '--builder_host when gs_bucket is used.')
   3058         if not opts.builder_port:
   3059           raise RuntimeError('Must specify try server port number using '
   3060                              '--builder_port when gs_bucket is used.')
   3061       if opts.target_platform == 'cros':
   3062         # Run sudo up front to make sure credentials are cached for later.
   3063         print 'Sudo is required to build cros:'
   3064         print
   3065         bisect_utils.RunProcess(['sudo', 'true'])
   3066 
   3067         if not opts.cros_board:
   3068           raise RuntimeError('missing required parameter: --cros_board')
   3069 
   3070         if not opts.cros_remote_ip:
   3071           raise RuntimeError('missing required parameter: --cros_remote_ip')
   3072 
   3073         if not opts.working_directory:
   3074           raise RuntimeError('missing required parameter: --working_directory')
   3075 
   3076       if opts.bisect_mode != BISECT_MODE_RETURN_CODE:
   3077         metric_values = opts.metric.split('/')
   3078         if len(metric_values) != 2:
   3079           raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
   3080         opts.metric = metric_values
   3081 
   3082       opts.repeat_test_count = min(max(opts.repeat_test_count, 1), 100)
   3083       opts.max_time_minutes = min(max(opts.max_time_minutes, 1), 60)
   3084       opts.truncate_percent = min(max(opts.truncate_percent, 0), 25)
   3085       opts.truncate_percent = opts.truncate_percent / 100.0
   3086 
   3087       for k, v in opts.__dict__.iteritems():
   3088         assert hasattr(self, k), 'Invalid %s attribute in BisectOptions.' % k
   3089         setattr(self, k, v)
   3090     except RuntimeError, e:
   3091       output_string = StringIO.StringIO()
   3092       parser.print_help(file=output_string)
   3093       error_message = '%s\n\n%s' % (e.message, output_string.getvalue())
   3094       output_string.close()
   3095       raise RuntimeError(error_message)
   3096 
   3097   @staticmethod
   3098   def FromDict(values):
   3099     """Creates an instance of BisectOptions from a dictionary.
   3100 
   3101     Args:
   3102       values: a dict containing options to set.
   3103 
   3104     Returns:
   3105       An instance of BisectOptions.
   3106     """
   3107     opts = BisectOptions()
   3108     for k, v in values.iteritems():
   3109       assert hasattr(opts, k), 'Invalid %s attribute in BisectOptions.' % k
   3110       setattr(opts, k, v)
   3111 
   3112     if opts.metric and opts.bisect_mode != BISECT_MODE_RETURN_CODE:
   3113       metric_values = opts.metric.split('/')
   3114       if len(metric_values) != 2:
   3115         raise RuntimeError('Invalid metric specified: [%s]' % opts.metric)
   3116       opts.metric = metric_values
   3117 
   3118     opts.repeat_test_count = min(max(opts.repeat_test_count, 1), 100)
   3119     opts.max_time_minutes = min(max(opts.max_time_minutes, 1), 60)
   3120     opts.truncate_percent = min(max(opts.truncate_percent, 0), 25)
   3121     opts.truncate_percent = opts.truncate_percent / 100.0
   3122 
   3123     return opts
   3124 
   3125 
   3126 def main():
   3127 
   3128   try:
   3129     opts = BisectOptions()
   3130     opts.ParseCommandLine()
   3131 
   3132     if opts.extra_src:
   3133       extra_src = bisect_utils.LoadExtraSrc(opts.extra_src)
   3134       if not extra_src:
   3135         raise RuntimeError('Invalid or missing --extra_src.')
   3136       _AddAdditionalDepotInfo(extra_src.GetAdditionalDepotInfo())
   3137 
   3138     if opts.working_directory:
   3139       custom_deps = bisect_utils.DEFAULT_GCLIENT_CUSTOM_DEPS
   3140       if opts.no_custom_deps:
   3141         custom_deps = None
   3142       bisect_utils.CreateBisectDirectoryAndSetupDepot(opts, custom_deps)
   3143 
   3144       os.chdir(os.path.join(os.getcwd(), 'src'))
   3145 
   3146       if not RemoveBuildFiles(opts.target_build_type):
   3147         raise RuntimeError('Something went wrong removing the build files.')
   3148 
   3149     if not _IsPlatformSupported():
   3150       raise RuntimeError('Sorry, this platform isn\'t supported yet.')
   3151 
   3152     # Check what source control method is being used, and create a
   3153     # SourceControl object if possible.
   3154     source_control = source_control_module.DetermineAndCreateSourceControl(opts)
   3155 
   3156     if not source_control:
   3157       raise RuntimeError(
   3158           'Sorry, only the git workflow is supported at the moment.')
   3159 
   3160     # gClient sync seems to fail if you're not in master branch.
   3161     if (not source_control.IsInProperBranch() and
   3162         not opts.debug_ignore_sync and
   3163         not opts.working_directory):
   3164       raise RuntimeError('You must switch to master branch to run bisection.')
   3165     bisect_test = BisectPerformanceMetrics(source_control, opts)
   3166     try:
   3167       bisect_results = bisect_test.Run(opts.command,
   3168                                        opts.bad_revision,
   3169                                        opts.good_revision,
   3170                                        opts.metric)
   3171       if bisect_results.error:
   3172         raise RuntimeError(bisect_results.error)
   3173       bisect_test.FormatAndPrintResults(bisect_results)
   3174       return 0
   3175     finally:
   3176       bisect_test.PerformCleanup()
   3177   except RuntimeError, e:
   3178     if opts.output_buildbot_annotations:
   3179       # The perf dashboard scrapes the "results" step in order to comment on
   3180       # bugs. If you change this, please update the perf dashboard as well.
   3181       bisect_utils.OutputAnnotationStepStart('Results')
   3182     print 'Error: %s' % e.message
   3183     if opts.output_buildbot_annotations:
   3184       bisect_utils.OutputAnnotationStepClosed()
   3185   return 1
   3186 
   3187 
   3188 if __name__ == '__main__':
   3189   sys.exit(main())
   3190