Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Snapshot Build Bisect Tool | 6 """Snapshot Build Bisect Tool |
| 7 | 7 |
| 8 This script bisects a snapshot archive using binary search. It starts at | 8 This script bisects a snapshot archive using binary search. It starts at |
| 9 a bad revision (it will try to guess HEAD) and asks for a last known-good | 9 a bad revision (it will try to guess HEAD) and asks for a last known-good |
| 10 revision. It will then binary search across this revision range by downloading, | 10 revision. It will then binary search across this revision range by downloading, |
| 11 unzipping, and opening Chromium for you. After testing the specific revision, | 11 unzipping, and opening Chromium for you. After testing the specific revision, |
| 12 it will ask you whether it is good or bad before continuing the search. | 12 it will ask you whether it is good or bad before continuing the search. |
| 13 """ | 13 """ |
| 14 | 14 |
| 15 # The root URL for storage. | 15 # The root URL for storage. |
| 16 BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-continuous' | 16 BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-snapshots' |
| 17 | 17 |
| 18 # URL to the ViewVC commit page. | 18 # URL to the ViewVC commit page. |
| 19 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d' | 19 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d' |
| 20 | 20 |
| 21 # Changelogs URL. | 21 # Changelogs URL. |
| 22 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ | 22 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ |
| 23 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' | 23 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' |
| 24 | 24 |
| 25 ############################################################################### | 25 ############################################################################### |
| 26 | 26 |
| 27 import math | 27 import math |
| 28 import optparse | 28 import optparse |
| 29 import os | 29 import os |
| 30 import pipes | 30 import pipes |
| 31 import re | 31 import re |
| 32 import shutil | 32 import shutil |
| 33 import sys | 33 import sys |
| 34 import tempfile | 34 import tempfile |
| 35 import urllib | 35 import urllib |
| 36 from xml.etree import ElementTree | 36 from xml.etree import ElementTree |
| 37 import zipfile | 37 import zipfile |
| 38 import subprocess | |
| 39 import threading | |
|
Evan Martin
2011/07/14 18:57:52
I think this list is alphabetized.
szager
2011/07/14 19:23:03
Done.
| |
| 38 | 40 |
| 39 class PathContext(object): | 41 class PathContext(object): |
| 40 """A PathContext is used to carry the information used to construct URLs and | 42 """A PathContext is used to carry the information used to construct URLs and |
| 41 paths when dealing with the storage server and archives.""" | 43 paths when dealing with the storage server and archives.""" |
| 42 def __init__(self, platform, good_revision, bad_revision): | 44 def __init__(self, platform, good_revision, bad_revision): |
| 43 super(PathContext, self).__init__() | 45 super(PathContext, self).__init__() |
| 44 # Store off the input parameters. | 46 # Store off the input parameters. |
| 45 self.platform = platform # What's passed in to the '-a/--archive' option. | 47 self.platform = platform # What's passed in to the '-a/--archive' option. |
| 46 self.good_revision = good_revision | 48 self.good_revision = good_revision |
| 47 self.bad_revision = bad_revision | 49 self.bad_revision = bad_revision |
| 48 | 50 |
| 49 # The name of the ZIP file in a revision directory on the server. | 51 # The name of the ZIP file in a revision directory on the server. |
| 50 self.archive_name = None | 52 self.archive_name = None |
| 51 | 53 |
| 52 # Set some internal members: | 54 # Set some internal members: |
| 53 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. | 55 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. |
| 54 # _archive_extract_dir = Uncompressed directory in the archive_name file. | 56 # _archive_extract_dir = Uncompressed directory in the archive_name file. |
| 55 # _binary_name = The name of the executable to run. | 57 # _binary_name = The name of the executable to run. |
| 56 if self.platform == 'linux' or self.platform == 'linux-64': | 58 if self.platform == 'linux' or self.platform == 'linux64': |
| 57 self._listing_platform_dir = 'Linux/' | 59 self._listing_platform_dir = 'Linux/' |
| 58 self.archive_name = 'chrome-linux.zip' | 60 self.archive_name = 'chrome-linux.zip' |
| 59 self._archive_extract_dir = 'chrome-linux' | 61 self._archive_extract_dir = 'chrome-linux' |
| 60 self._binary_name = 'chrome' | 62 self._binary_name = 'chrome' |
| 61 # Linux and x64 share all the same path data except for the archive dir. | 63 # Linux and x64 share all the same path data except for the archive dir. |
| 62 if self.platform == 'linux-64': | 64 if self.platform == 'linux64': |
| 63 self._listing_platform_dir = 'Linux_x64/' | 65 self._listing_platform_dir = 'Linux_x64/' |
| 64 elif self.platform == 'mac': | 66 elif self.platform == 'mac': |
| 65 self._listing_platform_dir = 'Mac/' | 67 self._listing_platform_dir = 'Mac/' |
| 66 self.archive_name = 'chrome-mac.zip' | 68 self.archive_name = 'chrome-mac.zip' |
| 67 self._archive_extract_dir = 'chrome-mac' | 69 self._archive_extract_dir = 'chrome-mac' |
| 68 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' | 70 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' |
| 69 elif self.platform == 'win': | 71 elif self.platform == 'win': |
| 70 self._listing_platform_dir = 'Win/' | 72 self._listing_platform_dir = 'Win/' |
| 71 self.archive_name = 'chrome-win32.zip' | 73 self.archive_name = 'chrome-win32.zip' |
| 72 self._archive_extract_dir = 'chrome-win32' | 74 self._archive_extract_dir = 'chrome-win32' |
| 73 self._binary_name = 'chrome.exe' | 75 self._binary_name = 'chrome.exe' |
| 74 else: | 76 else: |
| 75 raise Exception("Invalid platform") | 77 raise Exception("Invalid platform") |
| 76 | 78 |
| 77 def GetListingURL(self, marker=None): | 79 def GetListingURL(self, marker=None): |
| 78 """Returns the URL for a directory listing, with an optional marker.""" | 80 """Returns the URL for a directory listing, with an optional marker.""" |
| 79 marker_param = '' | 81 marker_param = '' |
| 80 if marker: | 82 if marker: |
| 81 marker_param = '&marker=' + str(marker) | 83 marker_param = '&marker=' + str(marker) |
| 82 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ | 84 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ |
| 83 marker_param | 85 marker_param |
| 84 | 86 |
| 85 def GetDownloadURL(self, revision): | 87 def GetDownloadURL(self, revision): |
| 86 """Gets the download URL for a build archive of a specific revision.""" | 88 """Gets the download URL for a build archive of a specific revision.""" |
| 87 return BASE_URL + '/' + self._listing_platform_dir + str(revision) + '/' + \ | 89 return "%s/%s%d/%s" % ( |
| 88 self.archive_name | 90 BASE_URL, self._listing_platform_dir, revision, self.archive_name) |
| 89 | 91 |
| 90 def GetLastChangeURL(self): | 92 def GetLastChangeURL(self): |
| 91 """Returns a URL to the LAST_CHANGE file.""" | 93 """Returns a URL to the LAST_CHANGE file.""" |
| 92 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' | 94 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' |
| 93 | 95 |
| 94 def GetLaunchPath(self): | 96 def GetLaunchPath(self): |
| 95 """Returns a relative path (presumably from the archive extraction location) | 97 """Returns a relative path (presumably from the archive extraction location) |
| 96 that is used to run the executable.""" | 98 that is used to run the executable.""" |
| 97 return os.path.join(self._archive_extract_dir, self._binary_name) | 99 return os.path.join(self._archive_extract_dir, self._binary_name) |
| 98 | 100 |
| 99 | 101 |
| 100 def UnzipFilenameToDir(filename, dir): | 102 def UnzipFilenameToDir(filename, dir): |
| 101 """Unzip |filename| to directory |dir|.""" | 103 """Unzip |filename| to directory |dir|.""" |
| 104 pushd = os.getcwd() | |
| 105 if not os.path.isabs(filename): | |
| 106 filename = os.path.join(pushd, filename) | |
| 102 zf = zipfile.ZipFile(filename) | 107 zf = zipfile.ZipFile(filename) |
| 103 # Make base. | 108 # Make base. |
| 104 pushd = os.getcwd() | |
| 105 try: | 109 try: |
| 106 if not os.path.isdir(dir): | 110 if not os.path.isdir(dir): |
| 107 os.mkdir(dir) | 111 os.mkdir(dir) |
| 108 os.chdir(dir) | 112 os.chdir(dir) |
| 109 # Extract files. | 113 # Extract files. |
| 110 for info in zf.infolist(): | 114 for info in zf.infolist(): |
| 111 name = info.filename | 115 name = info.filename |
| 112 if name.endswith('/'): # dir | 116 if name.endswith('/'): # dir |
| 113 if not os.path.isdir(name): | 117 if not os.path.isdir(name): |
| 114 os.makedirs(name) | 118 os.makedirs(name) |
| (...skipping 39 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 154 is_truncated = document.find(namespace + 'IsTruncated') | 158 is_truncated = document.find(namespace + 'IsTruncated') |
| 155 if is_truncated is not None and is_truncated.text.lower() == 'true': | 159 if is_truncated is not None and is_truncated.text.lower() == 'true': |
| 156 next_marker = document.find(namespace + 'NextMarker').text | 160 next_marker = document.find(namespace + 'NextMarker').text |
| 157 | 161 |
| 158 # Get a list of all the revisions. | 162 # Get a list of all the revisions. |
| 159 all_prefixes = document.findall(namespace + 'CommonPrefixes/' + | 163 all_prefixes = document.findall(namespace + 'CommonPrefixes/' + |
| 160 namespace + 'Prefix') | 164 namespace + 'Prefix') |
| 161 # The <Prefix> nodes have content of the form of | 165 # The <Prefix> nodes have content of the form of |
| 162 # |_listing_platform_dir/revision/|. Strip off the platform dir and the | 166 # |_listing_platform_dir/revision/|. Strip off the platform dir and the |
| 163 # trailing slash to just have a number. | 167 # trailing slash to just have a number. |
| 164 revisions = map(lambda x: x.text[len(prefix):-1], all_prefixes) | 168 #revisions = map(lambda x: x.text[len(prefix):-1], all_prefixes) |
|
Evan Martin
2011/07/14 18:57:52
Did you mean to leave in this commented-out code?
szager
2011/07/14 19:23:03
Deleted!
| |
| 169 revisions = [] | |
| 170 for pf in all_prefixes: | |
|
Robert Sesek
2011/07/15 16:27:14
What's "pf"? |prefix| is fine.
| |
| 171 revnum = pf.text[len(prefix):-1] | |
| 172 try: | |
| 173 revnum = int(revnum) | |
| 174 revisions.append(revnum) | |
| 175 except ValueError: | |
| 176 pass | |
| 165 return (revisions, next_marker) | 177 return (revisions, next_marker) |
| 166 | 178 |
| 167 # Fetch the first list of revisions. | 179 # Fetch the first list of revisions. |
| 168 (revisions, next_marker) = _FetchAndParse(context.GetListingURL()) | 180 (revisions, next_marker) = _FetchAndParse(context.GetListingURL()) |
| 169 # If the result list was truncated, refetch with the next marker. Do this | 181 # If the result list was truncated, refetch with the next marker. Do this |
| 170 # until an entire directory listing is done. | 182 # until an entire directory listing is done. |
| 171 while next_marker: | 183 while next_marker: |
| 172 (new_revisions, next_marker) = _FetchAndParse( | 184 (new_revisions, next_marker) = _FetchAndParse( |
| 173 context.GetListingURL(next_marker)) | 185 context.GetListingURL(next_marker)) |
| 174 revisions.extend(new_revisions) | 186 revisions.extend(new_revisions) |
| 175 | 187 |
| 176 return revisions | 188 return revisions |
| 177 | 189 |
| 178 | 190 |
| 179 def GetRevList(context): | 191 def GetRevList(context): |
| 180 """Gets the list of revision numbers between |good_revision| and | 192 """Gets the list of revision numbers between |good_revision| and |
| 181 |bad_revision| of the |context|.""" | 193 |bad_revision| of the |context|.""" |
| 182 # Download the revlist and filter for just the range between good and bad. | 194 # Download the revlist and filter for just the range between good and bad. |
| 183 rev_range = range(context.good_revision, context.bad_revision) | 195 rev_range = range(context.good_revision, context.bad_revision) |
| 184 revlist = map(int, ParseDirectoryIndex(context)) | 196 revlist = map(int, ParseDirectoryIndex(context)) |
| 185 revlist = filter(lambda r: r in rev_range, revlist) | 197 revlist = filter(lambda r: r in rev_range, revlist) |
| 186 revlist.sort() | 198 revlist.sort() |
| 187 return revlist | 199 return revlist |
| 188 | 200 |
| 201 def FetchRevision(context, rev, filename, quit_event=None) : | |
| 202 """Downloads and unzips revision |rev|""" | |
| 189 | 203 |
| 190 def TryRevision(context, rev, profile, args): | 204 def ReportHook(blocknum, blocksize, totalsize): |
| 191 """Downloads revision |rev|, unzips it, and opens it for the user to test. | 205 if quit_event and quit_event.is_set(): |
| 192 |profile| is the profile to use.""" | 206 raise RuntimeError("Aborting download of revision %d" % rev) |
| 193 # Do this in a temp dir so we don't collide with user files. | 207 |
| 208 download_url = context.GetDownloadURL(rev) | |
| 209 | |
| 210 try: | |
| 211 urllib.urlretrieve(download_url, filename, ReportHook) | |
| 212 except RuntimeError, e: | |
| 213 pass | |
| 214 | |
| 215 def RunRevision(context, revision, zipfile, profile, args) : | |
| 216 """Given a zipped revision, unzip it and run the test""" | |
| 217 | |
| 218 print "Trying revision %d..." % revision | |
| 219 | |
| 220 # Create a temp directory and unzip the revision into it | |
| 194 cwd = os.getcwd() | 221 cwd = os.getcwd() |
| 195 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') | 222 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') |
| 223 UnzipFilenameToDir(zipfile, tempdir) | |
| 196 os.chdir(tempdir) | 224 os.chdir(tempdir) |
| 197 | 225 |
| 198 # Download the file. | 226 # Run the test |
| 199 download_url = context.GetDownloadURL(rev) | 227 testargs = [context.GetLaunchPath(), '--user-data-dir=%s' % profile] + args |
| 200 def _ReportHook(blocknum, blocksize, totalsize): | 228 subproc = subprocess.Popen(testargs, |
| 201 size = blocknum * blocksize | 229 bufsize=-1, |
| 202 if totalsize == -1: # Total size not known. | 230 stdout=subprocess.PIPE, |
| 203 progress = "Received %d bytes" % size | 231 stderr=subprocess.PIPE) |
| 204 else: | 232 (stdout, stderr) = subproc.communicate() |
| 205 size = min(totalsize, size) | |
| 206 progress = "Received %d of %d bytes, %.2f%%" % ( | |
| 207 size, totalsize, 100.0 * size / totalsize) | |
| 208 # Send a \r to let all progress messages use just one line of output. | |
| 209 sys.stdout.write("\r" + progress) | |
| 210 sys.stdout.flush() | |
| 211 try: | |
| 212 print 'Fetching ' + download_url | |
| 213 urllib.urlretrieve(download_url, context.archive_name, _ReportHook) | |
| 214 print | |
| 215 except Exception, e: | |
| 216 print('Could not retrieve the download. Sorry.') | |
| 217 sys.exit(-1) | |
| 218 | |
| 219 # Unzip the file. | |
| 220 print 'Unzipping ...' | |
| 221 UnzipFilenameToDir(context.archive_name, os.curdir) | |
| 222 | |
| 223 # Tell the system to open the app. | |
| 224 args = ['--user-data-dir=%s' % profile] + args | |
| 225 flags = ' '.join(map(pipes.quote, args)) | |
| 226 cmd = '%s %s' % (context.GetLaunchPath(), flags) | |
| 227 print 'Running %s' % cmd | |
| 228 os.system(cmd) | |
| 229 | 233 |
| 230 os.chdir(cwd) | 234 os.chdir(cwd) |
| 231 print 'Cleaning temp dir ...' | |
| 232 try: | 235 try: |
| 233 shutil.rmtree(tempdir, True) | 236 shutil.rmtree(tempdir, True) |
| 234 except Exception, e: | 237 except Exception, e: |
| 235 pass | 238 pass |
| 236 | 239 |
| 240 return (subproc.returncode, stdout, stderr) | |
| 237 | 241 |
| 238 def AskIsGoodBuild(rev): | 242 def ResultIsGood(status, stdout, stderr) : |
| 239 """Ask the user whether build |rev| is good or bad.""" | 243 """Ask the user whether build |rev| is good or bad.""" |
| 240 # Loop until we get a response that we can parse. | 244 # Loop until we get a response that we can parse. |
| 241 while True: | 245 while True: |
| 242 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) | 246 response = raw_input('Build is [(g)ood/(b)ad/(q)uit]: ') |
| 243 if response and response in ('g', 'b'): | 247 if response and response in ('g', 'b'): |
| 244 return response == 'g' | 248 return response == 'g' |
| 249 if response and response == 'q': | |
| 250 raise SystemExit() | |
| 245 | 251 |
| 252 def bisect(platform=None, | |
|
Evan Martin
2011/07/14 18:57:52
Capital B on function names. (Our Python style is
szager
2011/07/14 19:23:03
Done.
| |
| 253 profile="", | |
| 254 run_args=(), | |
| 255 good_rev=0, | |
| 256 bad_rev=0, | |
| 257 predicate=ResultIsGood): | |
| 258 """Given known good and known bad revisions, run a binary search on all | |
| 259 archived revisions to determine the last known good revision.""" | |
| 260 | |
| 261 context = PathContext(platform, good_rev, bad_rev) | |
| 262 cwd = os.getcwd() | |
| 263 | |
| 264 revlist = GetRevList(context) | |
| 265 | |
| 266 # Get a list of revisions to bisect across. | |
| 267 if len(revlist) < 2: # Don't have enough builds to bisect | |
| 268 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist | |
| 269 raise RuntimeError(msg) | |
| 270 | |
| 271 # If we don't have a |good_rev|, set it to be the first revision possible. | |
| 272 if good_rev == 0: | |
| 273 good_rev = revlist[0] | |
| 274 | |
| 275 # Figure out our bookends and first pivot point; fetch the pivot revision | |
| 276 good = 0 | |
| 277 bad = len(revlist) - 1 | |
| 278 pivot = bad/2 | |
| 279 rev = revlist[pivot] | |
| 280 zipfile = os.path.join(cwd, '%d-%s' % (rev, context.archive_name)) | |
| 281 FetchRevision(context, rev, zipfile) | |
| 282 | |
| 283 # Binary search time! | |
| 284 while zipfile and bad - good > 1: | |
| 285 print "iterating with good=%d bad=%d pivot=%d" % ( | |
| 286 revlist[good], revlist[bad], revlist[pivot]) | |
| 287 | |
| 288 # Pre-fetch next two possible pivots | |
| 289 down_pivot = int((pivot - good) / 2) + good | |
| 290 down_thread = None | |
| 291 if down_pivot != pivot and down_pivot != good : | |
| 292 down_rev = revlist[down_pivot] | |
| 293 zipfile_base = '%d-%s' % (down_rev, context.archive_name) | |
| 294 down_zipfile = os.path.join(cwd, zipfile_base) | |
| 295 down_event = threading.Event() | |
| 296 fetchargs = (context, down_rev, down_zipfile, down_event) | |
| 297 down_thread = threading.Thread(target=FetchRevision, | |
| 298 name='down_fetch', | |
| 299 args=fetchargs) | |
| 300 down_thread.start() | |
| 301 | |
| 302 up_pivot = int((bad - pivot) / 2) + pivot | |
| 303 up_thread = None | |
| 304 if up_pivot != pivot and up_pivot != bad : | |
| 305 up_rev = revlist[up_pivot] | |
| 306 zipfile_base = '%d-%s' % (up_rev, context.archive_name) | |
| 307 up_zipfile = os.path.join(cwd, zipfile_base) | |
| 308 up_event = threading.Event() | |
| 309 fetchargs = (context, up_rev, up_zipfile, up_event) | |
| 310 up_thread = threading.Thread(target=FetchRevision, | |
| 311 name='up_fetch', | |
| 312 args=fetchargs) | |
| 313 up_thread.start() | |
| 314 | |
| 315 # Run test on the pivot revision | |
| 316 (status, stdout, stderr) = RunRevision(context, | |
| 317 rev, | |
| 318 zipfile, | |
| 319 profile, | |
| 320 run_args) | |
| 321 os.unlink(zipfile) | |
| 322 zipfile = None | |
| 323 try: | |
| 324 if predicate(status, stdout, stderr) : | |
| 325 good = pivot | |
| 326 if down_thread : | |
| 327 down_event.set() # Kill the download of older revision | |
| 328 down_thread.join() | |
| 329 os.unlink(down_zipfile) | |
| 330 if up_thread : | |
| 331 print "Downloading revision %d..." % up_rev | |
| 332 up_thread.join() # Wait for newer revision to finish downloading | |
| 333 pivot = up_pivot | |
| 334 zipfile = up_zipfile | |
| 335 else : | |
| 336 bad = pivot | |
| 337 if up_thread : | |
| 338 up_event.set() # Kill download of newer revision | |
| 339 up_thread.join() | |
| 340 os.unlink(up_zipfile) | |
| 341 if down_thread : | |
| 342 print "Downloading revision %d..." % down_rev | |
| 343 down_thread.join() # Wait for older revision to finish downloading | |
| 344 pivot = down_pivot | |
| 345 zipfile = down_zipfile | |
| 346 except SystemExit: | |
| 347 for f in [down_zipfile, up_zipfile]: | |
| 348 try: | |
| 349 os.unlink(f) | |
| 350 except OSError: | |
| 351 pass | |
| 352 sys.exit(0) | |
|
Evan Martin
2011/07/14 18:57:52
Is this both moving the code and making changes to
szager
2011/07/14 19:23:03
I had move the main loop out of main() to enable t
| |
| 353 | |
| 354 rev = revlist[pivot] | |
| 355 | |
| 356 return (good, bad) | |
| 357 | |
| 358 ################################################################################ | |
| 246 | 359 |
| 247 def main(): | 360 def main(): |
| 248 usage = ('%prog [options] [-- chromium-options]\n' | 361 usage = ('%prog [options] [-- chromium-options]\n' |
| 249 'Perform binary search on the snapshot builds.\n' | 362 'Perform binary search on the snapshot builds.\n' |
| 250 '\n' | 363 '\n' |
| 251 'Tip: add "-- --no-first-run" to bypass the first run prompts.') | 364 'Tip: add "-- --no-first-run" to bypass the first run prompts.') |
| 252 parser = optparse.OptionParser(usage=usage) | 365 parser = optparse.OptionParser(usage=usage) |
| 253 # Strangely, the default help output doesn't include the choice list. | 366 # Strangely, the default help output doesn't include the choice list. |
| 254 choices = ['mac', 'win', 'linux', 'linux64'] | 367 choices = ['mac', 'win', 'linux', 'linux64'] |
| 255 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 | 368 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 |
| (...skipping 18 matching lines...) Expand all Loading... | |
| 274 | 387 |
| 275 if opts.bad and opts.good and (opts.good > opts.bad): | 388 if opts.bad and opts.good and (opts.good > opts.bad): |
| 276 print ('The good revision (%d) must precede the bad revision (%d).\n' % | 389 print ('The good revision (%d) must precede the bad revision (%d).\n' % |
| 277 (opts.good, opts.bad)) | 390 (opts.good, opts.bad)) |
| 278 parser.print_help() | 391 parser.print_help() |
| 279 return 1 | 392 return 1 |
| 280 | 393 |
| 281 # Create the context. Initialize 0 for the revisions as they are set below. | 394 # Create the context. Initialize 0 for the revisions as they are set below. |
| 282 context = PathContext(opts.archive, 0, 0) | 395 context = PathContext(opts.archive, 0, 0) |
| 283 | 396 |
| 284 # Pick a starting point, try to get HEAD for this. | 397 if not opts.bad : |
| 285 if opts.bad: | |
| 286 bad_rev = opts.bad | |
| 287 else: | |
| 288 bad_rev = 0 | |
| 289 try: | 398 try: |
| 290 # Location of the latest build revision number | 399 # Location of the latest build revision number |
| 291 nh = urllib.urlopen(context.GetLastChangeURL()) | 400 nh = urllib.urlopen(context.GetLastChangeURL()) |
| 292 latest = int(nh.read()) | 401 latest = int(nh.read()) |
| 293 nh.close() | 402 nh.close() |
| 294 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) | 403 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) |
| 295 if (bad_rev == ''): | 404 if (bad_rev == ''): |
| 296 bad_rev = latest | 405 bad_rev = latest |
| 297 bad_rev = int(bad_rev) | 406 opts.bad = int(bad_rev) |
| 298 except Exception, e: | 407 except Exception, e: |
| 299 print('Could not determine latest revision. This could be bad...') | 408 print('Could not determine latest revision. This could be bad...') |
| 300 bad_rev = int(raw_input('Bad revision: ')) | 409 opts.bad = int(raw_input('Bad revision: ')) |
| 301 | 410 |
| 302 # Find out when we were good. | 411 if not opts.good : |
| 303 if opts.good: | |
| 304 good_rev = opts.good | |
| 305 else: | |
| 306 good_rev = 0 | |
| 307 try: | 412 try: |
| 308 good_rev = int(raw_input('Last known good [0]: ')) | 413 opts.good = int(raw_input('Last known good [0]: ')) |
| 309 except Exception, e: | 414 except Exception, e: |
| 310 pass | 415 pass |
| 311 | 416 |
| 312 # Set the input parameters now that they've been validated. | 417 try : |
| 313 context.good_revision = good_rev | 418 (good, bad) = bisect(opts.archive, opts.profile, args, opts.good, opts.bad) |
| 314 context.bad_revision = bad_rev | 419 # We're done. Let the user know the results in an official manner. |
| 420 print('You are probably looking for build %d.' % bad) | |
| 421 clog_url = CHANGELOG_URL % (good, bad) | |
| 422 print('CHANGELOG URL: %s' % clog_url) | |
| 423 build_url = BUILD_VIEWVC_URL % bad | |
| 424 print('Built at revision: %s' % build_url) | |
| 425 return 0 | |
| 426 except Exception, e: | |
| 427 print e | |
| 428 return 1 | |
| 315 | 429 |
| 316 # Get a list of revisions to bisect across. | |
| 317 revlist = GetRevList(context) | |
| 318 if len(revlist) < 2: # Don't have enough builds to bisect | |
| 319 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist | |
| 320 sys.exit(1) | |
| 321 | |
| 322 # If we don't have a |good_rev|, set it to be the first revision possible. | |
| 323 if good_rev == 0: | |
| 324 good_rev = revlist[0] | |
| 325 | |
| 326 # These are indexes of |revlist|. | |
| 327 good = 0 | |
| 328 bad = len(revlist) - 1 | |
| 329 last_known_good_rev = revlist[good] | |
| 330 | |
| 331 # Binary search time! | |
| 332 while good < bad: | |
| 333 candidates = revlist[good:bad] | |
| 334 num_poss = len(candidates) | |
| 335 if num_poss > 10: | |
| 336 print('%d candidates. %d tries left.' % | |
| 337 (num_poss, round(math.log(num_poss, 2)))) | |
| 338 else: | |
| 339 print('Candidates: %s' % revlist[good:bad]) | |
| 340 | |
| 341 # Cut the problem in half... | |
| 342 test = int((bad - good) / 2) + good | |
| 343 test_rev = revlist[test] | |
| 344 | |
| 345 # Let the user give this rev a spin (in her own profile, if she wants). | |
| 346 profile = opts.profile | |
| 347 if not profile: | |
| 348 profile = 'profile' # In a temp dir. | |
| 349 TryRevision(context, test_rev, profile, args) | |
| 350 if AskIsGoodBuild(test_rev): | |
| 351 last_known_good_rev = revlist[good] | |
| 352 good = test + 1 | |
| 353 else: | |
| 354 bad = test | |
| 355 | |
| 356 # We're done. Let the user know the results in an official manner. | |
| 357 print('You are probably looking for build %d.' % revlist[bad]) | |
| 358 print('CHANGELOG URL:') | |
| 359 print(CHANGELOG_URL % (last_known_good_rev, revlist[bad])) | |
| 360 print('Built at revision:') | |
| 361 print(BUILD_VIEWVC_URL % revlist[bad]) | |
| 362 | 430 |
| 363 if __name__ == '__main__': | 431 if __name__ == '__main__': |
| 364 sys.exit(main()) | 432 sys.exit(main()) |
| OLD | NEW |