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

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 75 matching lines...) Expand 10 before | Expand all | Expand 10 after
174 # Fetch the first list of revisions. 185 # Fetch the first list of revisions.
175 (revisions, next_marker) = _FetchAndParse(context.GetListingURL()) 186 (revisions, next_marker) = _FetchAndParse(context.GetListingURL())
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
Robert Sesek 2011/07/20 21:29:17 nit: two blank lines between top-level definitions
jbates 2011/07/20 21:32:10 Done.
196 def ParseDirectoryIndexRecent(context):
197 """Parses the recent builds directory listing into a list of revision
198 numbers."""
199 handle = urllib.urlopen(context.GetListingURLRecent())
200 document = handle.read()
201
202 # Looking for: <a href="92976/">92976/</a>
203 return re.findall(r"<a href=\"(\d+)/\">\1/</a>", document)
204
185 205
186 def GetRevList(context): 206 def GetRevList(context):
187 """Gets the list of revision numbers between |good_revision| and 207 """Gets the list of revision numbers between |good_revision| and
188 |bad_revision| of the |context|.""" 208 |bad_revision| of the |context|."""
189 # Download the revlist and filter for just the range between good and bad. 209 # Download the revlist and filter for just the range between good and bad.
190 rev_range = range(context.good_revision, context.bad_revision) 210 rev_range = range(context.good_revision, context.bad_revision)
191 revlist = map(int, ParseDirectoryIndex(context)) 211 revisions = []
212 if context.use_recent:
213 revisions = ParseDirectoryIndexRecent(context)
214 else:
215 revisions = ParseDirectoryIndex(context)
216 revlist = map(int, revisions)
192 revlist = filter(lambda r: r in rev_range, revlist) 217 revlist = filter(lambda r: r in rev_range, revlist)
193 revlist.sort() 218 revlist.sort()
194 return revlist 219 return revlist
195 220
196 221
197 def TryRevision(context, rev, profile, args): 222 def TryRevision(context, rev, profile, args):
198 """Downloads revision |rev|, unzips it, and opens it for the user to test. 223 """Downloads revision |rev|, unzips it, and opens it for the user to test.
199 |profile| is the profile to use.""" 224 |profile| is the profile to use."""
200 # Do this in a temp dir so we don't collide with user files. 225 # Do this in a temp dir so we don't collide with user files.
201 cwd = os.getcwd() 226 cwd = os.getcwd()
(...skipping 10 matching lines...) Expand all
212 size = min(totalsize, size) 237 size = min(totalsize, size)
213 progress = "Received %d of %d bytes, %.2f%%" % ( 238 progress = "Received %d of %d bytes, %.2f%%" % (
214 size, totalsize, 100.0 * size / totalsize) 239 size, totalsize, 100.0 * size / totalsize)
215 # Send a \r to let all progress messages use just one line of output. 240 # Send a \r to let all progress messages use just one line of output.
216 sys.stdout.write("\r" + progress) 241 sys.stdout.write("\r" + progress)
217 sys.stdout.flush() 242 sys.stdout.flush()
218 try: 243 try:
219 print 'Fetching ' + download_url 244 print 'Fetching ' + download_url
220 urllib.urlretrieve(download_url, context.archive_name, _ReportHook) 245 urllib.urlretrieve(download_url, context.archive_name, _ReportHook)
221 print 246 print
247 # Throw an exception if the download was less than 1000 bytes.
248 if os.path.getsize(context.archive_name) < 1000: raise Exception()
222 except Exception, e: 249 except Exception, e:
223 print('Could not retrieve the download. Sorry.') 250 print('Could not retrieve the download. Sorry.')
224 sys.exit(-1) 251 sys.exit(-1)
225 252
226 # Unzip the file. 253 # Unzip the file.
227 print 'Unzipping ...' 254 print 'Unzipping ...'
228 UnzipFilenameToDir(context.archive_name, os.curdir) 255 UnzipFilenameToDir(context.archive_name, os.curdir)
229 256
230 # Tell the system to open the app. 257 # Tell the system to open the app.
231 args = ['--user-data-dir=%s' % profile] + args 258 args = ['--user-data-dir=%s' % profile] + args
(...skipping 12 matching lines...) Expand all
244 271
245 def AskIsGoodBuild(rev): 272 def AskIsGoodBuild(rev):
246 """Ask the user whether build |rev| is good or bad.""" 273 """Ask the user whether build |rev| is good or bad."""
247 # Loop until we get a response that we can parse. 274 # Loop until we get a response that we can parse.
248 while True: 275 while True:
249 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) 276 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev))
250 if response and response in ('g', 'b'): 277 if response and response in ('g', 'b'):
251 return response == 'g' 278 return response == 'g'
252 279
253 280
254 def Bisect(good, 281 def Bisect(revlist,
255 bad,
256 revlist,
257 context, 282 context,
258 try_args=(), 283 try_args=(),
259 profile='profile', 284 profile='profile',
260 predicate=AskIsGoodBuild): 285 predicate=AskIsGoodBuild):
261 """Tries to find the exact commit where a regression was introduced by 286 """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. 287 running a binary search on all archived builds in a given revision range.
263 288
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. 289 @param revlist A list of chromium revision numbers to check.
267 @param context A PathContext object. 290 @param context A PathContext object.
268 @param try_args A tuple of arguments to pass to the predicate function. 291 @param try_args A tuple of arguments to pass to the predicate function.
269 @param profile The user profile with which to run chromium. 292 @param profile The user profile with which to run chromium.
270 @param predicate A predicate function which returns True iff the argument 293 @param predicate A predicate function which returns True iff the argument
271 chromium revision is good. 294 chromium revision is good.
272 """ 295 """
273 296
297 good = 0
298 bad = len(revlist) - 1
274 last_known_good_rev = revlist[good] 299 last_known_good_rev = revlist[good]
275 first_known_bad_rev = revlist[bad] 300 first_known_bad_rev = revlist[bad]
276 301
277 # Binary search time! 302 # Binary search time!
278 while good < bad: 303 while good < bad:
279 candidates = revlist[good:bad] 304 candidates = revlist[good:bad]
280 num_poss = len(candidates) 305 num_poss = len(candidates)
281 if num_poss > 10: 306 if num_poss > 10:
282 print('%d candidates. %d tries left.' % 307 print('%d candidates. %d tries left.' %
283 (num_poss, round(math.log(num_poss, 2)))) 308 (num_poss, round(math.log(num_poss, 2))))
284 else: 309 else:
285 print('Candidates: %s' % revlist[good:bad]) 310 print('Candidates: %s' % revlist[good:bad])
286 311
287 # Cut the problem in half... 312 # Cut the problem in half...
288 test = int((bad - good) / 2) + good 313 test = int((bad - good) / 2) + good
289 test_rev = revlist[test] 314 test_rev = revlist[test]
290 315
291 # Let the user give this rev a spin (in her own profile, if she wants). 316 # Let the user give this rev a spin (in her own profile, if she wants).
292 TryRevision(context, test_rev, profile, try_args) 317 TryRevision(context, test_rev, profile, try_args)
293 if predicate(test_rev): 318 if predicate(test_rev):
294 last_known_good_rev = revlist[good] 319 last_known_good_rev = test_rev
295 good = test + 1 320 good = test + 1
296 else: 321 else:
297 bad = test 322 bad = test
298 323
299 return (last_known_good_rev, first_known_bad_rev) 324 return (last_known_good_rev, first_known_bad_rev)
300 325
301 326
302 def main(): 327 def main():
303 usage = ('%prog [options] [-- chromium-options]\n' 328 usage = ('%prog [options] [-- chromium-options]\n'
304 'Perform binary search on the snapshot builds.\n' 329 'Perform binary search on the snapshot builds.\n'
305 '\n' 330 '\n'
306 'Tip: add "-- --no-first-run" to bypass the first run prompts.') 331 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
307 parser = optparse.OptionParser(usage=usage) 332 parser = optparse.OptionParser(usage=usage)
308 # Strangely, the default help output doesn't include the choice list. 333 # Strangely, the default help output doesn't include the choice list.
309 choices = ['mac', 'win', 'linux', 'linux64'] 334 choices = ['mac', 'win', 'linux', 'linux64']
310 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 335 # linux-chromiumos lacks a continuous archive http://crbug.com/78158
311 parser.add_option('-a', '--archive', 336 parser.add_option('-a', '--archive',
312 choices = choices, 337 choices = choices,
313 help = 'The buildbot archive to bisect [%s].' % 338 help = 'The buildbot archive to bisect [%s].' %
314 '|'.join(choices)) 339 '|'.join(choices))
315 parser.add_option('-b', '--bad', type = 'int', 340 parser.add_option('-b', '--bad', type = 'int',
316 help = 'The bad revision to bisect to.') 341 help = 'The bad revision to bisect to.')
317 parser.add_option('-g', '--good', type = 'int', 342 parser.add_option('-g', '--good', type = 'int',
318 help = 'The last known good revision to bisect from.') 343 help = 'The last known good revision to bisect from.')
319 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', 344 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str',
320 help = 'Profile to use; this will not reset every run. ' + 345 help = 'Profile to use; this will not reset every run. ' +
321 'Defaults to a clean profile.', default = 'profile') 346 'Defaults to a clean profile.', default = 'profile')
347 parser.add_option('-r', '--recent',
348 dest = "recent",
349 default = False,
350 action = "store_true",
351 help = 'Use recent builds from about the last 2 months ' +
352 'for higher granularity bisecting.')
322 (opts, args) = parser.parse_args() 353 (opts, args) = parser.parse_args()
323 354
324 if opts.archive is None: 355 if opts.archive is None:
325 print 'Error: missing required parameter: --archive' 356 print 'Error: missing required parameter: --archive'
326 print 357 print
327 parser.print_help() 358 parser.print_help()
328 return 1 359 return 1
329 360
330 if opts.bad and opts.good and (opts.good > opts.bad): 361 if opts.bad and opts.good and (opts.good > opts.bad):
331 print ('The good revision (%d) must precede the bad revision (%d).\n' % 362 print ('The good revision (%d) must precede the bad revision (%d).\n' %
332 (opts.good, opts.bad)) 363 (opts.good, opts.bad))
333 parser.print_help() 364 parser.print_help()
334 return 1 365 return 1
335 366
336 # Create the context. Initialize 0 for the revisions as they are set below. 367 # Create the context. Initialize 0 for the revisions as they are set below.
337 context = PathContext(opts.archive, 0, 0) 368 context = PathContext(opts.archive, 0, 0, opts.recent)
338 369
339 # Pick a starting point, try to get HEAD for this. 370 # Pick a starting point, try to get HEAD for this.
340 if opts.bad: 371 if opts.bad:
341 bad_rev = opts.bad 372 bad_rev = opts.bad
342 else: 373 else:
343 bad_rev = 0 374 bad_rev = 0
344 try: 375 try:
345 # Location of the latest build revision number 376 # Location of the latest build revision number
346 nh = urllib.urlopen(context.GetLastChangeURL()) 377 nh = urllib.urlopen(context.GetLastChangeURL())
347 latest = int(nh.read()) 378 latest = int(nh.read())
(...skipping 19 matching lines...) Expand all
367 # Set the input parameters now that they've been validated. 398 # Set the input parameters now that they've been validated.
368 context.good_revision = good_rev 399 context.good_revision = good_rev
369 context.bad_revision = bad_rev 400 context.bad_revision = bad_rev
370 401
371 # Get a list of revisions to bisect across. 402 # Get a list of revisions to bisect across.
372 revlist = GetRevList(context) 403 revlist = GetRevList(context)
373 if len(revlist) < 2: # Don't have enough builds to bisect 404 if len(revlist) < 2: # Don't have enough builds to bisect
374 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist 405 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist
375 sys.exit(1) 406 sys.exit(1)
376 407
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( 408 (last_known_good_rev, first_known_bad_rev) = Bisect(
386 good, bad, revlist, context, args, opts.profile) 409 revlist, context, args, opts.profile)
387 410
388 # We're done. Let the user know the results in an official manner. 411 # 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) 412 print('You are probably looking for build %d.' % first_known_bad_rev)
390 print('CHANGELOG URL:') 413 print('CHANGELOG URL:')
391 print(CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev)) 414 print(CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev))
392 print('Built at revision:') 415 print('Built at revision:')
393 print(BUILD_VIEWVC_URL % first_known_bad_rev) 416 print(BUILD_VIEWVC_URL % first_known_bad_rev)
394 417
395 if __name__ == '__main__': 418 if __name__ == '__main__':
396 sys.exit(main()) 419 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