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-snapshots' | 16 BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-snapshots' |
| 17 BASE_URL_RECENT = 'http://build.chromium.org/f/chromium/snapshots' | |
| 18 | 17 |
| 19 # URL to the ViewVC commit page. | 18 # URL to the ViewVC commit page. |
| 20 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' |
| 21 | 20 |
| 22 # Changelogs URL. | 21 # Changelogs URL. |
| 23 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ | 22 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ |
| 24 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' | 23 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' |
| 25 | 24 |
| 26 ############################################################################### | 25 ############################################################################### |
| 27 | 26 |
| 28 import math | 27 import math |
| 29 import optparse | 28 import optparse |
| 30 import os | 29 import os |
| 31 import pipes | 30 import pipes |
| 32 import re | 31 import re |
| 33 import shutil | 32 import shutil |
| 33 import subprocess | |
| 34 import sys | 34 import sys |
| 35 import tempfile | 35 import tempfile |
| 36 import threading | |
| 36 import urllib | 37 import urllib |
| 37 from xml.etree import ElementTree | 38 from xml.etree import ElementTree |
| 38 import zipfile | 39 import zipfile |
| 39 | 40 |
| 40 class PathContext(object): | 41 class PathContext(object): |
| 41 """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 |
| 42 paths when dealing with the storage server and archives.""" | 43 paths when dealing with the storage server and archives.""" |
| 43 def __init__(self, platform, good_revision, bad_revision, use_recent): | 44 def __init__(self, platform, good_revision, bad_revision): |
| 44 super(PathContext, self).__init__() | 45 super(PathContext, self).__init__() |
| 45 # Store off the input parameters. | 46 # Store off the input parameters. |
| 46 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. |
| 47 self.good_revision = good_revision | 48 self.good_revision = good_revision |
| 48 self.bad_revision = bad_revision | 49 self.bad_revision = bad_revision |
| 49 self.use_recent = use_recent | |
| 50 | 50 |
| 51 # 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. |
| 52 self.archive_name = None | 52 self.archive_name = None |
| 53 | 53 |
| 54 # Set some internal members: | 54 # Set some internal members: |
| 55 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. | 55 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. |
| 56 # _archive_extract_dir = Uncompressed directory in the archive_name file. | 56 # _archive_extract_dir = Uncompressed directory in the archive_name file. |
| 57 # _binary_name = The name of the executable to run. | 57 # _binary_name = The name of the executable to run. |
| 58 if self.platform == 'linux' or self.platform == 'linux64': | 58 if self.platform == 'linux' or self.platform == 'linux64': |
| 59 self._listing_platform_dir = 'Linux/' | 59 self._listing_platform_dir = 'Linux/' |
| 60 self.archive_name = 'chrome-linux.zip' | 60 self.archive_name = 'chrome-linux.zip' |
| 61 self._archive_extract_dir = 'chrome-linux' | 61 self._archive_extract_dir = 'chrome-linux' |
| 62 self._binary_name = 'chrome' | 62 self._binary_name = 'chrome' |
| 63 # 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. |
| 64 if self.platform == 'linux64': | 64 if self.platform == 'linux64': |
| 65 self._listing_platform_dir = 'Linux_x64/' | 65 self._listing_platform_dir = 'Linux_x64/' |
| 66 elif self.platform == 'mac': | 66 elif self.platform == 'mac': |
| 67 self._listing_platform_dir = 'Mac/' | 67 self._listing_platform_dir = 'Mac/' |
| 68 self.archive_name = 'chrome-mac.zip' | 68 self.archive_name = 'chrome-mac.zip' |
| 69 self._archive_extract_dir = 'chrome-mac' | 69 self._archive_extract_dir = 'chrome-mac' |
| 70 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' | 70 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' |
| 71 elif self.platform == 'win': | 71 elif self.platform == 'win': |
| 72 self._listing_platform_dir = 'Win/' | 72 self._listing_platform_dir = 'Win/' |
| 73 self.archive_name = 'chrome-win32.zip' | 73 self.archive_name = 'chrome-win32.zip' |
| 74 self._archive_extract_dir = 'chrome-win32' | 74 self._archive_extract_dir = 'chrome-win32' |
| 75 self._binary_name = 'chrome.exe' | 75 self._binary_name = 'chrome.exe' |
| 76 else: | 76 else: |
| 77 raise Exception("Invalid platform") | 77 raise Exception('Invalid platform: %s' % self.platform) |
| 78 | 78 |
| 79 def GetListingURL(self, marker=None): | 79 def GetListingURL(self, marker=None): |
| 80 """Returns the URL for a directory listing, with an optional marker.""" | 80 """Returns the URL for a directory listing, with an optional marker.""" |
| 81 marker_param = '' | 81 marker_param = '' |
| 82 if marker: | 82 if marker: |
| 83 marker_param = '&marker=' + str(marker) | 83 marker_param = '&marker=' + str(marker) |
| 84 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ | 84 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ |
| 85 marker_param | 85 marker_param |
| 86 | 86 |
| 87 def GetListingURLRecent(self): | |
| 88 """Returns the URL for a directory listing of recent builds.""" | |
| 89 return BASE_URL_RECENT + '/' + self._listing_platform_dir | |
| 90 | |
| 91 def GetDownloadURL(self, revision): | 87 def GetDownloadURL(self, revision): |
| 92 """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.""" |
| 93 if self.use_recent: | 89 return "%s/%s%d/%s" % ( |
| 94 return "%s/%s%d/%s" % ( | 90 BASE_URL, self._listing_platform_dir, revision, self.archive_name) |
| 95 BASE_URL_RECENT, self._listing_platform_dir, revision, | |
| 96 self.archive_name) | |
| 97 else: | |
| 98 return "%s/%s%d/%s" % ( | |
| 99 BASE_URL, self._listing_platform_dir, revision, self.archive_name) | |
| 100 | 91 |
| 101 def GetLastChangeURL(self): | 92 def GetLastChangeURL(self): |
| 102 """Returns a URL to the LAST_CHANGE file.""" | 93 """Returns a URL to the LAST_CHANGE file.""" |
| 103 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' | 94 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' |
| 104 | 95 |
| 105 def GetLaunchPath(self): | 96 def GetLaunchPath(self): |
| 106 """Returns a relative path (presumably from the archive extraction location) | 97 """Returns a relative path (presumably from the archive extraction location) |
| 107 that is used to run the executable.""" | 98 that is used to run the executable.""" |
| 108 return os.path.join(self._archive_extract_dir, self._binary_name) | 99 return os.path.join(self._archive_extract_dir, self._binary_name) |
| 109 | 100 |
| 101 def ParseDirectoryIndex(self): | |
| 102 """Parses the Google Storage directory listing into a list of revision | |
| 103 numbers. The range starts with self.good_revision and goes until | |
| 104 self.bad_revision.""" | |
| 105 | |
| 106 def _FetchAndParse(url): | |
| 107 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If | |
| 108 next-marker is not None, then the listing is a partial listing and another | |
| 109 fetch should be performed with next-marker being the marker= GET | |
| 110 parameter.""" | |
| 111 handle = urllib.urlopen(url) | |
| 112 document = ElementTree.parse(handle) | |
| 113 | |
| 114 # All nodes in the tree are namespaced. Get the root's tag name to extract | |
| 115 # the namespace. Etree does namespaces as |{namespace}tag|. | |
| 116 root_tag = document.getroot().tag | |
| 117 end_ns_pos = root_tag.find('}') | |
| 118 if end_ns_pos == -1: | |
| 119 raise Exception("Could not locate end namespace for directory index") | |
| 120 namespace = root_tag[:end_ns_pos + 1] | |
| 121 | |
| 122 # Find the prefix (_listing_platform_dir) and whether or not the list is | |
| 123 # truncated. | |
| 124 prefix_len = len(document.find(namespace + 'Prefix').text) | |
| 125 next_marker = None | |
| 126 is_truncated = document.find(namespace + 'IsTruncated') | |
| 127 if is_truncated is not None and is_truncated.text.lower() == 'true': | |
| 128 next_marker = document.find(namespace + 'NextMarker').text | |
| 129 | |
| 130 # Get a list of all the revisions. | |
| 131 all_prefixes = document.findall(namespace + 'CommonPrefixes/' + | |
| 132 namespace + 'Prefix') | |
| 133 # The <Prefix> nodes have content of the form of | |
| 134 # |_listing_platform_dir/revision/|. Strip off the platform dir and the | |
| 135 # trailing slash to just have a number. | |
| 136 revisions = [] | |
| 137 for prefix in all_prefixes: | |
| 138 revnum = prefix.text[prefix_len:-1] | |
| 139 try: | |
| 140 revnum = int(revnum) | |
| 141 revisions.append(revnum) | |
| 142 except ValueError: | |
| 143 pass | |
| 144 return (revisions, next_marker) | |
| 145 | |
| 146 # Fetch the first list of revisions. | |
| 147 (revisions, next_marker) = _FetchAndParse(self.GetListingURL()) | |
| 148 | |
| 149 # If the result list was truncated, refetch with the next marker. Do this | |
| 150 # until an entire directory listing is done. | |
| 151 while next_marker: | |
| 152 next_url = self.GetListingURL(next_marker) | |
| 153 (new_revisions, next_marker) = _FetchAndParse(next_url) | |
| 154 revisions.extend(new_revisions) | |
| 155 | |
| 156 return revisions | |
| 157 | |
| 158 def GetRevList(self): | |
| 159 """Gets the list of revision numbers between self.good_revision and | |
| 160 self.bad_revision.""" | |
| 161 # Download the revlist and filter for just the range between good and bad. | |
| 162 minrev = self.good_revision | |
| 163 maxrev = self.bad_revision | |
| 164 revlist = [int(x) for x in self.ParseDirectoryIndex()] | |
|
Robert Sesek
2011/07/25 16:13:07
map(int, self.ParseDirectoryIndex())?
szager
2011/07/25 17:39:04
Hmm... I am personally a fan of 'map', but my unde
| |
| 165 revlist = [x for x in revlist if x >= minrev and x <= maxrev] | |
| 166 revlist.sort() | |
| 167 return revlist | |
| 168 | |
| 110 | 169 |
| 111 def UnzipFilenameToDir(filename, dir): | 170 def UnzipFilenameToDir(filename, dir): |
| 112 """Unzip |filename| to directory |dir|.""" | 171 """Unzip |filename| to directory |dir|.""" |
| 172 pushd = os.getcwd() | |
|
Robert Sesek
2011/07/25 16:13:07
I know it's not yours, but could you rename this?
szager
2011/07/25 17:39:04
Done.
| |
| 173 if not os.path.isabs(filename): | |
| 174 filename = os.path.join(pushd, filename) | |
| 113 zf = zipfile.ZipFile(filename) | 175 zf = zipfile.ZipFile(filename) |
| 114 # Make base. | 176 # Make base. |
| 115 pushd = os.getcwd() | |
| 116 try: | 177 try: |
| 117 if not os.path.isdir(dir): | 178 if not os.path.isdir(dir): |
| 118 os.mkdir(dir) | 179 os.mkdir(dir) |
| 119 os.chdir(dir) | 180 os.chdir(dir) |
| 120 # Extract files. | 181 # Extract files. |
| 121 for info in zf.infolist(): | 182 for info in zf.infolist(): |
| 122 name = info.filename | 183 name = info.filename |
| 123 if name.endswith('/'): # dir | 184 if name.endswith('/'): # dir |
| 124 if not os.path.isdir(name): | 185 if not os.path.isdir(name): |
| 125 os.makedirs(name) | 186 os.makedirs(name) |
| 126 else: # file | 187 else: # file |
| 127 dir = os.path.dirname(name) | 188 dir = os.path.dirname(name) |
| 128 if not os.path.isdir(dir): | 189 if not os.path.isdir(dir): |
| 129 os.makedirs(dir) | 190 os.makedirs(dir) |
| 130 out = open(name, 'wb') | 191 out = open(name, 'wb') |
| 131 out.write(zf.read(name)) | 192 out.write(zf.read(name)) |
| 132 out.close() | 193 out.close() |
| 133 # Set permissions. Permission info in external_attr is shifted 16 bits. | 194 # Set permissions. Permission info in external_attr is shifted 16 bits. |
| 134 os.chmod(name, info.external_attr >> 16L) | 195 os.chmod(name, info.external_attr >> 16L) |
| 135 os.chdir(pushd) | 196 os.chdir(pushd) |
| 136 except Exception, e: | 197 except Exception, e: |
| 137 print >>sys.stderr, e | 198 print >>sys.stderr, e |
| 138 sys.exit(1) | 199 sys.exit(1) |
| 139 | 200 |
| 140 | 201 |
| 141 def ParseDirectoryIndex(context): | 202 def FetchRevision(context, rev, filename, quit_event=None): |
| 142 """Parses the Google Storage directory listing into a list of revision | 203 """Downloads and unzips revision |rev|""" |
|
Robert Sesek
2011/07/25 16:13:07
Document |quit_event|.
szager
2011/07/25 17:39:04
Done.
| |
| 143 numbers. The range starts with context.good_revision and goes until the latest | 204 def ReportHook(blocknum, blocksize, totalsize): |
| 144 revision.""" | 205 if quit_event and quit_event.is_set(): |
| 145 def _FetchAndParse(url): | 206 raise RuntimeError("Aborting download of revision %d" % rev) |
| 146 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If | |
| 147 next-marker is not None, then the listing is a partial listing and another | |
| 148 fetch should be performed with next-marker being the marker= GET | |
| 149 parameter.""" | |
| 150 handle = urllib.urlopen(url) | |
| 151 document = ElementTree.parse(handle) | |
| 152 | 207 |
| 153 # All nodes in the tree are namespaced. Get the root's tag name to extract | 208 download_url = context.GetDownloadURL(rev) |
| 154 # the namespace. Etree does namespaces as |{namespace}tag|. | 209 try: |
| 155 root_tag = document.getroot().tag | 210 urllib.urlretrieve(download_url, filename, ReportHook) |
| 156 end_ns_pos = root_tag.find('}') | 211 except RuntimeError, e: |
| 157 if end_ns_pos == -1: | 212 pass |
|
Robert Sesek
2011/07/25 16:13:07
Log?
szager
2011/07/25 17:39:04
This is not an actual error; this clause will be h
| |
| 158 raise Exception("Could not locate end namespace for directory index") | |
| 159 namespace = root_tag[:end_ns_pos + 1] | |
| 160 | |
| 161 # Find the prefix (_listing_platform_dir) and whether or not the list is | |
| 162 # truncated. | |
| 163 prefix_len = len(document.find(namespace + 'Prefix').text) | |
| 164 next_marker = None | |
| 165 is_truncated = document.find(namespace + 'IsTruncated') | |
| 166 if is_truncated is not None and is_truncated.text.lower() == 'true': | |
| 167 next_marker = document.find(namespace + 'NextMarker').text | |
| 168 | |
| 169 # Get a list of all the revisions. | |
| 170 all_prefixes = document.findall(namespace + 'CommonPrefixes/' + | |
| 171 namespace + 'Prefix') | |
| 172 # The <Prefix> nodes have content of the form of | |
| 173 # |_listing_platform_dir/revision/|. Strip off the platform dir and the | |
| 174 # trailing slash to just have a number. | |
| 175 revisions = [] | |
| 176 for prefix in all_prefixes: | |
| 177 revnum = prefix.text[prefix_len:-1] | |
| 178 try: | |
| 179 revnum = int(revnum) | |
| 180 revisions.append(revnum) | |
| 181 except ValueError: | |
| 182 pass | |
| 183 return (revisions, next_marker) | |
| 184 | |
| 185 # Fetch the first list of revisions. | |
| 186 (revisions, next_marker) = _FetchAndParse(context.GetListingURL()) | |
| 187 # If the result list was truncated, refetch with the next marker. Do this | |
| 188 # until an entire directory listing is done. | |
| 189 while next_marker: | |
| 190 (new_revisions, next_marker) = _FetchAndParse( | |
| 191 context.GetListingURL(next_marker)) | |
| 192 revisions.extend(new_revisions) | |
| 193 | |
| 194 return revisions | |
| 195 | 213 |
| 196 | 214 |
| 197 def ParseDirectoryIndexRecent(context): | 215 def RunRevision(context, revision, zipfile, profile, args) : |
| 198 """Parses the recent builds directory listing into a list of revision | 216 """Given a zipped revision, unzip it and run the test""" |
|
Robert Sesek
2011/07/25 16:13:07
Comments require proper punctuation; here and else
szager
2011/07/25 17:39:04
Done.
| |
| 199 numbers.""" | 217 print "Trying revision %d..." % revision |
| 200 handle = urllib.urlopen(context.GetListingURLRecent()) | |
| 201 document = handle.read() | |
| 202 | 218 |
| 203 # Looking for: <a href="92976/">92976/</a> | 219 # Create a temp directory and unzip the revision into it |
| 204 return re.findall(r"<a href=\"(\d+)/\">\1/</a>", document) | |
| 205 | |
| 206 | |
| 207 def FilterRevList(context, revlist): | |
| 208 """Filter revlist to the revisions between |good_revision| and | |
| 209 |bad_revision| of the |context|.""" | |
| 210 # Download the revlist and filter for just the range between good and bad. | |
| 211 rev_range = range(context.good_revision, context.bad_revision) | |
| 212 revlist = filter(lambda r: r in rev_range, revlist) | |
| 213 revlist.sort() | |
| 214 return revlist | |
| 215 | |
| 216 | |
| 217 def TryRevision(context, rev, profile, args): | |
| 218 """Downloads revision |rev|, unzips it, and opens it for the user to test. | |
| 219 |profile| is the profile to use.""" | |
| 220 # Do this in a temp dir so we don't collide with user files. | |
| 221 cwd = os.getcwd() | 220 cwd = os.getcwd() |
| 222 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') | 221 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') |
| 222 UnzipFilenameToDir(zipfile, tempdir) | |
| 223 os.chdir(tempdir) | 223 os.chdir(tempdir) |
| 224 | 224 |
| 225 # Download the file. | 225 # Run the test |
| 226 download_url = context.GetDownloadURL(rev) | 226 testargs = [context.GetLaunchPath(), '--user-data-dir=%s' % profile] + args |
| 227 def _ReportHook(blocknum, blocksize, totalsize): | 227 subproc = subprocess.Popen(testargs, |
| 228 size = blocknum * blocksize | 228 bufsize=-1, |
| 229 if totalsize == -1: # Total size not known. | 229 stdout=subprocess.PIPE, |
| 230 progress = "Received %d bytes" % size | 230 stderr=subprocess.PIPE) |
| 231 else: | 231 (stdout, stderr) = subproc.communicate() |
| 232 size = min(totalsize, size) | |
| 233 progress = "Received %d of %d bytes, %.2f%%" % ( | |
| 234 size, totalsize, 100.0 * size / totalsize) | |
| 235 # Send a \r to let all progress messages use just one line of output. | |
| 236 sys.stdout.write("\r" + progress) | |
| 237 sys.stdout.flush() | |
| 238 try: | |
| 239 print 'Fetching ' + download_url | |
| 240 urllib.urlretrieve(download_url, context.archive_name, _ReportHook) | |
| 241 print | |
| 242 # Throw an exception if the download was less than 1000 bytes. | |
| 243 if os.path.getsize(context.archive_name) < 1000: raise Exception() | |
| 244 except Exception, e: | |
| 245 print('Could not retrieve the download. Sorry.') | |
| 246 sys.exit(-1) | |
| 247 | |
| 248 # Unzip the file. | |
| 249 print 'Unzipping ...' | |
| 250 UnzipFilenameToDir(context.archive_name, os.curdir) | |
| 251 | |
| 252 # Tell the system to open the app. | |
| 253 args = ['--user-data-dir=%s' % profile] + args | |
| 254 flags = ' '.join(map(pipes.quote, args)) | |
| 255 cmd = '%s %s' % (context.GetLaunchPath(), flags) | |
| 256 print 'Running %s' % cmd | |
| 257 os.system(cmd) | |
| 258 | 232 |
| 259 os.chdir(cwd) | 233 os.chdir(cwd) |
| 260 print 'Cleaning temp dir ...' | |
| 261 try: | 234 try: |
| 262 shutil.rmtree(tempdir, True) | 235 shutil.rmtree(tempdir, True) |
| 263 except Exception, e: | 236 except Exception, e: |
| 264 pass | 237 pass |
| 265 | 238 |
| 239 return (subproc.returncode, stdout, stderr) | |
| 266 | 240 |
| 267 def AskIsGoodBuild(rev): | 241 def AskIsGoodBuild(rev, status, stdout, stderr): |
| 268 """Ask the user whether build |rev| is good or bad.""" | 242 """Ask the user whether build |rev| is good or bad.""" |
| 269 # Loop until we get a response that we can parse. | 243 # Loop until we get a response that we can parse. |
| 270 while True: | 244 while True: |
| 271 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) | 245 response = raw_input('\nRevision %d is [(g)ood/(b)ad/(q)uit]: ' % int(rev)) |
| 272 if response and response in ('g', 'b'): | 246 if response and response in ('g', 'b'): |
| 273 return response == 'g' | 247 return response == 'g' |
| 248 if response and response == 'q': | |
| 249 raise SystemExit() | |
| 274 | 250 |
| 251 def Bisect(platform, | |
| 252 good_rev=0, | |
| 253 bad_rev=0, | |
| 254 try_args=(), | |
| 255 profile=None, | |
| 256 predicate=AskIsGoodBuild): | |
| 257 """Given known good and known bad revisions, run a binary search on all | |
| 258 archived revisions to determine the last known good revision. | |
| 275 | 259 |
| 276 def Bisect(revlist, | 260 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). |
| 277 context, | 261 @param good_rev Number/tag of the last known good revision. |
| 278 try_args=(), | 262 @param bad_rev Number/tag of the first known bad revision. |
| 279 profile='profile', | 263 @param try_args A tuple of arguments to pass to the test application. |
| 280 predicate=AskIsGoodBuild): | 264 @param profile The name of the user profile to run with. |
| 281 """Tries to find the exact commit where a regression was introduced by | |
| 282 running a binary search on all archived builds in a given revision range. | |
| 283 | |
| 284 @param revlist A list of chromium revision numbers to check. | |
| 285 @param context A PathContext object. | |
| 286 @param try_args A tuple of arguments to pass to the predicate function. | |
| 287 @param profile The user profile with which to run chromium. | |
| 288 @param predicate A predicate function which returns True iff the argument | 265 @param predicate A predicate function which returns True iff the argument |
| 289 chromium revision is good. | 266 chromium revision is good. |
| 290 """ | 267 """ |
| 291 | 268 |
| 269 if profile is None: | |
|
Robert Sesek
2011/07/25 16:13:07
|if not profile:| is more idiomatic
szager
2011/07/25 17:39:04
Done.
| |
| 270 profile = 'profile' | |
| 271 | |
| 272 context = PathContext(platform, good_rev, bad_rev) | |
| 273 cwd = os.getcwd() | |
| 274 | |
| 275 revlist = context.GetRevList() | |
| 276 | |
| 277 # Get a list of revisions to bisect across. | |
| 278 if len(revlist) < 2: # Don't have enough builds to bisect | |
| 279 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist | |
| 280 raise RuntimeError(msg) | |
| 281 | |
| 282 # Figure out our bookends and first pivot point; fetch the pivot revision | |
| 292 good = 0 | 283 good = 0 |
| 293 bad = len(revlist) - 1 | 284 bad = len(revlist) - 1 |
| 294 last_known_good_rev = revlist[good] | 285 pivot = bad/2 |
|
Robert Sesek
2011/07/25 16:13:07
nit: Spaces around operators
szager
2011/07/25 17:39:04
Done.
| |
| 295 first_known_bad_rev = revlist[bad] | 286 rev = revlist[pivot] |
| 287 zipfile = os.path.join(cwd, '%d-%s' % (rev, context.archive_name)) | |
|
Robert Sesek
2011/07/25 16:13:07
You do this format enough places that this could b
szager
2011/07/25 17:39:04
I created a local function for this; I prefer not
Robert Sesek
2011/07/26 21:00:26
Not really true because it manipulates paths for t
| |
| 288 FetchRevision(context, rev, zipfile) | |
| 296 | 289 |
| 297 # Binary search time! | 290 # Binary search time! |
| 298 while good < bad: | 291 while zipfile and bad - good > 1: |
| 299 candidates = revlist[good:bad] | 292 print "iterating with good=%d bad=%d pivot=%d" % ( |
|
Robert Sesek
2011/07/25 16:13:07
Use proper capitalization, but do you think this l
szager
2011/07/25 17:39:04
Yeah, this is a bit TMI; I have removed it.
| |
| 300 num_poss = len(candidates) | 293 revlist[good], revlist[bad], revlist[pivot]) |
| 301 if num_poss > 10: | |
| 302 print('%d candidates. %d tries left.' % | |
| 303 (num_poss, round(math.log(num_poss, 2)))) | |
| 304 else: | |
| 305 print('Candidates: %s' % revlist[good:bad]) | |
| 306 | 294 |
| 307 # Cut the problem in half... | 295 # Pre-fetch next two possible pivots |
|
Robert Sesek
2011/07/25 16:13:07
This block of code is hard to follow. It should ha
szager
2011/07/25 17:39:04
Added this to the comments for the Bisect function
| |
| 308 test = int((bad - good) / 2) + good | 296 down_pivot = int((pivot - good) / 2) + good |
|
Robert Sesek
2011/07/25 16:13:07
I don't really understand the naming rationale of
szager
2011/07/25 17:39:04
Hopefully the high-level comments will clarify thi
| |
| 309 test_rev = revlist[test] | 297 down_thread = None |
| 298 if down_pivot != pivot and down_pivot != good : | |
|
Robert Sesek
2011/07/25 16:13:07
nit: no space before ':'. Here and elsewhere.
szager
2011/07/25 17:39:04
nit scratched.
| |
| 299 down_rev = revlist[down_pivot] | |
| 300 zipfile_base = '%d-%s' % (down_rev, context.archive_name) | |
| 301 down_zipfile = os.path.join(cwd, zipfile_base) | |
|
Robert Sesek
2011/07/25 16:13:07
This should really be a method on PathContext
szager
2011/07/25 17:39:04
See above comment
| |
| 302 down_event = threading.Event() | |
| 303 fetchargs = (context, down_rev, down_zipfile, down_event) | |
| 304 down_thread = threading.Thread(target=FetchRevision, | |
| 305 name='down_fetch', | |
| 306 args=fetchargs) | |
| 307 down_thread.start() | |
| 310 | 308 |
| 311 # Let the user give this rev a spin (in her own profile, if she wants). | 309 up_pivot = int((bad - pivot) / 2) + pivot |
| 312 TryRevision(context, test_rev, profile, try_args) | 310 up_thread = None |
| 313 if predicate(test_rev): | 311 if up_pivot != pivot and up_pivot != bad : |
| 314 last_known_good_rev = test_rev | 312 up_rev = revlist[up_pivot] |
| 315 good = test + 1 | 313 zipfile_base = '%d-%s' % (up_rev, context.archive_name) |
| 316 else: | 314 up_zipfile = os.path.join(cwd, zipfile_base) |
| 317 bad = test | 315 up_event = threading.Event() |
| 316 fetchargs = (context, up_rev, up_zipfile, up_event) | |
| 317 up_thread = threading.Thread(target=FetchRevision, | |
| 318 name='up_fetch', | |
| 319 args=fetchargs) | |
| 320 up_thread.start() | |
| 318 | 321 |
| 319 return (last_known_good_rev, first_known_bad_rev) | 322 # Run test on the pivot revision |
| 323 (status, stdout, stderr) = RunRevision(context, | |
| 324 rev, | |
| 325 zipfile, | |
| 326 profile, | |
| 327 try_args) | |
| 328 os.unlink(zipfile) | |
| 329 zipfile = None | |
| 330 try: | |
| 331 if predicate(rev, status, stdout, stderr) : | |
| 332 good = pivot | |
| 333 if down_thread : | |
| 334 down_event.set() # Kill the download of older revision | |
|
Robert Sesek
2011/07/25 16:13:07
nit: two spaces before comments
szager
2011/07/25 17:39:04
nit scratched.
| |
| 335 down_thread.join() | |
| 336 os.unlink(down_zipfile) | |
| 337 if up_thread : | |
| 338 print "Downloading revision %d..." % up_rev | |
| 339 up_thread.join() # Wait for newer revision to finish downloading | |
| 340 pivot = up_pivot | |
| 341 zipfile = up_zipfile | |
| 342 else : | |
| 343 bad = pivot | |
| 344 if up_thread : | |
| 345 up_event.set() # Kill download of newer revision | |
| 346 up_thread.join() | |
| 347 os.unlink(up_zipfile) | |
| 348 if down_thread : | |
| 349 print "Downloading revision %d..." % down_rev | |
| 350 down_thread.join() # Wait for older revision to finish downloading | |
| 351 pivot = down_pivot | |
| 352 zipfile = down_zipfile | |
| 353 except SystemExit: | |
| 354 for f in [down_zipfile, up_zipfile]: | |
| 355 try: | |
| 356 os.unlink(f) | |
| 357 except OSError: | |
| 358 pass | |
| 359 sys.exit(0) | |
| 360 | |
| 361 rev = revlist[pivot] | |
| 362 | |
| 363 return (revlist[good], revlist[bad]) | |
| 320 | 364 |
| 321 | 365 |
| 322 def main(): | 366 def main(): |
| 323 usage = ('%prog [options] [-- chromium-options]\n' | 367 usage = ('%prog [options] [-- chromium-options]\n' |
| 324 'Perform binary search on the snapshot builds.\n' | 368 'Perform binary search on the snapshot builds.\n' |
| 325 '\n' | 369 '\n' |
| 326 'Tip: add "-- --no-first-run" to bypass the first run prompts.') | 370 'Tip: add "-- --no-first-run" to bypass the first run prompts.') |
| 327 parser = optparse.OptionParser(usage=usage) | 371 parser = optparse.OptionParser(usage=usage) |
| 328 # Strangely, the default help output doesn't include the choice list. | 372 # Strangely, the default help output doesn't include the choice list. |
| 329 choices = ['mac', 'win', 'linux', 'linux64'] | 373 choices = ['mac', 'win', 'linux', 'linux64'] |
| (...skipping 17 matching lines...) Expand all Loading... | |
| 347 parser.print_help() | 391 parser.print_help() |
| 348 return 1 | 392 return 1 |
| 349 | 393 |
| 350 if opts.bad and opts.good and (opts.good > opts.bad): | 394 if opts.bad and opts.good and (opts.good > opts.bad): |
| 351 print ('The good revision (%d) must precede the bad revision (%d).\n' % | 395 print ('The good revision (%d) must precede the bad revision (%d).\n' % |
| 352 (opts.good, opts.bad)) | 396 (opts.good, opts.bad)) |
| 353 parser.print_help() | 397 parser.print_help() |
| 354 return 1 | 398 return 1 |
| 355 | 399 |
| 356 # Create the context. Initialize 0 for the revisions as they are set below. | 400 # Create the context. Initialize 0 for the revisions as they are set below. |
| 357 context = PathContext(opts.archive, 0, 0, use_recent=False) | 401 context = PathContext(opts.archive, 0, 0) |
| 358 | 402 |
| 359 # Pick a starting point, try to get HEAD for this. | 403 # Pick a starting point, try to get HEAD for this. |
| 360 if opts.bad: | 404 if opts.bad: |
| 361 bad_rev = opts.bad | 405 bad_rev = opts.bad |
| 362 else: | 406 else: |
| 363 bad_rev = 0 | 407 bad_rev = 0 |
| 364 try: | 408 try: |
| 365 # Location of the latest build revision number | 409 # Location of the latest build revision number |
| 366 nh = urllib.urlopen(context.GetLastChangeURL()) | 410 nh = urllib.urlopen(context.GetLastChangeURL()) |
| 367 latest = int(nh.read()) | 411 latest = int(nh.read()) |
| 368 nh.close() | 412 nh.close() |
| 369 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) | 413 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) |
| 370 if (bad_rev == ''): | 414 if (bad_rev == ''): |
| 371 bad_rev = latest | 415 bad_rev = latest |
| 372 bad_rev = int(bad_rev) | 416 bad_rev = int(bad_rev) |
| 373 except Exception, e: | 417 except Exception, e: |
| 374 print('Could not determine latest revision. This could be bad...') | 418 print('Could not determine latest revision. This could be bad...') |
| 375 bad_rev = int(raw_input('Bad revision: ')) | 419 bad_rev = int(raw_input('Bad revision: ')) |
| 376 | 420 |
| 377 # Find out when we were good. | 421 # Find out when we were good. |
| 378 if opts.good: | 422 if opts.good: |
| 379 good_rev = opts.good | 423 good_rev = opts.good |
| 380 else: | 424 else: |
| 381 good_rev = 0 | 425 good_rev = 0 |
| 382 try: | 426 try: |
| 383 good_rev = int(raw_input('Last known good [0]: ')) | 427 good_rev = int(raw_input('Last known good [0]: ')) |
| 384 except Exception, e: | 428 except Exception, e: |
| 385 pass | 429 pass |
| 386 | 430 |
| 387 # Set the input parameters now that they've been validated. | |
| 388 context.good_revision = good_rev | |
| 389 context.bad_revision = bad_rev | |
| 390 | |
| 391 # Get recent revision list and check whether it's sufficient. | |
| 392 all_revs_recent = map(int, ParseDirectoryIndexRecent(context)) | |
| 393 all_revs_recent.sort() | |
| 394 # Skipping 0 since it might be deleted off the server soon: | |
| 395 all_revs_recent = all_revs_recent[1:] | |
| 396 oldest_recent_rev = all_revs_recent[0] | |
| 397 if good_rev >= oldest_recent_rev: | |
| 398 # The range is within recent builds, so switch on use_recent. | |
| 399 context.use_recent = True | |
| 400 elif bad_rev >= oldest_recent_rev: | |
| 401 # The range spans both old and recent builds. | |
| 402 # If oldest_recent_rev is good, we bisect the recent builds. | |
| 403 context.use_recent = True | |
| 404 TryRevision(context, oldest_recent_rev, opts.profile, args) | |
| 405 if AskIsGoodBuild(oldest_recent_rev): | |
| 406 # context.use_recent is True | |
| 407 context.good_revision = oldest_recent_rev | |
| 408 else: | |
| 409 context.use_recent = False | |
| 410 context.bad_revision = oldest_recent_rev | |
| 411 | |
| 412 all_revs = [] | |
| 413 if context.use_recent: | |
| 414 all_revs = all_revs_recent | |
| 415 else: | |
| 416 all_revs = map(int, ParseDirectoryIndex(context)) | |
| 417 | |
| 418 # Filter list of revisions to bisect across. | |
| 419 revlist = FilterRevList(context, all_revs) | |
| 420 if len(revlist) < 2: # Don't have enough builds to bisect | |
| 421 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist | |
| 422 sys.exit(1) | |
| 423 | |
| 424 (last_known_good_rev, first_known_bad_rev) = Bisect( | 431 (last_known_good_rev, first_known_bad_rev) = Bisect( |
| 425 revlist, context, args, opts.profile) | 432 opts.archive, good_rev, bad_rev, args, opts.profile) |
| 426 | 433 |
| 427 # We're done. Let the user know the results in an official manner. | 434 # We're done. Let the user know the results in an official manner. |
| 428 print('You are probably looking for build %d.' % first_known_bad_rev) | 435 print('You are probably looking for build %d.' % first_known_bad_rev) |
| 429 print('CHANGELOG URL:') | 436 print('CHANGELOG URL:') |
| 430 print(CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev)) | 437 print(CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev)) |
| 431 print('Built at revision:') | 438 print('Built at revision:') |
| 432 print(BUILD_VIEWVC_URL % first_known_bad_rev) | 439 print(BUILD_VIEWVC_URL % first_known_bad_rev) |
| 433 | 440 |
| 434 if __name__ == '__main__': | 441 if __name__ == '__main__': |
| 435 sys.exit(main()) | 442 sys.exit(main()) |
| OLD | NEW |