Home | History | Annotate | Download | only in tools
      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 
     17 An example usage (using svn cl's):
     18 
     19 ./tools/bisect-perf-regression.py -c\
     20 "out/Release/performance_ui_tests --gtest_filter=ShutdownTest.SimpleUserQuit"\
     21 -g 168222 -b 168232 -m shutdown/simple-user-quit
     22 
     23 Be aware that if you're using the git workflow and specify an svn revision,
     24 the script will attempt to find the git SHA1 where svn changes up to that
     25 revision were merged in.
     26 
     27 
     28 An example usage (using git hashes):
     29 
     30 ./tools/bisect-perf-regression.py -c\
     31 "out/Release/performance_ui_tests --gtest_filter=ShutdownTest.SimpleUserQuit"\
     32 -g 1f6e67861535121c5c819c16a666f2436c207e7b\
     33 -b b732f23b4f81c382db0b23b9035f3dadc7d925bb\
     34 -m shutdown/simple-user-quit
     35 
     36 """
     37 
     38 import copy
     39 import datetime
     40 import errno
     41 import hashlib
     42 import math
     43 import optparse
     44 import os
     45 import re
     46 import shlex
     47 import shutil
     48 import StringIO
     49 import subprocess
     50 import sys
     51 import time
     52 import zipfile
     53 
     54 sys.path.append(os.path.join(os.path.dirname(__file__), 'telemetry'))
     55 
     56 import bisect_utils
     57 import post_perf_builder_job as bisect_builder
     58 from telemetry.page import cloud_storage
     59 
     60 # The additional repositories that might need to be bisected.
     61 # If the repository has any dependant repositories (such as skia/src needs
     62 # skia/include and skia/gyp to be updated), specify them in the 'depends'
     63 # so that they're synced appropriately.
     64 # Format is:
     65 # src: path to the working directory.
     66 # recurse: True if this repositry will get bisected.
     67 # depends: A list of other repositories that are actually part of the same
     68 #   repository in svn.
     69 # svn: Needed for git workflow to resolve hashes to svn revisions.
     70 # from: Parent depot that must be bisected before this is bisected.
     71 # deps_var: Key name in vars varible in DEPS file that has revision information.
     72 DEPOT_DEPS_NAME = {
     73   'chromium' : {
     74     "src" : "src",
     75     "recurse" : True,
     76     "depends" : None,
     77     "from" : ['cros', 'android-chrome'],
     78     'viewvc': 'http://src.chromium.org/viewvc/chrome?view=revision&revision=',
     79     'deps_var': 'chromium_rev'
     80   },
     81   'webkit' : {
     82     "src" : "src/third_party/WebKit",
     83     "recurse" : True,
     84     "depends" : None,
     85     "from" : ['chromium'],
     86     'viewvc': 'http://src.chromium.org/viewvc/blink?view=revision&revision=',
     87     'deps_var': 'webkit_revision'
     88   },
     89   'angle' : {
     90     "src" : "src/third_party/angle",
     91     "src_old" : "src/third_party/angle_dx11",
     92     "recurse" : True,
     93     "depends" : None,
     94     "from" : ['chromium'],
     95     "platform": 'nt',
     96     'deps_var': 'angle_revision'
     97   },
     98   'v8' : {
     99     "src" : "src/v8",
    100     "recurse" : True,
    101     "depends" : None,
    102     "from" : ['chromium'],
    103     "custom_deps": bisect_utils.GCLIENT_CUSTOM_DEPS_V8,
    104     'viewvc': 'https://code.google.com/p/v8/source/detail?r=',
    105     'deps_var': 'v8_revision'
    106   },
    107   'v8_bleeding_edge' : {
    108     "src" : "src/v8_bleeding_edge",
    109     "recurse" : True,
    110     "depends" : None,
    111     "svn": "https://v8.googlecode.com/svn/branches/bleeding_edge",
    112     "from" : ['v8'],
    113     'viewvc': 'https://code.google.com/p/v8/source/detail?r=',
    114     'deps_var': 'v8_revision'
    115   },
    116   'skia/src' : {
    117     "src" : "src/third_party/skia/src",
    118     "recurse" : True,
    119     "svn" : "http://skia.googlecode.com/svn/trunk/src",
    120     "depends" : ['skia/include', 'skia/gyp'],
    121     "from" : ['chromium'],
    122     'viewvc': 'https://code.google.com/p/skia/source/detail?r=',
    123     'deps_var': 'skia_revision'
    124   },
    125   'skia/include' : {
    126     "src" : "src/third_party/skia/include",
    127     "recurse" : False,
    128     "svn" : "http://skia.googlecode.com/svn/trunk/include",
    129     "depends" : None,
    130     "from" : ['chromium'],
    131     'viewvc': 'https://code.google.com/p/skia/source/detail?r=',
    132     'deps_var': 'None'
    133   },
    134   'skia/gyp' : {
    135     "src" : "src/third_party/skia/gyp",
    136     "recurse" : False,
    137     "svn" : "http://skia.googlecode.com/svn/trunk/gyp",
    138     "depends" : None,
    139     "from" : ['chromium'],
    140     'viewvc': 'https://code.google.com/p/skia/source/detail?r=',
    141     'deps_var': 'None'
    142   },
    143 }
    144 
    145 DEPOT_NAMES = DEPOT_DEPS_NAME.keys()
    146 CROS_SDK_PATH = os.path.join('..', 'cros', 'chromite', 'bin', 'cros_sdk')
    147 CROS_VERSION_PATTERN = 'new version number from %s'
    148 CROS_CHROMEOS_PATTERN = 'chromeos-base/chromeos-chrome'
    149 CROS_TEST_KEY_PATH = os.path.join('..', 'cros', 'chromite', 'ssh_keys',
    150                                   'testing_rsa')
    151 CROS_SCRIPT_KEY_PATH = os.path.join('..', 'cros', 'src', 'scripts',
    152                                     'mod_for_test_scripts', 'ssh_keys',
    153                                     'testing_rsa')
    154 
    155 BUILD_RESULT_SUCCEED = 0
    156 BUILD_RESULT_FAIL = 1
    157 BUILD_RESULT_SKIPPED = 2
    158 
    159 # Maximum time in seconds to wait after posting build request to tryserver.
    160 # TODO: Change these values based on the actual time taken by buildbots on
    161 # the tryserver.
    162 MAX_MAC_BUILD_TIME = 14400
    163 MAX_WIN_BUILD_TIME = 14400
    164 MAX_LINUX_BUILD_TIME = 14400
    165 
    166 # Patch template to add a new file, DEPS.sha under src folder.
    167 # This file contains SHA1 value of the DEPS changes made while bisecting
    168 # dependency repositories. This patch send along with DEPS patch to tryserver.
    169 # When a build requested is posted with a patch, bisect builders on tryserver,
    170 # once build is produced, it reads SHA value from this file and appends it
    171 # to build archive filename.
    172 DEPS_SHA_PATCH = """diff --git src/DEPS.sha src/DEPS.sha
    173 new file mode 100644
    174 --- /dev/null
    175 +++ src/DEPS.sha
    176 @@ -0,0 +1 @@
    177 +%(deps_sha)s
    178 """
    179 
    180 # The possible values of the --bisect_mode flag, which determines what to
    181 # use when classifying a revision as "good" or "bad".
    182 BISECT_MODE_MEAN = 'mean'
    183 BISECT_MODE_STD_DEV = 'std_dev'
    184 BISECT_MODE_RETURN_CODE = 'return_code'
    185 
    186 
    187 def _AddAdditionalDepotInfo(depot_info):
    188   """Adds additional depot info to the global depot variables."""
    189   global DEPOT_DEPS_NAME
    190   global DEPOT_NAMES
    191   DEPOT_DEPS_NAME = dict(DEPOT_DEPS_NAME.items() +
    192       depot_info.items())
    193   DEPOT_NAMES = DEPOT_DEPS_NAME.keys()
    194 
    195 
    196 def CalculateTruncatedMean(data_set, truncate_percent):
    197   """Calculates the truncated mean of a set of values.
    198 
    199   Note that this isn't just the mean of the set of values with the highest
    200   and lowest values discarded; the non-discarded values are also weighted
    201   differently depending how many values are discarded.
    202 
    203   Args:
    204     data_set: Non-empty list of values.
    205     truncate_percent: The % from the upper and lower portions of the data set
    206         to discard, expressed as a value in [0, 1].
    207 
    208   Returns:
    209     The truncated mean as a float.
    210 
    211   Raises:
    212     TypeError: The data set was empty after discarding values.
    213   """
    214   if len(data_set) > 2:
    215     data_set = sorted(data_set)
    216 
    217     discard_num_float = len(data_set) * truncate_percent
    218     discard_num_int = int(math.floor(discard_num_float))
    219     kept_weight = len(data_set) - discard_num_float * 2
    220 
    221     data_set = data_set[discard_num_int:len(data_set)-discard_num_int]
    222 
    223     weight_left = 1.0 - (discard_num_float - discard_num_int)
    224 
    225     if weight_left < 1:
    226       # If the % to discard leaves a fractional portion, need to weight those
    227       # values.
    228       unweighted_vals = data_set[1:len(data_set)-1]
    229       weighted_vals = [data_set[0], data_set[len(data_set)-1]]
    230       weighted_vals = [w * weight_left for w in weighted_vals]
    231       data_set = weighted_vals + unweighted_vals
    232   else:
    233     kept_weight = len(data_set)
    234 
    235   truncated_mean = reduce(lambda x, y: float(x) + float(y),
    236                           data_set) / kept_weight
    237 
    238   return truncated_mean
    239 
    240 
    241 def CalculateMean(values):
    242   """Calculates the arithmetic mean of a list of values."""
    243   return CalculateTruncatedMean(values, 0.0)
    244 
    245 
    246 def CalculateConfidence(good_results_lists, bad_results_lists):
    247   """Calculates a confidence percentage.
    248 
    249   This is calculated based on how distinct the "good" and "bad" values are,
    250   and how noisy the results are. More precisely, the confidence is the quotient
    251   of the difference between the closest values across the good and bad groups
    252   and the sum of the standard deviations of the good and bad groups.
    253 
    254   TODO(qyearsley): Replace this confidence function with a function that
    255       uses a Student's t-test. The confidence would be (1 - p-value), where
    256       p-value is the probability of obtaining the given a set of good and bad
    257       values just by chance.
    258 
    259   Args:
    260     good_results_lists: A list of lists of "good" result numbers.
    261     bad_results_lists: A list of lists of "bad" result numbers.
    262 
    263   Returns:
    264     A number between in the range [0, 100].
    265   """
    266   # Get the distance between the two groups.
    267   means_good = map(CalculateMean, good_results_lists)
    268   means_bad = map(CalculateMean, bad_results_lists)
    269   bounds_good = (min(means_good), max(means_good))
    270   bounds_bad = (min(means_bad), max(means_bad))
    271   dist_between_groups = min(
    272       math.fabs(bounds_bad[1] - bounds_good[0]),
    273       math.fabs(bounds_bad[0] - bounds_good[1]))
    274 
    275   # Get the sum of the standard deviations of the two groups.
    276   good_results_flattened = sum(good_results_lists, [])
    277   bad_results_flattened = sum(bad_results_lists, [])
    278   stddev_good = CalculateStandardDeviation(good_results_flattened)
    279   stddev_bad = CalculateStandardDeviation(bad_results_flattened)
    280   stddev_sum = stddev_good + stddev_bad
    281 
    282   confidence = dist_between_groups / (max(0.0001, stddev_sum))
    283   confidence = int(min(1.0, max(confidence, 0.0)) * 100.0)
    284   return confidence
    285 
    286 
    287 def CalculateStandardDeviation(values):
    288   """Calculates the sample standard deviation of the given list of values."""
    289   if len(values) == 1:
    290     return 0.0
    291 
    292   mean = CalculateMean(values)
    293   differences_from_mean = [float(x) - mean for x in values]
    294   squared_differences = [float(x * x) for x in differences_from_mean]
    295   variance = sum(squared_differences) / (len(values) - 1)
    296   std_dev = math.sqrt(variance)
    297 
    298   return std_dev
    299 
    300 
    301 def CalculateRelativeChange(before, after):
    302   """Returns the relative change of before and after, relative to before.
    303 
    304   There are several different ways to define relative difference between
    305   two numbers; sometimes it is defined as relative to the smaller number,
    306   or to the mean of the two numbers. This version returns the difference
    307   relative to the first of the two numbers.
    308 
    309   Args:
    310     before: A number representing an earlier value.
    311     after: Another number, representing a later value.
    312 
    313   Returns:
    314     A non-negative floating point number; 0.1 represents a 10% change.
    315   """
    316   if before == after:
    317     return 0.0
    318   if before == 0:
    319     return float('nan')
    320   difference = after - before
    321   return math.fabs(difference / before)
    322 
    323 
    324 def CalculatePooledStandardError(work_sets):
    325   numerator = 0.0
    326   denominator1 = 0.0
    327   denominator2 = 0.0
    328 
    329   for current_set in work_sets:
    330     std_dev = CalculateStandardDeviation(current_set)
    331     numerator += (len(current_set) - 1) * std_dev ** 2
    332     denominator1 += len(current_set) - 1
    333     denominator2 += 1.0 / len(current_set)
    334 
    335   if denominator1:
    336     return math.sqrt(numerator / denominator1) * math.sqrt(denominator2)
    337   return 0.0
    338 
    339 
    340 def CalculateStandardError(values):
    341   """Calculates the standard error of a list of values."""
    342   if len(values) <= 1:
    343     return 0.0
    344 
    345   std_dev = CalculateStandardDeviation(values)
    346 
    347   return std_dev / math.sqrt(len(values))
    348 
    349 
    350 def IsStringFloat(string_to_check):
    351   """Checks whether or not the given string can be converted to a floating
    352   point number.
    353 
    354   Args:
    355     string_to_check: Input string to check if it can be converted to a float.
    356 
    357   Returns:
    358     True if the string can be converted to a float.
    359   """
    360   try:
    361     float(string_to_check)
    362 
    363     return True
    364   except ValueError:
    365     return False
    366 
    367 
    368 def IsStringInt(string_to_check):
    369   """Checks whether or not the given string can be converted to a integer.
    370 
    371   Args:
    372     string_to_check: Input string to check if it can be converted to an int.
    373 
    374   Returns:
    375     True if the string can be converted to an int.
    376   """
    377   try:
    378     int(string_to_check)
    379 
    380     return True
    381   except ValueError:
    382     return False
    383 
    384 
    385 def IsWindows():
    386   """Checks whether or not the script is running on Windows.
    387 
    388   Returns:
    389     True if running on Windows.
    390   """
    391   return sys.platform == 'cygwin' or sys.platform.startswith('win')
    392 
    393 
    394 def Is64BitWindows():
    395   """Returns whether or not Windows is a 64-bit version.
    396 
    397   Returns:
    398     True if Windows is 64-bit, False if 32-bit.
    399   """
    400   platform = os.environ['PROCESSOR_ARCHITECTURE']
    401   try:
    402     platform = os.environ['PROCESSOR_ARCHITEW6432']
    403   except KeyError:
    404     # Must not be running in WoW64, so PROCESSOR_ARCHITECTURE is correct
    405     pass
    406 
    407   return platform in ['AMD64', 'I64']
    408 
    409 
    410 def IsLinux():
    411   """Checks whether or not the script is running on Linux.
    412 
    413   Returns:
    414     True if running on Linux.
    415   """
    416   return sys.platform.startswith('linux')
    417 
    418 
    419 def IsMac():
    420   """Checks whether or not the script is running on Mac.
    421 
    422   Returns:
    423     True if running on Mac.
    424   """
    425   return sys.platform.startswith('darwin')
    426 
    427 
    428 def GetSHA1HexDigest(contents):
    429   """Returns secured hash containing hexadecimal for the given contents."""
    430   return hashlib.sha1(contents).hexdigest()
    431 
    432 
    433 def GetZipFileName(build_revision=None, target_arch='ia32', patch_sha=None):
    434   """Gets the archive file name for the given revision."""
    435   def PlatformName():
    436     """Return a string to be used in paths for the platform."""
    437     if IsWindows():
    438       # Build archive for x64 is still stored with 'win32'suffix
    439       # (chromium_utils.PlatformName()).
    440       if Is64BitWindows() and target_arch == 'x64':
    441         return 'win32'
    442       return 'win32'
    443     if IsLinux():
    444       return 'linux'
    445     if IsMac():
    446       return 'mac'
    447     raise NotImplementedError('Unknown platform "%s".' % sys.platform)
    448 
    449   base_name = 'full-build-%s' % PlatformName()
    450   if not build_revision:
    451     return base_name
    452   if patch_sha:
    453     build_revision = '%s_%s' % (build_revision , patch_sha)
    454   return '%s_%s.zip' % (base_name, build_revision)
    455 
    456 
    457 def GetRemoteBuildPath(build_revision, target_arch='ia32', patch_sha=None):
    458   """Compute the url to download the build from."""
    459   def GetGSRootFolderName():
    460     """Gets Google Cloud Storage root folder names"""
    461     if IsWindows():
    462       if Is64BitWindows() and target_arch == 'x64':
    463         return 'Win x64 Builder'
    464       return 'Win Builder'
    465     if IsLinux():
    466       return 'Linux Builder'
    467     if IsMac():
    468       return 'Mac Builder'
    469     raise NotImplementedError('Unsupported Platform "%s".' % sys.platform)
    470 
    471   base_filename = GetZipFileName(build_revision, target_arch, patch_sha)
    472   builder_folder = GetGSRootFolderName()
    473   return '%s/%s' % (builder_folder, base_filename)
    474 
    475 
    476 def FetchFromCloudStorage(bucket_name, source_path, destination_path):
    477   """Fetches file(s) from the Google Cloud Storage.
    478 
    479   Args:
    480     bucket_name: Google Storage bucket name.
    481     source_path: Source file path.
    482     destination_path: Destination file path.
    483 
    484   Returns:
    485     Downloaded file path if exisits, otherwise None.
    486   """
    487   target_file = os.path.join(destination_path, os.path.basename(source_path))
    488   try:
    489     if cloud_storage.Exists(bucket_name, source_path):
    490       print 'Fetching file from gs//%s/%s ...' % (bucket_name, source_path)
    491       cloud_storage.Get(bucket_name, source_path, destination_path)
    492       if os.path.exists(target_file):
    493         return target_file
    494     else:
    495       print ('File gs://%s/%s not found in cloud storage.' % (
    496           bucket_name, source_path))
    497   except Exception as e:
    498     print 'Something went wrong while fetching file from cloud: %s' % e
    499     if os.path.exists(target_file):
    500       os.remove(target_file)
    501   return None
    502 
    503 
    504 # This is copied from Chromium's project build/scripts/common/chromium_utils.py.
    505 def MaybeMakeDirectory(*path):
    506   """Creates an entire path, if it doesn't already exist."""
    507   file_path = os.path.join(*path)
    508   try:
    509     os.makedirs(file_path)
    510   except OSError, e:
    511     if e.errno != errno.EEXIST:
    512       return False
    513   return True
    514 
    515 
    516 # This is copied from Chromium's project build/scripts/common/chromium_utils.py.
    517 def ExtractZip(filename, output_dir, verbose=True):
    518   """ Extract the zip archive in the output directory."""
    519   MaybeMakeDirectory(output_dir)
    520 
    521   # On Linux and Mac, we use the unzip command as it will
    522   # handle links and file bits (executable), which is much
    523   # easier then trying to do that with ZipInfo options.
    524   #
    525   # The Mac Version of unzip unfortunately does not support Zip64, whereas
    526   # the python module does, so we have to fallback to the python zip module
    527   # on Mac if the filesize is greater than 4GB.
    528   #
    529   # On Windows, try to use 7z if it is installed, otherwise fall back to python
    530   # zip module and pray we don't have files larger than 512MB to unzip.
    531   unzip_cmd = None
    532   if ((IsMac() and os.path.getsize(filename) < 4 * 1024 * 1024 * 1024)
    533       or IsLinux()):
    534     unzip_cmd = ['unzip', '-o']
    535   elif IsWindows() and os.path.exists('C:\\Program Files\\7-Zip\\7z.exe'):
    536     unzip_cmd = ['C:\\Program Files\\7-Zip\\7z.exe', 'x', '-y']
    537 
    538   if unzip_cmd:
    539     # Make sure path is absolute before changing directories.
    540     filepath = os.path.abspath(filename)
    541     saved_dir = os.getcwd()
    542     os.chdir(output_dir)
    543     command = unzip_cmd + [filepath]
    544     result = RunProcess(command)
    545     os.chdir(saved_dir)
    546     if result:
    547       raise IOError('unzip failed: %s => %s' % (str(command), result))
    548   else:
    549     assert IsWindows() or IsMac()
    550     zf = zipfile.ZipFile(filename)
    551     for name in zf.namelist():
    552       if verbose:
    553         print 'Extracting %s' % name
    554       zf.extract(name, output_dir)
    555       if IsMac():
    556         # Restore permission bits.
    557         os.chmod(os.path.join(output_dir, name),
    558                  zf.getinfo(name).external_attr >> 16L)
    559 
    560 
    561 def RunProcess(command):
    562   """Runs an arbitrary command.
    563 
    564   If output from the call is needed, use RunProcessAndRetrieveOutput instead.
    565 
    566   Args:
    567     command: A list containing the command and args to execute.
    568 
    569   Returns:
    570     The return code of the call.
    571   """
    572   # On Windows, use shell=True to get PATH interpretation.
    573   shell = IsWindows()
    574   return subprocess.call(command, shell=shell)
    575 
    576 
    577 def RunProcessAndRetrieveOutput(command, cwd=None):
    578   """Runs an arbitrary command, returning its output and return code.
    579 
    580   Since output is collected via communicate(), there will be no output until
    581   the call terminates. If you need output while the program runs (ie. so
    582   that the buildbot doesn't terminate the script), consider RunProcess().
    583 
    584   Args:
    585     command: A list containing the command and args to execute.
    586     cwd: A directory to change to while running the command. The command can be
    587         relative to this directory. If this is None, the command will be run in
    588         the current directory.
    589 
    590   Returns:
    591     A tuple of the output and return code.
    592   """
    593   if cwd:
    594     original_cwd = os.getcwd()
    595     os.chdir(cwd)
    596 
    597   # On Windows, use shell=True to get PATH interpretation.
    598   shell = IsWindows()
    599   proc = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE)
    600   (output, _) = proc.communicate()
    601 
    602   if cwd:
    603     os.chdir(original_cwd)
    604 
    605   return (output, proc.returncode)
    606 
    607 
    608 def RunGit(command, cwd=None):
    609   """Run a git subcommand, returning its output and return code.
    610 
    611   Args:
    612     command: A list containing the args to git.
    613     cwd: A directory to change to while running the git command (optional).
    614 
    615   Returns:
    616     A tuple of the output and return code.
    617   """
    618   command = ['git'] + command
    619 
    620   return RunProcessAndRetrieveOutput(command, cwd=cwd)
    621 
    622 
    623 def CheckRunGit(command, cwd=None):
    624   """Run a git subcommand, returning its output and return code. Asserts if
    625   the return code of the call is non-zero.
    626 
    627   Args:
    628     command: A list containing the args to git.
    629 
    630   Returns:
    631     A tuple of the output and return code.
    632   """
    633   (output, return_code) = RunGit(command, cwd=cwd)
    634 
    635   assert not return_code, 'An error occurred while running'\
    636                           ' "git %s"' % ' '.join(command)
    637   return output
    638 
    639 
    640 def SetBuildSystemDefault(build_system, use_goma):
    641   """Sets up any environment variables needed to build with the specified build
    642   system.
    643 
    644   Args:
    645     build_system: A string specifying build system. Currently only 'ninja' or
    646         'make' are supported."""
    647   if build_system == 'ninja':
    648     gyp_var = os.getenv('GYP_GENERATORS')
    649 
    650     if not gyp_var or not 'ninja' in gyp_var:
    651       if gyp_var:
    652         os.environ['GYP_GENERATORS'] = gyp_var + ',ninja'
    653       else:
    654         os.environ['GYP_GENERATORS'] = 'ninja'
    655 
    656       if IsWindows():
    657         os.environ['GYP_DEFINES'] = 'component=shared_library '\
    658             'incremental_chrome_dll=1 disable_nacl=1 fastbuild=1 '\
    659             'chromium_win_pch=0'
    660 
    661   elif build_system == 'make':
    662     os.environ['GYP_GENERATORS'] = 'make'
    663   else:
    664     raise RuntimeError('%s build not supported.' % build_system)
    665 
    666   if use_goma:
    667     os.environ['GYP_DEFINES'] = '%s %s' % (os.getenv('GYP_DEFINES', ''),
    668                                               'use_goma=1')
    669 
    670 
    671 def BuildWithMake(threads, targets, build_type='Release'):
    672   cmd = ['make', 'BUILDTYPE=%s' % build_type]
    673 
    674   if threads:
    675     cmd.append('-j%d' % threads)
    676 
    677   cmd += targets
    678 
    679   return_code = RunProcess(cmd)
    680 
    681   return not return_code
    682 
    683 
    684 def BuildWithNinja(threads, targets, build_type='Release'):
    685   cmd = ['ninja', '-C', os.path.join('out', build_type)]
    686 
    687   if threads:
    688     cmd.append('-j%d' % threads)
    689 
    690   cmd += targets
    691 
    692   return_code = RunProcess(cmd)
    693 
    694   return not return_code
    695 
    696 
    697 def BuildWithVisualStudio(targets, build_type='Release'):
    698   path_to_devenv = os.path.abspath(
    699       os.path.join(os.environ['VS100COMNTOOLS'], '..', 'IDE', 'devenv.com'))
    700   path_to_sln = os.path.join(os.getcwd(), 'chrome', 'chrome.sln')
    701   cmd = [path_to_devenv, '/build', build_type, path_to_sln]
    702 
    703   for t in targets:
    704     cmd.extend(['/Project', t])
    705 
    706   return_code = RunProcess(cmd)
    707 
    708   return not return_code
    709 
    710 
    711 def WriteStringToFile(text, file_name):
    712   try:
    713     with open(file_name, "wb") as f:
    714       f.write(text)
    715   except IOError as e:
    716     raise RuntimeError('Error writing to file [%s]' % file_name )
    717 
    718 
    719 def ReadStringFromFile(file_name):
    720   try:
    721     with open(file_name) as f:
    722       return f.read()
    723   except IOError as e:
    724     raise RuntimeError('Error reading file [%s]' % file_name )
    725 
    726 
    727 def ChangeBackslashToSlashInPatch(diff_text):
    728   """Formats file paths in the given text to unix-style paths."""
    729   if diff_text:
    730     diff_lines = diff_text.split('\n')
    731     for i in range(len(diff_lines)):
    732       if (diff_lines[i].startswith('--- ') or
    733           diff_lines[i].startswith('+++ ')):
    734         diff_lines[i] = diff_lines[i].replace('\\', '/')
    735     return '\n'.join(diff_lines)
    736   return None
    737 
    738 
    739 class Builder(object):
    740   """Builder is used by the bisect script to build relevant targets and deploy.
    741   """
    742   def __init__(self, opts):
    743     """Performs setup for building with target build system.
    744 
    745     Args:
    746         opts: Options parsed from command line.
    747     """
    748     if IsWindows():
    749       if not opts.build_preference:
    750         opts.build_preference = 'msvs'
    751 
    752       if opts.build_preference == 'msvs':
    753         if not os.getenv('VS100COMNTOOLS'):
    754           raise RuntimeError(
    755               'Path to visual studio could not be determined.')
    756       else:
    757         SetBuildSystemDefault(opts.build_preference, opts.use_goma)
    758     else:
    759       if not opts.build_preference:
    760         if 'ninja' in os.getenv('GYP_GENERATORS'):
    761           opts.build_preference = 'ninja'
    762         else:
    763           opts.build_preference = 'make'
    764 
    765       SetBuildSystemDefault(opts.build_preference, opts.use_goma)
    766 
    767     if not bisect_utils.SetupPlatformBuildEnvironment(opts):
    768       raise RuntimeError('Failed to set platform environment.')
    769 
    770   @staticmethod
    771   def FromOpts(opts):
    772     builder = None
    773     if opts.target_platform == 'cros':
    774       builder = CrosBuilder(opts)
    775     elif opts.target_platform == 'android':
    776       builder = AndroidBuilder(opts)
    777     elif opts.target_platform == 'android-chrome':
    778       builder = AndroidChromeBuilder(opts)
    779     else:
    780       builder = DesktopBuilder(opts)
    781     return builder
    782 
    783   def Build(self, depot, opts):
    784     raise NotImplementedError()
    785 
    786   def GetBuildOutputDirectory(self, opts, src_dir=None):
    787     raise NotImplementedError()
    788 
    789 
    790 class DesktopBuilder(Builder):
    791   """DesktopBuilder is used to build Chromium on linux/mac/windows."""
    792   def __init__(self, opts):
    793     super(DesktopBuilder, self).__init__(opts)
    794 
    795   def Build(self, depot, opts):
    796     """Builds chromium_builder_perf target using options passed into
    797     the script.
    798 
    799     Args:
    800         depot: Current depot being bisected.
    801         opts: The options parsed from the command line.
    802 
    803     Returns:
    804         True if build was successful.
    805     """
    806     targets = ['chromium_builder_perf']
    807 
    808     threads = None
    809     if opts.use_goma:
    810       threads = 64
    811 
    812     build_success = False
    813     if opts.build_preference == 'make':
    814       build_success = BuildWithMake(threads, targets, opts.target_build_type)
    815     elif opts.build_preference == 'ninja':
    816       build_success = BuildWithNinja(threads, targets, opts.target_build_type)
    817     elif opts.build_preference == 'msvs':
    818       assert IsWindows(), 'msvs is only supported on Windows.'
    819       build_success = BuildWithVisualStudio(targets, opts.target_build_type)
    820     else:
    821       assert False, 'No build system defined.'
    822     return build_success
    823 
    824   def GetBuildOutputDirectory(self, opts, src_dir=None):
    825     """Returns the path to the build directory, relative to the checkout root.
    826 
    827       Assumes that the current working directory is the checkout root.
    828     """
    829     src_dir = src_dir or 'src'
    830     if opts.build_preference == 'ninja' or IsLinux():
    831       return os.path.join(src_dir, 'out')
    832     if IsMac():
    833       return os.path.join(src_dir, 'xcodebuild')
    834     if IsWindows():
    835       return os.path.join(src_dir, 'build')
    836     raise NotImplementedError('Unexpected platform %s' % sys.platform)
    837 
    838 
    839 class AndroidBuilder(Builder):
    840   """AndroidBuilder is used to build on android."""
    841   def __init__(self, opts):
    842     super(AndroidBuilder, self).__init__(opts)
    843 
    844   def _GetTargets(self):
    845     return ['chrome_shell_apk', 'cc_perftests_apk', 'android_tools']
    846 
    847   def Build(self, depot, opts):
    848     """Builds the android content shell and other necessary tools using options
    849     passed into the script.
    850 
    851     Args:
    852         depot: Current depot being bisected.
    853         opts: The options parsed from the command line.
    854 
    855     Returns:
    856         True if build was successful.
    857     """
    858     threads = None
    859     if opts.use_goma:
    860       threads = 64
    861 
    862     build_success = False
    863     if opts.build_preference == 'ninja':
    864       build_success = BuildWithNinja(
    865           threads, self._GetTargets(), opts.target_build_type)
    866     else:
    867       assert False, 'No build system defined.'
    868 
    869     return build_success
    870 
    871 
    872 class AndroidChromeBuilder(AndroidBuilder):
    873   """AndroidBuilder is used to build on android's chrome."""
    874   def __init__(self, opts):
    875     super(AndroidChromeBuilder, self).__init__(opts)
    876 
    877   def _GetTargets(self):
    878     return AndroidBuilder._GetTargets(self) + ['chrome_apk']
    879 
    880 
    881 class CrosBuilder(Builder):
    882   """CrosBuilder is used to build and image ChromeOS/Chromium when cros is the
    883   target platform."""
    884   def __init__(self, opts):
    885     super(CrosBuilder, self).__init__(opts)
    886 
    887   def ImageToTarget(self, opts):
    888     """Installs latest image to target specified by opts.cros_remote_ip.
    889 
    890     Args:
    891         opts: Program options containing cros_board and cros_remote_ip.
    892 
    893     Returns:
    894         True if successful.
    895     """
    896     try:
    897       # Keys will most likely be set to 0640 after wiping the chroot.
    898       os.chmod(CROS_SCRIPT_KEY_PATH, 0600)
    899       os.chmod(CROS_TEST_KEY_PATH, 0600)
    900       cmd = [CROS_SDK_PATH, '--', './bin/cros_image_to_target.py',
    901              '--remote=%s' % opts.cros_remote_ip,
    902              '--board=%s' % opts.cros_board, '--test', '--verbose']
    903 
    904       return_code = RunProcess(cmd)
    905       return not return_code
    906     except OSError, e:
    907       return False
    908 
    909   def BuildPackages(self, opts, depot):
    910     """Builds packages for cros.
    911 
    912     Args:
    913         opts: Program options containing cros_board.
    914         depot: The depot being bisected.
    915 
    916     Returns:
    917         True if successful.
    918     """
    919     cmd = [CROS_SDK_PATH]
    920 
    921     if depot != 'cros':
    922       path_to_chrome = os.path.join(os.getcwd(), '..')
    923       cmd += ['--chrome_root=%s' % path_to_chrome]
    924 
    925     cmd += ['--']
    926 
    927     if depot != 'cros':
    928       cmd += ['CHROME_ORIGIN=LOCAL_SOURCE']
    929 
    930     cmd += ['BUILDTYPE=%s' % opts.target_build_type, './build_packages',
    931         '--board=%s' % opts.cros_board]
    932     return_code = RunProcess(cmd)
    933 
    934     return not return_code
    935 
    936   def BuildImage(self, opts, depot):
    937     """Builds test image for cros.
    938 
    939     Args:
    940         opts: Program options containing cros_board.
    941         depot: The depot being bisected.
    942 
    943     Returns:
    944         True if successful.
    945     """
    946     cmd = [CROS_SDK_PATH]
    947 
    948     if depot != 'cros':
    949       path_to_chrome = os.path.join(os.getcwd(), '..')
    950       cmd += ['--chrome_root=%s' % path_to_chrome]
    951 
    952     cmd += ['--']
    953 
    954     if depot != 'cros':
    955       cmd += ['CHROME_ORIGIN=LOCAL_SOURCE']
    956 
    957     cmd += ['BUILDTYPE=%s' % opts.target_build_type, '--', './build_image',
    958         '--board=%s' % opts.cros_board, 'test']
    959 
    960     return_code = RunProcess(cmd)
    961 
    962     return not return_code
    963 
    964   def Build(self, depot, opts):
    965     """Builds targets using options passed into the script.
    966 
    967     Args:
    968         depot: Current depot being bisected.
    969         opts: The options parsed from the command line.
    970 
    971     Returns:
    972         True if build was successful.
    973     """
    974     if self.BuildPackages(opts, depot):
    975       if self.BuildImage(opts, depot):
    976         return self.ImageToTarget(opts)
    977     return False
    978 
    979 
    980 class SourceControl(object):
    981   """SourceControl is an abstraction over the underlying source control
    982   system used for chromium. For now only git is supported, but in the
    983   future, the svn workflow could be added as well."""
    984   def __init__(self):
    985     super(SourceControl, self).__init__()
    986 
    987   def SyncToRevisionWithGClient(self, revision):
    988     """Uses gclient to sync to the specified revision.
    989 
    990     ie. gclient sync --revision <revision>
    991 
    992     Args:
    993       revision: The git SHA1 or svn CL (depending on workflow).
    994 
    995     Returns:
    996       The return code of the call.
    997     """
    998     return bisect_utils.RunGClient(['sync', '--verbose', '--reset', '--force',
    999         '--delete_unversioned_trees', '--nohooks', '--revision', revision])
   1000 
   1001   def SyncToRevisionWithRepo(self, timestamp):
   1002     """Uses repo to sync all the underlying git depots to the specified
   1003     time.
   1004 
   1005     Args:
   1006       timestamp: The unix timestamp to sync to.
   1007 
   1008     Returns:
   1009       The return code of the call.
   1010     """
   1011     return bisect_utils.RunRepoSyncAtTimestamp(timestamp)
   1012 
   1013 
   1014 class GitSourceControl(SourceControl):
   1015   """GitSourceControl is used to query the underlying source control. """
   1016   def __init__(self, opts):
   1017     super(GitSourceControl, self).__init__()
   1018     self.opts = opts
   1019 
   1020   def IsGit(self):
   1021     return True
   1022 
   1023   def GetRevisionList(self, revision_range_end, revision_range_start, cwd=None):
   1024     """Retrieves a list of revisions between |revision_range_start| and
   1025     |revision_range_end|.
   1026 
   1027     Args:
   1028       revision_range_end: The SHA1 for the end of the range.
   1029       revision_range_start: The SHA1 for the beginning of the range.
   1030 
   1031     Returns:
   1032       A list of the revisions between |revision_range_start| and
   1033       |revision_range_end| (inclusive).
   1034     """
   1035     revision_range = '%s..%s' % (revision_range_start, revision_range_end)
   1036     cmd = ['log', '--format=%H', '-10000', '--first-parent', revision_range]
   1037     log_output = CheckRunGit(cmd, cwd=cwd)
   1038 
   1039     revision_hash_list = log_output.split()
   1040     revision_hash_list.append(revision_range_start)
   1041 
   1042     return revision_hash_list
   1043 
   1044   def SyncToRevision(self, revision, sync_client=None):
   1045     """Syncs to the specified revision.
   1046 
   1047     Args:
   1048       revision: The revision to sync to.
   1049       use_gclient: Specifies whether or not we should sync using gclient or
   1050         just use source control directly.
   1051 
   1052     Returns:
   1053       True if successful.
   1054     """
   1055 
   1056     if not sync_client:
   1057       results = RunGit(['checkout', revision])[1]
   1058     elif sync_client == 'gclient':
   1059       results = self.SyncToRevisionWithGClient(revision)
   1060     elif sync_client == 'repo':
   1061       results = self.SyncToRevisionWithRepo(revision)
   1062 
   1063     return not results
   1064 
   1065   def ResolveToRevision(self, revision_to_check, depot, search, cwd=None):
   1066     """If an SVN revision is supplied, try to resolve it to a git SHA1.
   1067 
   1068     Args:
   1069       revision_to_check: The user supplied revision string that may need to be
   1070         resolved to a git SHA1.
   1071       depot: The depot the revision_to_check is from.
   1072       search: The number of changelists to try if the first fails to resolve
   1073         to a git hash. If the value is negative, the function will search
   1074         backwards chronologically, otherwise it will search forward.
   1075 
   1076     Returns:
   1077       A string containing a git SHA1 hash, otherwise None.
   1078     """
   1079     # Android-chrome is git only, so no need to resolve this to anything else.
   1080     if depot == 'android-chrome':
   1081       return revision_to_check
   1082 
   1083     if depot != 'cros':
   1084       if not IsStringInt(revision_to_check):
   1085         return revision_to_check
   1086 
   1087       depot_svn = 'svn://svn.chromium.org/chrome/trunk/src'
   1088 
   1089       if depot != 'chromium':
   1090         depot_svn = DEPOT_DEPS_NAME[depot]['svn']
   1091 
   1092       svn_revision = int(revision_to_check)
   1093       git_revision = None
   1094 
   1095       if search > 0:
   1096         search_range = xrange(svn_revision, svn_revision + search, 1)
   1097       else:
   1098         search_range = xrange(svn_revision, svn_revision + search, -1)
   1099 
   1100       for i in search_range:
   1101         svn_pattern = 'git-svn-id: %s@%d' % (depot_svn, i)
   1102         cmd = ['log', '--format=%H', '-1', '--grep', svn_pattern,
   1103                'origin/master']
   1104 
   1105         (log_output, return_code) = RunGit(cmd, cwd=cwd)
   1106 
   1107         assert not return_code, 'An error occurred while running'\
   1108                                 ' "git %s"' % ' '.join(cmd)
   1109 
   1110         if not return_code:
   1111           log_output = log_output.strip()
   1112 
   1113           if log_output:
   1114             git_revision = log_output
   1115 
   1116             break
   1117 
   1118       return git_revision
   1119     else:
   1120       if IsStringInt(revision_to_check):
   1121         return int(revision_to_check)
   1122       else:
   1123         cwd = os.getcwd()
   1124         os.chdir(os.path.join(os.getcwd(), 'src', 'third_party',
   1125             'chromiumos-overlay'))
   1126         pattern = CROS_VERSION_PATTERN % revision_to_check
   1127         cmd = ['log', '--format=%ct', '-1', '--grep', pattern]
   1128 
   1129         git_revision = None
   1130 
   1131         log_output = CheckRunGit(cmd, cwd=cwd)
   1132         if log_output:
   1133           git_revision = log_output
   1134           git_revision = int(log_output.strip())
   1135         os.chdir(cwd)
   1136 
   1137         return git_revision
   1138 
   1139   def IsInProperBranch(self):
   1140     """Confirms they're in the master branch for performing the bisection.
   1141     This is needed or gclient will fail to sync properly.
   1142 
   1143     Returns:
   1144       True if the current branch on src is 'master'
   1145     """
   1146     cmd = ['rev-parse', '--abbrev-ref', 'HEAD']
   1147     log_output = CheckRunGit(cmd)
   1148     log_output = log_output.strip()
   1149 
   1150     return log_output == "master"
   1151 
   1152   def SVNFindRev(self, revision, cwd=None):
   1153     """Maps directly to the 'git svn find-rev' command.
   1154 
   1155     Args:
   1156       revision: The git SHA1 to use.
   1157 
   1158     Returns:
   1159       An integer changelist #, otherwise None.
   1160     """
   1161 
   1162     cmd = ['svn', 'find-rev', revision]
   1163 
   1164     output = CheckRunGit(cmd, cwd)
   1165     svn_revision = output.strip()
   1166 
   1167     if IsStringInt(svn_revision):
   1168       return int(svn_revision)
   1169 
   1170     return None
   1171 
   1172   def QueryRevisionInfo(self, revision, cwd=None):
   1173     """Gathers information on a particular revision, such as author's name,
   1174     email, subject, and date.
   1175 
   1176     Args:
   1177       revision: Revision you want to gather information on.
   1178     Returns:
   1179       A dict in the following format:
   1180       {
   1181         'author': %s,
   1182         'email': %s,
   1183         'date': %s,
   1184         'subject': %s,
   1185         'body': %s,
   1186       }
   1187     """
   1188     commit_info = {}
   1189 
   1190     formats = ['%cN', '%cE', '%s', '%cD', '%b']
   1191     targets = ['author', 'email', 'subject', 'date', 'body']
   1192 
   1193     for i in xrange(len(formats)):
   1194       cmd = ['log', '--format=%s' % formats[i], '-1', revision]
   1195       output = CheckRunGit(cmd, cwd=cwd)
   1196       commit_info[targets[i]] = output.rstrip()
   1197 
   1198     return commit_info
   1199 
   1200   def CheckoutFileAtRevision(self, file_name, revision, cwd=None):
   1201     """Performs a checkout on a file at the given revision.
   1202 
   1203     Returns:
   1204       True if successful.
   1205     """
   1206     return not RunGit(['checkout', revision, file_name], cwd=cwd)[1]
   1207 
   1208   def RevertFileToHead(self, file_name):
   1209     """Unstages a file and returns it to HEAD.
   1210 
   1211     Returns:
   1212       True if successful.
   1213     """
   1214     # Reset doesn't seem to return 0 on success.
   1215     RunGit(['reset', 'HEAD', file_name])
   1216 
   1217     return not RunGit(['checkout', bisect_utils.FILE_DEPS_GIT])[1]
   1218 
   1219   def QueryFileRevisionHistory(self, filename, revision_start, revision_end):
   1220     """Returns a list of commits that modified this file.
   1221 
   1222     Args:
   1223         filename: Name of file.
   1224         revision_start: Start of revision range.
   1225         revision_end: End of revision range.
   1226 
   1227     Returns:
   1228         Returns a list of commits that touched this file.
   1229     """
   1230     cmd = ['log', '--format=%H', '%s~1..%s' % (revision_start, revision_end),
   1231            filename]
   1232     output = CheckRunGit(cmd)
   1233 
   1234     return [o for o in output.split('\n') if o]
   1235 
   1236 
   1237 class BisectPerformanceMetrics(object):
   1238   """This class contains functionality to perform a bisection of a range of
   1239   revisions to narrow down where performance regressions may have occurred.
   1240 
   1241   The main entry-point is the Run method.
   1242   """
   1243 
   1244   def __init__(self, source_control, opts):
   1245     super(BisectPerformanceMetrics, self).__init__()
   1246 
   1247     self.opts = opts
   1248     self.source_control = source_control
   1249     self.src_cwd = os.getcwd()
   1250     self.cros_cwd = os.path.join(os.getcwd(), '..', 'cros')
   1251     self.depot_cwd = {}
   1252     self.cleanup_commands = []
   1253     self.warnings = []
   1254     self.builder = Builder.FromOpts(opts)
   1255 
   1256     # This always starts true since the script grabs latest first.
   1257     self.was_blink = True
   1258 
   1259     for d in DEPOT_NAMES:
   1260       # The working directory of each depot is just the path to the depot, but
   1261       # since we're already in 'src', we can skip that part.
   1262 
   1263       self.depot_cwd[d] = os.path.join(
   1264           self.src_cwd, DEPOT_DEPS_NAME[d]['src'][4:])
   1265 
   1266   def PerformCleanup(self):
   1267     """Performs cleanup when script is finished."""
   1268     os.chdir(self.src_cwd)
   1269     for c in self.cleanup_commands:
   1270       if c[0] == 'mv':
   1271         shutil.move(c[1], c[2])
   1272       else:
   1273         assert False, 'Invalid cleanup command.'
   1274 
   1275   def GetRevisionList(self, depot, bad_revision, good_revision):
   1276     """Retrieves a list of all the commits between the bad revision and
   1277     last known good revision."""
   1278 
   1279     revision_work_list = []
   1280 
   1281     if depot == 'cros':
   1282       revision_range_start = good_revision
   1283       revision_range_end = bad_revision
   1284 
   1285       cwd = os.getcwd()
   1286       self.ChangeToDepotWorkingDirectory('cros')
   1287 
   1288       # Print the commit timestamps for every commit in the revision time
   1289       # range. We'll sort them and bisect by that. There is a remote chance that
   1290       # 2 (or more) commits will share the exact same timestamp, but it's
   1291       # probably safe to ignore that case.
   1292       cmd = ['repo', 'forall', '-c',
   1293           'git log --format=%%ct --before=%d --after=%d' % (
   1294           revision_range_end, revision_range_start)]
   1295       (output, return_code) = RunProcessAndRetrieveOutput(cmd)
   1296 
   1297       assert not return_code, 'An error occurred while running'\
   1298                               ' "%s"' % ' '.join(cmd)
   1299 
   1300       os.chdir(cwd)
   1301 
   1302       revision_work_list = list(set(
   1303           [int(o) for o in output.split('\n') if IsStringInt(o)]))
   1304       revision_work_list = sorted(revision_work_list, reverse=True)
   1305     else:
   1306       cwd = self._GetDepotDirectory(depot)
   1307       revision_work_list = self.source_control.GetRevisionList(bad_revision,
   1308           good_revision, cwd=cwd)
   1309 
   1310     return revision_work_list
   1311 
   1312   def _GetV8BleedingEdgeFromV8TrunkIfMappable(self, revision):
   1313     svn_revision = self.source_control.SVNFindRev(revision)
   1314 
   1315     if IsStringInt(svn_revision):
   1316       # V8 is tricky to bisect, in that there are only a few instances when
   1317       # we can dive into bleeding_edge and get back a meaningful result.
   1318       # Try to detect a V8 "business as usual" case, which is when:
   1319       #  1. trunk revision N has description "Version X.Y.Z"
   1320       #  2. bleeding_edge revision (N-1) has description "Prepare push to
   1321       #     trunk. Now working on X.Y.(Z+1)."
   1322       #
   1323       # As of 01/24/2014, V8 trunk descriptions are formatted:
   1324       # "Version 3.X.Y (based on bleeding_edge revision rZ)"
   1325       # So we can just try parsing that out first and fall back to the old way.
   1326       v8_dir = self._GetDepotDirectory('v8')
   1327       v8_bleeding_edge_dir = self._GetDepotDirectory('v8_bleeding_edge')
   1328 
   1329       revision_info = self.source_control.QueryRevisionInfo(revision,
   1330           cwd=v8_dir)
   1331 
   1332       version_re = re.compile("Version (?P<values>[0-9,.]+)")
   1333 
   1334       regex_results = version_re.search(revision_info['subject'])
   1335 
   1336       if regex_results:
   1337         git_revision = None
   1338 
   1339         # Look for "based on bleeding_edge" and parse out revision
   1340         if 'based on bleeding_edge' in revision_info['subject']:
   1341           try:
   1342             bleeding_edge_revision = revision_info['subject'].split(
   1343                 'bleeding_edge revision r')[1]
   1344             bleeding_edge_revision = int(bleeding_edge_revision.split(')')[0])
   1345             git_revision = self.source_control.ResolveToRevision(
   1346                 bleeding_edge_revision, 'v8_bleeding_edge', 1,
   1347                 cwd=v8_bleeding_edge_dir)
   1348             return git_revision
   1349           except (IndexError, ValueError):
   1350             pass
   1351 
   1352         if not git_revision:
   1353           # Wasn't successful, try the old way of looking for "Prepare push to"
   1354           git_revision = self.source_control.ResolveToRevision(
   1355               int(svn_revision) - 1, 'v8_bleeding_edge', -1,
   1356               cwd=v8_bleeding_edge_dir)
   1357 
   1358           if git_revision:
   1359             revision_info = self.source_control.QueryRevisionInfo(git_revision,
   1360                 cwd=v8_bleeding_edge_dir)
   1361 
   1362             if 'Prepare push to trunk' in revision_info['subject']:
   1363               return git_revision
   1364     return None
   1365 
   1366   def _GetNearestV8BleedingEdgeFromTrunk(self, revision, search_forward=True):
   1367     cwd = self._GetDepotDirectory('v8')
   1368     cmd = ['log', '--format=%ct', '-1', revision]
   1369     output = CheckRunGit(cmd, cwd=cwd)
   1370     commit_time = int(output)
   1371     commits = []
   1372 
   1373     if search_forward:
   1374       cmd = ['log', '--format=%H', '-10', '--after=%d' % commit_time,
   1375           'origin/master']
   1376       output = CheckRunGit(cmd, cwd=cwd)
   1377       output = output.split()
   1378       commits = output
   1379       commits = reversed(commits)
   1380     else:
   1381       cmd = ['log', '--format=%H', '-10', '--before=%d' % commit_time,
   1382           'origin/master']
   1383       output = CheckRunGit(cmd, cwd=cwd)
   1384       output = output.split()
   1385       commits = output
   1386 
   1387     bleeding_edge_revision = None
   1388 
   1389     for c in commits:
   1390       bleeding_edge_revision = self._GetV8BleedingEdgeFromV8TrunkIfMappable(c)
   1391       if bleeding_edge_revision:
   1392         break
   1393 
   1394     return bleeding_edge_revision
   1395 
   1396   def _ParseRevisionsFromDEPSFileManually(self, deps_file_contents):
   1397     """Manually parses the vars section of the DEPS file to determine
   1398     chromium/blink/etc... revisions.
   1399 
   1400     Returns:
   1401       A dict in the format {depot:revision} if successful, otherwise None.
   1402     """
   1403     # We'll parse the "vars" section of the DEPS file.
   1404     rxp = re.compile('vars = {(?P<vars_body>[^}]+)', re.MULTILINE)
   1405     re_results = rxp.search(deps_file_contents)
   1406     locals = {}
   1407 
   1408     if not re_results:
   1409       return None
   1410 
   1411     # We should be left with a series of entries in the vars component of
   1412     # the DEPS file with the following format:
   1413     # 'depot_name': 'revision',
   1414     vars_body = re_results.group('vars_body')
   1415     rxp = re.compile("'(?P<depot_body>[\w_-]+)':[\s]+'(?P<rev_body>[\w@]+)'",
   1416                      re.MULTILINE)
   1417     re_results = rxp.findall(vars_body)
   1418 
   1419     return dict(re_results)
   1420 
   1421   def _ParseRevisionsFromDEPSFile(self, depot):
   1422     """Parses the local DEPS file to determine blink/skia/v8 revisions which may
   1423     be needed if the bisect recurses into those depots later.
   1424 
   1425     Args:
   1426       depot: Depot being bisected.
   1427 
   1428     Returns:
   1429       A dict in the format {depot:revision} if successful, otherwise None.
   1430     """
   1431     try:
   1432       deps_data = {'Var': lambda _: deps_data["vars"][_],
   1433                    'From': lambda *args: None
   1434                   }
   1435       execfile(bisect_utils.FILE_DEPS_GIT, {}, deps_data)
   1436       deps_data = deps_data['deps']
   1437 
   1438       rxp = re.compile(".git@(?P<revision>[a-fA-F0-9]+)")
   1439       results = {}
   1440       for depot_name, depot_data in DEPOT_DEPS_NAME.iteritems():
   1441         if (depot_data.get('platform') and
   1442             depot_data.get('platform') != os.name):
   1443           continue
   1444 
   1445         if (depot_data.get('recurse') and depot in depot_data.get('from')):
   1446           depot_data_src = depot_data.get('src') or depot_data.get('src_old')
   1447           src_dir = deps_data.get(depot_data_src)
   1448           if src_dir:
   1449             self.depot_cwd[depot_name] = os.path.join(self.src_cwd,
   1450                                                       depot_data_src[4:])
   1451             re_results = rxp.search(src_dir)
   1452             if re_results:
   1453               results[depot_name] = re_results.group('revision')
   1454             else:
   1455               warning_text = ('Couldn\'t parse revision for %s while bisecting '
   1456                               '%s' % (depot_name, depot))
   1457               if not warning_text in self.warnings:
   1458                 self.warnings.append(warning_text)
   1459           else:
   1460             results[depot_name] = None
   1461       return results
   1462     except ImportError:
   1463       deps_file_contents = ReadStringFromFile(bisect_utils.FILE_DEPS_GIT)
   1464       parse_results = self._ParseRevisionsFromDEPSFileManually(
   1465           deps_file_contents)
   1466       results = {}
   1467       for depot_name, depot_revision in parse_results.iteritems():
   1468         depot_revision = depot_revision.strip('@')
   1469         print depot_name, depot_revision
   1470         for current_name, current_data in DEPOT_DEPS_NAME.iteritems():
   1471           if (current_data.has_key('deps_var') and
   1472               current_data['deps_var'] == depot_name):
   1473             src_name = current_name
   1474             results[src_name] = depot_revision
   1475             break
   1476       return results
   1477 
   1478   def Get3rdPartyRevisionsFromCurrentRevision(self, depot, revision):
   1479     """Parses the DEPS file to determine WebKit/v8/etc... versions.
   1480 
   1481     Returns:
   1482       A dict in the format {depot:revision} if successful, otherwise None.
   1483     """
   1484     cwd = os.getcwd()
   1485     self.ChangeToDepotWorkingDirectory(depot)
   1486 
   1487     results = {}
   1488 
   1489     if depot == 'chromium' or depot == 'android-chrome':
   1490       results = self._ParseRevisionsFromDEPSFile(depot)
   1491       os.chdir(cwd)
   1492     elif depot == 'cros':
   1493       cmd = [CROS_SDK_PATH, '--', 'portageq-%s' % self.opts.cros_board,
   1494              'best_visible', '/build/%s' % self.opts.cros_board, 'ebuild',
   1495              CROS_CHROMEOS_PATTERN]
   1496       (output, return_code) = RunProcessAndRetrieveOutput(cmd)
   1497 
   1498       assert not return_code, 'An error occurred while running' \
   1499                               ' "%s"' % ' '.join(cmd)
   1500 
   1501       if len(output) > CROS_CHROMEOS_PATTERN:
   1502         output = output[len(CROS_CHROMEOS_PATTERN):]
   1503 
   1504       if len(output) > 1:
   1505         output = output.split('_')[0]
   1506 
   1507         if len(output) > 3:
   1508           contents = output.split('.')
   1509 
   1510           version = contents[2]
   1511 
   1512           if contents[3] != '0':
   1513             warningText = 'Chrome version: %s.%s but using %s.0 to bisect.' % \
   1514                 (version, contents[3], version)
   1515             if not warningText in self.warnings:
   1516               self.warnings.append(warningText)
   1517 
   1518           cwd = os.getcwd()
   1519           self.ChangeToDepotWorkingDirectory('chromium')
   1520           return_code = CheckRunGit(['log', '-1', '--format=%H',
   1521               '--author=chrome-release (at] google.com', '--grep=to %s' % version,
   1522               'origin/master'])
   1523           os.chdir(cwd)
   1524 
   1525           results['chromium'] = output.strip()
   1526     elif depot == 'v8':
   1527       # We can't try to map the trunk revision to bleeding edge yet, because
   1528       # we don't know which direction to try to search in. Have to wait until
   1529       # the bisect has narrowed the results down to 2 v8 rolls.
   1530       results['v8_bleeding_edge'] = None
   1531 
   1532     return results
   1533 
   1534   def BackupOrRestoreOutputdirectory(self, restore=False, build_type='Release'):
   1535     """Backs up or restores build output directory based on restore argument.
   1536 
   1537     Args:
   1538       restore: Indicates whether to restore or backup. Default is False(Backup)
   1539       build_type: Target build type ('Release', 'Debug', 'Release_x64' etc.)
   1540 
   1541     Returns:
   1542       Path to backup or restored location as string. otherwise None if it fails.
   1543     """
   1544     build_dir = os.path.abspath(
   1545         self.builder.GetBuildOutputDirectory(self.opts, self.src_cwd))
   1546     source_dir = os.path.join(build_dir, build_type)
   1547     destination_dir = os.path.join(build_dir, '%s.bak' % build_type)
   1548     if restore:
   1549       source_dir, destination_dir = destination_dir, source_dir
   1550     if os.path.exists(source_dir):
   1551       RmTreeAndMkDir(destination_dir, skip_makedir=True)
   1552       shutil.move(source_dir, destination_dir)
   1553       return destination_dir
   1554     return None
   1555 
   1556   def GetBuildArchiveForRevision(self, revision, gs_bucket, target_arch,
   1557                                  patch_sha, out_dir):
   1558     """Checks and downloads build archive for a given revision.
   1559 
   1560     Checks for build archive with Git hash or SVN revision. If either of the
   1561     file exists, then downloads the archive file.
   1562 
   1563     Args:
   1564       revision: A Git hash revision.
   1565       gs_bucket: Cloud storage bucket name
   1566       target_arch: 32 or 64 bit build target
   1567       patch: A DEPS patch (used while bisecting 3rd party repositories).
   1568       out_dir: Build output directory where downloaded file is stored.
   1569 
   1570     Returns:
   1571       Downloaded archive file path if exists, otherwise None.
   1572     """
   1573     # Source archive file path on cloud storage using Git revision.
   1574     source_file = GetRemoteBuildPath(revision, target_arch, patch_sha)
   1575     downloaded_archive = FetchFromCloudStorage(gs_bucket, source_file, out_dir)
   1576     if not downloaded_archive:
   1577       # Get SVN revision for the given SHA.
   1578       svn_revision = self.source_control.SVNFindRev(revision)
   1579       if svn_revision:
   1580         # Source archive file path on cloud storage using SVN revision.
   1581         source_file = GetRemoteBuildPath(svn_revision, target_arch, patch_sha)
   1582         return FetchFromCloudStorage(gs_bucket, source_file, out_dir)
   1583     return downloaded_archive
   1584 
   1585   def DownloadCurrentBuild(self, revision, build_type='Release', patch=None):
   1586     """Downloads the build archive for the given revision.
   1587 
   1588     Args:
   1589       revision: The Git revision to download or build.
   1590       build_type: Target build type ('Release', 'Debug', 'Release_x64' etc.)
   1591       patch: A DEPS patch (used while bisecting 3rd party repositories).
   1592 
   1593     Returns:
   1594       True if download succeeds, otherwise False.
   1595     """
   1596     patch_sha = None
   1597     if patch:
   1598       # Get the SHA of the DEPS changes patch.
   1599       patch_sha = GetSHA1HexDigest(patch)
   1600 
   1601       # Update the DEPS changes patch with a patch to create a new file named
   1602       # 'DEPS.sha' and add patch_sha evaluated above to it.
   1603       patch = '%s\n%s' % (patch, DEPS_SHA_PATCH % {'deps_sha': patch_sha})
   1604 
   1605     # Get Build output directory
   1606     abs_build_dir = os.path.abspath(
   1607         self.builder.GetBuildOutputDirectory(self.opts, self.src_cwd))
   1608 
   1609     fetch_build_func = lambda: self.GetBuildArchiveForRevision(
   1610       revision, self.opts.gs_bucket, self.opts.target_arch,
   1611       patch_sha, abs_build_dir)
   1612 
   1613     # Downloaded archive file path, downloads build archive for given revision.
   1614     downloaded_file = fetch_build_func()
   1615 
   1616     # When build archive doesn't exists, post a build request to tryserver
   1617     # and wait for the build to be produced.
   1618     if not downloaded_file:
   1619       downloaded_file = self.PostBuildRequestAndWait(
   1620           revision, fetch_build=fetch_build_func, patch=patch)
   1621       if not downloaded_file:
   1622         return False
   1623 
   1624     # Generic name for the archive, created when archive file is extracted.
   1625     output_dir = os.path.join(
   1626         abs_build_dir, GetZipFileName(target_arch=self.opts.target_arch))
   1627     # Unzip build archive directory.
   1628     try:
   1629       RmTreeAndMkDir(output_dir, skip_makedir=True)
   1630       ExtractZip(downloaded_file, abs_build_dir)
   1631       if os.path.exists(output_dir):
   1632         self.BackupOrRestoreOutputdirectory(restore=False)
   1633         # Build output directory based on target(e.g. out/Release, out/Debug).
   1634         target_build_output_dir = os.path.join(abs_build_dir, build_type)
   1635         print 'Moving build from %s to %s' % (
   1636             output_dir, target_build_output_dir)
   1637         shutil.move(output_dir, target_build_output_dir)
   1638         return True
   1639       raise IOError('Missing extracted folder %s ' % output_dir)
   1640     except Exception as e:
   1641       print 'Somewthing went wrong while extracting archive file: %s' % e
   1642       self.BackupOrRestoreOutputdirectory(restore=True)
   1643       # Cleanup any leftovers from unzipping.
   1644       if os.path.exists(output_dir):
   1645         RmTreeAndMkDir(output_dir, skip_makedir=True)
   1646     finally:
   1647       # Delete downloaded archive
   1648       if os.path.exists(downloaded_file):
   1649         os.remove(downloaded_file)
   1650     return False
   1651 
   1652   def WaitUntilBuildIsReady(self, fetch_build, bot_name, builder_host,
   1653                             builder_port, build_request_id, max_timeout):
   1654     """Waits until build is produced by bisect builder on tryserver.
   1655 
   1656     Args:
   1657       fetch_build: Function to check and download build from cloud storage.
   1658       bot_name: Builder bot name on tryserver.
   1659       builder_host Tryserver hostname.
   1660       builder_port: Tryserver port.
   1661       build_request_id: A unique ID of the build request posted to tryserver.
   1662       max_timeout: Maximum time to wait for the build.
   1663 
   1664     Returns:
   1665        Downloaded archive file path if exists, otherwise None.
   1666     """
   1667     # Build number on the tryserver.
   1668     build_num = None
   1669     # Interval to check build on cloud storage.
   1670     poll_interval = 60
   1671     # Interval to check build status on tryserver.
   1672     status_check_interval = 600
   1673     last_status_check = time.time()
   1674     start_time = time.time()
   1675     while True:
   1676       # Checks for build on gs://chrome-perf and download if exists.
   1677       res = fetch_build()
   1678       if res:
   1679         return (res, 'Build successfully found')
   1680       elapsed_status_check = time.time() - last_status_check
   1681       # To avoid overloading tryserver with status check requests, we check
   1682       # build status for every 10 mins.
   1683       if elapsed_status_check > status_check_interval:
   1684         last_status_check = time.time()
   1685         if not build_num:
   1686           # Get the build number on tryserver for the current build.
   1687           build_num = bisect_builder.GetBuildNumFromBuilder(
   1688               build_request_id, bot_name, builder_host, builder_port)
   1689         # Check the status of build using the build number.
   1690         # Note: Build is treated as PENDING if build number is not found
   1691         # on the the tryserver.
   1692         build_status, status_link = bisect_builder.GetBuildStatus(
   1693             build_num, bot_name, builder_host, builder_port)
   1694         if build_status == bisect_builder.FAILED:
   1695           return (None, 'Failed to produce build, log: %s' % status_link)
   1696       elapsed_time = time.time() - start_time
   1697       if elapsed_time > max_timeout:
   1698         return (None, 'Timed out: %ss without build' % max_timeout)
   1699 
   1700       print 'Time elapsed: %ss without build.' % elapsed_time
   1701       time.sleep(poll_interval)
   1702 
   1703   def PostBuildRequestAndWait(self, revision, fetch_build, patch=None):
   1704     """POSTs the build request job to the tryserver instance.
   1705 
   1706     A try job build request is posted to tryserver.chromium.perf master,
   1707     and waits for the binaries to be produced and archived on cloud storage.
   1708     Once the build is ready and stored onto cloud, build archive is downloaded
   1709     into the output folder.
   1710 
   1711     Args:
   1712       revision: A Git hash revision.
   1713       fetch_build: Function to check and download build from cloud storage.
   1714       patch: A DEPS patch (used while bisecting 3rd party repositories).
   1715 
   1716     Returns:
   1717       Downloaded archive file path when requested build exists and download is
   1718       successful, otherwise None.
   1719     """
   1720     # Get SVN revision for the given SHA.
   1721     svn_revision = self.source_control.SVNFindRev(revision)
   1722     if not svn_revision:
   1723       raise RuntimeError(
   1724           'Failed to determine SVN revision for %s' % revision)
   1725 
   1726     def GetBuilderNameAndBuildTime(target_arch='ia32'):
   1727       """Gets builder bot name and buildtime in seconds based on platform."""
   1728       # Bot names should match the one listed in tryserver.chromium's
   1729       # master.cfg which produces builds for bisect.
   1730       if IsWindows():
   1731         if Is64BitWindows() and target_arch == 'x64':
   1732           return ('win_perf_bisect_builder', MAX_WIN_BUILD_TIME)
   1733         return ('win_perf_bisect_builder', MAX_WIN_BUILD_TIME)
   1734       if IsLinux():
   1735         return ('linux_perf_bisect_builder', MAX_LINUX_BUILD_TIME)
   1736       if IsMac():
   1737         return ('mac_perf_bisect_builder', MAX_MAC_BUILD_TIME)
   1738       raise NotImplementedError('Unsupported Platform "%s".' % sys.platform)
   1739     if not fetch_build:
   1740       return False
   1741 
   1742     bot_name, build_timeout = GetBuilderNameAndBuildTime(self.opts.target_arch)
   1743     builder_host = self.opts.builder_host
   1744     builder_port = self.opts.builder_port
   1745     # Create a unique ID for each build request posted to tryserver builders.
   1746     # This ID is added to "Reason" property in build's json.
   1747     build_request_id = GetSHA1HexDigest(
   1748         '%s-%s-%s' % (svn_revision, patch, time.time()))
   1749 
   1750     # Creates a try job description.
   1751     job_args = {'host': builder_host,
   1752                 'port': builder_port,
   1753                 'revision': 'src@%s' % svn_revision,
   1754                 'bot': bot_name,
   1755                 'name': build_request_id
   1756                }
   1757     # Update patch information if supplied.
   1758     if patch:
   1759       job_args['patch'] = patch
   1760     # Posts job to build the revision on the server.
   1761     if bisect_builder.PostTryJob(job_args):
   1762       target_file, error_msg = self.WaitUntilBuildIsReady(fetch_build,
   1763                                                           bot_name,
   1764                                                           builder_host,
   1765                                                           builder_port,
   1766                                                           build_request_id,
   1767                                                           build_timeout)
   1768       if not target_file:
   1769         print '%s [revision: %s]' % (error_msg, svn_revision)
   1770         return None
   1771       return target_file
   1772     print 'Failed to post build request for revision: [%s]' % svn_revision
   1773     return None
   1774 
   1775   def IsDownloadable(self, depot):
   1776     """Checks if build is downloadable based on target platform and depot."""
   1777     if self.opts.target_platform in ['chromium'] and self.opts.gs_bucket:
   1778       return (depot == 'chromium' or
   1779               'chromium' in DEPOT_DEPS_NAME[depot]['from'] or
   1780               'v8' in DEPOT_DEPS_NAME[depot]['from'])
   1781     return False
   1782 
   1783   def UpdateDeps(self, revision, depot, deps_file):
   1784     """Updates DEPS file with new revision of dependency repository.
   1785 
   1786     This method search DEPS for a particular pattern in which depot revision
   1787     is specified (e.g "webkit_revision": "123456"). If a match is found then
   1788     it resolves the given git hash to SVN revision and replace it in DEPS file.
   1789 
   1790     Args:
   1791       revision: A git hash revision of the dependency repository.
   1792       depot: Current depot being bisected.
   1793       deps_file: Path to DEPS file.
   1794 
   1795     Returns:
   1796       True if DEPS file is modified successfully, otherwise False.
   1797     """
   1798     if not os.path.exists(deps_file):
   1799       return False
   1800 
   1801     deps_var = DEPOT_DEPS_NAME[depot]['deps_var']
   1802     # Don't update DEPS file if deps_var is not set in DEPOT_DEPS_NAME.
   1803     if not deps_var:
   1804       print 'DEPS update not supported for Depot: %s', depot
   1805       return False
   1806 
   1807     # Hack to Angle repository because, in DEPS file "vars" dictionary variable
   1808     # contains "angle_revision" key that holds git hash instead of SVN revision.
   1809     # And sometime "angle_revision" key is not specified in "vars" variable,
   1810     # in such cases check "deps" dictionary variable that matches
   1811     # angle.git@[a-fA-F0-9]{40}$ and replace git hash.
   1812     if depot == 'angle':
   1813       return self.UpdateDEPSForAngle(revision, depot, deps_file)
   1814 
   1815     try:
   1816       deps_contents = ReadStringFromFile(deps_file)
   1817       # Check whether the depot and revision pattern in DEPS file vars
   1818       # e.g. for webkit the format is "webkit_revision": "12345".
   1819       deps_revision = re.compile(r'(?<="%s": ")([0-9]+)(?=")' % deps_var,
   1820                                  re.MULTILINE)
   1821       match = re.search(deps_revision, deps_contents)
   1822       if match:
   1823         svn_revision = self.source_control.SVNFindRev(
   1824             revision, self._GetDepotDirectory(depot))
   1825         if not svn_revision:
   1826           print 'Could not determine SVN revision for %s' % revision
   1827           return False
   1828         # Update the revision information for the given depot
   1829         new_data = re.sub(deps_revision, str(svn_revision), deps_contents)
   1830 
   1831         # For v8_bleeding_edge revisions change V8 branch in order
   1832         # to fetch bleeding edge revision.
   1833         if depot == 'v8_bleeding_edge':
   1834           new_data = self.UpdateV8Branch(new_data)
   1835           if not new_data:
   1836             return False
   1837         # Write changes to DEPS file
   1838         WriteStringToFile(new_data, deps_file)
   1839         return True
   1840     except IOError, e:
   1841       print 'Something went wrong while updating DEPS file. [%s]' % e
   1842     return False
   1843 
   1844   def UpdateV8Branch(self, deps_content):
   1845     """Updates V8 branch in DEPS file to process v8_bleeding_edge.
   1846 
   1847     Check for "v8_branch" in DEPS file if exists update its value
   1848     with v8_bleeding_edge branch. Note: "v8_branch" is added to DEPS
   1849     variable from DEPS revision 254916, therefore check for "src/v8":
   1850     <v8 source path> in DEPS in order to support prior DEPS revisions
   1851     and update it.
   1852 
   1853     Args:
   1854       deps_content: DEPS file contents to be modified.
   1855 
   1856     Returns:
   1857       Modified DEPS file contents as a string.
   1858     """
   1859     new_branch = r'branches/bleeding_edge'
   1860     v8_branch_pattern = re.compile(r'(?<="v8_branch": ")(.*)(?=")')
   1861     if re.search(v8_branch_pattern, deps_content):
   1862       deps_content = re.sub(v8_branch_pattern, new_branch, deps_content)
   1863     else:
   1864       # Replaces the branch assigned to "src/v8" key in DEPS file.
   1865       # Format of "src/v8" in DEPS:
   1866       # "src/v8":
   1867       #    (Var("googlecode_url") % "v8") + "/trunk@" + Var("v8_revision"),
   1868       # So, "/trunk@" is replace with "/branches/bleeding_edge@"
   1869       v8_src_pattern = re.compile(
   1870           r'(?<="v8"\) \+ "/)(.*)(?=@" \+ Var\("v8_revision"\))', re.MULTILINE)
   1871       if re.search(v8_src_pattern, deps_content):
   1872         deps_content = re.sub(v8_src_pattern, new_branch, deps_content)
   1873     return deps_content
   1874 
   1875   def UpdateDEPSForAngle(self, revision, depot, deps_file):
   1876     """Updates DEPS file with new revision for Angle repository.
   1877 
   1878     This is a hack for Angle depot case because, in DEPS file "vars" dictionary
   1879     variable contains "angle_revision" key that holds git hash instead of
   1880     SVN revision.
   1881 
   1882     And sometimes "angle_revision" key is not specified in "vars" variable,
   1883     in such cases check "deps" dictionary variable that matches
   1884     angle.git@[a-fA-F0-9]{40}$ and replace git hash.
   1885     """
   1886     deps_var = DEPOT_DEPS_NAME[depot]['deps_var']
   1887     try:
   1888       deps_contents = ReadStringFromFile(deps_file)
   1889       # Check whether the depot and revision pattern in DEPS file vars variable
   1890       # e.g. "angle_revision": "fa63e947cb3eccf463648d21a05d5002c9b8adfa".
   1891       angle_rev_pattern = re.compile(r'(?<="%s": ")([a-fA-F0-9]{40})(?=")' %
   1892                                      deps_var, re.MULTILINE)
   1893       match = re.search(angle_rev_pattern % deps_var, deps_contents)
   1894       if match:
   1895         # Update the revision information for the given depot
   1896         new_data = re.sub(angle_rev_pattern, revision, deps_contents)
   1897       else:
   1898         # Check whether the depot and revision pattern in DEPS file deps
   1899         # variable. e.g.,
   1900         # "src/third_party/angle": Var("chromium_git") +
   1901         # "/angle/angle.git@fa63e947cb3eccf463648d21a05d5002c9b8adfa",.
   1902         angle_rev_pattern = re.compile(
   1903             r'(?<=angle\.git@)([a-fA-F0-9]{40})(?=")', re.MULTILINE)
   1904         match = re.search(angle_rev_pattern, deps_contents)
   1905         if not match:
   1906           print 'Could not find angle revision information in DEPS file.'
   1907           return False
   1908         new_data = re.sub(angle_rev_pattern, revision, deps_contents)
   1909       # Write changes to DEPS file
   1910       WriteStringToFile(new_data, deps_file)
   1911       return True
   1912     except IOError, e:
   1913       print 'Something went wrong while updating DEPS file, %s' % e
   1914     return False
   1915 
   1916   def CreateDEPSPatch(self, depot, revision):
   1917     """Modifies DEPS and returns diff as text.
   1918 
   1919     Args:
   1920       depot: Current depot being bisected.
   1921       revision: A git hash revision of the dependency repository.
   1922 
   1923     Returns:
   1924       A tuple with git hash of chromium revision and DEPS patch text.
   1925     """
   1926     deps_file_path = os.path.join(self.src_cwd, bisect_utils.FILE_DEPS)
   1927     if not os.path.exists(deps_file_path):
   1928       raise RuntimeError('DEPS file does not exists.[%s]' % deps_file_path)
   1929     # Get current chromium revision (git hash).
   1930     chromium_sha = CheckRunGit(['rev-parse', 'HEAD']).strip()
   1931     if not chromium_sha:
   1932       raise RuntimeError('Failed to determine Chromium revision for %s' %
   1933                          revision)
   1934     if ('chromium' in DEPOT_DEPS_NAME[depot]['from'] or
   1935         'v8' in DEPOT_DEPS_NAME[depot]['from']):
   1936       # Checkout DEPS file for the current chromium revision.
   1937       if self.source_control.CheckoutFileAtRevision(bisect_utils.FILE_DEPS,
   1938                                                     chromium_sha,
   1939                                                     cwd=self.src_cwd):
   1940         if self.UpdateDeps(revision, depot, deps_file_path):
   1941           diff_command = ['diff',
   1942                           '--src-prefix=src/',
   1943                           '--dst-prefix=src/',
   1944                           '--no-ext-diff',
   1945                            bisect_utils.FILE_DEPS]
   1946           diff_text = CheckRunGit(diff_command, cwd=self.src_cwd)
   1947           return (chromium_sha, ChangeBackslashToSlashInPatch(diff_text))
   1948         else:
   1949           raise RuntimeError('Failed to update DEPS file for chromium: [%s]' %
   1950                              chromium_sha)
   1951       else:
   1952         raise RuntimeError('DEPS checkout Failed for chromium revision : [%s]' %
   1953                            chromium_sha)
   1954     return (None, None)
   1955 
   1956   def BuildCurrentRevision(self, depot, revision=None):
   1957     """Builds chrome and performance_ui_tests on the current revision.
   1958 
   1959     Returns:
   1960       True if the build was successful.
   1961     """
   1962     if self.opts.debug_ignore_build:
   1963       return True
   1964     cwd = os.getcwd()
   1965     os.chdir(self.src_cwd)
   1966     # Fetch build archive for the given revision from the cloud storage when
   1967     # the storage bucket is passed.
   1968     if self.IsDownloadable(depot) and revision:
   1969       deps_patch = None
   1970       if depot != 'chromium':
   1971         # Create a DEPS patch with new revision for dependency repository.
   1972         (revision, deps_patch) = self.CreateDEPSPatch(depot, revision)
   1973       if self.DownloadCurrentBuild(revision, patch=deps_patch):
   1974         os.chdir(cwd)
   1975         if deps_patch:
   1976           # Reverts the changes to DEPS file.
   1977           self.source_control.CheckoutFileAtRevision(bisect_utils.FILE_DEPS,
   1978                                                      revision,
   1979                                                      cwd=self.src_cwd)
   1980         return True
   1981       return False
   1982 
   1983     # These codes are executed when bisect bots builds binaries locally.
   1984     build_success = self.builder.Build(depot, self.opts)
   1985     os.chdir(cwd)
   1986     return build_success
   1987 
   1988   def RunGClientHooks(self):
   1989     """Runs gclient with runhooks command.
   1990 
   1991     Returns:
   1992       True if gclient reports no errors.
   1993     """
   1994 
   1995     if self.opts.debug_ignore_build:
   1996       return True
   1997 
   1998     return not bisect_utils.RunGClient(['runhooks'], cwd=self.src_cwd)
   1999 
   2000   def TryParseHistogramValuesFromOutput(self, metric, text):
   2001     """Attempts to parse a metric in the format HISTOGRAM <graph: <trace>.
   2002 
   2003     Args:
   2004       metric: The metric as a list of [<trace>, <value>] strings.
   2005       text: The text to parse the metric values from.
   2006 
   2007     Returns:
   2008       A list of floating point numbers found.
   2009     """
   2010     metric_formatted = 'HISTOGRAM %s: %s= ' % (metric[0], metric[1])
   2011 
   2012     text_lines = text.split('\n')
   2013     values_list = []
   2014 
   2015     for current_line in text_lines:
   2016       if metric_formatted in current_line:
   2017         current_line = current_line[len(metric_formatted):]
   2018 
   2019         try:
   2020           histogram_values = eval(current_line)
   2021 
   2022           for b in histogram_values['buckets']:
   2023             average_for_bucket = float(b['high'] + b['low']) * 0.5
   2024             # Extends the list with N-elements with the average for that bucket.
   2025             values_list.extend([average_for_bucket] * b['count'])
   2026         except:
   2027           pass
   2028 
   2029     return values_list
   2030 
   2031   def TryParseResultValuesFromOutput(self, metric, text):
   2032     """Attempts to parse a metric in the format RESULT <graph>: <trace>= ...
   2033 
   2034     Args:
   2035       metric: The metric as a list of [<trace>, <value>] strings.
   2036       text: The text to parse the metric values from.
   2037 
   2038     Returns:
   2039       A list of floating point numbers found.
   2040     """
   2041     # Format is: RESULT <graph>: <trace>= <value> <units>
   2042     metric_re = re.escape('RESULT %s: %s=' % (metric[0], metric[1]))
   2043 
   2044     # The log will be parsed looking for format:
   2045     # <*>RESULT <graph_name>: <trace_name>= <value>
   2046     single_result_re = re.compile(
   2047         metric_re + '\s*(?P<VALUE>[-]?\d*(\.\d*)?)')
   2048 
   2049     # The log will be parsed looking for format:
   2050     # <*>RESULT <graph_name>: <trace_name>= [<value>,value,value,...]
   2051     multi_results_re = re.compile(
   2052         metric_re + '\s*\[\s*(?P<VALUES>[-]?[\d\., ]+)\s*\]')
   2053 
   2054     # The log will be parsed looking for format:
   2055     # <*>RESULT <graph_name>: <trace_name>= {<mean>, <std deviation>}
   2056     mean_stddev_re = re.compile(
   2057         metric_re +
   2058         '\s*\{\s*(?P<MEAN>[-]?\d*(\.\d*)?),\s*(?P<STDDEV>\d+(\.\d*)?)\s*\}')
   2059 
   2060     text_lines = text.split('\n')
   2061     values_list = []
   2062     for current_line in text_lines:
   2063       # Parse the output from the performance test for the metric we're
   2064       # interested in.
   2065       single_result_match = single_result_re.search(current_line)
   2066       multi_results_match = multi_results_re.search(current_line)
   2067       mean_stddev_match = mean_stddev_re.search(current_line)
   2068       if (not single_result_match is None and
   2069           single_result_match.group('VALUE')):
   2070         values_list += [single_result_match.group('VALUE')]
   2071       elif (not multi_results_match is None and
   2072             multi_results_match.group('VALUES')):
   2073         metric_values = multi_results_match.group('VALUES')
   2074         values_list += metric_values.split(',')
   2075       elif (not mean_stddev_match is None and
   2076             mean_stddev_match.group('MEAN')):
   2077         values_list += [mean_stddev_match.group('MEAN')]
   2078 
   2079     values_list = [float(v) for v in values_list if IsStringFloat(v)]
   2080 
   2081     # If the metric is times/t, we need to sum the timings in order to get
   2082     # similar regression results as the try-bots.
   2083     metrics_to_sum = [['times', 't'], ['times', 'page_load_time'],
   2084         ['cold_times', 'page_load_time'], ['warm_times', 'page_load_time']]
   2085 
   2086     if metric in metrics_to_sum:
   2087       if values_list:
   2088         values_list = [reduce(lambda x, y: float(x) + float(y), values_list)]
   2089 
   2090     return values_list
   2091 
   2092   def ParseMetricValuesFromOutput(self, metric, text):
   2093     """Parses output from performance_ui_tests and retrieves the results for
   2094     a given metric.
   2095 
   2096     Args:
   2097       metric: The metric as a list of [<trace>, <value>] strings.
   2098       text: The text to parse the metric values from.
   2099 
   2100     Returns:
   2101       A list of floating point numbers found.
   2102     """
   2103     metric_values = self.TryParseResultValuesFromOutput(metric, text)
   2104 
   2105     if not metric_values:
   2106       metric_values = self.TryParseHistogramValuesFromOutput(metric, text)
   2107 
   2108     return metric_values
   2109 
   2110   def _GenerateProfileIfNecessary(self, command_args):
   2111     """Checks the command line of the performance test for dependencies on
   2112     profile generation, and runs tools/perf/generate_profile as necessary.
   2113 
   2114     Args:
   2115       command_args: Command line being passed to performance test, as a list.
   2116 
   2117     Returns:
   2118       False if profile generation was necessary and failed, otherwise True.
   2119     """
   2120 
   2121     if '--profile-dir' in ' '.join(command_args):
   2122       # If we were using python 2.7+, we could just use the argparse
   2123       # module's parse_known_args to grab --profile-dir. Since some of the
   2124       # bots still run 2.6, have to grab the arguments manually.
   2125       arg_dict = {}
   2126       args_to_parse = ['--profile-dir', '--browser']
   2127 
   2128       for arg_to_parse in args_to_parse:
   2129         for i, current_arg in enumerate(command_args):
   2130           if arg_to_parse in current_arg:
   2131             current_arg_split = current_arg.split('=')
   2132 
   2133             # Check 2 cases, --arg=<val> and --arg <val>
   2134             if len(current_arg_split) == 2:
   2135               arg_dict[arg_to_parse] = current_arg_split[1]
   2136             elif i + 1 < len(command_args):
   2137               arg_dict[arg_to_parse] = command_args[i+1]
   2138 
   2139       path_to_generate = os.path.join('tools', 'perf', 'generate_profile')
   2140 
   2141       if arg_dict.has_key('--profile-dir') and arg_dict.has_key('--browser'):
   2142         profile_path, profile_type = os.path.split(arg_dict['--profile-dir'])
   2143         return not RunProcess(['python', path_to_generate,
   2144             '--profile-type-to-generate', profile_type,
   2145             '--browser', arg_dict['--browser'], '--output-dir', profile_path])
   2146       return False
   2147     return True
   2148 
   2149   def _IsBisectModeUsingMetric(self):
   2150     return self.opts.bisect_mode in [BISECT_MODE_MEAN, BISECT_MODE_STD_DEV]
   2151 
   2152   def _IsBisectModeReturnCode(self):
   2153     return self.opts.bisect_mode in [BISECT_MODE_RETURN_CODE]
   2154 
   2155   def _IsBisectModeStandardDeviation(self):
   2156     return self.opts.bisect_mode in [BISECT_MODE_STD_DEV]
   2157 
   2158   def RunPerformanceTestAndParseResults(
   2159       self, command_to_run, metric, reset_on_first_run=False,
   2160       upload_on_last_run=False, results_label=None):
   2161     """Runs a performance test on the current revision and parses the results.
   2162 
   2163     Args:
   2164       command_to_run: The command to be run to execute the performance test.
   2165       metric: The metric to parse out from the results of the performance test.
   2166           This is the result chart name and trace name, separated by slash.
   2167       reset_on_first_run: If True, pass the flag --reset-results on first run.
   2168       upload_on_last_run: If True, pass the flag --upload-results on last run.
   2169       results_label: A value for the option flag --results-label.
   2170           The arguments reset_on_first_run, upload_on_last_run and results_label
   2171           are all ignored if the test is not a Telemetry test.
   2172 
   2173     Returns:
   2174       (values dict, 0) if --debug_ignore_perf_test was passed.
   2175       (values dict, 0, test output) if the test was run successfully.
   2176       (error message, -1) if the test couldn't be run.
   2177       (error message, -1, test output) if the test ran but there was an error.
   2178     """
   2179     success_code, failure_code = 0, -1
   2180 
   2181     if self.opts.debug_ignore_perf_test:
   2182       fake_results = {
   2183           'mean': 0.0,
   2184           'std_err': 0.0,
   2185           'std_dev': 0.0,
   2186           'values': [0.0]
   2187       }
   2188       return (fake_results, success_code)
   2189 
   2190     # For Windows platform set posix=False, to parse windows paths correctly.
   2191     # On Windows, path separators '\' or '\\' are replace by '' when posix=True,
   2192     # refer to http://bugs.python.org/issue1724822. By default posix=True.
   2193     args = shlex.split(command_to_run, posix=not IsWindows())
   2194 
   2195     if not self._GenerateProfileIfNecessary(args):
   2196       err_text = 'Failed to generate profile for performance test.'
   2197       return (err_text, failure_code)
   2198 
   2199     # If running a Telemetry test for Chrome OS, insert the remote IP and
   2200     # identity parameters.
   2201     is_telemetry = bisect_utils.IsTelemetryCommand(command_to_run)
   2202     if self.opts.target_platform == 'cros' and is_telemetry:
   2203       args.append('--remote=%s' % self.opts.cros_remote_ip)
   2204       args.append('--identity=%s' % CROS_TEST_KEY_PATH)
   2205 
   2206     start_time = time.time()
   2207 
   2208     metric_values = []
   2209     output_of_all_runs = ''
   2210     for i in xrange(self.opts.repeat_test_count):
   2211       # Can ignore the return code since if the tests fail, it won't return 0.
   2212       current_args = copy.copy(args)
   2213       if is_telemetry:
   2214         if i == 0 and reset_on_first_run:
   2215           current_args.append('--reset-results')
   2216         elif i == self.opts.repeat_test_count - 1 and upload_on_last_run:
   2217           current_args.append('--upload-results')
   2218         if results_label:
   2219           current_args.append('--results-label=%s' % results_label)
   2220       try:
   2221         (output, return_code) = RunProcessAndRetrieveOutput(current_args,
   2222                                                             cwd=self.src_cwd)
   2223       except OSError, e:
   2224         if e.errno == errno.ENOENT:
   2225           err_text  = ('Something went wrong running the performance test. '
   2226                        'Please review the command line:\n\n')
   2227           if 'src/' in ' '.join(args):
   2228             err_text += ('Check that you haven\'t accidentally specified a '
   2229                          'path with src/ in the command.\n\n')
   2230           err_text += ' '.join(args)
   2231           err_text += '\n'
   2232 
   2233           return (err_text, failure_code)
   2234         raise
   2235 
   2236       output_of_all_runs += output
   2237       if self.opts.output_buildbot_annotations:
   2238         print output
   2239 
   2240       if self._IsBisectModeUsingMetric():
   2241         metric_values += self.ParseMetricValuesFromOutput(metric, output)
   2242         # If we're bisecting on a metric (ie, changes in the mean or
   2243         # standard deviation) and no metric values are produced, bail out.
   2244         if not metric_values:
   2245           break
   2246       elif self._IsBisectModeReturnCode():
   2247         metric_values.append(return_code)
   2248 
   2249       elapsed_minutes = (time.time() - start_time) / 60.0
   2250       if elapsed_minutes >= self.opts.max_time_minutes:
   2251         break
   2252 
   2253     if len(metric_values) == 0:
   2254       err_text = 'Metric %s was not found in the test output.' % metric
   2255       # TODO(qyearsley): Consider also getting and displaying a list of metrics
   2256       # that were found in the output here.
   2257       return (err_text, failure_code, output_of_all_runs)
   2258 
   2259     # If we're bisecting on return codes, we're really just looking for zero vs
   2260     # non-zero.
   2261     if self._IsBisectModeReturnCode():
   2262       # If any of the return codes is non-zero, output 1.
   2263       overall_return_code = 0 if (
   2264           all(current_value == 0 for current_value in metric_values)) else 1
   2265 
   2266       values = {
   2267         'mean': overall_return_code,
   2268         'std_err': 0.0,
   2269         'std_dev': 0.0,
   2270         'values': metric_values,
   2271       }
   2272 
   2273       print 'Results of performance test: Command returned with %d' % (
   2274           overall_return_code)
   2275       print
   2276     else:
   2277       # Need to get the average value if there were multiple values.
   2278       truncated_mean = CalculateTruncatedMean(metric_values,
   2279           self.opts.truncate_percent)
   2280       standard_err = CalculateStandardError(metric_values)
   2281       standard_dev = CalculateStandardDeviation(metric_values)
   2282 
   2283       if self._IsBisectModeStandardDeviation():
   2284         metric_values = [standard_dev]
   2285 
   2286       values = {
   2287         'mean': truncated_mean,
   2288         'std_err': standard_err,
   2289         'std_dev': standard_dev,
   2290         'values': metric_values,
   2291       }
   2292 
   2293       print 'Results of performance test: %12f %12f' % (
   2294           truncated_mean, standard_err)
   2295       print
   2296     return (values, success_code, output_of_all_runs)
   2297 
   2298   def FindAllRevisionsToSync(self, revision, depot):
   2299     """Finds all dependant revisions and depots that need to be synced for a
   2300     given revision. This is only useful in the git workflow, as an svn depot
   2301     may be split into multiple mirrors.
   2302 
   2303     ie. skia is broken up into 3 git mirrors over skia/src, skia/gyp, and
   2304     skia/include. To sync skia/src properly, one has to find the proper
   2305     revisions in skia/gyp and skia/include.
   2306 
   2307     Args:
   2308       revision: The revision to sync to.
   2309       depot: The depot in use at the moment (probably skia).
   2310 
   2311     Returns:
   2312       A list of [depot, revision] pairs that need to be synced.
   2313     """
   2314     revisions_to_sync = [[depot, revision]]
   2315 
   2316     is_base = ((depot == 'chromium') or (depot == 'cros') or
   2317         (depot == 'android-chrome'))
   2318 
   2319     # Some SVN depots were split into multiple git depots, so we need to
   2320     # figure out for each mirror which git revision to grab. There's no
   2321     # guarantee that the SVN revision will exist for each of the dependant
   2322     # depots, so we have to grep the git logs and grab the next earlier one.
   2323     if not is_base and\
   2324        DEPOT_DEPS_NAME[depot]['depends'] and\
   2325        self.source_control.IsGit():
   2326       svn_rev = self.source_control.SVNFindRev(revision)
   2327 
   2328       for d in DEPOT_DEPS_NAME[depot]['depends']:
   2329         self.ChangeToDepotWorkingDirectory(d)
   2330 
   2331         dependant_rev = self.source_control.ResolveToRevision(svn_rev, d, -1000)
   2332 
   2333         if dependant_rev:
   2334           revisions_to_sync.append([d, dependant_rev])
   2335 
   2336       num_resolved = len(revisions_to_sync)
   2337       num_needed = len(DEPOT_DEPS_NAME[depot]['depends'])
   2338 
   2339       self.ChangeToDepotWorkingDirectory(depot)
   2340 
   2341       if not ((num_resolved - 1) == num_needed):
   2342         return None
   2343 
   2344     return revisions_to_sync
   2345 
   2346   def PerformPreBuildCleanup(self):
   2347     """Performs necessary cleanup between runs."""
   2348     print 'Cleaning up between runs.'
   2349     print
   2350 
   2351     # Having these pyc files around between runs can confuse the
   2352     # perf tests and cause them to crash.
   2353     for (path, _, files) in os.walk(self.src_cwd):
   2354       for cur_file in files:
   2355         if cur_file.endswith('.pyc'):
   2356           path_to_file = os.path.join(path, cur_file)
   2357           os.remove(path_to_file)
   2358 
   2359   def PerformWebkitDirectoryCleanup(self, revision):
   2360     """If the script is switching between Blink and WebKit during bisect,
   2361     its faster to just delete the directory rather than leave it up to git
   2362     to sync.
   2363 
   2364     Returns:
   2365       True if successful.
   2366     """
   2367     if not self.source_control.CheckoutFileAtRevision(
   2368         bisect_utils.FILE_DEPS_GIT, revision, cwd=self.src_cwd):
   2369       return False
   2370 
   2371     cwd = os.getcwd()
   2372     os.chdir(self.src_cwd)
   2373 
   2374     is_blink = bisect_utils.IsDepsFileBlink()
   2375 
   2376     os.chdir(cwd)
   2377 
   2378     if not self.source_control.RevertFileToHead(
   2379         bisect_utils.FILE_DEPS_GIT):
   2380       return False
   2381 
   2382     if self.was_blink != is_blink:
   2383       self.was_blink = is_blink
   2384       # Removes third_party/Webkit directory.
   2385       return bisect_utils.RemoveThirdPartyDirectory('Webkit')
   2386     return True
   2387 
   2388   def PerformCrosChrootCleanup(self):
   2389     """Deletes the chroot.
   2390 
   2391     Returns:
   2392         True if successful.
   2393     """
   2394     cwd = os.getcwd()
   2395     self.ChangeToDepotWorkingDirectory('cros')
   2396     cmd = [CROS_SDK_PATH, '--delete']
   2397     return_code = RunProcess(cmd)
   2398     os.chdir(cwd)
   2399     return not return_code
   2400 
   2401   def CreateCrosChroot(self):
   2402     """Creates a new chroot.
   2403 
   2404     Returns:
   2405         True if successful.
   2406     """
   2407     cwd = os.getcwd()
   2408     self.ChangeToDepotWorkingDirectory('cros')
   2409     cmd = [CROS_SDK_PATH, '--create']
   2410     return_code = RunProcess(cmd)
   2411     os.chdir(cwd)
   2412     return not return_code
   2413 
   2414   def PerformPreSyncCleanup(self, revision, depot):
   2415     """Performs any necessary cleanup before syncing.
   2416 
   2417     Returns:
   2418       True if successful.
   2419     """
   2420     if depot == 'chromium' or depot == 'android-chrome':
   2421       # Removes third_party/libjingle. At some point, libjingle was causing
   2422       # issues syncing when using the git workflow (crbug.com/266324).
   2423       os.chdir(self.src_cwd)
   2424       if not bisect_utils.RemoveThirdPartyDirectory('libjingle'):
   2425         return False
   2426       # Removes third_party/skia. At some point, skia was causing
   2427       #  issues syncing when using the git workflow (crbug.com/377951).
   2428       if not bisect_utils.RemoveThirdPartyDirectory('skia'):
   2429         return False
   2430       if depot == 'chromium':
   2431         # The fast webkit cleanup doesn't work for android_chrome
   2432         # The switch from Webkit to Blink that this deals with now happened
   2433         # quite a long time ago so this is unlikely to be a problem.
   2434         return self.PerformWebkitDirectoryCleanup(revision)
   2435     elif depot == 'cros':
   2436       return self.PerformCrosChrootCleanup()
   2437     return True
   2438 
   2439   def RunPostSync(self, depot):
   2440     """Performs any work after syncing.
   2441 
   2442     Returns:
   2443       True if successful.
   2444     """
   2445     if self.opts.target_platform == 'android':
   2446       if not bisect_utils.SetupAndroidBuildEnvironment(self.opts,
   2447           path_to_src=self.src_cwd):
   2448         return False
   2449 
   2450     if depot == 'cros':
   2451       return self.CreateCrosChroot()
   2452     else:
   2453       return self.RunGClientHooks()
   2454     return True
   2455 
   2456   def ShouldSkipRevision(self, depot, revision):
   2457     """Some commits can be safely skipped (such as a DEPS roll), since the tool
   2458     is git based those changes would have no effect.
   2459 
   2460     Args:
   2461       depot: The depot being bisected.
   2462       revision: Current revision we're synced to.
   2463 
   2464     Returns:
   2465       True if we should skip building/testing this revision.
   2466     """
   2467     if depot == 'chromium':
   2468       if self.source_control.IsGit():
   2469         cmd = ['diff-tree', '--no-commit-id', '--name-only', '-r', revision]
   2470         output = CheckRunGit(cmd)
   2471 
   2472         files = output.splitlines()
   2473 
   2474         if len(files) == 1 and files[0] == 'DEPS':
   2475           return True
   2476 
   2477     return False
   2478 
   2479   def SyncBuildAndRunRevision(self, revision, depot, command_to_run, metric,
   2480       skippable=False):
   2481     """Performs a full sync/build/run of the specified revision.
   2482 
   2483     Args:
   2484       revision: The revision to sync to.
   2485       depot: The depot that's being used at the moment (src, webkit, etc.)
   2486       command_to_run: The command to execute the performance test.
   2487       metric: The performance metric being tested.
   2488 
   2489     Returns:
   2490       On success, a tuple containing the results of the performance test.
   2491       Otherwise, a tuple with the error message.
   2492     """
   2493     sync_client = None
   2494     if depot == 'chromium' or depot == 'android-chrome':
   2495       sync_client = 'gclient'
   2496     elif depot == 'cros':
   2497       sync_client = 'repo'
   2498 
   2499     revisions_to_sync = self.FindAllRevisionsToSync(revision, depot)
   2500 
   2501     if not revisions_to_sync:
   2502       return ('Failed to resolve dependant depots.', BUILD_RESULT_FAIL)
   2503 
   2504     if not self.PerformPreSyncCleanup(revision, depot):
   2505       return ('Failed to perform pre-sync cleanup.', BUILD_RESULT_FAIL)
   2506 
   2507     success = True
   2508 
   2509     if not self.opts.debug_ignore_sync:
   2510       for r in revisions_to_sync:
   2511         self.ChangeToDepotWorkingDirectory(r[0])
   2512 
   2513         if sync_client:
   2514           self.PerformPreBuildCleanup()
   2515 
   2516         # If you're using gclient to sync, you need to specify the depot you
   2517         # want so that all the dependencies sync properly as well.
   2518         # ie. gclient sync src@<SHA1>
   2519         current_revision = r[1]
   2520         if sync_client == 'gclient':
   2521           current_revision = '%s@%s' % (DEPOT_DEPS_NAME[depot]['src'],
   2522               current_revision)
   2523         if not self.source_control.SyncToRevision(current_revision,
   2524             sync_client):
   2525           success = False
   2526 
   2527           break
   2528 
   2529     if success:
   2530       success = self.RunPostSync(depot)
   2531       if success:
   2532         if skippable and self.ShouldSkipRevision(depot, revision):
   2533           return ('Skipped revision: [%s]' % str(revision),
   2534               BUILD_RESULT_SKIPPED)
   2535 
   2536         start_build_time = time.time()
   2537         if self.BuildCurrentRevision(depot, revision):
   2538           after_build_time = time.time()
   2539           results = self.RunPerformanceTestAndParseResults(command_to_run,
   2540                                                            metric)
   2541           # Restore build output directory once the tests are done, to avoid
   2542           # any descrepancy.
   2543           if self.IsDownloadable(depot) and revision:
   2544             self.BackupOrRestoreOutputdirectory(restore=True)
   2545 
   2546           if results[1] == 0:
   2547             external_revisions = self.Get3rdPartyRevisionsFromCurrentRevision(
   2548                 depot, revision)
   2549 
   2550             if not external_revisions is None:
   2551               return (results[0], results[1], external_revisions,
   2552                   time.time() - after_build_time, after_build_time -
   2553                   start_build_time)
   2554             else:
   2555               return ('Failed to parse DEPS file for external revisions.',
   2556                   BUILD_RESULT_FAIL)
   2557           else:
   2558             return results
   2559         else:
   2560           return ('Failed to build revision: [%s]' % (str(revision, )),
   2561               BUILD_RESULT_FAIL)
   2562       else:
   2563         return ('Failed to run [gclient runhooks].', BUILD_RESULT_FAIL)
   2564     else:
   2565       return ('Failed to sync revision: [%s]' % (str(revision, )),
   2566           BUILD_RESULT_FAIL)
   2567 
   2568   def _CheckIfRunPassed(self, current_value, known_good_value, known_bad_value):
   2569     """Given known good and bad values, decide if the current_value passed
   2570     or failed.
   2571 
   2572     Args:
   2573       current_value: The value of the metric being checked.
   2574       known_bad_value: The reference value for a "failed" run.
   2575       known_good_value: The reference value for a "passed" run.
   2576 
   2577     Returns:
   2578       True if the current_value is closer to the known_good_value than the
   2579       known_bad_value.
   2580     """
   2581     if self.opts.bisect_mode == BISECT_MODE_STD_DEV:
   2582       dist_to_good_value = abs(current_value['std_dev'] -
   2583           known_good_value['std_dev'])
   2584       dist_to_bad_value = abs(current_value['std_dev'] -
   2585           known_bad_value['std_dev'])
   2586     else:
   2587       dist_to_good_value = abs(current_value['mean'] - known_good_value['mean'])
   2588       dist_to_bad_value = abs(current_value['mean'] - known_bad_value['mean'])
   2589 
   2590     return dist_to_good_value < dist_to_bad_value
   2591 
   2592   def _GetDepotDirectory(self, depot_name):
   2593     if depot_name == 'chromium':
   2594       return self.src_cwd
   2595     elif depot_name == 'cros':
   2596       return self.cros_cwd
   2597     elif depot_name in DEPOT_NAMES:
   2598       return self.depot_cwd[depot_name]
   2599     else:
   2600       assert False, 'Unknown depot [ %s ] encountered. Possibly a new one'\
   2601                     ' was added without proper support?' % depot_name
   2602 
   2603   def ChangeToDepotWorkingDirectory(self, depot_name):
   2604     """Given a depot, changes to the appropriate working directory.
   2605 
   2606     Args:
   2607       depot_name: The name of the depot (see DEPOT_NAMES).
   2608     """
   2609     os.chdir(self._GetDepotDirectory(depot_name))
   2610 
   2611   def _FillInV8BleedingEdgeInfo(self, min_revision_data, max_revision_data):
   2612     r1 = self._GetNearestV8BleedingEdgeFromTrunk(min_revision_data['revision'],
   2613         search_forward=True)
   2614     r2 = self._GetNearestV8BleedingEdgeFromTrunk(max_revision_data['revision'],
   2615         search_forward=False)
   2616     min_revision_data['external']['v8_bleeding_edge'] = r1
   2617     max_revision_data['external']['v8_bleeding_edge'] = r2
   2618 
   2619     if (not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
   2620             min_revision_data['revision']) or
   2621         not self._GetV8BleedingEdgeFromV8TrunkIfMappable(
   2622             max_revision_data['revision'])):
   2623       self.warnings.append('Trunk revisions in V8 did not map directly to '
   2624           'bleeding_edge. Attempted to expand the range to find V8 rolls which '
   2625           'did map directly to bleeding_edge revisions, but results might not '
   2626           'be valid.')
   2627 
   2628   def _FindNextDepotToBisect(self, current_depot, current_revision,
   2629       min_revision_data, max_revision_data):
   2630     """Given the state of the bisect, decides which depot the script should
   2631     dive into next (if any).
   2632 
   2633     Args:
   2634       current_depot: Current depot being bisected.
   2635       current_revision: Current revision synced to.
   2636       min_revision_data: Data about the earliest revision in the bisect range.
   2637       max_revision_data: Data about the latest revision in the bisect range.
   2638 
   2639     Returns:
   2640       The depot to bisect next, or None.
   2641     """
   2642     external_depot = None
   2643     for next_depot in DEPOT_NAMES:
   2644       if DEPOT_DEPS_NAME[next_depot].has_key('platform'):
   2645         if DEPOT_DEPS_NAME[next_depot]['platform'] != os.name:
   2646           continue
   2647 
   2648       if not (DEPOT_DEPS_NAME[next_depot]["recurse"] and
   2649           min_revision_data['depot'] in DEPOT_DEPS_NAME[next_depot]['from']):
   2650         continue
   2651 
   2652       if current_depot == 'v8':
   2653         # We grab the bleeding_edge info here rather than earlier because we
   2654         # finally have the revision range. From that we can search forwards and
   2655         # backwards to try to match trunk revisions to bleeding_edge.
   2656         self._FillInV8BleedingEdgeInfo(min_revision_data, max_revision_data)
   2657 
   2658       if (min_revision_data['external'].get(next_depot) ==
   2659           max_revision_data['external'].get(next_depot)):
   2660         continue
   2661 
   2662       if (min_revision_data['external'].get(next_depot) and
   2663           max_revision_data['external'].get(next_depot)):
   2664         external_depot = next_depot
   2665         break
   2666 
   2667     return external_depot
   2668 
   2669   def PrepareToBisectOnDepot(self,
   2670                              current_depot,
   2671                              end_revision,
   2672                              start_revision,
   2673                              previous_depot,
   2674                              previous_revision):
   2675     """Changes to the appropriate directory and gathers a list of revisions
   2676     to bisect between |start_revision| and |end_revision|.
   2677 
   2678     Args:
   2679       current_depot: The depot we want to bisect.
   2680       end_revision: End of the revision range.
   2681       start_revision: Start of the revision range.
   2682       previous_depot: The depot we were previously bisecting.
   2683       previous_revision: The last revision we synced to on |previous_depot|.
   2684 
   2685     Returns:
   2686       A list containing the revisions between |start_revision| and
   2687       |end_revision| inclusive.
   2688     """
   2689     # Change into working directory of external library to run
   2690     # subsequent commands.
   2691     self.ChangeToDepotWorkingDirectory(current_depot)
   2692 
   2693     # V8 (and possibly others) is merged in periodically. Bisecting
   2694     # this directory directly won't give much good info.
   2695     if DEPOT_DEPS_NAME[current_depot].has_key('custom_deps'):
   2696       config_path = os.path.join(self.src_cwd, '..')
   2697       if bisect_utils.RunGClientAndCreateConfig(self.opts,
   2698           DEPOT_DEPS_NAME[current_depot]['custom_deps'], cwd=config_path):
   2699         return []
   2700       if bisect_utils.RunGClient(
   2701           ['sync', '--revision', previous_revision], cwd=self.src_cwd):
   2702         return []
   2703 
   2704     if current_depot == 'v8_bleeding_edge':
   2705       self.ChangeToDepotWorkingDirectory('chromium')
   2706 
   2707       shutil.move('v8', 'v8.bak')
   2708       shutil.move('v8_bleeding_edge', 'v8')
   2709 
   2710       self.cleanup_commands.append(['mv', 'v8', 'v8_bleeding_edge'])
   2711       self.cleanup_commands.append(['mv', 'v8.bak', 'v8'])
   2712 
   2713       self.depot_cwd['v8_bleeding_edge'] = os.path.join(self.src_cwd, 'v8')
   2714       self.depot_cwd['v8'] = os.path.join(self.src_cwd, 'v8.bak')
   2715 
   2716       self.ChangeToDepotWorkingDirectory(current_depot)
   2717 
   2718     depot_revision_list = self.GetRevisionList(current_depot,
   2719                                                end_revision,
   2720                                                start_revision)
   2721 
   2722     self.ChangeToDepotWorkingDirectory('chromium')
   2723 
   2724     return depot_revision_list
   2725 
   2726   def GatherReferenceValues(self, good_rev, bad_rev, cmd, metric, target_depot):
   2727     """Gathers reference values by running the performance tests on the
   2728     known good and bad revisions.
   2729 
   2730     Args:
   2731       good_rev: The last known good revision where the performance regression
   2732         has not occurred yet.
   2733       bad_rev: A revision where the performance regression has already occurred.
   2734       cmd: The command to execute the performance test.
   2735       metric: The metric being tested for regression.
   2736 
   2737     Returns:
   2738       A tuple with the results of building and running each revision.
   2739     """
   2740     bad_run_results = self.SyncBuildAndRunRevision(bad_rev,
   2741                                                    target_depot,
   2742                                                    cmd,
   2743                                                    metric)
   2744 
   2745     good_run_results = None
   2746 
   2747     if not bad_run_results[1]:
   2748       good_run_results = self.SyncBuildAndRunRevision(good_rev,
   2749                                                       target_depot,
   2750                                                       cmd,
   2751                                                       metric)
   2752 
   2753     return (bad_run_results, good_run_results)
   2754 
   2755   def AddRevisionsIntoRevisionData(self, revisions, depot, sort, revision_data):
   2756     """Adds new revisions to the revision_data dict and initializes them.
   2757 
   2758     Args:
   2759       revisions: List of revisions to add.
   2760       depot: Depot that's currently in use (src, webkit, etc...)
   2761       sort: Sorting key for displaying revisions.
   2762       revision_data: A dict to add the new revisions into. Existing revisions
   2763         will have their sort keys offset.
   2764     """
   2765 
   2766     num_depot_revisions = len(revisions)
   2767 
   2768     for _, v in revision_data.iteritems():
   2769       if v['sort'] > sort:
   2770         v['sort'] += num_depot_revisions
   2771 
   2772     for i in xrange(num_depot_revisions):
   2773       r = revisions[i]
   2774 
   2775       revision_data[r] = {'revision' : r,
   2776                           'depot' : depot,
   2777                           'value' : None,
   2778                           'perf_time' : 0,
   2779                           'build_time' : 0,
   2780                           'passed' : '?',
   2781                           'sort' : i + sort + 1}
   2782 
   2783   def PrintRevisionsToBisectMessage(self, revision_list, depot):
   2784     if self.opts.output_buildbot_annotations:
   2785       step_name = 'Bisection Range: [%s - %s]' % (
   2786           revision_list[len(revision_list)-1], revision_list[0])
   2787       bisect_utils.OutputAnnotationStepStart(step_name)
   2788 
   2789     print
   2790     print 'Revisions to bisect on [%s]:' % depot
   2791     for revision_id in revision_list:
   2792       print '  -> %s' % (revision_id, )
   2793     print
   2794 
   2795     if self.opts.output_buildbot_annotations:
   2796       bisect_utils.OutputAnnotationStepClosed()
   2797 
   2798   def NudgeRevisionsIfDEPSChange(self, bad_revision, good_revision):
   2799     """Checks to see if changes to DEPS file occurred, and that the revision
   2800     range also includes the change to .DEPS.git. If it doesn't, attempts to
   2801     expand the revision range to include it.
   2802 
   2803     Args:
   2804         bad_rev: First known bad revision.
   2805         good_revision: Last known good revision.
   2806 
   2807     Returns:
   2808         A tuple with the new bad and good revisions.
   2809     """
   2810     if self.source_control.IsGit() and self.opts.target_platform == 'chromium':
   2811       changes_to_deps = self.source_control.QueryFileRevisionHistory(
   2812           'DEPS', good_revision, bad_revision)
   2813 
   2814       if changes_to_deps:
   2815         # DEPS file was changed, search from the oldest change to DEPS file to
   2816         # bad_revision to see if there are matching .DEPS.git changes.
   2817         oldest_deps_change = changes_to_deps[-1]
   2818         changes_to_gitdeps = self.source_control.QueryFileRevisionHistory(
   2819             bisect_utils.FILE_DEPS_GIT, oldest_deps_change, bad_revision)
   2820 
   2821         if len(changes_to_deps) != len(changes_to_gitdeps):
   2822           # Grab the timestamp of the last DEPS change
   2823           cmd = ['log', '--format=%ct', '-1', changes_to_deps[0]]
   2824           output = CheckRunGit(cmd)
   2825           commit_time = int(output)
   2826 
   2827           # Try looking for a commit that touches the .DEPS.git file in the
   2828           # next 15 minutes after the DEPS file change.
   2829           cmd = ['log', '--format=%H', '-1',
   2830               '--before=%d' % (commit_time + 900), '--after=%d' % commit_time,
   2831               'origin/master', bisect_utils.FILE_DEPS_GIT]
   2832           output = CheckRunGit(cmd)
   2833           output = output.strip()
   2834           if output:
   2835             self.warnings.append('Detected change to DEPS and modified '
   2836                 'revision range to include change to .DEPS.git')
   2837             return (output, good_revision)
   2838           else:
   2839             self.warnings.append('Detected change to DEPS but couldn\'t find '
   2840                 'matching change to .DEPS.git')
   2841     return (bad_revision, good_revision)
   2842 
   2843   def CheckIfRevisionsInProperOrder(self,
   2844                                     target_depot,
   2845                                     good_revision,
   2846                                     bad_revision):
   2847     """Checks that |good_revision| is an earlier revision than |bad_revision|.
   2848 
   2849     Args:
   2850         good_revision: Number/tag of the known good revision.
   2851         bad_revision: Number/tag of the known bad revision.
   2852 
   2853     Returns:
   2854         True if the revisions are in the proper order (good earlier than bad).
   2855     """
   2856     if self.source_control.IsGit() and target_depot != 'cros':
   2857       cmd = ['log', '--format=%ct', '-1', good_revision]
   2858       cwd = self._GetDepotDirectory(target_depot)
   2859 
   2860       output = CheckRunGit(cmd, cwd=cwd)
   2861       good_commit_time = int(output)
   2862 
   2863       cmd = ['log', '--format=%ct', '-1', bad_revision]
   2864       output = CheckRunGit(cmd, cwd=cwd)
   2865       bad_commit_time = int(output)
   2866 
   2867       return good_commit_time <= bad_commit_time
   2868     else:
   2869       # Cros/svn use integers
   2870       return int(good_revision) <= int(bad_revision)
   2871 
   2872   def Run(self, command_to_run, bad_revision_in, good_revision_in, metric):
   2873     """Given known good and bad revisions, run a binary search on all
   2874     intermediate revisions to determine the CL where the performance regression
   2875     occurred.
   2876 
   2877     Args:
   2878         command_to_run: Specify the command to execute the performance test.
   2879         good_revision: Number/tag of the known good revision.
   2880         bad_revision: Number/tag of the known bad revision.
   2881         metric: The performance metric to monitor.
   2882 
   2883     Returns:
   2884         A dict with 2 members, 'revision_data' and 'error'. On success,
   2885         'revision_data' will contain a dict mapping revision ids to
   2886         data about that revision. Each piece of revision data consists of a
   2887         dict with the following keys:
   2888 
   2889         'passed': Represents whether the performance test was successful at
   2890             that revision. Possible values include: 1 (passed), 0 (failed),
   2891             '?' (skipped), 'F' (build failed).
   2892         'depot': The depot that this revision is from (ie. WebKit)
   2893         'external': If the revision is a 'src' revision, 'external' contains
   2894             the revisions of each of the external libraries.
   2895         'sort': A sort value for sorting the dict in order of commits.
   2896 
   2897         For example:
   2898         {
   2899           'error':None,
   2900           'revision_data':
   2901           {
   2902             'CL #1':
   2903             {
   2904               'passed':False,
   2905               'depot':'chromium',
   2906               'external':None,
   2907               'sort':0
   2908             }
   2909           }
   2910         }
   2911 
   2912         If an error occurred, the 'error' field will contain the message and
   2913         'revision_data' will be empty.
   2914     """
   2915     results = {'revision_data' : {},
   2916                'error' : None}
   2917 
   2918     # Choose depot to bisect first
   2919     target_depot = 'chromium'
   2920     if self.opts.target_platform == 'cros':
   2921       target_depot = 'cros'
   2922     elif self.opts.target_platform == 'android-chrome':
   2923       target_depot = 'android-chrome'
   2924 
   2925     cwd = os.getcwd()
   2926     self.ChangeToDepotWorkingDirectory(target_depot)
   2927 
   2928     # If they passed SVN CL's, etc... we can try match them to git SHA1's.
   2929     bad_revision = self.source_control.ResolveToRevision(bad_revision_in,
   2930                                                          target_depot, 100)
   2931     good_revision = self.source_control.ResolveToRevision(good_revision_in,
   2932                                                           target_depot, -100)
   2933 
   2934     os.chdir(cwd)
   2935 
   2936 
   2937     if bad_revision is None:
   2938       results['error'] = 'Could\'t resolve [%s] to SHA1.' % (bad_revision_in,)
   2939       return results
   2940 
   2941     if good_revision is None:
   2942       results['error'] = 'Could\'t resolve [%s] to SHA1.' % (good_revision_in,)
   2943       return results
   2944 
   2945     # Check that they didn't accidentally swap good and bad revisions.
   2946     if not self.CheckIfRevisionsInProperOrder(
   2947         target_depot, good_revision, bad_revision):
   2948       results['error'] = 'bad_revision < good_revision, did you swap these '\
   2949           'by mistake?'
   2950       return results
   2951 
   2952     (bad_revision, good_revision) = self.NudgeRevisionsIfDEPSChange(
   2953         bad_revision, good_revision)
   2954 
   2955     if self.opts.output_buildbot_annotations:
   2956       bisect_utils.OutputAnnotationStepStart('Gathering Revisions')
   2957 
   2958     print 'Gathering revision range for bisection.'
   2959     # Retrieve a list of revisions to do bisection on.
   2960     src_revision_list = self.GetRevisionList(target_depot,
   2961                                              bad_revision,
   2962                                              good_revision)
   2963 
   2964     if self.opts.output_buildbot_annotations:
   2965       bisect_utils.OutputAnnotationStepClosed()
   2966 
   2967     if src_revision_list:
   2968       # revision_data will store information about a revision such as the
   2969       # depot it came from, the webkit/V8 revision at that time,
   2970       # performance timing, build state, etc...
   2971       revision_data = results['revision_data']
   2972 
   2973       # revision_list is the list we're binary searching through at the moment.
   2974       revision_list = []
   2975 
   2976       sort_key_ids = 0
   2977 
   2978       for current_revision_id in src_revision_list:
   2979         sort_key_ids += 1
   2980 
   2981         revision_data[current_revision_id] = {'value' : None,
   2982                                               'passed' : '?',
   2983                                               'depot' : target_depot,
   2984                                               'external' : None,
   2985                                               'perf_time' : 0,
   2986                                               'build_time' : 0,
   2987                                               'sort' : sort_key_ids}
   2988         revision_list.append(current_revision_id)
   2989 
   2990       min_revision = 0
   2991       max_revision = len(revision_list) - 1
   2992 
   2993       self.PrintRevisionsToBisectMessage(revision_list, target_depot)
   2994 
   2995       if self.opts.output_buildbot_annotations:
   2996         bisect_utils.OutputAnnotationStepStart('Gathering Reference Values')
   2997 
   2998       print 'Gathering reference values for bisection.'
   2999 
   3000       # Perform the performance tests on the good and bad revisions, to get
   3001       # reference values.
   3002       (bad_results, good_results) = self.GatherReferenceValues(good_revision,
   3003                                                                bad_revision,
   3004                                                                command_to_run,
   3005                                                                metric,
   3006                                                                target_depot)
   3007 
   3008       if self.opts.output_buildbot_annotations:
   3009         bisect_utils.OutputAnnotationStepClosed()
   3010 
   3011       if bad_results[1]:
   3012         results['error'] = ('An error occurred while building and running '
   3013             'the \'bad\' reference value. The bisect cannot continue without '
   3014             'a working \'bad\' revision to start from.\n\nError: %s' %
   3015                 bad_results[0])
   3016         return results
   3017 
   3018       if good_results[1]:
   3019         results['error'] = ('An error occurred while building and running '
   3020             'the \'good\' reference value. The bisect cannot continue without '
   3021             'a working \'good\' revision to start from.\n\nError: %s' %
   3022                 good_results[0])
   3023         return results
   3024 
   3025 
   3026       # We need these reference values to determine if later runs should be
   3027       # classified as pass or fail.
   3028       known_bad_value = bad_results[0]
   3029       known_good_value = good_results[0]
   3030 
   3031       # Can just mark the good and bad revisions explicitly here since we
   3032       # already know the results.
   3033       bad_revision_data = revision_data[revision_list[0]]
   3034       bad_revision_data['external'] = bad_results[2]
   3035       bad_revision_data['perf_time'] = bad_results[3]
   3036       bad_revision_data['build_time'] = bad_results[4]
   3037       bad_revision_data['passed'] = False
   3038       bad_revision_data['value'] = known_bad_value
   3039 
   3040       good_revision_data = revision_data[revision_list[max_revision]]
   3041       good_revision_data['external'] = good_results[2]
   3042       good_revision_data['perf_time'] = good_results[3]
   3043       good_revision_data['build_time'] = good_results[4]
   3044       good_revision_data['passed'] = True
   3045       good_revision_data['value'] = known_good_value
   3046 
   3047       next_revision_depot = target_depot
   3048 
   3049       while True:
   3050         if not revision_list:
   3051           break
   3052 
   3053         min_revision_data = revision_data[revision_list[min_revision]]
   3054         max_revision_data = revision_data[revision_list[max_revision]]
   3055 
   3056         if max_revision - min_revision <= 1:
   3057           current_depot = min_revision_data['depot']
   3058           if min_revision_data['passed'] == '?':
   3059             next_revision_index = min_revision
   3060           elif max_revision_data['passed'] == '?':
   3061             next_revision_index = max_revision
   3062           elif current_depot in ['android-chrome', 'cros', 'chromium', 'v8']:
   3063             previous_revision = revision_list[min_revision]
   3064             # If there were changes to any of the external libraries we track,
   3065             # should bisect the changes there as well.
   3066             external_depot = self._FindNextDepotToBisect(current_depot,
   3067                 previous_revision, min_revision_data, max_revision_data)
   3068 
   3069             # If there was no change in any of the external depots, the search
   3070             # is over.
   3071             if not external_depot:
   3072               if current_depot == 'v8':
   3073                 self.warnings.append('Unfortunately, V8 bisection couldn\'t '
   3074                     'continue any further. The script can only bisect into '
   3075                     'V8\'s bleeding_edge repository if both the current and '
   3076                     'previous revisions in trunk map directly to revisions in '
   3077                     'bleeding_edge.')
   3078               break
   3079 
   3080             earliest_revision = max_revision_data['external'][external_depot]
   3081             latest_revision = min_revision_data['external'][external_depot]
   3082 
   3083             new_revision_list = self.PrepareToBisectOnDepot(external_depot,
   3084                                                             latest_revision,
   3085                                                             earliest_revision,
   3086                                                             next_revision_depot,
   3087                                                             previous_revision)
   3088 
   3089             if not new_revision_list:
   3090               results['error'] = 'An error occurred attempting to retrieve'\
   3091                                  ' revision range: [%s..%s]' % \
   3092                                  (earliest_revision, latest_revision)
   3093               return results
   3094 
   3095             self.AddRevisionsIntoRevisionData(new_revision_list,
   3096                                               external_depot,
   3097                                               min_revision_data['sort'],
   3098                                               revision_data)
   3099 
   3100             # Reset the bisection and perform it on the newly inserted
   3101             # changelists.
   3102             revision_list = new_revision_list
   3103             min_revision = 0
   3104             max_revision = len(revision_list) - 1
   3105             sort_key_ids += len(revision_list)
   3106 
   3107             print 'Regression in metric:%s appears to be the result of changes'\
   3108                   ' in [%s].' % (metric, external_depot)
   3109 
   3110             self.PrintRevisionsToBisectMessage(revision_list, external_depot)
   3111 
   3112             continue
   3113           else:
   3114             break
   3115         else:
   3116           next_revision_index = int((max_revision - min_revision) / 2) +\
   3117                                 min_revision
   3118 
   3119         next_revision_id = revision_list[next_revision_index]
   3120         next_revision_data = revision_data[next_revision_id]
   3121         next_revision_depot = next_revision_data['depot']
   3122 
   3123         self.ChangeToDepotWorkingDirectory(next_revision_depot)
   3124 
   3125         if self.opts.output_buildbot_annotations:
   3126           step_name = 'Working on [%s]' % next_revision_id
   3127           bisect_utils.OutputAnnotationStepStart(step_name)
   3128 
   3129         print 'Working on revision: [%s]' % next_revision_id
   3130 
   3131         run_results = self.SyncBuildAndRunRevision(next_revision_id,
   3132                                                    next_revision_depot,
   3133                                                    command_to_run,
   3134                                                    metric, skippable=True)
   3135 
   3136         # If the build is successful, check whether or not the metric
   3137         # had regressed.
   3138         if not run_results[1]:
   3139           if len(run_results) > 2:
   3140             next_revision_data['external'] = run_results[2]
   3141             next_revision_data['perf_time'] = run_results[3]
   3142             next_revision_data['build_time'] = run_results[4]
   3143 
   3144           passed_regression = self._CheckIfRunPassed(run_results[0],
   3145                                                      known_good_value,
   3146                                                      known_bad_value)
   3147 
   3148           next_revision_data['passed'] = passed_regression
   3149           next_revision_data['value'] = run_results[0]
   3150 
   3151           if passed_regression:
   3152             max_revision = next_revision_index
   3153           else:
   3154             min_revision = next_revision_index
   3155         else:
   3156           if run_results[1] == BUILD_RESULT_SKIPPED:
   3157             next_revision_data['passed'] = 'Skipped'
   3158           elif run_results[1] == BUILD_RESULT_FAIL:
   3159             next_revision_data['passed'] = 'Build Failed'
   3160 
   3161           print run_results[0]
   3162 
   3163           # If the build is broken, remove it and redo search.
   3164           revision_list.pop(next_revision_index)
   3165 
   3166           max_revision -= 1
   3167 
   3168         if self.opts.output_buildbot_annotations:
   3169           self._PrintPartialResults(results)
   3170           bisect_utils.OutputAnnotationStepClosed()
   3171     else:
   3172       # Weren't able to sync and retrieve the revision range.
   3173       results['error'] = 'An error occurred attempting to retrieve revision '\
   3174                          'range: [%s..%s]' % (good_revision, bad_revision)
   3175 
   3176     return results
   3177 
   3178   def _PrintPartialResults(self, results_dict):
   3179     revision_data = results_dict['revision_data']
   3180     revision_data_sorted = sorted(revision_data.iteritems(),
   3181                                   key = lambda x: x[1]['sort'])
   3182     results_dict = self._GetResultsDict(revision_data, revision_data_sorted)
   3183 
   3184     self._PrintTestedCommitsTable(revision_data_sorted,
   3185                                   results_dict['first_working_revision'],
   3186                                   results_dict['last_broken_revision'],
   3187                                   100, final_step=False)
   3188 
   3189   def _PrintConfidence(self, results_dict):
   3190     # The perf dashboard specifically looks for the string
   3191     # "Confidence in Bisection Results: 100%" to decide whether or not
   3192     # to cc the author(s). If you change this, please update the perf
   3193     # dashboard as well.
   3194     print 'Confidence in Bisection Results: %d%%' % results_dict['confidence']
   3195 
   3196   def _PrintBanner(self, results_dict):
   3197     print
   3198     print " __o_\___          Aw Snap! We hit a speed bump!"
   3199     print "=-O----O-'__.~.___________________________________"
   3200     print
   3201     if self._IsBisectModeReturnCode():
   3202       print ('Bisect reproduced a change in return codes while running the '
   3203           'performance test.')
   3204     else:
   3205       print ('Bisect reproduced a %.02f%% (+-%.02f%%) change in the '
   3206           '%s metric.' % (results_dict['regression_size'],
   3207           results_dict['regression_std_err'], '/'.join(self.opts.metric)))
   3208     self._PrintConfidence(results_dict)
   3209 
   3210   def _PrintFailedBanner(self, results_dict):
   3211     print
   3212     if self._IsBisectModeReturnCode():
   3213       print 'Bisect could not reproduce a change in the return code.'
   3214     else:
   3215       print ('Bisect could not reproduce a change in the '
   3216           '%s metric.' % '/'.join(self.opts.metric))
   3217     print
   3218 
   3219   def _GetViewVCLinkFromDepotAndHash(self, cl, depot):
   3220     info = self.source_control.QueryRevisionInfo(cl,
   3221         self._GetDepotDirectory(depot))
   3222     if depot and DEPOT_DEPS_NAME[depot].has_key('viewvc'):
   3223       try:
   3224         # Format is "git-svn-id: svn://....@123456 <other data>"
   3225         svn_line = [i for i in info['body'].splitlines() if 'git-svn-id:' in i]
   3226         svn_revision = svn_line[0].split('@')
   3227         svn_revision = svn_revision[1].split(' ')[0]
   3228         return DEPOT_DEPS_NAME[depot]['viewvc'] + svn_revision
   3229       except IndexError:
   3230         return ''
   3231     return ''
   3232 
   3233   def _PrintRevisionInfo(self, cl, info, depot=None):
   3234     # The perf dashboard specifically looks for the string
   3235     # "Author  : " to parse out who to cc on a bug. If you change the
   3236     # formatting here, please update the perf dashboard as well.
   3237     print
   3238     print 'Subject : %s' % info['subject']
   3239     print 'Author  : %s' % info['author']
   3240     if not info['email'].startswith(info['author']):
   3241       print 'Email   : %s' % info['email']
   3242     commit_link = self._GetViewVCLinkFromDepotAndHash(cl, depot)
   3243     if commit_link:
   3244       print 'Link    : %s' % commit_link
   3245     else:
   3246       print
   3247       print 'Failed to parse svn revision from body:'
   3248       print
   3249       print info['body']
   3250       print
   3251     print 'Commit  : %s' % cl
   3252     print 'Date    : %s' % info['date']
   3253 
   3254   def _PrintTableRow(self, column_widths, row_data):
   3255     assert len(column_widths) == len(row_data)
   3256 
   3257     text = ''
   3258     for i in xrange(len(column_widths)):
   3259       current_row_data = row_data[i].center(column_widths[i], ' ')
   3260       text += ('%%%ds' % column_widths[i]) % current_row_data
   3261     print text
   3262 
   3263   def _PrintTestedCommitsHeader(self):
   3264     if self.opts.bisect_mode == BISECT_MODE_MEAN:
   3265       self._PrintTableRow(
   3266           [20, 70, 14, 12, 13],
   3267           ['Depot', 'Commit SHA', 'Mean', 'Std. Error', 'State'])
   3268     elif self.opts.bisect_mode == BISECT_MODE_STD_DEV:
   3269       self._PrintTableRow(
   3270           [20, 70, 14, 12, 13],
   3271           ['Depot', 'Commit SHA', 'Std. Error', 'Mean', 'State'])
   3272     elif self.opts.bisect_mode == BISECT_MODE_RETURN_CODE:
   3273       self._PrintTableRow(
   3274           [20, 70, 14, 13],
   3275           ['Depot', 'Commit SHA', 'Return Code', 'State'])
   3276     else:
   3277       assert False, "Invalid bisect_mode specified."
   3278       print '  %20s  %70s  %14s %13s' % ('Depot'.center(20, ' '),
   3279           'Commit SHA'.center(70, ' '), 'Return Code'.center(14, ' '),
   3280            'State'.center(13, ' '))
   3281 
   3282   def _PrintTestedCommitsEntry(self, current_data, cl_link, state_str):
   3283     if self.opts.bisect_mode == BISECT_MODE_MEAN:
   3284       std_error = '+-%.02f' % current_data['value']['std_err']
   3285       mean = '%.02f' % current_data['value']['mean']
   3286       self._PrintTableRow(
   3287           [20, 70, 12, 14, 13],
   3288           [current_data['depot'], cl_link, mean, std_error, state_str])
   3289     elif self.opts.bisect_mode == BISECT_MODE_STD_DEV:
   3290       std_error = '+-%.02f' % current_data['value']['std_err']
   3291       mean = '%.02f' % current_data['value']['mean']
   3292       self._PrintTableRow(
   3293           [20, 70, 12, 14, 13],
   3294           [current_data['depot'], cl_link, std_error, mean, state_str])
   3295     elif self.opts.bisect_mode == BISECT_MODE_RETURN_CODE:
   3296       mean = '%d' % current_data['value']['mean']
   3297       self._PrintTableRow(
   3298           [20, 70, 14, 13],
   3299           [current_data['depot'], cl_link, mean, state_str])
   3300 
   3301   def _PrintTestedCommitsTable(self, revision_data_sorted,
   3302       first_working_revision, last_broken_revision, confidence,
   3303       final_step=True):
   3304     print
   3305     if final_step:
   3306       print 'Tested commits:'
   3307     else:
   3308       print 'Partial results:'
   3309     self._PrintTestedCommitsHeader()
   3310     state = 0
   3311     for current_id, current_data in revision_data_sorted:
   3312       if current_data['value']:
   3313         if (current_id == last_broken_revision or
   3314             current_id == first_working_revision):
   3315           # If confidence is too low, don't add this empty line since it's
   3316           # used to put focus on a suspected CL.
   3317           if confidence and final_step:
   3318             print
   3319           state += 1
   3320           if state == 2 and not final_step:
   3321             # Just want a separation between "bad" and "good" cl's.
   3322             print
   3323 
   3324         state_str = 'Bad'
   3325         if state == 1 and final_step:
   3326           state_str = 'Suspected CL'
   3327         elif state == 2:
   3328           state_str = 'Good'
   3329 
   3330         # If confidence is too low, don't bother outputting good/bad.
   3331         if not confidence:
   3332           state_str = ''
   3333         state_str = state_str.center(13, ' ')
   3334 
   3335         cl_link = self._GetViewVCLinkFromDepotAndHash(current_id,
   3336             current_data['depot'])
   3337         if not cl_link:
   3338           cl_link = current_id
   3339         self._PrintTestedCommitsEntry(current_data, cl_link, state_str)
   3340 
   3341   def _PrintReproSteps(self):
   3342     print
   3343     print 'To reproduce locally:'
   3344     print '$ ' + self.opts.command
   3345     if bisect_utils.IsTelemetryCommand(self.opts.command):
   3346       print
   3347       print 'Also consider passing --profiler=list to see available profilers.'
   3348 
   3349   def _PrintOtherRegressions(self, other_regressions, revision_data):
   3350     print
   3351     print 'Other regressions may have occurred:'
   3352     print '  %8s  %70s  %10s' % ('Depot'.center(8, ' '),
   3353         'Range'.center(70, ' '), 'Confidence'.center(10, ' '))
   3354     for regression in other_regressions:
   3355       current_id, previous_id, confidence = regression
   3356       current_data = revision_data[current_id]
   3357       previous_data = revision_data[previous_id]
   3358 
   3359       current_link = self._GetViewVCLinkFromDepotAndHash(current_id,
   3360           current_data['depot'])
   3361       previous_link = self._GetViewVCLinkFromDepotAndHash(previous_id,
   3362           previous_data['depot'])
   3363 
   3364       # If we can't map it to a viewable URL, at least show the original hash.
   3365       if not current_link:
   3366         current_link = current_id
   3367       if not previous_link:
   3368         previous_link = previous_id
   3369 
   3370       print '  %8s  %70s %s' % (
   3371           current_data['depot'], current_link,
   3372           ('%d%%' % confidence).center(10, ' '))
   3373       print '  %8s  %70s' % (
   3374           previous_data['depot'], previous_link)
   3375       print
   3376 
   3377   def _PrintStepTime(self, revision_data_sorted):
   3378     step_perf_time_avg = 0.0
   3379     step_build_time_avg = 0.0
   3380     step_count = 0.0
   3381     for _, current_data in revision_data_sorted:
   3382       if current_data['value']:
   3383         step_perf_time_avg += current_data['perf_time']
   3384         step_build_time_avg += current_data['build_time']
   3385         step_count += 1
   3386     if step_count:
   3387       step_perf_time_avg = step_perf_time_avg / step_count
   3388       step_build_time_avg = step_build_time_avg / step_count
   3389     print
   3390     print 'Average build time : %s' % datetime.timedelta(
   3391         seconds=int(step_build_time_avg))
   3392     print 'Average test time  : %s' % datetime.timedelta(
   3393         seconds=int(step_perf_time_avg))
   3394 
   3395   def _PrintWarnings(self):
   3396     if not self.warnings:
   3397       return
   3398     print
   3399     print 'WARNINGS:'
   3400     for w in set(self.warnings):
   3401       print '  !!! %s' % w
   3402 
   3403   def _FindOtherRegressions(self, revision_data_sorted, bad_greater_than_good):
   3404     other_regressions = []
   3405     previous_values = []
   3406     previous_id = None
   3407     for current_id, current_data in revision_data_sorted:
   3408       current_values = current_data['value']
   3409       if current_values:
   3410         current_values = current_values['values']
   3411         if previous_values:
   3412           confidence = CalculateConfidence(previous_values, [current_values])
   3413           mean_of_prev_runs = CalculateMean(sum(previous_values, []))
   3414           mean_of_current_runs = CalculateMean(current_values)
   3415 
   3416           # Check that the potential regression is in the same direction as
   3417           # the overall regression. If the mean of the previous runs < the
   3418           # mean of the current runs, this local regression is in same
   3419           # direction.
   3420           prev_less_than_current = mean_of_prev_runs < mean_of_current_runs
   3421           is_same_direction = (prev_less_than_current if
   3422               bad_greater_than_good else not prev_less_than_current)
   3423 
   3424           # Only report potential regressions with high confidence.
   3425           if is_same_direction and confidence > 50:
   3426             other_regressions.append([current_id, previous_id, confidence])
   3427         previous_values.append(current_values)
   3428         previous_id = current_id
   3429     return other_regressions
   3430 
   3431 
   3432   def _GetResultsDict(self, revision_data, revision_data_sorted):
   3433     # Find range where it possibly broke.
   3434     first_working_revision = None
   3435     first_working_revision_index = -1
   3436     last_broken_revision = None
   3437     last_broken_revision_index = -1
   3438 
   3439     for i in xrange(len(revision_data_sorted)):
   3440       k, v = revision_data_sorted[i]
   3441       if v['passed'] == 1:
   3442         if not first_working_revision:
   3443           first_working_revision = k
   3444           first_working_revision_index = i
   3445 
   3446       if not v['passed']:
   3447         last_broken_revision = k
   3448         last_broken_revision_index = i
   3449 
   3450     if last_broken_revision != None and first_working_revision != None:
   3451       broken_means = []
   3452       for i in xrange(0, last_broken_revision_index + 1):
   3453         if revision_data_sorted[i][1]['value']:
   3454           broken_means.append(revision_data_sorted[i][1]['value']['values'])
   3455 
   3456       working_means = []
   3457       for i in xrange(first_working_revision_index, len(revision_data_sorted)):
   3458         if revision_data_sorted[i][1]['value']:
   3459           working_means.append(revision_data_sorted[i][1]['value']['values'])
   3460 
   3461       # Flatten the lists to calculate mean of all values.
   3462       working_mean = sum(working_means, [])
   3463       broken_mean = sum(broken_means, [])
   3464 
   3465       # Calculate the approximate size of the regression
   3466       mean_of_bad_runs = CalculateMean(broken_mean)
   3467       mean_of_good_runs = CalculateMean(working_mean)
   3468 
   3469       regression_size = 100 * CalculateRelativeChange(mean_of_good_runs,
   3470                                                       mean_of_bad_runs)
   3471       if math.isnan(regression_size):
   3472         regression_size = 'zero-to-nonzero'
   3473 
   3474       regression_std_err = math.fabs(CalculatePooledStandardError(
   3475           [working_mean, broken_mean]) /
   3476           max(0.0001, min(mean_of_good_runs, mean_of_bad_runs))) * 100.0
   3477 
   3478       # Give a "confidence" in the bisect. At the moment we use how distinct the
   3479       # values are before and after the last broken revision, and how noisy the
   3480       # overall graph is.
   3481       confidence = CalculateConfidence(working_means, broken_means)
   3482 
   3483       culprit_revisions = []
   3484 
   3485       cwd = os.getcwd()
   3486       self.ChangeToDepotWorkingDirectory(
   3487           revision_data[last_broken_revision]['depot'])
   3488 
   3489       if revision_data[last_broken_revision]['depot'] == 'cros':
   3490         # Want to get a list of all the commits and what depots they belong
   3491         # to so that we can grab info about each.
   3492         cmd = ['repo', 'forall', '-c',
   3493             'pwd ; git log --pretty=oneline --before=%d --after=%d' % (
   3494             last_broken_revision, first_working_revision + 1)]
   3495         (output, return_code) = RunProcessAndRetrieveOutput(cmd)
   3496 
   3497         changes = []
   3498         assert not return_code, 'An error occurred while running'\
   3499                                 ' "%s"' % ' '.join(cmd)
   3500         last_depot = None
   3501         cwd = os.getcwd()
   3502         for l in output.split('\n'):
   3503           if l:
   3504             # Output will be in form:
   3505             # /path_to_depot
   3506             # /path_to_other_depot
   3507             # <SHA1>
   3508             # /path_again
   3509             # <SHA1>
   3510             # etc.
   3511             if l[0] == '/':
   3512               last_depot = l
   3513             else:
   3514               contents = l.split(' ')
   3515               if len(contents) > 1:
   3516                 changes.append([last_depot, contents[0]])
   3517         for c in changes:
   3518           os.chdir(c[0])
   3519           info = self.source_control.QueryRevisionInfo(c[1])
   3520           culprit_revisions.append((c[1], info, None))
   3521       else:
   3522         for i in xrange(last_broken_revision_index, len(revision_data_sorted)):
   3523           k, v = revision_data_sorted[i]
   3524           if k == first_working_revision:
   3525             break
   3526           self.ChangeToDepotWorkingDirectory(v['depot'])
   3527           info = self.source_control.QueryRevisionInfo(k)
   3528           culprit_revisions.append((k, info, v['depot']))
   3529       os.chdir(cwd)
   3530 
   3531       # Check for any other possible regression ranges
   3532       other_regressions = self._FindOtherRegressions(revision_data_sorted,
   3533           mean_of_bad_runs > mean_of_good_runs)
   3534 
   3535     return {
   3536         'first_working_revision': first_working_revision,
   3537         'last_broken_revision': last_broken_revision,
   3538         'culprit_revisions': culprit_revisions,
   3539         'other_regressions': other_regressions,
   3540         'regression_size': regression_size,
   3541         'regression_std_err': regression_std_err,
   3542         'confidence': confidence,
   3543         }
   3544 
   3545   def _CheckForWarnings(self, results_dict):
   3546     if len(results_dict['culprit_revisions']) > 1:
   3547       self.warnings.append('Due to build errors, regression range could '
   3548                            'not be narrowed down to a single commit.')
   3549     if self.opts.repeat_test_count == 1:
   3550       self.warnings.append('Tests were only set to run once. This may '
   3551                            'be insufficient to get meaningful results.')
   3552     if results_dict['confidence'] < 100:
   3553       if results_dict['confidence']:
   3554         self.warnings.append(
   3555             'Confidence is less than 100%. There could be other candidates '
   3556             'for this regression. Try bisecting again with increased '
   3557             'repeat_count or on a sub-metric that shows the regression more '
   3558             'clearly.')
   3559       else:
   3560         self.warnings.append(
   3561           'Confidence is 0%. Try bisecting again on another platform, with '
   3562           'increased repeat_count or on a sub-metric that shows the '
   3563           'regression more clearly.')
   3564 
   3565   def FormatAndPrintResults(self, bisect_results):
   3566     """Prints the results from a bisection run in a readable format.
   3567 
   3568     Args
   3569       bisect_results: The results from a bisection test run.
   3570     """
   3571     revision_data = bisect_results['revision_data']
   3572     revision_data_sorted = sorted(revision_data.iteritems(),
   3573                                   key = lambda x: x[1]['sort'])
   3574     results_dict = self._GetResultsDict(revision_data, revision_data_sorted)
   3575 
   3576     self._CheckForWarnings(results_dict)
   3577 
   3578     if self.opts.output_buildbot_annotations:
   3579       bisect_utils.OutputAnnotationStepStart('Build Status Per Revision')
   3580 
   3581     print
   3582     print 'Full results of bisection:'
   3583     for current_id, current_data  in revision_data_sorted:
   3584       build_status = current_data['passed']
   3585 
   3586       if type(build_status) is bool:
   3587         if build_status:
   3588           build_status = 'Good'
   3589         else:
   3590           build_status = 'Bad'
   3591 
   3592       print '  %20s  %40s  %s' % (current_data['depot'],
   3593                                   current_id, build_status)
   3594     print
   3595 
   3596     if self.opts.output_buildbot_annotations:
   3597       bisect_utils.OutputAnnotationStepClosed()
   3598       # The perf dashboard scrapes the "results" step in order to comment on
   3599       # bugs. If you change this, please update the perf dashboard as well.
   3600       bisect_utils.OutputAnnotationStepStart('Results')
   3601 
   3602     if results_dict['culprit_revisions'] and results_dict['confidence']:
   3603       self._PrintBanner(results_dict)
   3604       for culprit in results_dict['culprit_revisions']:
   3605         cl, info, depot = culprit
   3606         self._PrintRevisionInfo(cl, info, depot)
   3607       self._PrintReproSteps()
   3608       if results_dict['other_regressions']:
   3609         self._PrintOtherRegressions(results_dict['other_regressions'],
   3610                                     revision_data)
   3611     else:
   3612       self._PrintFailedBanner(results_dict)
   3613       self._PrintReproSteps()
   3614 
   3615     self._PrintTestedCommitsTable(revision_data_sorted,
   3616                                   results_dict['first_working_revision'],
   3617                                   results_dict['last_broken_revision'],
   3618                                   results_dict['confidence'])
   3619     self._PrintStepTime(revision_data_sorted)
   3620     self._PrintWarnings()
   3621 
   3622     if self.opts.output_buildbot_annotations:
   3623       bisect_utils.OutputAnnotationStepClosed()
   3624 
   3625 
   3626 def DetermineAndCreateSourceControl(opts):
   3627   """Attempts to determine the underlying source control workflow and returns
   3628   a SourceControl object.
   3629 
   3630   Returns:
   3631     An instance of a SourceControl object, or None if the current workflow
   3632     is unsupported.
   3633   """
   3634 
   3635   (output, _) = RunGit(['rev-parse', '--is-inside-work-tree'])
   3636 
   3637   if output.strip() == 'true':
   3638     return GitSourceControl(opts)
   3639 
   3640   return None
   3641 
   3642 
   3643 def IsPlatformSupported(opts):
   3644   """Checks that this platform and build system are supported.
   3645 
   3646   Args:
   3647     opts: The options parsed from the command line.
   3648 
   3649   Returns:
   3650     True if the platform and build system are supported.
   3651   """
   3652   # Haven't tested the script out on any other platforms yet.
   3653   supported = ['posix', 'nt']
   3654   return os.name in supported
   3655 
   3656 
   3657 def RmTreeAndMkDir(path_to_dir, skip_makedir=False):
   3658   """Removes the directory tree specified, and then creates an empty
   3659   directory in the same location (if not specified to skip).
   3660 
   3661   Args:
   3662     path_to_dir: Path to the directory tree.
   3663     skip_makedir: Whether to skip creating empty directory, default is False.
   3664 
   3665   Returns:
   3666     True if successful, False if an error occurred.
   3667   """
   3668   try:
   3669     if os.path.exists(path_to_dir):
   3670       shutil.rmtree(path_to_dir)
   3671   except OSError, e:
   3672     if e.errno != errno.ENOENT:
   3673       return False
   3674 
   3675   if not skip_makedir:
   3676     return MaybeMakeDirectory(path_to_dir)
   3677 
   3678   return True
   3679 
   3680 
   3681 def RemoveBuildFiles(build_type):
   3682   """Removes build files from previous runs."""
   3683   if RmTreeAndMkDir(os.path.join('out', build_type)):
   3684     if RmTreeAndMkDir(os.path.join('build', build_type)):
   3685       return True
   3686   return False
   3687 
   3688 
   3689 class BisectOptions(object):
   3690   """Options to be used when running bisection."""
   3691   def __init__(self):
   3692     super(BisectOptions, self).__init__()
   3693 
   3694     self.target_platform = 'chromium'
   3695     self.build_preference = None
   3696     self.good_revision = None
   3697     self.bad_revision = None
   3698     self.use_goma = None
   3699     self.cros_board = None
   3700     self.cros_remote_ip = None
   3701     self.repeat_test_count = 20
   3702     self.truncate_percent = 25
   3703     self.max_time_minutes = 20
   3704     self.metric = None
   3705     self.command = None
   3706     self.output_buildbot_annotations = None
   3707     self.no_custom_deps = False
   3708     self.working_directory = None
   3709     self.extra_src = None
   3710     self.debug_ignore_build = None
   3711     self.debug_ignore_sync = None
   3712     self.debug_ignore_perf_test = None
   3713     self.gs_bucket = None
   3714     self.target_arch = 'ia32'
   3715     self.target_build_type = 'Release'
   3716     self.builder_host = None
   3717     self.builder_port = None
   3718     self.bisect_mode = BISECT_MODE_MEAN
   3719 
   3720   def _CreateCommandLineParser(self):
   3721     """Creates a parser with bisect options.
   3722 
   3723     Returns:
   3724       An instance of optparse.OptionParser.
   3725     """
   3726     usage = ('%prog [options] [-- chromium-options]\n'
   3727              'Perform binary search on revision history to find a minimal '
   3728              'range of revisions where a peformance metric regressed.\n')
   3729 
   3730     parser = optparse.OptionParser(usage=usage)
   3731 
   3732     group = optparse.OptionGroup(parser, 'Bisect options')
   3733     group.add_option('-c', '--command',
   3734                      type='str',
   3735                      help='A command to execute your performance test at' +
   3736                      ' each point in the bisection.')
   3737     group.add_option('-b', '--bad_revision',
   3738                      type='str',
   3739                      help='A bad revision to start bisection. ' +
   3740                      'Must be later than good revision. May be either a git' +
   3741                      ' or svn revision.')
   3742     group.add_option('-g', '--good_revision',
   3743                      type='str',
   3744                      help='A revision to start bisection where performance' +
   3745                      ' test is known to pass. Must be earlier than the ' +
   3746                      'bad revision. May be either a git or svn revision.')
   3747     group.add_option('-m', '--metric',
   3748                      type='str',
   3749                      help='The desired metric to bisect on. For example ' +
   3750                      '"vm_rss_final_b/vm_rss_f_b"')
   3751     group.add_option('-r', '--repeat_test_count',
   3752                      type='int',
   3753                      default=20,
   3754                      help='The number of times to repeat the performance '
   3755                      'test. Values will be clamped to range [1, 100]. '
   3756                      'Default value is 20.')
   3757     group.add_option('--max_time_minutes',
   3758                      type='int',
   3759                      default=20,
   3760                      help='The maximum time (in minutes) to take running the '
   3761                      'performance tests. The script will run the performance '
   3762                      'tests according to --repeat_test_count, so long as it '
   3763                      'doesn\'t exceed --max_time_minutes. Values will be '
   3764                      'clamped to range [1, 60].'
   3765                      'Default value is 20.')
   3766     group.add_option('-t', '--truncate_percent',
   3767                      type='int',
   3768                      default=25,
   3769                      help='The highest/lowest % are discarded to form a '
   3770                      'truncated mean. Values will be clamped to range [0, '
   3771                      '25]. Default value is 25 (highest/lowest 25% will be '
   3772                      'discarded).')
   3773     group.add_option('--bisect_mode',
   3774                      type='choice',
   3775                      choices=[BISECT_MODE_MEAN, BISECT_MODE_STD_DEV,
   3776                         BISECT_MODE_RETURN_CODE],
   3777                      default=BISECT_MODE_MEAN,
   3778                      help='The bisect mode. Choices are to bisect on the '
   3779                      'difference in mean, std_dev, or return_code.')
   3780     parser.add_option_group(group)
   3781 
   3782     group = optparse.OptionGroup(parser, 'Build options')
   3783     group.add_option('-w', '--working_directory',
   3784                      type='str',
   3785                      help='Path to the working directory where the script '
   3786                      'will do an initial checkout of the chromium depot. The '
   3787                      'files will be placed in a subdirectory "bisect" under '
   3788                      'working_directory and that will be used to perform the '
   3789                      'bisection. This parameter is optional, if it is not '
   3790                      'supplied, the script will work from the current depot.')
   3791     group.add_option('--build_preference',
   3792                      type='choice',
   3793                      choices=['msvs', 'ninja', 'make'],
   3794                      help='The preferred build system to use. On linux/mac '
   3795                      'the options are make/ninja. On Windows, the options '
   3796                      'are msvs/ninja.')
   3797     group.add_option('--target_platform',
   3798                      type='choice',
   3799                      choices=['chromium', 'cros', 'android', 'android-chrome'],
   3800                      default='chromium',
   3801                      help='The target platform. Choices are "chromium" '
   3802                      '(current platform), "cros", or "android". If you '
   3803                      'specify something other than "chromium", you must be '
   3804                      'properly set up to build that platform.')
   3805     group.add_option('--no_custom_deps',
   3806                      dest='no_custom_deps',
   3807                      action="store_true",
   3808                      default=False,
   3809                      help='Run the script with custom_deps or not.')
   3810     group.add_option('--extra_src',
   3811                      type='str',
   3812                      help='Path to a script which can be used to modify '
   3813                      'the bisect script\'s behavior.')
   3814     group.add_option('--cros_board',
   3815                      type='str',
   3816                      help='The cros board type to build.')
   3817     group.add_option('--cros_remote_ip',
   3818                      type='str',
   3819                      help='The remote machine to image to.')
   3820     group.add_option('--use_goma',
   3821                      action="store_true",
   3822                      help='Add a bunch of extra threads for goma, and enable '
   3823                      'goma')
   3824     group.add_option('--output_buildbot_annotations',
   3825                      action="store_true",
   3826                      help='Add extra annotation output for buildbot.')
   3827     group.add_option('--gs_bucket',
   3828                      default='',
   3829                      dest='gs_bucket',
   3830                      type='str',
   3831                      help=('Name of Google Storage bucket to upload or '
   3832                      'download build. e.g., chrome-perf'))
   3833     group.add_option('--target_arch',
   3834                      type='choice',
   3835                      choices=['ia32', 'x64', 'arm'],
   3836                      default='ia32',
   3837                      dest='target_arch',
   3838                      help=('The target build architecture. Choices are "ia32" '
   3839                      '(default), "x64" or "arm".'))
   3840     group.add_option('--target_build_type',
   3841                      type='choice',
   3842                      choices=['Release', 'Debug'],
   3843                      default='Release',
   3844                      help='The target build type. Choices are "Release" '
   3845                      '(default), or "Debug".')
   3846     group.add_option('--builder_host',
   3847                      dest='builder_host',
   3848                      type='str',
   3849                      help=('Host address of server to produce build by posting'
   3850                            ' try job request.'))
   3851     group.add_option('--builder_port',
   3852                      dest='builder_port',
   3853                      type='int',
   3854                      help=('HTTP port of the server to produce build by posting'
   3855                            ' try job request.'))
   3856     parser.add_option_group(group)
   3857 
   3858     group = optparse.OptionGroup(parser, 'Debug options')
   3859     group.add_option('--debug_ignore_build',
   3860                      action="store_true",
   3861                      help='DEBUG: Don\'t perform builds.')
   3862     group.add_option('--debug_ignore_sync',
   3863                      action="store_true",
   3864                      help='DEBUG: Don\'t perform syncs.')
   3865     group.add_option('--debug_ignore_perf_test',
   3866                      action="store_true",
   3867                      help='DEBUG: Don\'t perform performance tests.')
   3868     parser.add_option_group(group)
   3869     return parser
   3870 
   3871   def ParseCommandLine(self):
   3872     """Parses the command line for bisect options."""
   3873     parser = self._CreateCommandLineParser()
   3874     (opts, _) = parser.parse_args()
   3875 
   3876     try:
   3877       if not opts.command:
   3878         raise RuntimeError('missing required parameter: --command')
   3879 
   3880       if not opts.good_revision:
   3881         raise RuntimeError('missing required parameter: --good_revision')
   3882 
   3883       if not opts.bad_revision:
   3884         raise RuntimeError('missing required parameter: --bad_revision')
   3885 
   3886       if not opts.metric and opts.bisect_mode != BISECT_MODE_RETURN_CODE:
   3887         raise RuntimeError('missing required parameter: --metric')
   3888 
   3889       if opts.gs_bucket:
   3890         if not cloud_storage.List(opts.gs_bucket):
   3891           raise RuntimeError('Invalid Google Storage: gs://%s' % opts.gs_bucket)
   3892         if not opts.builder_host:
   3893           raise RuntimeError('Must specify try server hostname, when '
   3894                              'gs_bucket is used: --builder_host')
   3895         if not opts.builder_port:
   3896           raise RuntimeError('Must specify try server port number, when '
   3897                              'gs_bucket is used: --builder_port')
   3898       if opts.target_platform == 'cros':
   3899         # Run sudo up front to make sure credentials are cached for later.
   3900         print 'Sudo is required to build cros:'
   3901         print
   3902         RunProcess(['sudo', 'true'])
   3903 
   3904         if not opts.cros_board:
   3905           raise RuntimeError('missing required parameter: --cros_board')
   3906 
   3907         if not opts.cros_remote_ip:
   3908           raise RuntimeError('missing required parameter: --cros_remote_ip')
   3909 
   3910         if not opts.working_directory:
   3911           raise RuntimeError('missing required parameter: --working_directory')
   3912 
   3913       metric_values = opts.metric.split('/')
   3914       if (len(metric_values) != 2 and
   3915           opts.bisect_mode != BISECT_MODE_RETURN_CODE):
   3916         raise RuntimeError("Invalid metric specified: [%s]" % opts.metric)
   3917 
   3918       opts.metric = metric_values
   3919       opts.repeat_test_count = min(max(opts.repeat_test_count, 1), 100)
   3920       opts.max_time_minutes = min(max(opts.max_time_minutes, 1), 60)
   3921       opts.truncate_percent = min(max(opts.truncate_percent, 0), 25)
   3922       opts.truncate_percent = opts.truncate_percent / 100.0
   3923 
   3924       for k, v in opts.__dict__.iteritems():
   3925         assert hasattr(self, k), "Invalid %s attribute in BisectOptions." % k
   3926         setattr(self, k, v)
   3927     except RuntimeError, e:
   3928       output_string = StringIO.StringIO()
   3929       parser.print_help(file=output_string)
   3930       error_message = '%s\n\n%s' % (e.message, output_string.getvalue())
   3931       output_string.close()
   3932       raise RuntimeError(error_message)
   3933 
   3934   @staticmethod
   3935   def FromDict(values):
   3936     """Creates an instance of BisectOptions with the values parsed from a
   3937     .cfg file.
   3938 
   3939     Args:
   3940       values: a dict containing options to set.
   3941 
   3942     Returns:
   3943       An instance of BisectOptions.
   3944     """
   3945     opts = BisectOptions()
   3946     for k, v in values.iteritems():
   3947       assert hasattr(opts, k), 'Invalid %s attribute in '\
   3948           'BisectOptions.' % k
   3949       setattr(opts, k, v)
   3950 
   3951     metric_values = opts.metric.split('/')
   3952     if len(metric_values) != 2:
   3953       raise RuntimeError("Invalid metric specified: [%s]" % opts.metric)
   3954 
   3955     opts.metric = metric_values
   3956     opts.repeat_test_count = min(max(opts.repeat_test_count, 1), 100)
   3957     opts.max_time_minutes = min(max(opts.max_time_minutes, 1), 60)
   3958     opts.truncate_percent = min(max(opts.truncate_percent, 0), 25)
   3959     opts.truncate_percent = opts.truncate_percent / 100.0
   3960 
   3961     return opts
   3962 
   3963 
   3964 def main():
   3965 
   3966   try:
   3967     opts = BisectOptions()
   3968     opts.ParseCommandLine()
   3969 
   3970     if opts.extra_src:
   3971       extra_src = bisect_utils.LoadExtraSrc(opts.extra_src)
   3972       if not extra_src:
   3973         raise RuntimeError("Invalid or missing --extra_src.")
   3974       _AddAdditionalDepotInfo(extra_src.GetAdditionalDepotInfo())
   3975 
   3976     if opts.working_directory:
   3977       custom_deps = bisect_utils.DEFAULT_GCLIENT_CUSTOM_DEPS
   3978       if opts.no_custom_deps:
   3979         custom_deps = None
   3980       bisect_utils.CreateBisectDirectoryAndSetupDepot(opts, custom_deps)
   3981 
   3982       os.chdir(os.path.join(os.getcwd(), 'src'))
   3983 
   3984       if not RemoveBuildFiles(opts.target_build_type):
   3985         raise RuntimeError('Something went wrong removing the build files.')
   3986 
   3987     if not IsPlatformSupported(opts):
   3988       raise RuntimeError("Sorry, this platform isn't supported yet.")
   3989 
   3990     # Check what source control method they're using. Only support git workflow
   3991     # at the moment.
   3992     source_control = DetermineAndCreateSourceControl(opts)
   3993 
   3994     if not source_control:
   3995       raise RuntimeError("Sorry, only the git workflow is supported at the "
   3996           "moment.")
   3997 
   3998     # gClient sync seems to fail if you're not in master branch.
   3999     if (not source_control.IsInProperBranch() and
   4000         not opts.debug_ignore_sync and
   4001         not opts.working_directory):
   4002       raise RuntimeError("You must switch to master branch to run bisection.")
   4003     bisect_test = BisectPerformanceMetrics(source_control, opts)
   4004     try:
   4005       bisect_results = bisect_test.Run(opts.command,
   4006                                        opts.bad_revision,
   4007                                        opts.good_revision,
   4008                                        opts.metric)
   4009       if bisect_results['error']:
   4010         raise RuntimeError(bisect_results['error'])
   4011       bisect_test.FormatAndPrintResults(bisect_results)
   4012       return 0
   4013     finally:
   4014       bisect_test.PerformCleanup()
   4015   except RuntimeError, e:
   4016     if opts.output_buildbot_annotations:
   4017       # The perf dashboard scrapes the "results" step in order to comment on
   4018       # bugs. If you change this, please update the perf dashboard as well.
   4019       bisect_utils.OutputAnnotationStepStart('Results')
   4020     print 'Error: %s' % e.message
   4021     if opts.output_buildbot_annotations:
   4022       bisect_utils.OutputAnnotationStepClosed()
   4023   return 1
   4024 
   4025 if __name__ == '__main__':
   4026   sys.exit(main())
   4027