OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 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 | 17 |
18 # The root URL for official builds. | |
19 OFFICIAL_BASE_URL = 'http://chrome4linux.mtv.corp.google.com/archives' | |
20 | |
18 # URL to the ViewVC commit page. | 21 # URL to the ViewVC commit page. |
19 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d' | 22 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d' |
20 | 23 |
21 # Changelogs URL. | 24 # Changelogs URL. |
22 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ | 25 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ |
23 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' | 26 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' |
Robert Sesek
2012/05/23 21:24:01
nit: blank line after
anantha
2012/05/24 18:32:11
Done.
| |
27 # Official Changelogs URL. | |
28 OFFICIAL_CHANGELOG_URL = 'http://omahaproxy.appspot.com/'\ | |
29 'changelog?old_version=%s&new_version=%s' | |
24 | 30 |
25 # DEPS file URL. | 31 # DEPS file URL. |
26 DEPS_FILE= 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d' | 32 DEPS_FILE= 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d' |
27 | |
28 # WebKit Changelogs URL. | 33 # WebKit Changelogs URL. |
29 WEBKIT_CHANGELOG_URL = 'http://trac.webkit.org/log/' \ | 34 WEBKIT_CHANGELOG_URL = 'http://trac.webkit.org/log/' \ |
30 'trunk/?rev=%d&stop_rev=%d&verbose=on' | 35 'trunk/?rev=%d&stop_rev=%d&verbose=on' |
31 | 36 |
32 ############################################################################### | 37 ############################################################################### |
33 | 38 |
34 import math | 39 import math |
35 import optparse | 40 import optparse |
36 import os | 41 import os |
37 import pipes | 42 import pipes |
38 import re | 43 import re |
39 import shutil | 44 import shutil |
40 import subprocess | 45 import subprocess |
41 import sys | 46 import sys |
42 import tempfile | 47 import tempfile |
43 import threading | 48 import threading |
44 import urllib | 49 import urllib |
50 from distutils.version import LooseVersion | |
45 from xml.etree import ElementTree | 51 from xml.etree import ElementTree |
46 import zipfile | 52 import zipfile |
47 | 53 |
48 | 54 |
49 class PathContext(object): | 55 class PathContext(object): |
50 """A PathContext is used to carry the information used to construct URLs and | 56 """A PathContext is used to carry the information used to construct URLs and |
51 paths when dealing with the storage server and archives.""" | 57 paths when dealing with the storage server and archives.""" |
52 def __init__(self, platform, good_revision, bad_revision): | 58 def __init__(self, platform, good_revision, bad_revision, is_official): |
53 super(PathContext, self).__init__() | 59 super(PathContext, self).__init__() |
54 # Store off the input parameters. | 60 # Store off the input parameters. |
55 self.platform = platform # What's passed in to the '-a/--archive' option. | 61 self.platform = platform # What's passed in to the '-a/--archive' option. |
56 self.good_revision = good_revision | 62 self.good_revision = good_revision |
57 self.bad_revision = bad_revision | 63 self.bad_revision = bad_revision |
64 self.is_official = is_official | |
58 | 65 |
59 # The name of the ZIP file in a revision directory on the server. | 66 # The name of the ZIP file in a revision directory on the server. |
60 self.archive_name = None | 67 self.archive_name = None |
61 | 68 |
62 # Set some internal members: | 69 # Set some internal members: |
63 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. | 70 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. |
64 # _archive_extract_dir = Uncompressed directory in the archive_name file. | 71 # _archive_extract_dir = Uncompressed directory in the archive_name file. |
65 # _binary_name = The name of the executable to run. | 72 # _binary_name = The name of the executable to run. |
66 if self.platform == 'linux' or self.platform == 'linux64': | 73 if self.platform == 'linux' or self.platform == 'linux64': |
67 self._listing_platform_dir = 'Linux/' | |
68 self.archive_name = 'chrome-linux.zip' | |
69 self._archive_extract_dir = 'chrome-linux' | |
70 self._binary_name = 'chrome' | 74 self._binary_name = 'chrome' |
71 # Linux and x64 share all the same path data except for the archive dir. | |
72 if self.platform == 'linux64': | |
73 self._listing_platform_dir = 'Linux_x64/' | |
74 elif self.platform == 'mac': | 75 elif self.platform == 'mac': |
75 self._listing_platform_dir = 'Mac/' | |
76 self.archive_name = 'chrome-mac.zip' | 76 self.archive_name = 'chrome-mac.zip' |
77 self._archive_extract_dir = 'chrome-mac' | 77 self._archive_extract_dir = 'chrome-mac' |
78 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' | |
79 elif self.platform == 'win': | 78 elif self.platform == 'win': |
80 self._listing_platform_dir = 'Win/' | |
81 self.archive_name = 'chrome-win32.zip' | 79 self.archive_name = 'chrome-win32.zip' |
82 self._archive_extract_dir = 'chrome-win32' | 80 self._archive_extract_dir = 'chrome-win32' |
83 self._binary_name = 'chrome.exe' | 81 self._binary_name = 'chrome.exe' |
84 else: | 82 else: |
85 raise Exception('Invalid platform: %s' % self.platform) | 83 raise Exception('Invalid platform: %s' % self.platform) |
86 | 84 |
85 if is_official: | |
86 if self.platform == 'linux': | |
87 self._listing_platform_dir = 'lucid32bit/' | |
88 self.archive_name = 'chrome-lucid32bit.zip' | |
89 self._archive_extract_dir = 'chrome-lucid32bit' | |
90 elif self.platform == 'linux64': | |
91 self._listing_platform_dir = 'lucid64bit/' | |
92 self.archive_name = 'chrome-lucid64bit.zip' | |
93 self._archive_extract_dir = 'chrome-lucid64bit' | |
94 elif self.platform == 'mac': | |
95 self._listing_platform_dir = 'mac/' | |
96 self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome' | |
97 elif self.platform == 'win': | |
98 self._listing_platform_dir = 'win/' | |
99 else: | |
100 if self.platform == 'linux' or self.platform == 'linux64': | |
101 self.archive_name = 'chrome-linux.zip' | |
102 self._archive_extract_dir = 'chrome-linux' | |
103 if self.platform == 'linux': | |
104 self._listing_platform_dir = 'Linux/' | |
105 elif self.platform == 'linux64': | |
106 self._listing_platform_dir = 'Linux_x64/' | |
107 elif self.platform == 'mac': | |
108 self._listing_platform_dir = 'Mac/' | |
109 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' | |
110 elif self.platform == 'win': | |
111 self._listing_platform_dir = 'Win/' | |
112 | |
87 def GetListingURL(self, marker=None): | 113 def GetListingURL(self, marker=None): |
88 """Returns the URL for a directory listing, with an optional marker.""" | 114 """Returns the URL for a directory listing, with an optional marker.""" |
89 marker_param = '' | 115 marker_param = '' |
90 if marker: | 116 if marker: |
91 marker_param = '&marker=' + str(marker) | 117 marker_param = '&marker=' + str(marker) |
92 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ | 118 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ |
93 marker_param | 119 marker_param |
94 | 120 |
95 def GetDownloadURL(self, revision): | 121 def GetDownloadURL(self, revision): |
96 """Gets the download URL for a build archive of a specific revision.""" | 122 """Gets the download URL for a build archive of a specific revision.""" |
97 return "%s/%s%d/%s" % ( | 123 if self.is_official: |
98 BASE_URL, self._listing_platform_dir, revision, self.archive_name) | 124 return "%s/%s/%s%s" % ( |
125 OFFICIAL_BASE_URL, revision, self._listing_platform_dir, | |
126 self.archive_name) | |
127 else: | |
128 return "%s/%s%s/%s" % ( | |
129 BASE_URL, self._listing_platform_dir, revision, self.archive_name) | |
99 | 130 |
100 def GetLastChangeURL(self): | 131 def GetLastChangeURL(self): |
101 """Returns a URL to the LAST_CHANGE file.""" | 132 """Returns a URL to the LAST_CHANGE file.""" |
102 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' | 133 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' |
103 | 134 |
104 def GetLaunchPath(self): | 135 def GetLaunchPath(self): |
105 """Returns a relative path (presumably from the archive extraction location) | 136 """Returns a relative path (presumably from the archive extraction location) |
106 that is used to run the executable.""" | 137 that is used to run the executable.""" |
107 return os.path.join(self._archive_extract_dir, self._binary_name) | 138 return os.path.join(self._archive_extract_dir, self._binary_name) |
108 | 139 |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
143 # trailing slash to just have a number. | 174 # trailing slash to just have a number. |
144 revisions = [] | 175 revisions = [] |
145 for prefix in all_prefixes: | 176 for prefix in all_prefixes: |
146 revnum = prefix.text[prefix_len:-1] | 177 revnum = prefix.text[prefix_len:-1] |
147 try: | 178 try: |
148 revnum = int(revnum) | 179 revnum = int(revnum) |
149 revisions.append(revnum) | 180 revisions.append(revnum) |
150 except ValueError: | 181 except ValueError: |
151 pass | 182 pass |
152 return (revisions, next_marker) | 183 return (revisions, next_marker) |
153 | |
154 # Fetch the first list of revisions. | 184 # Fetch the first list of revisions. |
155 (revisions, next_marker) = _FetchAndParse(self.GetListingURL()) | 185 (revisions, next_marker) = _FetchAndParse(self.GetListingURL()) |
156 | 186 |
157 # If the result list was truncated, refetch with the next marker. Do this | 187 # If the result list was truncated, refetch with the next marker. Do this |
158 # until an entire directory listing is done. | 188 # until an entire directory listing is done. |
159 while next_marker: | 189 while next_marker: |
160 next_url = self.GetListingURL(next_marker) | 190 next_url = self.GetListingURL(next_marker) |
161 (new_revisions, next_marker) = _FetchAndParse(next_url) | 191 (new_revisions, next_marker) = _FetchAndParse(next_url) |
162 revisions.extend(new_revisions) | 192 revisions.extend(new_revisions) |
163 | |
164 return revisions | 193 return revisions |
165 | 194 |
166 def GetRevList(self): | 195 def GetRevList(self): |
167 """Gets the list of revision numbers between self.good_revision and | 196 """Gets the list of revision numbers between self.good_revision and |
168 self.bad_revision.""" | 197 self.bad_revision.""" |
169 # Download the revlist and filter for just the range between good and bad. | 198 # Download the revlist and filter for just the range between good and bad. |
170 minrev = self.good_revision | 199 minrev = self.good_revision |
171 maxrev = self.bad_revision | 200 maxrev = self.bad_revision |
172 revlist = map(int, self.ParseDirectoryIndex()) | 201 revlist = map(int, self.ParseDirectoryIndex()) |
173 revlist = [x for x in revlist if x >= minrev and x <= maxrev] | 202 revlist = [x for x in revlist if x >= int(minrev) and x <= int(maxrev)] |
174 revlist.sort() | 203 revlist.sort() |
175 return revlist | 204 return revlist |
176 | 205 |
206 def GetOfficialBuildsList(self): | |
207 """Gets the list of official build numbers between self.good_revision and | |
208 self.bad_revision.""" | |
209 # Download the revlist and filter for just the range between good and bad. | |
210 minrev = self.good_revision | |
211 maxrev = self.bad_revision | |
212 handle = urllib.urlopen(OFFICIAL_BASE_URL) | |
213 dirindex = handle.read() | |
214 handle.close() | |
215 build_numbers = re.findall(r'<a href="([0-9][0-9].*)/">', dirindex) | |
216 final_list = [] | |
217 start_index = '0' | |
218 end_index = '0' | |
219 i = 0 | |
220 | |
221 parsed_build_numbers = [LooseVersion(x) for x in build_numbers] | |
222 for build_number in sorted(parsed_build_numbers): | |
223 path = OFFICIAL_BASE_URL + '/' + str(build_number) + '/' + \ | |
224 self._listing_platform_dir + self.archive_name | |
225 i = i+1 | |
Robert Sesek
2012/05/23 21:24:01
nit: spaces around operators
anantha
2012/05/24 18:32:11
Done.
| |
226 try: | |
227 connection = urllib.urlopen(path) | |
Robert Sesek
2012/05/23 21:24:01
What's this for? To test that the file exists?
anantha
2012/05/24 18:32:11
Correct.
On 2012/05/23 21:24:01, rsesek wrote:
| |
228 connection.close() | |
229 final_list.append(str(build_number)) | |
230 if str(build_number) == minrev: | |
231 start_index = i | |
232 if str(build_number) == maxrev: | |
233 end_index = i | |
234 except urllib.HTTPError, e: | |
235 print e.getcode() | |
Robert Sesek
2012/05/23 21:24:01
This won't be a helpful error message.
anantha
2012/05/24 18:32:11
changed it to pass.
On 2012/05/23 21:24:01, rsesek
| |
236 return final_list[start_index:end_index] | |
177 | 237 |
178 def UnzipFilenameToDir(filename, dir): | 238 def UnzipFilenameToDir(filename, dir): |
179 """Unzip |filename| to directory |dir|.""" | 239 """Unzip |filename| to directory |dir|.""" |
180 cwd = os.getcwd() | 240 cwd = os.getcwd() |
181 if not os.path.isabs(filename): | 241 if not os.path.isabs(filename): |
182 filename = os.path.join(cwd, filename) | 242 filename = os.path.join(cwd, filename) |
183 zf = zipfile.ZipFile(filename) | 243 zf = zipfile.ZipFile(filename) |
184 # Make base. | 244 # Make base. |
185 try: | 245 try: |
186 if not os.path.isdir(dir): | 246 if not os.path.isdir(dir): |
(...skipping 26 matching lines...) Expand all Loading... | |
213 @param rev The Chromium revision number/tag to download. | 273 @param rev The Chromium revision number/tag to download. |
214 @param filename The destination for the downloaded file. | 274 @param filename The destination for the downloaded file. |
215 @param quit_event A threading.Event which will be set by the master thread to | 275 @param quit_event A threading.Event which will be set by the master thread to |
216 indicate that the download should be aborted. | 276 indicate that the download should be aborted. |
217 @param progress_event A threading.Event which will be set by the master thread | 277 @param progress_event A threading.Event which will be set by the master thread |
218 to indicate that the progress of the download should be | 278 to indicate that the progress of the download should be |
219 displayed. | 279 displayed. |
220 """ | 280 """ |
221 def ReportHook(blocknum, blocksize, totalsize): | 281 def ReportHook(blocknum, blocksize, totalsize): |
222 if quit_event and quit_event.isSet(): | 282 if quit_event and quit_event.isSet(): |
223 raise RuntimeError("Aborting download of revision %d" % rev) | 283 raise RuntimeError("Aborting download of revision %s" % str(rev)) |
224 if progress_event and progress_event.isSet(): | 284 if progress_event and progress_event.isSet(): |
225 size = blocknum * blocksize | 285 size = blocknum * blocksize |
226 if totalsize == -1: # Total size not known. | 286 if totalsize == -1: # Total size not known. |
227 progress = "Received %d bytes" % size | 287 progress = "Received %d bytes" % size |
228 else: | 288 else: |
229 size = min(totalsize, size) | 289 size = min(totalsize, size) |
230 progress = "Received %d of %d bytes, %.2f%%" % ( | 290 progress = "Received %d of %d bytes, %.2f%%" % ( |
231 size, totalsize, 100.0 * size / totalsize) | 291 size, totalsize, 100.0 * size / totalsize) |
232 # Send a \r to let all progress messages use just one line of output. | 292 # Send a \r to let all progress messages use just one line of output. |
233 sys.stdout.write("\r" + progress) | 293 sys.stdout.write("\r" + progress) |
234 sys.stdout.flush() | 294 sys.stdout.flush() |
235 | 295 |
236 download_url = context.GetDownloadURL(rev) | 296 download_url = context.GetDownloadURL(rev) |
237 try: | 297 try: |
238 urllib.urlretrieve(download_url, filename, ReportHook) | 298 urllib.urlretrieve(download_url, filename, ReportHook) |
239 if progress_event and progress_event.isSet(): | 299 if progress_event and progress_event.isSet(): |
240 print | 300 print |
241 except RuntimeError, e: | 301 except RuntimeError, e: |
242 pass | 302 pass |
243 | 303 |
244 | 304 |
245 def RunRevision(context, revision, zipfile, profile, num_runs, args): | 305 def RunRevision(context, revision, zipfile, profile, num_runs, args): |
246 """Given a zipped revision, unzip it and run the test.""" | 306 """Given a zipped revision, unzip it and run the test.""" |
247 print "Trying revision %d..." % revision | 307 print "Trying revision %s..." % str(revision) |
248 | 308 |
249 # Create a temp directory and unzip the revision into it. | 309 # Create a temp directory and unzip the revision into it. |
250 cwd = os.getcwd() | 310 cwd = os.getcwd() |
251 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') | 311 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') |
252 UnzipFilenameToDir(zipfile, tempdir) | 312 UnzipFilenameToDir(zipfile, tempdir) |
253 os.chdir(tempdir) | 313 os.chdir(tempdir) |
254 | 314 |
255 # Run the build as many times as specified. | 315 if context.platform == 'linux' or context.platform == 'linux64': |
Robert Sesek
2012/05/23 21:24:01
This can be simplified to just:
testargs = [conte
anantha
2012/05/24 18:32:11
Done.
| |
256 testargs = [context.GetLaunchPath(), '--user-data-dir=%s' % profile] + args | 316 if context.is_official: |
317 # Run the build as many times as specified. | |
318 testargs = [context.GetLaunchPath(), '--no-sandbox', | |
319 '--user-data-dir=%s' % profile] + args | |
320 else: | |
321 # Run the build as many times as specified. | |
322 testargs = [context.GetLaunchPath(), '--user-data-dir=%s' % profile] + \ | |
323 args | |
324 else: | |
325 # Run the build as many times as specified. | |
326 testargs = [context.GetLaunchPath(), '--user-data-dir=%s' % profile] + args | |
327 | |
257 for i in range(0, num_runs): | 328 for i in range(0, num_runs): |
258 subproc = subprocess.Popen(testargs, | 329 subproc = subprocess.Popen(testargs, |
259 bufsize=-1, | 330 bufsize=-1, |
260 stdout=subprocess.PIPE, | 331 stdout=subprocess.PIPE, |
261 stderr=subprocess.PIPE) | 332 stderr=subprocess.PIPE) |
262 (stdout, stderr) = subproc.communicate() | 333 (stdout, stderr) = subproc.communicate() |
263 | 334 |
264 os.chdir(cwd) | 335 os.chdir(cwd) |
265 try: | 336 try: |
266 shutil.rmtree(tempdir, True) | 337 shutil.rmtree(tempdir, True) |
267 except Exception, e: | 338 except Exception, e: |
268 pass | 339 pass |
269 | 340 |
270 return (subproc.returncode, stdout, stderr) | 341 return (subproc.returncode, stdout, stderr) |
271 | 342 |
272 | 343 |
273 def AskIsGoodBuild(rev, status, stdout, stderr): | 344 def AskIsGoodBuild(rev, official_builds, status, stdout, stderr): |
274 """Ask the user whether build |rev| is good or bad.""" | 345 """Ask the user whether build |rev| is good or bad.""" |
275 # Loop until we get a response that we can parse. | 346 # Loop until we get a response that we can parse. |
276 while True: | 347 while True: |
277 response = raw_input('Revision %d is [(g)ood/(b)ad/(q)uit]: ' % int(rev)) | 348 response = raw_input('Revision %s is [(g)ood/(b)ad/(q)uit]: ' % str(rev)) |
278 if response and response in ('g', 'b'): | 349 if response and response in ('g', 'b'): |
279 return response == 'g' | 350 return response == 'g' |
280 if response and response == 'q': | 351 if response and response == 'q': |
281 raise SystemExit() | 352 raise SystemExit() |
282 | 353 |
283 | 354 |
284 def Bisect(platform, | 355 def Bisect(platform, |
356 official_builds, | |
285 good_rev=0, | 357 good_rev=0, |
286 bad_rev=0, | 358 bad_rev=0, |
287 num_runs=1, | 359 num_runs=1, |
288 try_args=(), | 360 try_args=(), |
289 profile=None, | 361 profile=None, |
290 predicate=AskIsGoodBuild): | 362 predicate=AskIsGoodBuild): |
291 """Given known good and known bad revisions, run a binary search on all | 363 """Given known good and known bad revisions, run a binary search on all |
292 archived revisions to determine the last known good revision. | 364 archived revisions to determine the last known good revision. |
293 | 365 |
294 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). | 366 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). |
295 @param good_rev Number/tag of the last known good revision. | 367 @param good_rev Number/tag of the last known good revision. |
Robert Sesek
2012/05/23 21:24:01
Need a @param offiical_builds ... before this
anantha
2012/05/24 18:32:11
Done.
| |
296 @param bad_rev Number/tag of the first known bad revision. | 368 @param bad_rev Number/tag of the first known bad revision. |
297 @param num_runs Number of times to run each build for asking good/bad. | 369 @param num_runs Number of times to run each build for asking good/bad. |
298 @param try_args A tuple of arguments to pass to the test application. | 370 @param try_args A tuple of arguments to pass to the test application. |
299 @param profile The name of the user profile to run with. | 371 @param profile The name of the user profile to run with. |
300 @param predicate A predicate function which returns True iff the argument | 372 @param predicate A predicate function which returns True iff the argument |
301 chromium revision is good. | 373 chromium revision is good. |
302 | 374 |
303 Threading is used to fetch Chromium revisions in the background, speeding up | 375 Threading is used to fetch Chromium revisions in the background, speeding up |
304 the user's experience. For example, suppose the bounds of the search are | 376 the user's experience. For example, suppose the bounds of the search are |
305 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on | 377 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on |
306 whether revision 50 is good or bad, the next revision to check will be either | 378 whether revision 50 is good or bad, the next revision to check will be either |
307 25 or 75. So, while revision 50 is being checked, the script will download | 379 25 or 75. So, while revision 50 is being checked, the script will download |
308 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is | 380 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is |
309 known: | 381 known: |
310 | 382 |
311 - If rev 50 is good, the download of rev 25 is cancelled, and the next test | 383 - If rev 50 is good, the download of rev 25 is cancelled, and the next test |
312 is run on rev 75. | 384 is run on rev 75. |
313 | 385 |
314 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test | 386 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test |
315 is run on rev 25. | 387 is run on rev 25. |
316 """ | 388 """ |
317 | 389 |
318 if not profile: | 390 if not profile: |
319 profile = 'profile' | 391 profile = 'profile' |
320 | 392 |
321 context = PathContext(platform, good_rev, bad_rev) | 393 context = PathContext(platform, good_rev, bad_rev, official_builds) |
322 cwd = os.getcwd() | 394 cwd = os.getcwd() |
323 | 395 |
324 _GetDownloadPath = lambda rev: os.path.join(cwd, | 396 |
325 '%d-%s' % (rev, context.archive_name)) | |
326 | 397 |
327 print "Downloading list of known revisions..." | 398 print "Downloading list of known revisions..." |
328 | 399 _GetDownloadPath = lambda rev: os.path.join(cwd, |
329 revlist = context.GetRevList() | 400 '%s-%s' % (str(rev), context.archive_name)) |
Robert Sesek
2012/05/23 21:24:01
nit: continuations are indented 4
anantha
2012/05/24 18:32:11
Done.
| |
401 if official_builds: | |
402 revlist = context.GetOfficialBuildsList() | |
403 else: | |
404 revlist = context.GetRevList() | |
330 | 405 |
331 # Get a list of revisions to bisect across. | 406 # Get a list of revisions to bisect across. |
332 if len(revlist) < 2: # Don't have enough builds to bisect. | 407 if len(revlist) < 2: # Don't have enough builds to bisect. |
333 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist | 408 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist |
334 raise RuntimeError(msg) | 409 raise RuntimeError(msg) |
335 | 410 |
336 # Figure out our bookends and first pivot point; fetch the pivot revision. | 411 # Figure out our bookends and first pivot point; fetch the pivot revision. |
337 good = 0 | 412 good = 0 |
338 bad = len(revlist) - 1 | 413 bad = len(revlist) - 1 |
339 pivot = bad / 2 | 414 pivot = bad / 2 |
340 rev = revlist[pivot] | 415 rev = revlist[pivot] |
341 zipfile = _GetDownloadPath(rev) | 416 zipfile = _GetDownloadPath(rev) |
342 progress_event = threading.Event() | 417 progress_event = threading.Event() |
343 progress_event.set() | 418 progress_event.set() |
344 print "Downloading revision %d..." % rev | 419 print "Downloading revision %s..." % str(rev) |
345 FetchRevision(context, rev, zipfile, | 420 FetchRevision(context, rev, zipfile, |
346 quit_event=None, progress_event=progress_event) | 421 quit_event=None, progress_event=progress_event) |
347 | 422 |
348 # Binary search time! | 423 # Binary search time! |
349 while zipfile and bad - good > 1: | 424 while zipfile and bad - good > 1: |
350 # Pre-fetch next two possible pivots | 425 # Pre-fetch next two possible pivots |
351 # - down_pivot is the next revision to check if the current revision turns | 426 # - down_pivot is the next revision to check if the current revision turns |
352 # out to be bad. | 427 # out to be bad. |
353 # - up_pivot is the next revision to check if the current revision turns | 428 # - up_pivot is the next revision to check if the current revision turns |
354 # out to be good. | 429 # out to be good. |
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
393 profile, | 468 profile, |
394 num_runs, | 469 num_runs, |
395 try_args) | 470 try_args) |
396 os.unlink(zipfile) | 471 os.unlink(zipfile) |
397 zipfile = None | 472 zipfile = None |
398 | 473 |
399 # Call the predicate function to see if the current revision is good or bad. | 474 # Call the predicate function to see if the current revision is good or bad. |
400 # On that basis, kill one of the background downloads and complete the | 475 # On that basis, kill one of the background downloads and complete the |
401 # other, as described in the comments above. | 476 # other, as described in the comments above. |
402 try: | 477 try: |
403 if predicate(rev, status, stdout, stderr): | 478 if predicate(rev, official_builds, status, stdout, stderr): |
404 good = pivot | 479 good = pivot |
405 if down_thread: | 480 if down_thread: |
406 down_quit_event.set() # Kill the download of older revision. | 481 down_quit_event.set() # Kill the download of older revision. |
407 down_thread.join() | 482 down_thread.join() |
408 os.unlink(down_zipfile) | 483 os.unlink(down_zipfile) |
409 if up_thread: | 484 if up_thread: |
410 print "Downloading revision %d..." % up_rev | 485 print "Downloading revision %s..." % str(up_rev) |
411 up_progress_event.set() # Display progress of download. | 486 up_progress_event.set() # Display progress of download. |
412 up_thread.join() # Wait for newer revision to finish downloading. | 487 up_thread.join() # Wait for newer revision to finish downloading. |
413 pivot = up_pivot | 488 pivot = up_pivot |
414 zipfile = up_zipfile | 489 zipfile = up_zipfile |
415 else: | 490 else: |
416 bad = pivot | 491 bad = pivot |
417 if up_thread: | 492 if up_thread: |
418 up_quit_event.set() # Kill download of newer revision. | 493 up_quit_event.set() # Kill download of newer revision. |
419 up_thread.join() | 494 up_thread.join() |
420 os.unlink(up_zipfile) | 495 os.unlink(up_zipfile) |
421 if down_thread: | 496 if down_thread: |
422 print "Downloading revision %d..." % down_rev | 497 print "Downloading revision %s..." % str(down_rev) |
423 down_progress_event.set() # Display progress of download. | 498 down_progress_event.set() # Display progress of download. |
424 down_thread.join() # Wait for older revision to finish downloading. | 499 down_thread.join() # Wait for older revision to finish downloading. |
425 pivot = down_pivot | 500 pivot = down_pivot |
426 zipfile = down_zipfile | 501 zipfile = down_zipfile |
427 except SystemExit: | 502 except SystemExit: |
428 print "Cleaning up..." | 503 print "Cleaning up..." |
429 for f in [_GetDownloadPath(revlist[down_pivot]), | 504 for f in [_GetDownloadPath(revlist[down_pivot]), |
430 _GetDownloadPath(revlist[up_pivot])]: | 505 _GetDownloadPath(revlist[up_pivot])]: |
431 try: | 506 try: |
432 os.unlink(f) | 507 os.unlink(f) |
(...skipping 26 matching lines...) Expand all Loading... | |
459 '\n' | 534 '\n' |
460 'Tip: add "-- --no-first-run" to bypass the first run prompts.') | 535 'Tip: add "-- --no-first-run" to bypass the first run prompts.') |
461 parser = optparse.OptionParser(usage=usage) | 536 parser = optparse.OptionParser(usage=usage) |
462 # Strangely, the default help output doesn't include the choice list. | 537 # Strangely, the default help output doesn't include the choice list. |
463 choices = ['mac', 'win', 'linux', 'linux64'] | 538 choices = ['mac', 'win', 'linux', 'linux64'] |
464 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 | 539 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 |
465 parser.add_option('-a', '--archive', | 540 parser.add_option('-a', '--archive', |
466 choices = choices, | 541 choices = choices, |
467 help = 'The buildbot archive to bisect [%s].' % | 542 help = 'The buildbot archive to bisect [%s].' % |
468 '|'.join(choices)) | 543 '|'.join(choices)) |
469 parser.add_option('-b', '--bad', type = 'int', | 544 parser.add_option('-o', action="store_true", dest='official_builds', |
545 help = 'Specify if it is a chromium or official build.') | |
Robert Sesek
2012/05/23 21:24:01
help = 'Bisect across official Chrome builds (inte
anantha
2012/05/24 18:32:11
Done.
| |
546 parser.add_option('-b', '--bad', type = 'str', | |
470 help = 'The bad revision to bisect to.') | 547 help = 'The bad revision to bisect to.') |
471 parser.add_option('-g', '--good', type = 'int', | 548 parser.add_option('-g', '--good', type = 'str', |
472 help = 'The last known good revision to bisect from.') | 549 help = 'The last known good revision to bisect from.') |
473 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', | 550 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', |
474 help = 'Profile to use; this will not reset every run. ' + | 551 help = 'Profile to use; this will not reset every run. ' + |
475 'Defaults to a clean profile.', default = 'profile') | 552 'Defaults to a clean profile.', default = 'profile') |
476 parser.add_option('-t', '--times', type = 'int', | 553 parser.add_option('-t', '--times', type = 'int', |
477 help = 'Number of times to run each build before asking ' + | 554 help = 'Number of times to run each build before asking ' + |
478 'if it\'s good or bad. Temporary profiles are reused.', | 555 'if it\'s good or bad. Temporary profiles are reused.', |
479 default = 1) | 556 default = 1) |
480 (opts, args) = parser.parse_args() | 557 (opts, args) = parser.parse_args() |
481 | 558 |
482 if opts.archive is None: | 559 if opts.archive is None: |
483 print 'Error: missing required parameter: --archive' | 560 print 'Error: missing required parameter: --archive' |
484 print | 561 print |
485 parser.print_help() | 562 parser.print_help() |
486 return 1 | 563 return 1 |
487 | 564 |
488 if opts.bad and opts.good and (opts.good > opts.bad): | 565 if opts.bad and opts.good and (opts.good > opts.bad): |
489 print ('The good revision (%d) must precede the bad revision (%d).\n' % | 566 print ('The good revision (%d) must precede the bad revision (%d).\n' % |
490 (opts.good, opts.bad)) | 567 (opts.good, opts.bad)) |
491 parser.print_help() | 568 parser.print_help() |
492 return 1 | 569 return 1 |
493 | 570 |
494 # Create the context. Initialize 0 for the revisions as they are set below. | 571 # Create the context. Initialize 0 for the revisions as they are set below. |
495 context = PathContext(opts.archive, 0, 0) | 572 context = PathContext(opts.archive, 0, 0, opts.official_builds) |
573 | |
574 if opts.official_builds and opts.bad is None: | |
575 print ' Needs a bad build range' | |
576 print | |
577 parser.print_help() | |
578 return 1 | |
496 | 579 |
497 # Pick a starting point, try to get HEAD for this. | 580 # Pick a starting point, try to get HEAD for this. |
498 if opts.bad: | 581 if opts.bad: |
499 bad_rev = opts.bad | 582 bad_rev = opts.bad |
500 else: | 583 else: |
501 bad_rev = 0 | 584 bad_rev = 0 |
502 try: | 585 try: |
503 # Location of the latest build revision number | 586 # Location of the latest build revision number |
504 nh = urllib.urlopen(context.GetLastChangeURL()) | 587 nh = urllib.urlopen(context.GetLastChangeURL()) |
505 latest = int(nh.read()) | 588 latest = int(nh.read()) |
(...skipping 16 matching lines...) Expand all Loading... | |
522 except Exception, e: | 605 except Exception, e: |
523 pass | 606 pass |
524 | 607 |
525 if opts.times < 1: | 608 if opts.times < 1: |
526 print('Number of times to run (%d) must be greater than or equal to 1.' % | 609 print('Number of times to run (%d) must be greater than or equal to 1.' % |
527 opts.times) | 610 opts.times) |
528 parser.print_help() | 611 parser.print_help() |
529 return 1 | 612 return 1 |
530 | 613 |
531 (last_known_good_rev, first_known_bad_rev) = Bisect( | 614 (last_known_good_rev, first_known_bad_rev) = Bisect( |
532 opts.archive, good_rev, bad_rev, opts.times, args, opts.profile) | 615 opts.archive, opts.official_builds, good_rev, bad_rev, opts.times, args, |
616 opts.profile) | |
533 | 617 |
534 # Get corresponding webkit revisions. | 618 # Get corresponding webkit revisions. |
535 try: | 619 try: |
536 last_known_good_webkit_rev = GetWebKitRevisionForChromiumRevision( | 620 last_known_good_webkit_rev = GetWebKitRevisionForChromiumRevision( |
537 last_known_good_rev) | 621 last_known_good_rev) |
538 first_known_bad_webkit_rev = GetWebKitRevisionForChromiumRevision( | 622 first_known_bad_webkit_rev = GetWebKitRevisionForChromiumRevision( |
539 first_known_bad_rev) | 623 first_known_bad_rev) |
540 except Exception, e: | 624 except Exception, e: |
541 # Silently ignore the failure. | 625 # Silently ignore the failure. |
542 last_known_good_webkit_rev, first_known_bad_webkit_rev = 0, 0 | 626 last_known_good_webkit_rev, first_known_bad_webkit_rev = 0, 0 |
543 | 627 |
544 # We're done. Let the user know the results in an official manner. | 628 # We're done. Let the user know the results in an official manner. |
545 print('You are probably looking for build %d.' % first_known_bad_rev) | 629 print('You are probably looking for build %s.' % str(first_known_bad_rev)) |
630 | |
546 if last_known_good_webkit_rev != first_known_bad_webkit_rev: | 631 if last_known_good_webkit_rev != first_known_bad_webkit_rev: |
547 print 'WEBKIT CHANGELOG URL:' | 632 print 'WEBKIT CHANGELOG URL:' |
548 print WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, | 633 print WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, |
549 last_known_good_webkit_rev) | 634 last_known_good_webkit_rev) |
550 print 'CHANGELOG URL:' | 635 print 'CHANGELOG URL:' |
551 print CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) | 636 if opts.official_builds: |
552 print 'Built at revision:' | 637 print OFFICIAL_CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) |
553 print BUILD_VIEWVC_URL % first_known_bad_rev | 638 else: |
639 print CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) | |
640 print 'Built at revision:' | |
641 print BUILD_VIEWVC_URL % first_known_bad_rev | |
554 | 642 |
555 | 643 |
556 if __name__ == '__main__': | 644 if __name__ == '__main__': |
557 sys.exit(main()) | 645 sys.exit(main()) |
OLD | NEW |