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
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
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
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
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
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
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())
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