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/' | |
Robert Sesek
2012/05/21 21:39:47
Do not end this with a / for consistency with the
Robert Sesek
2012/05/21 21:39:47
This instead of chrome-master2.mtv/official_builds
anantha
2012/05/22 22:55:39
Done.
anantha
2012/05/22 22:55:39
This location has files from chrome-master2.mtv an
| |
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' |
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, build_type): |
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.build_type = build_type | |
Robert Sesek
2012/05/21 21:39:47
self.is_official per comment in Main()
anantha
2012/05/22 22:55:39
Done.
| |
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/' | 74 self._listing_platform_dir = 'Linux/' |
68 self.archive_name = 'chrome-linux.zip' | 75 if build_type != 'Official': |
Robert Sesek
2012/05/21 21:39:47
This is getting messy. Beak this down into two top
anantha
2012/05/22 22:55:39
Done.
| |
69 self._archive_extract_dir = 'chrome-linux' | 76 self._archive_extract_dir = 'chrome-linux' |
77 else: | |
78 self._archive_extract_dir = 'chrome-lucid64bit' | |
70 self._binary_name = 'chrome' | 79 self._binary_name = 'chrome' |
71 # Linux and x64 share all the same path data except for the archive dir. | 80 # Linux and x64 share all the same path data except for the archive dir. |
72 if self.platform == 'linux64': | 81 if self.platform == 'linux64': |
73 self._listing_platform_dir = 'Linux_x64/' | 82 if build_type != 'Official': |
83 self.archive_name = 'chrome-linux.zip' | |
84 self._listing_platform_dir = 'Linux_x64/' | |
85 else: | |
86 self.archive_name = 'chrome-lucid64bit.zip' | |
87 self._listing_platform_dir = 'lucid64bit/' | |
88 else: | |
89 if build_type != 'Official': | |
90 self.archive_name = 'chrome-linux.zip' | |
91 self._listing_platform_dir = 'Linux_x32/' | |
92 else: | |
93 self.archive_name = 'chrome-lucid32bit.zip' | |
94 self._listing_platform_dir = 'lucid32bit/' | |
74 elif self.platform == 'mac': | 95 elif self.platform == 'mac': |
75 self._listing_platform_dir = 'Mac/' | 96 if build_type != 'Official': |
97 self._listing_platform_dir = 'Mac/' | |
98 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' | |
99 else: | |
100 self._listing_platform_dir = 'mac/' | |
101 self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome' | |
76 self.archive_name = 'chrome-mac.zip' | 102 self.archive_name = 'chrome-mac.zip' |
77 self._archive_extract_dir = 'chrome-mac' | 103 self._archive_extract_dir = 'chrome-mac' |
78 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' | 104 |
79 elif self.platform == 'win': | 105 elif self.platform == 'win': |
80 self._listing_platform_dir = 'Win/' | 106 if build_type != 'Official': |
107 self._listing_platform_dir = 'Win/' | |
108 else: | |
109 self._listing_platform_dir = 'win/' | |
81 self.archive_name = 'chrome-win32.zip' | 110 self.archive_name = 'chrome-win32.zip' |
82 self._archive_extract_dir = 'chrome-win32' | 111 self._archive_extract_dir = 'chrome-win32' |
83 self._binary_name = 'chrome.exe' | 112 self._binary_name = 'chrome.exe' |
84 else: | 113 else: |
85 raise Exception('Invalid platform: %s' % self.platform) | 114 raise Exception('Invalid platform: %s' % self.platform) |
86 | 115 |
87 def GetListingURL(self, marker=None): | 116 def GetListingURL(self, marker=None): |
88 """Returns the URL for a directory listing, with an optional marker.""" | 117 """Returns the URL for a directory listing, with an optional marker.""" |
89 marker_param = '' | 118 marker_param = '' |
90 if marker: | 119 if marker: |
91 marker_param = '&marker=' + str(marker) | 120 marker_param = '&marker=' + str(marker) |
92 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ | 121 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ |
93 marker_param | 122 marker_param |
94 | 123 |
95 def GetDownloadURL(self, revision): | 124 def GetDownloadURL(self, revision): |
96 """Gets the download URL for a build archive of a specific revision.""" | 125 """Gets the download URL for a build archive of a specific revision.""" |
97 return "%s/%s%d/%s" % ( | 126 if self.build_type != 'Official': |
98 BASE_URL, self._listing_platform_dir, revision, self.archive_name) | 127 return "%s/%s%d/%s" % ( |
128 BASE_URL, self._listing_platform_dir, revision, self.archive_name) | |
129 else: | |
130 return "%s%s/%s%s" % ( | |
131 OFFICIAL_BASE_URL, revision, self._listing_platform_dir, | |
132 self.archive_name) | |
99 | 133 |
100 def GetLastChangeURL(self): | 134 def GetLastChangeURL(self): |
101 """Returns a URL to the LAST_CHANGE file.""" | 135 """Returns a URL to the LAST_CHANGE file.""" |
102 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' | 136 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' |
103 | 137 |
104 def GetLaunchPath(self): | 138 def GetLaunchPath(self): |
105 """Returns a relative path (presumably from the archive extraction location) | 139 """Returns a relative path (presumably from the archive extraction location) |
106 that is used to run the executable.""" | 140 that is used to run the executable.""" |
107 return os.path.join(self._archive_extract_dir, self._binary_name) | 141 return os.path.join(self._archive_extract_dir, self._binary_name) |
108 | 142 |
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
143 # trailing slash to just have a number. | 177 # trailing slash to just have a number. |
144 revisions = [] | 178 revisions = [] |
145 for prefix in all_prefixes: | 179 for prefix in all_prefixes: |
146 revnum = prefix.text[prefix_len:-1] | 180 revnum = prefix.text[prefix_len:-1] |
147 try: | 181 try: |
148 revnum = int(revnum) | 182 revnum = int(revnum) |
149 revisions.append(revnum) | 183 revisions.append(revnum) |
150 except ValueError: | 184 except ValueError: |
151 pass | 185 pass |
152 return (revisions, next_marker) | 186 return (revisions, next_marker) |
187 if self.build_type != 'Official': | |
Robert Sesek
2012/05/21 21:39:47
This logic doesn't belong in this function, as it'
anantha
2012/05/22 22:55:39
Correct. I forgot to clean this up.
| |
188 # Fetch the first list of revisions. | |
189 (revisions, next_marker) = _FetchAndParse(self.GetListingURL()) | |
153 | 190 |
154 # Fetch the first list of revisions. | 191 # If the result list was truncated, refetch with the next marker. Do this |
155 (revisions, next_marker) = _FetchAndParse(self.GetListingURL()) | 192 # until an entire directory listing is done. |
156 | 193 while next_marker: |
157 # If the result list was truncated, refetch with the next marker. Do this | 194 next_url = self.GetListingURL(next_marker) |
158 # until an entire directory listing is done. | 195 (new_revisions, next_marker) = _FetchAndParse(next_url) |
159 while next_marker: | 196 revisions.extend(new_revisions) |
160 next_url = self.GetListingURL(next_marker) | 197 return revisions |
161 (new_revisions, next_marker) = _FetchAndParse(next_url) | 198 else: |
162 revisions.extend(new_revisions) | 199 handle = urllib.urlopen(OFFICIAL_BASE_URL) |
163 | 200 dirindex = handle.read() |
164 return revisions | 201 handle.close() |
202 return re.findall(r'<a href="([0-9][0-9].*)/">', dirindex) | |
165 | 203 |
166 def GetRevList(self): | 204 def GetRevList(self): |
167 """Gets the list of revision numbers between self.good_revision and | 205 """Gets the list of revision numbers between self.good_revision and |
168 self.bad_revision.""" | 206 self.bad_revision.""" |
169 # Download the revlist and filter for just the range between good and bad. | 207 # Download the revlist and filter for just the range between good and bad. |
170 minrev = self.good_revision | 208 minrev = self.good_revision |
171 maxrev = self.bad_revision | 209 maxrev = self.bad_revision |
172 revlist = map(int, self.ParseDirectoryIndex()) | 210 revlist = map(int, self.ParseDirectoryIndex()) |
173 revlist = [x for x in revlist if x >= minrev and x <= maxrev] | 211 revlist = [x for x in revlist if x >= int(minrev) and x <= int(maxrev)] |
174 revlist.sort() | 212 revlist.sort() |
175 return revlist | 213 return revlist |
176 | 214 |
215 def GetBuildsList(self): | |
Robert Sesek
2012/05/21 21:39:47
naming: GetOfficialBuildsList
anantha
2012/05/22 22:55:39
Done.
| |
216 """Gets the list of revision numbers between self.good_revision and | |
Robert Sesek
2012/05/21 21:39:47
nit: this isn't returning revision numbers, this i
anantha
2012/05/22 22:55:39
Done.
anantha
2012/05/22 22:55:39
Done.
| |
217 self.bad_revision.""" | |
218 # Download the revlist and filter for just the range between good and bad. | |
219 minrev = self.good_revision | |
220 maxrev = self.bad_revision | |
221 handle = urllib.urlopen(OFFICIAL_BASE_URL) | |
222 dirindex = handle.read() | |
223 handle.close() | |
224 build_numbers = re.findall(r'<a href="([0-9][0-9].*)/">', dirindex) | |
Robert Sesek
2012/05/21 21:39:47
This logic is duplicated above.
anantha
2012/05/22 22:55:39
Removed from above.
| |
225 final_list = [] | |
226 start_index = '0' | |
227 end_index = '0' | |
228 i = 0 | |
229 | |
230 parsed_build_numbers = [LooseVersion(x) for x in build_numbers] | |
231 for build_number in sorted(parsed_build_numbers): | |
232 path = OFFICIAL_BASE_URL + str(build_number) + '/' + \ | |
Robert Sesek
2012/05/21 21:39:47
nit: should be indented 2
anantha
2012/05/22 22:55:39
Done.
| |
233 self._listing_platform_dir + self.archive_name | |
234 i = i+1 | |
235 try: | |
236 connection = urllib.urlopen(path) | |
Robert Sesek
2012/05/21 21:39:47
nit: over-indented
anantha
2012/05/22 22:55:39
Done.
anantha
2012/05/22 22:55:39
Done.
| |
237 connection.close() | |
238 final_list.append(str(build_number)) | |
239 if str(build_number) == minrev: | |
240 start_index = i | |
241 if str(build_number) == maxrev: | |
242 end_index = i | |
243 except urllib.HTTPError, e: | |
244 print e.getcode() | |
245 return final_list[start_index:end_index] | |
177 | 246 |
178 def UnzipFilenameToDir(filename, dir): | 247 def UnzipFilenameToDir(filename, dir): |
179 """Unzip |filename| to directory |dir|.""" | 248 """Unzip |filename| to directory |dir|.""" |
180 cwd = os.getcwd() | 249 cwd = os.getcwd() |
181 if not os.path.isabs(filename): | 250 if not os.path.isabs(filename): |
182 filename = os.path.join(cwd, filename) | 251 filename = os.path.join(cwd, filename) |
183 zf = zipfile.ZipFile(filename) | 252 zf = zipfile.ZipFile(filename) |
184 # Make base. | 253 # Make base. |
185 try: | 254 try: |
186 if not os.path.isdir(dir): | 255 if not os.path.isdir(dir): |
(...skipping 26 matching lines...) Expand all Loading... | |
213 @param rev The Chromium revision number/tag to download. | 282 @param rev The Chromium revision number/tag to download. |
214 @param filename The destination for the downloaded file. | 283 @param filename The destination for the downloaded file. |
215 @param quit_event A threading.Event which will be set by the master thread to | 284 @param quit_event A threading.Event which will be set by the master thread to |
216 indicate that the download should be aborted. | 285 indicate that the download should be aborted. |
217 @param progress_event A threading.Event which will be set by the master thread | 286 @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 | 287 to indicate that the progress of the download should be |
219 displayed. | 288 displayed. |
220 """ | 289 """ |
221 def ReportHook(blocknum, blocksize, totalsize): | 290 def ReportHook(blocknum, blocksize, totalsize): |
222 if quit_event and quit_event.isSet(): | 291 if quit_event and quit_event.isSet(): |
223 raise RuntimeError("Aborting download of revision %d" % rev) | 292 if context.build_type != 'Official': |
Robert Sesek
2012/05/21 21:39:47
Don't do this. Just cast rev to a string and forma
anantha
2012/05/22 22:55:39
Done.
| |
293 raise RuntimeError("Aborting download of revision %d" % rev) | |
294 else: | |
295 raise RuntimeError("Aborting download of revision %s" % rev) | |
224 if progress_event and progress_event.isSet(): | 296 if progress_event and progress_event.isSet(): |
225 size = blocknum * blocksize | 297 size = blocknum * blocksize |
226 if totalsize == -1: # Total size not known. | 298 if totalsize == -1: # Total size not known. |
227 progress = "Received %d bytes" % size | 299 progress = "Received %d bytes" % size |
228 else: | 300 else: |
229 size = min(totalsize, size) | 301 size = min(totalsize, size) |
230 progress = "Received %d of %d bytes, %.2f%%" % ( | 302 progress = "Received %d of %d bytes, %.2f%%" % ( |
231 size, totalsize, 100.0 * size / totalsize) | 303 size, totalsize, 100.0 * size / totalsize) |
232 # Send a \r to let all progress messages use just one line of output. | 304 # Send a \r to let all progress messages use just one line of output. |
233 sys.stdout.write("\r" + progress) | 305 sys.stdout.write("\r" + progress) |
234 sys.stdout.flush() | 306 sys.stdout.flush() |
235 | 307 |
236 download_url = context.GetDownloadURL(rev) | 308 download_url = context.GetDownloadURL(rev) |
237 try: | 309 try: |
238 urllib.urlretrieve(download_url, filename, ReportHook) | 310 urllib.urlretrieve(download_url, filename, ReportHook) |
239 if progress_event and progress_event.isSet(): | |
Robert Sesek
2012/05/21 21:39:47
Why'd you remove this?
| |
240 print | |
241 except RuntimeError, e: | 311 except RuntimeError, e: |
242 pass | 312 pass |
Robert Sesek
2012/05/21 21:39:47
nit: over-indented
anantha
2012/05/22 22:55:39
Done.
| |
243 | 313 |
244 | 314 |
245 def RunRevision(context, revision, zipfile, profile, num_runs, args): | 315 def RunRevision(context, revision, zipfile, profile, num_runs, args): |
246 """Given a zipped revision, unzip it and run the test.""" | 316 """Given a zipped revision, unzip it and run the test.""" |
247 print "Trying revision %d..." % revision | 317 if context.build_type != 'Official': |
318 print "Trying revision %d..." % revision | |
319 else: | |
320 print "Trying revision %s..." % revision | |
248 | 321 |
249 # Create a temp directory and unzip the revision into it. | 322 # Create a temp directory and unzip the revision into it. |
250 cwd = os.getcwd() | 323 cwd = os.getcwd() |
251 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') | 324 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') |
252 UnzipFilenameToDir(zipfile, tempdir) | 325 UnzipFilenameToDir(zipfile, tempdir) |
253 os.chdir(tempdir) | 326 os.chdir(tempdir) |
254 | 327 |
255 # Run the build as many times as specified. | 328 if context.platform == 'linux' or context.platform == 'linux64': |
256 testargs = [context.GetLaunchPath(), '--user-data-dir=%s' % profile] + args | 329 if context.build_type == 'Official': |
330 # Run the build as many times as specified. | |
331 testargs = [context.GetLaunchPath(), '--no-sandbox', | |
Robert Sesek
2012/05/21 21:39:47
Why this? I think this should just be done manuall
anantha
2012/05/22 22:55:39
Looks like we need root permission to setup the sa
| |
332 '--user-data-dir=%s' % profile] + args | |
333 else: | |
334 # Run the build as many times as specified. | |
335 testargs = [context.GetLaunchPath(), | |
336 '--user-data-dir=%s' % profile] + args | |
337 else: | |
338 # Run the build as many times as specified. | |
339 testargs = [context.GetLaunchPath(), '--user-data-dir=%s' % profile] + args | |
340 | |
257 for i in range(0, num_runs): | 341 for i in range(0, num_runs): |
258 subproc = subprocess.Popen(testargs, | 342 subproc = subprocess.Popen(testargs, |
259 bufsize=-1, | 343 bufsize=-1, |
260 stdout=subprocess.PIPE, | 344 stdout=subprocess.PIPE, |
261 stderr=subprocess.PIPE) | 345 stderr=subprocess.PIPE) |
262 (stdout, stderr) = subproc.communicate() | 346 (stdout, stderr) = subproc.communicate() |
263 | 347 |
264 os.chdir(cwd) | 348 os.chdir(cwd) |
265 try: | 349 try: |
266 shutil.rmtree(tempdir, True) | 350 shutil.rmtree(tempdir, True) |
267 except Exception, e: | 351 except Exception, e: |
268 pass | 352 pass |
269 | 353 |
270 return (subproc.returncode, stdout, stderr) | 354 return (subproc.returncode, stdout, stderr) |
271 | 355 |
272 | 356 |
273 def AskIsGoodBuild(rev, status, stdout, stderr): | 357 def AskIsGoodBuild(rev, build_type, status, stdout, stderr): |
274 """Ask the user whether build |rev| is good or bad.""" | 358 """Ask the user whether build |rev| is good or bad.""" |
275 # Loop until we get a response that we can parse. | 359 # Loop until we get a response that we can parse. |
276 while True: | 360 while True: |
277 response = raw_input('Revision %d is [(g)ood/(b)ad/(q)uit]: ' % int(rev)) | 361 if build_type != 'Official': |
362 response = raw_input('Revision %d is [(g)ood/(b)ad/(q)uit]: ' % int(rev)) | |
363 else: | |
364 response = raw_input('Revision %s is [(g)ood/(b)ad/(q)uit]: ' % str(rev)) | |
278 if response and response in ('g', 'b'): | 365 if response and response in ('g', 'b'): |
279 return response == 'g' | 366 return response == 'g' |
280 if response and response == 'q': | 367 if response and response == 'q': |
281 raise SystemExit() | 368 raise SystemExit() |
282 | 369 |
283 | 370 |
284 def Bisect(platform, | 371 def Bisect(platform, build_type, |
Robert Sesek
2012/05/21 21:39:47
nit: goes on its own line, and the parameter also
anantha
2012/05/22 22:55:39
Done.
| |
285 good_rev=0, | 372 good_rev=0, |
286 bad_rev=0, | 373 bad_rev=0, |
287 num_runs=1, | 374 num_runs=1, |
288 try_args=(), | 375 try_args=(), |
289 profile=None, | 376 profile=None, |
290 predicate=AskIsGoodBuild): | 377 predicate=AskIsGoodBuild): |
291 """Given known good and known bad revisions, run a binary search on all | 378 """Given known good and known bad revisions, run a binary search on all |
292 archived revisions to determine the last known good revision. | 379 archived revisions to determine the last known good revision. |
293 | 380 |
294 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). | 381 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). |
(...skipping 16 matching lines...) Expand all Loading... | |
311 - If rev 50 is good, the download of rev 25 is cancelled, and the next test | 398 - If rev 50 is good, the download of rev 25 is cancelled, and the next test |
312 is run on rev 75. | 399 is run on rev 75. |
313 | 400 |
314 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test | 401 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test |
315 is run on rev 25. | 402 is run on rev 25. |
316 """ | 403 """ |
317 | 404 |
318 if not profile: | 405 if not profile: |
319 profile = 'profile' | 406 profile = 'profile' |
320 | 407 |
321 context = PathContext(platform, good_rev, bad_rev) | 408 context = PathContext(platform, good_rev, bad_rev, build_type) |
322 cwd = os.getcwd() | 409 cwd = os.getcwd() |
323 | 410 |
324 _GetDownloadPath = lambda rev: os.path.join(cwd, | 411 |
325 '%d-%s' % (rev, context.archive_name)) | |
326 | 412 |
327 print "Downloading list of known revisions..." | 413 print "Downloading list of known revisions..." |
328 | 414 if build_type != 'Official': |
329 revlist = context.GetRevList() | 415 _GetDownloadPath = lambda rev: os.path.join(cwd, |
416 '%d-%s' % (rev, context.archive_name)) | |
417 revlist = context.GetRevList() | |
418 else: | |
419 _GetDownloadPath = lambda rev: os.path.join(cwd, | |
420 '%s-%s' % (rev, context.archive_name)) | |
421 revlist = context.GetBuildsList() | |
330 | 422 |
331 # Get a list of revisions to bisect across. | 423 # Get a list of revisions to bisect across. |
332 if len(revlist) < 2: # Don't have enough builds to bisect. | 424 if len(revlist) < 2: # Don't have enough builds to bisect. |
333 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist | 425 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist |
334 raise RuntimeError(msg) | 426 raise RuntimeError(msg) |
335 | 427 |
336 # Figure out our bookends and first pivot point; fetch the pivot revision. | 428 # Figure out our bookends and first pivot point; fetch the pivot revision. |
337 good = 0 | 429 good = 0 |
338 bad = len(revlist) - 1 | 430 bad = len(revlist) - 1 |
339 pivot = bad / 2 | 431 pivot = bad / 2 |
340 rev = revlist[pivot] | 432 rev = revlist[pivot] |
341 zipfile = _GetDownloadPath(rev) | 433 zipfile = _GetDownloadPath(rev) |
342 progress_event = threading.Event() | 434 progress_event = threading.Event() |
343 progress_event.set() | 435 progress_event.set() |
344 print "Downloading revision %d..." % rev | 436 if build_type != 'Official': |
437 print "Downloading revision %d..." % rev | |
438 else: | |
439 print "Downloading revision %s..." % rev | |
345 FetchRevision(context, rev, zipfile, | 440 FetchRevision(context, rev, zipfile, |
346 quit_event=None, progress_event=progress_event) | 441 quit_event=None, progress_event=progress_event) |
347 | 442 |
348 # Binary search time! | 443 # Binary search time! |
349 while zipfile and bad - good > 1: | 444 while zipfile and bad - good > 1: |
350 # Pre-fetch next two possible pivots | 445 # Pre-fetch next two possible pivots |
351 # - down_pivot is the next revision to check if the current revision turns | 446 # - down_pivot is the next revision to check if the current revision turns |
352 # out to be bad. | 447 # out to be bad. |
353 # - up_pivot is the next revision to check if the current revision turns | 448 # - up_pivot is the next revision to check if the current revision turns |
354 # out to be good. | 449 # out to be good. |
(...skipping 38 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
393 profile, | 488 profile, |
394 num_runs, | 489 num_runs, |
395 try_args) | 490 try_args) |
396 os.unlink(zipfile) | 491 os.unlink(zipfile) |
397 zipfile = None | 492 zipfile = None |
398 | 493 |
399 # Call the predicate function to see if the current revision is good or bad. | 494 # 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 | 495 # On that basis, kill one of the background downloads and complete the |
401 # other, as described in the comments above. | 496 # other, as described in the comments above. |
402 try: | 497 try: |
403 if predicate(rev, status, stdout, stderr): | 498 if predicate(rev, build_type, status, stdout, stderr): |
404 good = pivot | 499 good = pivot |
405 if down_thread: | 500 if down_thread: |
406 down_quit_event.set() # Kill the download of older revision. | 501 down_quit_event.set() # Kill the download of older revision. |
407 down_thread.join() | 502 down_thread.join() |
408 os.unlink(down_zipfile) | 503 os.unlink(down_zipfile) |
409 if up_thread: | 504 if up_thread: |
410 print "Downloading revision %d..." % up_rev | 505 if build_type == 'Official': |
506 print "Downloading revision %s..." % up_rev | |
507 else: | |
508 print "Downloading revision %d..." % up_rev | |
411 up_progress_event.set() # Display progress of download. | 509 up_progress_event.set() # Display progress of download. |
412 up_thread.join() # Wait for newer revision to finish downloading. | 510 up_thread.join() # Wait for newer revision to finish downloading. |
413 pivot = up_pivot | 511 pivot = up_pivot |
414 zipfile = up_zipfile | 512 zipfile = up_zipfile |
415 else: | 513 else: |
416 bad = pivot | 514 bad = pivot |
417 if up_thread: | 515 if up_thread: |
418 up_quit_event.set() # Kill download of newer revision. | 516 up_quit_event.set() # Kill download of newer revision. |
419 up_thread.join() | 517 up_thread.join() |
420 os.unlink(up_zipfile) | 518 os.unlink(up_zipfile) |
421 if down_thread: | 519 if down_thread: |
422 print "Downloading revision %d..." % down_rev | 520 if build_type == 'Official': |
521 print "Downloading revision %s..." % down_rev | |
522 else: | |
523 print "Downloading revision %d..." % down_rev | |
423 down_progress_event.set() # Display progress of download. | 524 down_progress_event.set() # Display progress of download. |
424 down_thread.join() # Wait for older revision to finish downloading. | 525 down_thread.join() # Wait for older revision to finish downloading. |
425 pivot = down_pivot | 526 pivot = down_pivot |
426 zipfile = down_zipfile | 527 zipfile = down_zipfile |
427 except SystemExit: | 528 except SystemExit: |
428 print "Cleaning up..." | 529 print "Cleaning up..." |
429 for f in [_GetDownloadPath(revlist[down_pivot]), | 530 for f in [_GetDownloadPath(revlist[down_pivot]), |
430 _GetDownloadPath(revlist[up_pivot])]: | 531 _GetDownloadPath(revlist[up_pivot])]: |
431 try: | 532 try: |
432 os.unlink(f) | 533 os.unlink(f) |
(...skipping 26 matching lines...) Expand all Loading... | |
459 '\n' | 560 '\n' |
460 'Tip: add "-- --no-first-run" to bypass the first run prompts.') | 561 'Tip: add "-- --no-first-run" to bypass the first run prompts.') |
461 parser = optparse.OptionParser(usage=usage) | 562 parser = optparse.OptionParser(usage=usage) |
462 # Strangely, the default help output doesn't include the choice list. | 563 # Strangely, the default help output doesn't include the choice list. |
463 choices = ['mac', 'win', 'linux', 'linux64'] | 564 choices = ['mac', 'win', 'linux', 'linux64'] |
464 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 | 565 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 |
465 parser.add_option('-a', '--archive', | 566 parser.add_option('-a', '--archive', |
466 choices = choices, | 567 choices = choices, |
467 help = 'The buildbot archive to bisect [%s].' % | 568 help = 'The buildbot archive to bisect [%s].' % |
468 '|'.join(choices)) | 569 '|'.join(choices)) |
469 parser.add_option('-b', '--bad', type = 'int', | 570 parser.add_option('-o', '--build_type', type = 'str', |
Robert Sesek
2012/05/21 21:39:47
This should just be type bool and make the flag --
anantha
2012/05/22 22:55:39
Done.
| |
571 help = 'Specify if it is a chromium or official build.') | |
572 parser.add_option('-b', '--bad', type = 'str', | |
470 help = 'The bad revision to bisect to.') | 573 help = 'The bad revision to bisect to.') |
471 parser.add_option('-g', '--good', type = 'int', | 574 parser.add_option('-g', '--good', type = 'str', |
472 help = 'The last known good revision to bisect from.') | 575 help = 'The last known good revision to bisect from.') |
473 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', | 576 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', |
474 help = 'Profile to use; this will not reset every run. ' + | 577 help = 'Profile to use; this will not reset every run. ' + |
475 'Defaults to a clean profile.', default = 'profile') | 578 'Defaults to a clean profile.', default = 'profile') |
476 parser.add_option('-t', '--times', type = 'int', | 579 parser.add_option('-t', '--times', type = 'int', |
477 help = 'Number of times to run each build before asking ' + | 580 help = 'Number of times to run each build before asking ' + |
478 'if it\'s good or bad. Temporary profiles are reused.', | 581 'if it\'s good or bad. Temporary profiles are reused.', |
479 default = 1) | 582 default = 1) |
480 (opts, args) = parser.parse_args() | 583 (opts, args) = parser.parse_args() |
481 | 584 |
482 if opts.archive is None: | 585 if opts.archive is None: |
483 print 'Error: missing required parameter: --archive' | 586 print 'Error: missing required parameter: --archive' |
484 print | 587 print |
485 parser.print_help() | 588 parser.print_help() |
486 return 1 | 589 return 1 |
487 | 590 |
488 if opts.bad and opts.good and (opts.good > opts.bad): | 591 if opts.bad and opts.good and (opts.good > opts.bad): |
489 print ('The good revision (%d) must precede the bad revision (%d).\n' % | 592 print ('The good revision (%d) must precede the bad revision (%d).\n' % |
490 (opts.good, opts.bad)) | 593 (opts.good, opts.bad)) |
491 parser.print_help() | 594 parser.print_help() |
492 return 1 | 595 return 1 |
493 | 596 |
494 # Create the context. Initialize 0 for the revisions as they are set below. | 597 # Create the context. Initialize 0 for the revisions as they are set below. |
495 context = PathContext(opts.archive, 0, 0) | 598 context = PathContext(opts.archive, 0, 0, opts.build_type) |
599 | |
600 if opts.build_type == 'Official': | |
601 if opts.bad is None: | |
Robert Sesek
2012/05/21 21:39:47
Join this and the previous line with an |and|
anantha
2012/05/22 22:55:39
Done.
| |
602 print ' Need a bad build range' | |
Robert Sesek
2012/05/21 21:39:47
print >>sys.stderr, 'Needs...'
anantha
2012/05/22 22:55:39
Done.
Robert Sesek
2012/05/23 21:24:00
Not done. Also, maybe change the error to "Bisecti
| |
603 print | |
604 parser.print_help() | |
605 return 1 | |
496 | 606 |
497 # Pick a starting point, try to get HEAD for this. | 607 # Pick a starting point, try to get HEAD for this. |
498 if opts.bad: | 608 if opts.bad: |
499 bad_rev = opts.bad | 609 bad_rev = opts.bad |
500 else: | 610 else: |
501 bad_rev = 0 | 611 bad_rev = 0 |
502 try: | 612 try: |
503 # Location of the latest build revision number | 613 # Location of the latest build revision number |
504 nh = urllib.urlopen(context.GetLastChangeURL()) | 614 nh = urllib.urlopen(context.GetLastChangeURL()) |
505 latest = int(nh.read()) | 615 latest = int(nh.read()) |
(...skipping 16 matching lines...) Expand all Loading... | |
522 except Exception, e: | 632 except Exception, e: |
523 pass | 633 pass |
524 | 634 |
525 if opts.times < 1: | 635 if opts.times < 1: |
526 print('Number of times to run (%d) must be greater than or equal to 1.' % | 636 print('Number of times to run (%d) must be greater than or equal to 1.' % |
527 opts.times) | 637 opts.times) |
528 parser.print_help() | 638 parser.print_help() |
529 return 1 | 639 return 1 |
530 | 640 |
531 (last_known_good_rev, first_known_bad_rev) = Bisect( | 641 (last_known_good_rev, first_known_bad_rev) = Bisect( |
532 opts.archive, good_rev, bad_rev, opts.times, args, opts.profile) | 642 opts.archive, opts.build_type, good_rev, bad_rev, opts.times, args, |
643 opts.profile) | |
533 | 644 |
534 # Get corresponding webkit revisions. | 645 # Get corresponding webkit revisions. |
535 try: | 646 try: |
536 last_known_good_webkit_rev = GetWebKitRevisionForChromiumRevision( | 647 last_known_good_webkit_rev = GetWebKitRevisionForChromiumRevision( |
537 last_known_good_rev) | 648 last_known_good_rev) |
538 first_known_bad_webkit_rev = GetWebKitRevisionForChromiumRevision( | 649 first_known_bad_webkit_rev = GetWebKitRevisionForChromiumRevision( |
539 first_known_bad_rev) | 650 first_known_bad_rev) |
540 except Exception, e: | 651 except Exception, e: |
541 # Silently ignore the failure. | 652 # Silently ignore the failure. |
542 last_known_good_webkit_rev, first_known_bad_webkit_rev = 0, 0 | 653 last_known_good_webkit_rev, first_known_bad_webkit_rev = 0, 0 |
543 | 654 |
544 # We're done. Let the user know the results in an official manner. | 655 # 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) | 656 if opts.build_type != 'Official': |
657 print('You are probably looking for build %d.' % first_known_bad_rev) | |
658 else: | |
659 print('You are probably looking for build %s.' % first_known_bad_rev) | |
660 | |
546 if last_known_good_webkit_rev != first_known_bad_webkit_rev: | 661 if last_known_good_webkit_rev != first_known_bad_webkit_rev: |
547 print 'WEBKIT CHANGELOG URL:' | 662 print 'WEBKIT CHANGELOG URL:' |
548 print WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, | 663 print WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, |
549 last_known_good_webkit_rev) | 664 last_known_good_webkit_rev) |
550 print 'CHANGELOG URL:' | 665 print 'CHANGELOG URL:' |
551 print CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) | 666 if opts.build_type == 'Official': |
552 print 'Built at revision:' | 667 print OFFICIAL_CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) |
553 print BUILD_VIEWVC_URL % first_known_bad_rev | 668 else: |
669 print CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) | |
670 print 'Built at revision:' | |
671 print BUILD_VIEWVC_URL % first_known_bad_rev | |
554 | 672 |
555 | 673 |
556 if __name__ == '__main__': | 674 if __name__ == '__main__': |
557 sys.exit(main()) | 675 sys.exit(main()) |
Robert Sesek
2012/05/21 21:39:47
What's this change?
anantha
2012/05/22 22:55:39
I somehow ended up adding two spaces. So deleted t
| |
OLD | NEW |