Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(4)

Side by Side Diff: tools/bisect-builds.py

Issue 7461012: Update bisect-builds.py to support bisecting at better granularity (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: . Created 9 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
26 # For BASE_URL_RECENT, the string prefix before each revision number.
27 RECENT_REV_HTML_PREFIX = \
28 '<img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="'
29
25 ############################################################################### 30 ###############################################################################
26 31
27 import math 32 import math
28 import optparse 33 import optparse
29 import os 34 import os
30 import pipes 35 import pipes
31 import re 36 import re
32 import shutil 37 import shutil
33 import sys 38 import sys
34 import tempfile 39 import tempfile
35 import urllib 40 import urllib
36 from xml.etree import ElementTree 41 from xml.etree import ElementTree
37 import zipfile 42 import zipfile
38 43
39 class PathContext(object): 44 class PathContext(object):
40 """A PathContext is used to carry the information used to construct URLs and 45 """A PathContext is used to carry the information used to construct URLs and
41 paths when dealing with the storage server and archives.""" 46 paths when dealing with the storage server and archives."""
42 def __init__(self, platform, good_revision, bad_revision): 47 def __init__(self, platform, good_revision, bad_revision, is_recent):
Robert Sesek 2011/07/20 19:05:11 |is_recent| sounds like it's describing the contex
jbates 2011/07/20 21:08:42 Done.
43 super(PathContext, self).__init__() 48 super(PathContext, self).__init__()
44 # Store off the input parameters. 49 # Store off the input parameters.
45 self.platform = platform # What's passed in to the '-a/--archive' option. 50 self.platform = platform # What's passed in to the '-a/--archive' option.
46 self.good_revision = good_revision 51 self.good_revision = good_revision
47 self.bad_revision = bad_revision 52 self.bad_revision = bad_revision
53 self.is_recent = is_recent
48 54
49 # The name of the ZIP file in a revision directory on the server. 55 # The name of the ZIP file in a revision directory on the server.
50 self.archive_name = None 56 self.archive_name = None
51 57
52 # Set some internal members: 58 # Set some internal members:
53 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. 59 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
54 # _archive_extract_dir = Uncompressed directory in the archive_name file. 60 # _archive_extract_dir = Uncompressed directory in the archive_name file.
55 # _binary_name = The name of the executable to run. 61 # _binary_name = The name of the executable to run.
56 if self.platform == 'linux' or self.platform == 'linux64': 62 if self.platform == 'linux' or self.platform == 'linux64':
57 self._listing_platform_dir = 'Linux/' 63 self._listing_platform_dir = 'Linux/'
(...skipping 17 matching lines...) Expand all
75 raise Exception("Invalid platform") 81 raise Exception("Invalid platform")
76 82
77 def GetListingURL(self, marker=None): 83 def GetListingURL(self, marker=None):
78 """Returns the URL for a directory listing, with an optional marker.""" 84 """Returns the URL for a directory listing, with an optional marker."""
79 marker_param = '' 85 marker_param = ''
80 if marker: 86 if marker:
81 marker_param = '&marker=' + str(marker) 87 marker_param = '&marker=' + str(marker)
82 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ 88 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \
83 marker_param 89 marker_param
84 90
91 def GetListingURLRecent(self):
92 """Returns the URL for a directory listing of recent builds."""
93 return BASE_URL_RECENT + '/' + self._listing_platform_dir
94
85 def GetDownloadURL(self, revision): 95 def GetDownloadURL(self, revision):
86 """Gets the download URL for a build archive of a specific revision.""" 96 """Gets the download URL for a build archive of a specific revision."""
87 return "%s/%s%d/%s" % ( 97 if self.is_recent:
88 BASE_URL, self._listing_platform_dir, revision, self.archive_name) 98 return "%s/%s%d/%s" % (
99 BASE_URL_RECENT, self._listing_platform_dir, revision,
100 self.archive_name)
101 else:
102 return "%s/%s%d/%s" % (
103 BASE_URL, self._listing_platform_dir, revision, self.archive_name)
89 104
90 def GetLastChangeURL(self): 105 def GetLastChangeURL(self):
91 """Returns a URL to the LAST_CHANGE file.""" 106 """Returns a URL to the LAST_CHANGE file."""
92 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' 107 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE'
93 108
94 def GetLaunchPath(self): 109 def GetLaunchPath(self):
95 """Returns a relative path (presumably from the archive extraction location) 110 """Returns a relative path (presumably from the archive extraction location)
96 that is used to run the executable.""" 111 that is used to run the executable."""
97 return os.path.join(self._archive_extract_dir, self._binary_name) 112 return os.path.join(self._archive_extract_dir, self._binary_name)
98 113
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after
175 (revisions, next_marker) = _FetchAndParse(context.GetListingURL()) 190 (revisions, next_marker) = _FetchAndParse(context.GetListingURL())
176 # If the result list was truncated, refetch with the next marker. Do this 191 # If the result list was truncated, refetch with the next marker. Do this
177 # until an entire directory listing is done. 192 # until an entire directory listing is done.
178 while next_marker: 193 while next_marker:
179 (new_revisions, next_marker) = _FetchAndParse( 194 (new_revisions, next_marker) = _FetchAndParse(
180 context.GetListingURL(next_marker)) 195 context.GetListingURL(next_marker))
181 revisions.extend(new_revisions) 196 revisions.extend(new_revisions)
182 197
183 return revisions 198 return revisions
184 199
200 def ParseDirectoryIndexRecent(context):
201 """Parses the recent builds directory listing into a list of revision
202 numbers."""
203 revisions = []
204 handle = urllib.urlopen(context.GetListingURLRecent())
205 document = handle.read()
206
207 index = document.find(RECENT_REV_HTML_PREFIX)
Robert Sesek 2011/07/20 19:05:11 Why not use a regex to do this? Use re.compile for
jbates 2011/07/20 21:08:42 Done.
208 while index > 0:
209 index += len(RECENT_REV_HTML_PREFIX)
210 end_slash_index = document.find('/', index)
211 rev = int(document[index: end_slash_index])
212 revisions.append(rev)
213 index = document.find(RECENT_REV_HTML_PREFIX, index)
214
215 return revisions
216
185 217
186 def GetRevList(context): 218 def GetRevList(context):
187 """Gets the list of revision numbers between |good_revision| and 219 """Gets the list of revision numbers between |good_revision| and
188 |bad_revision| of the |context|.""" 220 |bad_revision| of the |context|."""
189 # Download the revlist and filter for just the range between good and bad. 221 # Download the revlist and filter for just the range between good and bad.
190 rev_range = range(context.good_revision, context.bad_revision) 222 rev_range = range(context.good_revision, context.bad_revision)
191 revlist = map(int, ParseDirectoryIndex(context)) 223 revisions = []
224 if context.is_recent:
225 revisions = ParseDirectoryIndexRecent(context)
226 else:
227 revisions = ParseDirectoryIndex(context)
228 revlist = map(int, revisions)
192 revlist = filter(lambda r: r in rev_range, revlist) 229 revlist = filter(lambda r: r in rev_range, revlist)
193 revlist.sort() 230 revlist.sort()
194 return revlist 231 return revlist
195 232
196 233
197 def TryRevision(context, rev, profile, args): 234 def TryRevision(context, rev, profile, args):
198 """Downloads revision |rev|, unzips it, and opens it for the user to test. 235 """Downloads revision |rev|, unzips it, and opens it for the user to test.
199 |profile| is the profile to use.""" 236 |profile| is the profile to use."""
200 # Do this in a temp dir so we don't collide with user files. 237 # Do this in a temp dir so we don't collide with user files.
201 cwd = os.getcwd() 238 cwd = os.getcwd()
(...skipping 10 matching lines...) Expand all
212 size = min(totalsize, size) 249 size = min(totalsize, size)
213 progress = "Received %d of %d bytes, %.2f%%" % ( 250 progress = "Received %d of %d bytes, %.2f%%" % (
214 size, totalsize, 100.0 * size / totalsize) 251 size, totalsize, 100.0 * size / totalsize)
215 # Send a \r to let all progress messages use just one line of output. 252 # Send a \r to let all progress messages use just one line of output.
216 sys.stdout.write("\r" + progress) 253 sys.stdout.write("\r" + progress)
217 sys.stdout.flush() 254 sys.stdout.flush()
218 try: 255 try:
219 print 'Fetching ' + download_url 256 print 'Fetching ' + download_url
220 urllib.urlretrieve(download_url, context.archive_name, _ReportHook) 257 urllib.urlretrieve(download_url, context.archive_name, _ReportHook)
221 print 258 print
259 # Throw an exception if the download was less than 1000 bytes.
260 if os.path.getsize(context.archive_name) < 1000: raise Exception()
222 except Exception, e: 261 except Exception, e:
223 print('Could not retrieve the download. Sorry.') 262 print('Could not retrieve the download. Sorry.')
224 sys.exit(-1) 263 sys.exit(-1)
225 264
226 # Unzip the file. 265 # Unzip the file.
227 print 'Unzipping ...' 266 print 'Unzipping ...'
228 UnzipFilenameToDir(context.archive_name, os.curdir) 267 UnzipFilenameToDir(context.archive_name, os.curdir)
229 268
230 # Tell the system to open the app. 269 # Tell the system to open the app.
231 args = ['--user-data-dir=%s' % profile] + args 270 args = ['--user-data-dir=%s' % profile] + args
(...skipping 12 matching lines...) Expand all
244 283
245 def AskIsGoodBuild(rev): 284 def AskIsGoodBuild(rev):
246 """Ask the user whether build |rev| is good or bad.""" 285 """Ask the user whether build |rev| is good or bad."""
247 # Loop until we get a response that we can parse. 286 # Loop until we get a response that we can parse.
248 while True: 287 while True:
249 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) 288 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev))
250 if response and response in ('g', 'b'): 289 if response and response in ('g', 'b'):
251 return response == 'g' 290 return response == 'g'
252 291
253 292
254 def Bisect(good, 293 def Bisect(revlist,
255 bad,
256 revlist,
257 context, 294 context,
258 try_args=(), 295 try_args=(),
259 profile='profile', 296 profile='profile',
260 predicate=AskIsGoodBuild): 297 predicate=AskIsGoodBuild):
261 """Tries to find the exact commit where a regression was introduced by 298 """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. 299 running a binary search on all archived builds in a given revision range.
263 300
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. 301 @param revlist A list of chromium revision numbers to check.
267 @param context A PathContext object. 302 @param context A PathContext object.
268 @param try_args A tuple of arguments to pass to the predicate function. 303 @param try_args A tuple of arguments to pass to the predicate function.
269 @param profile The user profile with which to run chromium. 304 @param profile The user profile with which to run chromium.
270 @param predicate A predicate function which returns True iff the argument 305 @param predicate A predicate function which returns True iff the argument
271 chromium revision is good. 306 chromium revision is good.
272 """ 307 """
273 308
309 good = 0
310 bad = len(revlist) - 1
jbates 2011/07/20 00:47:17 Cleanup
274 last_known_good_rev = revlist[good] 311 last_known_good_rev = revlist[good]
275 first_known_bad_rev = revlist[bad] 312 first_known_bad_rev = revlist[bad]
276 313
277 # Binary search time! 314 # Binary search time!
278 while good < bad: 315 while good < bad:
279 candidates = revlist[good:bad] 316 candidates = revlist[good:bad]
280 num_poss = len(candidates) 317 num_poss = len(candidates)
281 if num_poss > 10: 318 if num_poss > 10:
282 print('%d candidates. %d tries left.' % 319 print('%d candidates. %d tries left.' %
283 (num_poss, round(math.log(num_poss, 2)))) 320 (num_poss, round(math.log(num_poss, 2))))
284 else: 321 else:
285 print('Candidates: %s' % revlist[good:bad]) 322 print('Candidates: %s' % revlist[good:bad])
286 323
287 # Cut the problem in half... 324 # Cut the problem in half...
288 test = int((bad - good) / 2) + good 325 test = int((bad - good) / 2) + good
289 test_rev = revlist[test] 326 test_rev = revlist[test]
290 327
291 # Let the user give this rev a spin (in her own profile, if she wants). 328 # Let the user give this rev a spin (in her own profile, if she wants).
292 TryRevision(context, test_rev, profile, try_args) 329 TryRevision(context, test_rev, profile, try_args)
293 if predicate(test_rev): 330 if predicate(test_rev):
294 last_known_good_rev = revlist[good] 331 last_known_good_rev = test_rev
jbates 2011/07/20 00:47:17 This fixes a bug that sometimes caused the reporte
Robert Sesek 2011/07/20 19:05:11 Looks like this fixes http://code.google.com/p/chr
jbates 2011/07/20 21:08:42 Done.
295 good = test + 1 332 good = test + 1
296 else: 333 else:
297 bad = test 334 bad = test
298 335
299 return (last_known_good_rev, first_known_bad_rev) 336 return (last_known_good_rev, first_known_bad_rev)
300 337
301 338
302 def main(): 339 def main():
303 usage = ('%prog [options] [-- chromium-options]\n' 340 usage = ('%prog [options] [-- chromium-options]\n'
304 'Perform binary search on the snapshot builds.\n' 341 'Perform binary search on the snapshot builds.\n'
305 '\n' 342 '\n'
306 'Tip: add "-- --no-first-run" to bypass the first run prompts.') 343 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
307 parser = optparse.OptionParser(usage=usage) 344 parser = optparse.OptionParser(usage=usage)
308 # Strangely, the default help output doesn't include the choice list. 345 # Strangely, the default help output doesn't include the choice list.
309 choices = ['mac', 'win', 'linux', 'linux64'] 346 choices = ['mac', 'win', 'linux', 'linux64']
310 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 347 # linux-chromiumos lacks a continuous archive http://crbug.com/78158
311 parser.add_option('-a', '--archive', 348 parser.add_option('-a', '--archive',
312 choices = choices, 349 choices = choices,
313 help = 'The buildbot archive to bisect [%s].' % 350 help = 'The buildbot archive to bisect [%s].' %
314 '|'.join(choices)) 351 '|'.join(choices))
315 parser.add_option('-b', '--bad', type = 'int', 352 parser.add_option('-b', '--bad', type = 'int',
316 help = 'The bad revision to bisect to.') 353 help = 'The bad revision to bisect to.')
317 parser.add_option('-g', '--good', type = 'int', 354 parser.add_option('-g', '--good', type = 'int',
318 help = 'The last known good revision to bisect from.') 355 help = 'The last known good revision to bisect from.')
319 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', 356 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str',
320 help = 'Profile to use; this will not reset every run. ' + 357 help = 'Profile to use; this will not reset every run. ' +
321 'Defaults to a clean profile.', default = 'profile') 358 'Defaults to a clean profile.', default = 'profile')
359 parser.add_option('-r', '--recent',
360 dest = "recent",
361 default = False,
362 action = "store_true",
363 help = 'Use recent builds from about the last 2 months ' +
364 'for higher granularity bisecting.')
365 parser.add_option('-h', '--help',
Robert Sesek 2011/07/20 19:05:11 I think this is done for you automatically by the
jbates 2011/07/20 21:08:42 Done.
366 dest = "help",
367 default = False,
368 action = "store_true",
369 help = 'Show usage.')
322 (opts, args) = parser.parse_args() 370 (opts, args) = parser.parse_args()
323 371
372 if opts.help:
373 parser.print_help()
374 return 0
375
324 if opts.archive is None: 376 if opts.archive is None:
325 print 'Error: missing required parameter: --archive' 377 print 'Error: missing required parameter: --archive'
326 print 378 print
327 parser.print_help() 379 parser.print_help()
328 return 1 380 return 1
329 381
330 if opts.bad and opts.good and (opts.good > opts.bad): 382 if opts.bad and opts.good and (opts.good > opts.bad):
331 print ('The good revision (%d) must precede the bad revision (%d).\n' % 383 print ('The good revision (%d) must precede the bad revision (%d).\n' %
332 (opts.good, opts.bad)) 384 (opts.good, opts.bad))
333 parser.print_help() 385 parser.print_help()
334 return 1 386 return 1
335 387
336 # Create the context. Initialize 0 for the revisions as they are set below. 388 # Create the context. Initialize 0 for the revisions as they are set below.
337 context = PathContext(opts.archive, 0, 0) 389 context = PathContext(opts.archive, 0, 0, False)
338 390
339 # Pick a starting point, try to get HEAD for this. 391 # Pick a starting point, try to get HEAD for this.
340 if opts.bad: 392 if opts.bad:
341 bad_rev = opts.bad 393 bad_rev = opts.bad
342 else: 394 else:
343 bad_rev = 0 395 bad_rev = 0
344 try: 396 try:
345 # Location of the latest build revision number 397 # Location of the latest build revision number
346 nh = urllib.urlopen(context.GetLastChangeURL()) 398 nh = urllib.urlopen(context.GetLastChangeURL())
347 latest = int(nh.read()) 399 latest = int(nh.read())
(...skipping 12 matching lines...) Expand all
360 else: 412 else:
361 good_rev = 0 413 good_rev = 0
362 try: 414 try:
363 good_rev = int(raw_input('Last known good [0]: ')) 415 good_rev = int(raw_input('Last known good [0]: '))
364 except Exception, e: 416 except Exception, e:
365 pass 417 pass
366 418
367 # Set the input parameters now that they've been validated. 419 # Set the input parameters now that they've been validated.
368 context.good_revision = good_rev 420 context.good_revision = good_rev
369 context.bad_revision = bad_rev 421 context.bad_revision = bad_rev
422 context.is_recent = opts.recent
Robert Sesek 2011/07/20 19:05:11 You're not validating this, so why not pass it dir
jbates 2011/07/20 21:08:42 Done.
370 423
371 # Get a list of revisions to bisect across. 424 # Get a list of revisions to bisect across.
372 revlist = GetRevList(context) 425 revlist = GetRevList(context)
373 if len(revlist) < 2: # Don't have enough builds to bisect 426 if len(revlist) < 2: # Don't have enough builds to bisect
374 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist 427 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist
375 sys.exit(1) 428 sys.exit(1)
376 429
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]
jbates 2011/07/20 00:47:17 good_rev is not used anymore, so this was unnecess
380
381 # These are indexes of |revlist|.
382 good = 0
383 bad = len(revlist) - 1
jbates 2011/07/20 00:47:17 Moved to inside Bisect -- it already is passed the
384
385 (last_known_good_rev, first_known_bad_rev) = Bisect( 430 (last_known_good_rev, first_known_bad_rev) = Bisect(
386 good, bad, revlist, context, args, opts.profile) 431 revlist, context, args, opts.profile)
387 432
388 # We're done. Let the user know the results in an official manner. 433 # 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) 434 print('You are probably looking for build %d.' % first_known_bad_rev)
390 print('CHANGELOG URL:') 435 print('CHANGELOG URL:')
391 print(CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev)) 436 print(CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev))
392 print('Built at revision:') 437 print('Built at revision:')
393 print(BUILD_VIEWVC_URL % first_known_bad_rev) 438 print(BUILD_VIEWVC_URL % first_known_bad_rev)
394 439
395 if __name__ == '__main__': 440 if __name__ == '__main__':
396 sys.exit(main()) 441 sys.exit(main())
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698