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' |
17 | 18 |
18 # URL to the ViewVC commit page. | 19 # URL to the ViewVC commit page. |
19 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d' | 20 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d' |
20 | 21 |
21 # Changelogs URL. | 22 # Changelogs URL. |
22 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ | 23 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ |
23 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' | 24 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' |
24 | 25 |
25 ############################################################################### | 26 ############################################################################### |
26 | 27 |
27 import math | 28 import math |
28 import optparse | 29 import optparse |
29 import os | 30 import os |
30 import pipes | 31 import pipes |
31 import re | 32 import re |
32 import shutil | 33 import shutil |
33 import sys | 34 import sys |
34 import tempfile | 35 import tempfile |
35 import urllib | 36 import urllib |
36 from xml.etree import ElementTree | 37 from xml.etree import ElementTree |
37 import zipfile | 38 import zipfile |
38 | 39 |
39 class PathContext(object): | 40 class PathContext(object): |
40 """A PathContext is used to carry the information used to construct URLs and | 41 """A PathContext is used to carry the information used to construct URLs and |
41 paths when dealing with the storage server and archives.""" | 42 paths when dealing with the storage server and archives.""" |
42 def __init__(self, platform, good_revision, bad_revision): | 43 def __init__(self, platform, good_revision, bad_revision, use_recent): |
43 super(PathContext, self).__init__() | 44 super(PathContext, self).__init__() |
44 # Store off the input parameters. | 45 # Store off the input parameters. |
45 self.platform = platform # What's passed in to the '-a/--archive' option. | 46 self.platform = platform # What's passed in to the '-a/--archive' option. |
46 self.good_revision = good_revision | 47 self.good_revision = good_revision |
47 self.bad_revision = bad_revision | 48 self.bad_revision = bad_revision |
| 49 self.use_recent = use_recent |
48 | 50 |
49 # The name of the ZIP file in a revision directory on the server. | 51 # The name of the ZIP file in a revision directory on the server. |
50 self.archive_name = None | 52 self.archive_name = None |
51 | 53 |
52 # Set some internal members: | 54 # Set some internal members: |
53 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. | 55 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. |
54 # _archive_extract_dir = Uncompressed directory in the archive_name file. | 56 # _archive_extract_dir = Uncompressed directory in the archive_name file. |
55 # _binary_name = The name of the executable to run. | 57 # _binary_name = The name of the executable to run. |
56 if self.platform == 'linux' or self.platform == 'linux64': | 58 if self.platform == 'linux' or self.platform == 'linux64': |
57 self._listing_platform_dir = 'Linux/' | 59 self._listing_platform_dir = 'Linux/' |
(...skipping 17 matching lines...) Expand all Loading... |
75 raise Exception("Invalid platform") | 77 raise Exception("Invalid platform") |
76 | 78 |
77 def GetListingURL(self, marker=None): | 79 def GetListingURL(self, marker=None): |
78 """Returns the URL for a directory listing, with an optional marker.""" | 80 """Returns the URL for a directory listing, with an optional marker.""" |
79 marker_param = '' | 81 marker_param = '' |
80 if marker: | 82 if marker: |
81 marker_param = '&marker=' + str(marker) | 83 marker_param = '&marker=' + str(marker) |
82 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ | 84 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ |
83 marker_param | 85 marker_param |
84 | 86 |
| 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 |
85 def GetDownloadURL(self, revision): | 91 def GetDownloadURL(self, revision): |
86 """Gets the download URL for a build archive of a specific revision.""" | 92 """Gets the download URL for a build archive of a specific revision.""" |
87 return "%s/%s%d/%s" % ( | 93 if self.use_recent: |
88 BASE_URL, self._listing_platform_dir, revision, self.archive_name) | 94 return "%s/%s%d/%s" % ( |
| 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) |
89 | 100 |
90 def GetLastChangeURL(self): | 101 def GetLastChangeURL(self): |
91 """Returns a URL to the LAST_CHANGE file.""" | 102 """Returns a URL to the LAST_CHANGE file.""" |
92 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' | 103 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' |
93 | 104 |
94 def GetLaunchPath(self): | 105 def GetLaunchPath(self): |
95 """Returns a relative path (presumably from the archive extraction location) | 106 """Returns a relative path (presumably from the archive extraction location) |
96 that is used to run the executable.""" | 107 that is used to run the executable.""" |
97 return os.path.join(self._archive_extract_dir, self._binary_name) | 108 return os.path.join(self._archive_extract_dir, self._binary_name) |
98 | 109 |
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
176 # 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 |
177 # until an entire directory listing is done. | 188 # until an entire directory listing is done. |
178 while next_marker: | 189 while next_marker: |
179 (new_revisions, next_marker) = _FetchAndParse( | 190 (new_revisions, next_marker) = _FetchAndParse( |
180 context.GetListingURL(next_marker)) | 191 context.GetListingURL(next_marker)) |
181 revisions.extend(new_revisions) | 192 revisions.extend(new_revisions) |
182 | 193 |
183 return revisions | 194 return revisions |
184 | 195 |
185 | 196 |
| 197 def ParseDirectoryIndexRecent(context): |
| 198 """Parses the recent builds directory listing into a list of revision |
| 199 numbers.""" |
| 200 handle = urllib.urlopen(context.GetListingURLRecent()) |
| 201 document = handle.read() |
| 202 |
| 203 # Looking for: <a href="92976/">92976/</a> |
| 204 return re.findall(r"<a href=\"(\d+)/\">\1/</a>", document) |
| 205 |
| 206 |
186 def GetRevList(context): | 207 def GetRevList(context): |
187 """Gets the list of revision numbers between |good_revision| and | 208 """Gets the list of revision numbers between |good_revision| and |
188 |bad_revision| of the |context|.""" | 209 |bad_revision| of the |context|.""" |
189 # Download the revlist and filter for just the range between good and bad. | 210 # Download the revlist and filter for just the range between good and bad. |
190 rev_range = range(context.good_revision, context.bad_revision) | 211 rev_range = range(context.good_revision, context.bad_revision) |
191 revlist = map(int, ParseDirectoryIndex(context)) | 212 revisions = [] |
| 213 if context.use_recent: |
| 214 revisions = ParseDirectoryIndexRecent(context) |
| 215 else: |
| 216 revisions = ParseDirectoryIndex(context) |
| 217 revlist = map(int, revisions) |
192 revlist = filter(lambda r: r in rev_range, revlist) | 218 revlist = filter(lambda r: r in rev_range, revlist) |
193 revlist.sort() | 219 revlist.sort() |
194 return revlist | 220 return revlist |
195 | 221 |
196 | 222 |
197 def TryRevision(context, rev, profile, args): | 223 def TryRevision(context, rev, profile, args): |
198 """Downloads revision |rev|, unzips it, and opens it for the user to test. | 224 """Downloads revision |rev|, unzips it, and opens it for the user to test. |
199 |profile| is the profile to use.""" | 225 |profile| is the profile to use.""" |
200 # Do this in a temp dir so we don't collide with user files. | 226 # Do this in a temp dir so we don't collide with user files. |
201 cwd = os.getcwd() | 227 cwd = os.getcwd() |
(...skipping 10 matching lines...) Expand all Loading... |
212 size = min(totalsize, size) | 238 size = min(totalsize, size) |
213 progress = "Received %d of %d bytes, %.2f%%" % ( | 239 progress = "Received %d of %d bytes, %.2f%%" % ( |
214 size, totalsize, 100.0 * size / totalsize) | 240 size, totalsize, 100.0 * size / totalsize) |
215 # Send a \r to let all progress messages use just one line of output. | 241 # Send a \r to let all progress messages use just one line of output. |
216 sys.stdout.write("\r" + progress) | 242 sys.stdout.write("\r" + progress) |
217 sys.stdout.flush() | 243 sys.stdout.flush() |
218 try: | 244 try: |
219 print 'Fetching ' + download_url | 245 print 'Fetching ' + download_url |
220 urllib.urlretrieve(download_url, context.archive_name, _ReportHook) | 246 urllib.urlretrieve(download_url, context.archive_name, _ReportHook) |
221 print | 247 print |
| 248 # Throw an exception if the download was less than 1000 bytes. |
| 249 if os.path.getsize(context.archive_name) < 1000: raise Exception() |
222 except Exception, e: | 250 except Exception, e: |
223 print('Could not retrieve the download. Sorry.') | 251 print('Could not retrieve the download. Sorry.') |
224 sys.exit(-1) | 252 sys.exit(-1) |
225 | 253 |
226 # Unzip the file. | 254 # Unzip the file. |
227 print 'Unzipping ...' | 255 print 'Unzipping ...' |
228 UnzipFilenameToDir(context.archive_name, os.curdir) | 256 UnzipFilenameToDir(context.archive_name, os.curdir) |
229 | 257 |
230 # Tell the system to open the app. | 258 # Tell the system to open the app. |
231 args = ['--user-data-dir=%s' % profile] + args | 259 args = ['--user-data-dir=%s' % profile] + args |
(...skipping 12 matching lines...) Expand all Loading... |
244 | 272 |
245 def AskIsGoodBuild(rev): | 273 def AskIsGoodBuild(rev): |
246 """Ask the user whether build |rev| is good or bad.""" | 274 """Ask the user whether build |rev| is good or bad.""" |
247 # Loop until we get a response that we can parse. | 275 # Loop until we get a response that we can parse. |
248 while True: | 276 while True: |
249 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) | 277 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) |
250 if response and response in ('g', 'b'): | 278 if response and response in ('g', 'b'): |
251 return response == 'g' | 279 return response == 'g' |
252 | 280 |
253 | 281 |
254 def Bisect(good, | 282 def Bisect(revlist, |
255 bad, | |
256 revlist, | |
257 context, | 283 context, |
258 try_args=(), | 284 try_args=(), |
259 profile='profile', | 285 profile='profile', |
260 predicate=AskIsGoodBuild): | 286 predicate=AskIsGoodBuild): |
261 """Tries to find the exact commit where a regression was introduced by | 287 """Tries to find the exact commit where a regression was introduced by |
262 running a binary search on all archived builds in a given revision range. | 288 running a binary search on all archived builds in a given revision range. |
263 | 289 |
264 @param good The index in revlist of the last known good revision. | |
265 @param bad The index in revlist of the first known bad revision. | |
266 @param revlist A list of chromium revision numbers to check. | 290 @param revlist A list of chromium revision numbers to check. |
267 @param context A PathContext object. | 291 @param context A PathContext object. |
268 @param try_args A tuple of arguments to pass to the predicate function. | 292 @param try_args A tuple of arguments to pass to the predicate function. |
269 @param profile The user profile with which to run chromium. | 293 @param profile The user profile with which to run chromium. |
270 @param predicate A predicate function which returns True iff the argument | 294 @param predicate A predicate function which returns True iff the argument |
271 chromium revision is good. | 295 chromium revision is good. |
272 """ | 296 """ |
273 | 297 |
| 298 good = 0 |
| 299 bad = len(revlist) - 1 |
274 last_known_good_rev = revlist[good] | 300 last_known_good_rev = revlist[good] |
275 first_known_bad_rev = revlist[bad] | 301 first_known_bad_rev = revlist[bad] |
276 | 302 |
277 # Binary search time! | 303 # Binary search time! |
278 while good < bad: | 304 while good < bad: |
279 candidates = revlist[good:bad] | 305 candidates = revlist[good:bad] |
280 num_poss = len(candidates) | 306 num_poss = len(candidates) |
281 if num_poss > 10: | 307 if num_poss > 10: |
282 print('%d candidates. %d tries left.' % | 308 print('%d candidates. %d tries left.' % |
283 (num_poss, round(math.log(num_poss, 2)))) | 309 (num_poss, round(math.log(num_poss, 2)))) |
284 else: | 310 else: |
285 print('Candidates: %s' % revlist[good:bad]) | 311 print('Candidates: %s' % revlist[good:bad]) |
286 | 312 |
287 # Cut the problem in half... | 313 # Cut the problem in half... |
288 test = int((bad - good) / 2) + good | 314 test = int((bad - good) / 2) + good |
289 test_rev = revlist[test] | 315 test_rev = revlist[test] |
290 | 316 |
291 # Let the user give this rev a spin (in her own profile, if she wants). | 317 # Let the user give this rev a spin (in her own profile, if she wants). |
292 TryRevision(context, test_rev, profile, try_args) | 318 TryRevision(context, test_rev, profile, try_args) |
293 if predicate(test_rev): | 319 if predicate(test_rev): |
294 last_known_good_rev = revlist[good] | 320 last_known_good_rev = test_rev |
295 good = test + 1 | 321 good = test + 1 |
296 else: | 322 else: |
297 bad = test | 323 bad = test |
298 | 324 |
299 return (last_known_good_rev, first_known_bad_rev) | 325 return (last_known_good_rev, first_known_bad_rev) |
300 | 326 |
301 | 327 |
302 def main(): | 328 def main(): |
303 usage = ('%prog [options] [-- chromium-options]\n' | 329 usage = ('%prog [options] [-- chromium-options]\n' |
304 'Perform binary search on the snapshot builds.\n' | 330 'Perform binary search on the snapshot builds.\n' |
305 '\n' | 331 '\n' |
306 'Tip: add "-- --no-first-run" to bypass the first run prompts.') | 332 'Tip: add "-- --no-first-run" to bypass the first run prompts.') |
307 parser = optparse.OptionParser(usage=usage) | 333 parser = optparse.OptionParser(usage=usage) |
308 # Strangely, the default help output doesn't include the choice list. | 334 # Strangely, the default help output doesn't include the choice list. |
309 choices = ['mac', 'win', 'linux', 'linux64'] | 335 choices = ['mac', 'win', 'linux', 'linux64'] |
310 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 | 336 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 |
311 parser.add_option('-a', '--archive', | 337 parser.add_option('-a', '--archive', |
312 choices = choices, | 338 choices = choices, |
313 help = 'The buildbot archive to bisect [%s].' % | 339 help = 'The buildbot archive to bisect [%s].' % |
314 '|'.join(choices)) | 340 '|'.join(choices)) |
315 parser.add_option('-b', '--bad', type = 'int', | 341 parser.add_option('-b', '--bad', type = 'int', |
316 help = 'The bad revision to bisect to.') | 342 help = 'The bad revision to bisect to.') |
317 parser.add_option('-g', '--good', type = 'int', | 343 parser.add_option('-g', '--good', type = 'int', |
318 help = 'The last known good revision to bisect from.') | 344 help = 'The last known good revision to bisect from.') |
319 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', | 345 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', |
320 help = 'Profile to use; this will not reset every run. ' + | 346 help = 'Profile to use; this will not reset every run. ' + |
321 'Defaults to a clean profile.', default = 'profile') | 347 'Defaults to a clean profile.', default = 'profile') |
| 348 parser.add_option('-r', '--recent', |
| 349 dest = "recent", |
| 350 default = False, |
| 351 action = "store_true", |
| 352 help = 'Use recent builds from about the last 2 months ' + |
| 353 'for higher granularity bisecting.') |
322 (opts, args) = parser.parse_args() | 354 (opts, args) = parser.parse_args() |
323 | 355 |
324 if opts.archive is None: | 356 if opts.archive is None: |
325 print 'Error: missing required parameter: --archive' | 357 print 'Error: missing required parameter: --archive' |
326 print | 358 print |
327 parser.print_help() | 359 parser.print_help() |
328 return 1 | 360 return 1 |
329 | 361 |
330 if opts.bad and opts.good and (opts.good > opts.bad): | 362 if opts.bad and opts.good and (opts.good > opts.bad): |
331 print ('The good revision (%d) must precede the bad revision (%d).\n' % | 363 print ('The good revision (%d) must precede the bad revision (%d).\n' % |
332 (opts.good, opts.bad)) | 364 (opts.good, opts.bad)) |
333 parser.print_help() | 365 parser.print_help() |
334 return 1 | 366 return 1 |
335 | 367 |
336 # Create the context. Initialize 0 for the revisions as they are set below. | 368 # Create the context. Initialize 0 for the revisions as they are set below. |
337 context = PathContext(opts.archive, 0, 0) | 369 context = PathContext(opts.archive, 0, 0, opts.recent) |
338 | 370 |
339 # Pick a starting point, try to get HEAD for this. | 371 # Pick a starting point, try to get HEAD for this. |
340 if opts.bad: | 372 if opts.bad: |
341 bad_rev = opts.bad | 373 bad_rev = opts.bad |
342 else: | 374 else: |
343 bad_rev = 0 | 375 bad_rev = 0 |
344 try: | 376 try: |
345 # Location of the latest build revision number | 377 # Location of the latest build revision number |
346 nh = urllib.urlopen(context.GetLastChangeURL()) | 378 nh = urllib.urlopen(context.GetLastChangeURL()) |
347 latest = int(nh.read()) | 379 latest = int(nh.read()) |
(...skipping 19 matching lines...) Expand all Loading... |
367 # Set the input parameters now that they've been validated. | 399 # Set the input parameters now that they've been validated. |
368 context.good_revision = good_rev | 400 context.good_revision = good_rev |
369 context.bad_revision = bad_rev | 401 context.bad_revision = bad_rev |
370 | 402 |
371 # Get a list of revisions to bisect across. | 403 # Get a list of revisions to bisect across. |
372 revlist = GetRevList(context) | 404 revlist = GetRevList(context) |
373 if len(revlist) < 2: # Don't have enough builds to bisect | 405 if len(revlist) < 2: # Don't have enough builds to bisect |
374 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist | 406 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist |
375 sys.exit(1) | 407 sys.exit(1) |
376 | 408 |
377 # If we don't have a |good_rev|, set it to be the first revision possible. | |
378 if good_rev == 0: | |
379 good_rev = revlist[0] | |
380 | |
381 # These are indexes of |revlist|. | |
382 good = 0 | |
383 bad = len(revlist) - 1 | |
384 | |
385 (last_known_good_rev, first_known_bad_rev) = Bisect( | 409 (last_known_good_rev, first_known_bad_rev) = Bisect( |
386 good, bad, revlist, context, args, opts.profile) | 410 revlist, context, args, opts.profile) |
387 | 411 |
388 # We're done. Let the user know the results in an official manner. | 412 # We're done. Let the user know the results in an official manner. |
389 print('You are probably looking for build %d.' % first_known_bad_rev) | 413 print('You are probably looking for build %d.' % first_known_bad_rev) |
390 print('CHANGELOG URL:') | 414 print('CHANGELOG URL:') |
391 print(CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev)) | 415 print(CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev)) |
392 print('Built at revision:') | 416 print('Built at revision:') |
393 print(BUILD_VIEWVC_URL % first_known_bad_rev) | 417 print(BUILD_VIEWVC_URL % first_known_bad_rev) |
394 | 418 |
395 if __name__ == '__main__': | 419 if __name__ == '__main__': |
396 sys.exit(main()) | 420 sys.exit(main()) |
OLD | NEW |