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

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

Issue 11266025: Make tools/bisect_builds.py handle reversed bad/good revision ranges (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 8 years, 1 month 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/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,
(...skipping 14 matching lines...) Expand all
25 # Official Changelogs URL. 25 # Official Changelogs URL.
26 OFFICIAL_CHANGELOG_URL = 'http://omahaproxy.appspot.com/'\ 26 OFFICIAL_CHANGELOG_URL = 'http://omahaproxy.appspot.com/'\
27 'changelog?old_version=%s&new_version=%s' 27 'changelog?old_version=%s&new_version=%s'
28 28
29 # DEPS file URL. 29 # DEPS file URL.
30 DEPS_FILE= 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d' 30 DEPS_FILE= 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d'
31 # WebKit Changelogs URL. 31 # WebKit Changelogs URL.
32 WEBKIT_CHANGELOG_URL = 'http://trac.webkit.org/log/' \ 32 WEBKIT_CHANGELOG_URL = 'http://trac.webkit.org/log/' \
33 'trunk/?rev=%d&stop_rev=%d&verbose=on&limit=10000' 33 'trunk/?rev=%d&stop_rev=%d&verbose=on&limit=10000'
34 34
35 DONE_MESSAGE = 'You are probably looking for a change made after ' \ 35 DONE_MESSAGE_GOOD_MIN = 'You are probably looking for a change made after %s ' \
36 '%s (known good), but no later than %s (first known bad).' 36 '(known good), but no later than %s (first known bad).'
37 DONE_MESSAGE_GOOD_MAX = 'You are probably looking for a change made after %s ' \
38 '(known bad), but no later than %s (first known good).'
37 39
38 ############################################################################### 40 ###############################################################################
39 41
40 import math 42 import math
41 import optparse 43 import optparse
42 import os 44 import os
43 import pipes 45 import pipes
44 import re 46 import re
45 import shutil 47 import shutil
46 import subprocess 48 import subprocess
(...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after
133 """Returns a URL to the LAST_CHANGE file.""" 135 """Returns a URL to the LAST_CHANGE file."""
134 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' 136 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE'
135 137
136 def GetLaunchPath(self): 138 def GetLaunchPath(self):
137 """Returns a relative path (presumably from the archive extraction location) 139 """Returns a relative path (presumably from the archive extraction location)
138 that is used to run the executable.""" 140 that is used to run the executable."""
139 return os.path.join(self._archive_extract_dir, self._binary_name) 141 return os.path.join(self._archive_extract_dir, self._binary_name)
140 142
141 def ParseDirectoryIndex(self): 143 def ParseDirectoryIndex(self):
142 """Parses the Google Storage directory listing into a list of revision 144 """Parses the Google Storage directory listing into a list of revision
143 numbers. The range starts with self.good_revision and goes until 145 numbers. The range starts with self.good_revision and goes until
Nico 2012/11/01 19:06:28 It looks like the 2nd sentence in the comment is n
enne (OOO) 2012/11/02 22:26:57 Done.
144 self.bad_revision.""" 146 self.bad_revision."""
145 147
146 def _FetchAndParse(url): 148 def _FetchAndParse(url):
147 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If 149 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If
148 next-marker is not None, then the listing is a partial listing and another 150 next-marker is not None, then the listing is a partial listing and another
149 fetch should be performed with next-marker being the marker= GET 151 fetch should be performed with next-marker being the marker= GET
150 parameter.""" 152 parameter."""
151 handle = urllib.urlopen(url) 153 handle = urllib.urlopen(url)
152 document = ElementTree.parse(handle) 154 document = ElementTree.parse(handle)
153 155
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
191 while next_marker: 193 while next_marker:
192 next_url = self.GetListingURL(next_marker) 194 next_url = self.GetListingURL(next_marker)
193 (new_revisions, next_marker) = _FetchAndParse(next_url) 195 (new_revisions, next_marker) = _FetchAndParse(next_url)
194 revisions.extend(new_revisions) 196 revisions.extend(new_revisions)
195 return revisions 197 return revisions
196 198
197 def GetRevList(self): 199 def GetRevList(self):
198 """Gets the list of revision numbers between self.good_revision and 200 """Gets the list of revision numbers between self.good_revision and
199 self.bad_revision.""" 201 self.bad_revision."""
200 # Download the revlist and filter for just the range between good and bad. 202 # Download the revlist and filter for just the range between good and bad.
201 minrev = self.good_revision 203 minrev = min(self.good_revision, self.bad_revision)
202 maxrev = self.bad_revision 204 maxrev = max(self.good_revision, self.bad_revision)
203 revlist = map(int, self.ParseDirectoryIndex()) 205 revlist = map(int, self.ParseDirectoryIndex())
204 revlist = [x for x in revlist if x >= int(minrev) and x <= int(maxrev)] 206 revlist = [x for x in revlist if x >= int(minrev) and x <= int(maxrev)]
205 revlist.sort() 207 revlist.sort()
206 return revlist 208 return revlist
207 209
208 def GetOfficialBuildsList(self): 210 def GetOfficialBuildsList(self):
209 """Gets the list of official build numbers between self.good_revision and 211 """Gets the list of official build numbers between self.good_revision and
210 self.bad_revision.""" 212 self.bad_revision."""
211 # Download the revlist and filter for just the range between good and bad. 213 # Download the revlist and filter for just the range between good and bad.
212 minrev = self.good_revision 214 minrev = min(self.good_revision, self.bad_revision)
213 maxrev = self.bad_revision 215 maxrev = max(self.good_revision, self.bad_revision)
214 handle = urllib.urlopen(OFFICIAL_BASE_URL) 216 handle = urllib.urlopen(OFFICIAL_BASE_URL)
215 dirindex = handle.read() 217 dirindex = handle.read()
216 handle.close() 218 handle.close()
217 build_numbers = re.findall(r'<a href="([0-9][0-9].*)/">', dirindex) 219 build_numbers = re.findall(r'<a href="([0-9][0-9].*)/">', dirindex)
218 final_list = [] 220 final_list = []
219 i = 0 221 i = 0
220 parsed_build_numbers = [LooseVersion(x) for x in build_numbers] 222 parsed_build_numbers = [LooseVersion(x) for x in build_numbers]
221 for build_number in sorted(parsed_build_numbers): 223 for build_number in sorted(parsed_build_numbers):
222 path = OFFICIAL_BASE_URL + '/' + str(build_number) + '/' + \ 224 path = OFFICIAL_BASE_URL + '/' + str(build_number) + '/' + \
223 self._listing_platform_dir + self.archive_name 225 self._listing_platform_dir + self.archive_name
(...skipping 161 matching lines...) Expand 10 before | Expand all | Expand 10 after
385 bad_rev=0, 387 bad_rev=0,
386 num_runs=1, 388 num_runs=1,
387 try_args=(), 389 try_args=(),
388 profile=None, 390 profile=None,
389 evaluate=AskIsGoodBuild): 391 evaluate=AskIsGoodBuild):
390 """Given known good and known bad revisions, run a binary search on all 392 """Given known good and known bad revisions, run a binary search on all
391 archived revisions to determine the last known good revision. 393 archived revisions to determine the last known good revision.
392 394
393 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). 395 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.).
394 @param official_builds Specify build type (Chromium or Official build). 396 @param official_builds Specify build type (Chromium or Official build).
395 @param good_rev Number/tag of the last known good revision. 397 @param good_rev Number/tag of the known good revision.
396 @param bad_rev Number/tag of the first known bad revision. 398 @param bad_rev Number/tag of the known bad revision.
397 @param num_runs Number of times to run each build for asking good/bad. 399 @param num_runs Number of times to run each build for asking good/bad.
398 @param try_args A tuple of arguments to pass to the test application. 400 @param try_args A tuple of arguments to pass to the test application.
399 @param profile The name of the user profile to run with. 401 @param profile The name of the user profile to run with.
400 @param evaluate A function which returns 'g' if the argument build is good, 402 @param evaluate A function which returns 'g' if the argument build is good,
401 'b' if it's bad or 'u' if unknown. 403 'b' if it's bad or 'u' if unknown.
402 404
403 Threading is used to fetch Chromium revisions in the background, speeding up 405 Threading is used to fetch Chromium revisions in the background, speeding up
404 the user's experience. For example, suppose the bounds of the search are 406 the user's experience. For example, suppose the bounds of the search are
405 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on 407 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on
406 whether revision 50 is good or bad, the next revision to check will be either 408 whether revision 50 is good or bad, the next revision to check will be either
(...skipping 22 matching lines...) Expand all
429 if official_builds: 431 if official_builds:
430 revlist = context.GetOfficialBuildsList() 432 revlist = context.GetOfficialBuildsList()
431 else: 433 else:
432 revlist = context.GetRevList() 434 revlist = context.GetRevList()
433 435
434 # Get a list of revisions to bisect across. 436 # Get a list of revisions to bisect across.
435 if len(revlist) < 2: # Don't have enough builds to bisect. 437 if len(revlist) < 2: # Don't have enough builds to bisect.
436 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist 438 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist
437 raise RuntimeError(msg) 439 raise RuntimeError(msg)
438 440
439 print 'Bisecting range [%s, %s].' % (revlist[0], revlist[-1])
440
441 # Figure out our bookends and first pivot point; fetch the pivot revision. 441 # Figure out our bookends and first pivot point; fetch the pivot revision.
442 good = 0 442 minrev = 0
443 bad = len(revlist) - 1 443 maxrev = len(revlist) - 1
444 pivot = bad / 2 444 pivot = maxrev / 2
445 rev = revlist[pivot] 445 rev = revlist[pivot]
446 zipfile = _GetDownloadPath(rev) 446 zipfile = _GetDownloadPath(rev)
447 initial_fetch = DownloadJob(context, 'initial_fetch', rev, zipfile) 447 initial_fetch = DownloadJob(context, 'initial_fetch', rev, zipfile)
448 initial_fetch.Start() 448 initial_fetch.Start()
449 initial_fetch.WaitFor() 449 initial_fetch.WaitFor()
450 450
451 # Binary search time! 451 # Binary search time!
452 while zipfile and bad - good > 1: 452 while zipfile and maxrev - minrev > 1:
453 min_str, max_str = ["good", "bad"][::1 - 2 * (bad_rev < good_rev)]
Nico 2012/11/01 19:06:28 What is this, perl? Just use an if and 4 lines.
enne (OOO) 2012/11/01 19:20:32 Haha. Will do.
454 print 'Bisecting range [%s (%s), %s (%s)].' % (revlist[minrev], min_str, \
455 revlist[maxrev], max_str)
456
453 # Pre-fetch next two possible pivots 457 # Pre-fetch next two possible pivots
454 # - down_pivot is the next revision to check if the current revision turns 458 # - down_pivot is the next revision to check if the current revision turns
455 # out to be bad. 459 # out to be bad.
456 # - up_pivot is the next revision to check if the current revision turns 460 # - up_pivot is the next revision to check if the current revision turns
457 # out to be good. 461 # out to be good.
458 down_pivot = int((pivot - good) / 2) + good 462 down_pivot = int((pivot - minrev) / 2) + minrev
459 down_fetch = None 463 down_fetch = None
460 if down_pivot != pivot and down_pivot != good: 464 if down_pivot != pivot and down_pivot != minrev:
461 down_rev = revlist[down_pivot] 465 down_rev = revlist[down_pivot]
462 down_fetch = DownloadJob(context, 'down_fetch', down_rev, 466 down_fetch = DownloadJob(context, 'down_fetch', down_rev,
463 _GetDownloadPath(down_rev)) 467 _GetDownloadPath(down_rev))
464 down_fetch.Start() 468 down_fetch.Start()
465 469
466 up_pivot = int((bad - pivot) / 2) + pivot 470 up_pivot = int((maxrev - pivot) / 2) + pivot
467 up_fetch = None 471 up_fetch = None
468 if up_pivot != pivot and up_pivot != bad: 472 if up_pivot != pivot and up_pivot != maxrev:
469 up_rev = revlist[up_pivot] 473 up_rev = revlist[up_pivot]
470 up_fetch = DownloadJob(context, 'up_fetch', up_rev, 474 up_fetch = DownloadJob(context, 'up_fetch', up_rev,
471 _GetDownloadPath(up_rev)) 475 _GetDownloadPath(up_rev))
472 up_fetch.Start() 476 up_fetch.Start()
473 477
474 # Run test on the pivot revision. 478 # Run test on the pivot revision.
475 status = None 479 status = None
476 stdout = None 480 stdout = None
477 stderr = None 481 stderr = None
478 try: 482 try:
479 (status, stdout, stderr) = RunRevision(context, 483 (status, stdout, stderr) = RunRevision(context,
480 rev, 484 rev,
481 zipfile, 485 zipfile,
482 profile, 486 profile,
483 num_runs, 487 num_runs,
484 try_args) 488 try_args)
485 except Exception, e: 489 except Exception, e:
486 print >>sys.stderr, e 490 print >>sys.stderr, e
487 os.unlink(zipfile) 491 os.unlink(zipfile)
488 zipfile = None 492 zipfile = None
489 493
490 # Call the evaluate function to see if the current revision is good or bad. 494 # Call the evaluate function to see if the current revision is good or bad.
491 # 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
492 # other, as described in the comments above. 496 # other, as described in the comments above.
493 try: 497 try:
494 answer = evaluate(rev, official_builds, status, stdout, stderr) 498 answer = evaluate(rev, official_builds, status, stdout, stderr)
495 if answer == 'g': 499 if answer == 'g' and good_rev < bad_rev or \
496 good = pivot 500 answer == 'b' and bad_rev < good_rev:
501 minrev = pivot
497 if down_fetch: 502 if down_fetch:
498 down_fetch.Stop() # Kill the download of the older revision. 503 down_fetch.Stop() # Kill the download of the older revision.
499 if up_fetch: 504 if up_fetch:
500 up_fetch.WaitFor() 505 up_fetch.WaitFor()
501 pivot = up_pivot 506 pivot = up_pivot
502 zipfile = up_fetch.zipfile 507 zipfile = up_fetch.zipfile
503 elif answer == 'b': 508 elif answer == 'b' and good_rev < bad_rev or \
504 bad = pivot 509 answer == 'g' and bad_rev < good_rev:
510 maxrev = pivot
505 if up_fetch: 511 if up_fetch:
506 up_fetch.Stop() # Kill the download of the newer revision. 512 up_fetch.Stop() # Kill the download of the newer revision.
507 if down_fetch: 513 if down_fetch:
508 down_fetch.WaitFor() 514 down_fetch.WaitFor()
509 pivot = down_pivot 515 pivot = down_pivot
510 zipfile = down_fetch.zipfile 516 zipfile = down_fetch.zipfile
511 elif answer == 'u': 517 elif answer == 'u':
512 # Nuke the revision from the revlist and choose a new pivot. 518 # Nuke the revision from the revlist and choose a new pivot.
513 revlist.pop(pivot) 519 revlist.pop(pivot)
514 bad -= 1 # Assumes bad >= pivot. 520 maxrev -= 1 # Assumes maxrev >= pivot.
515 521
516 fetch = None 522 fetch = None
517 if bad - good > 1: 523 if maxrev - minrev > 1:
518 # Alternate between using down_pivot or up_pivot for the new pivot 524 # Alternate between using down_pivot or up_pivot for the new pivot
519 # point, without affecting the range. Do this instead of setting the 525 # point, without affecting the range. Do this instead of setting the
520 # pivot to the midpoint of the new range because adjacent revisions 526 # pivot to the midpoint of the new range because adjacent revisions
521 # are likely affected by the same issue that caused the (u)nknown 527 # are likely affected by the same issue that caused the (u)nknown
522 # response. 528 # response.
523 if up_fetch and down_fetch: 529 if up_fetch and down_fetch:
524 fetch = [up_fetch, down_fetch][len(revlist) % 2] 530 fetch = [up_fetch, down_fetch][len(revlist) % 2]
525 elif up_fetch: 531 elif up_fetch:
526 fetch = up_fetch 532 fetch = up_fetch
527 else: 533 else:
(...skipping 16 matching lines...) Expand all
544 for f in [_GetDownloadPath(revlist[down_pivot]), 550 for f in [_GetDownloadPath(revlist[down_pivot]),
545 _GetDownloadPath(revlist[up_pivot])]: 551 _GetDownloadPath(revlist[up_pivot])]:
546 try: 552 try:
547 os.unlink(f) 553 os.unlink(f)
548 except OSError: 554 except OSError:
549 pass 555 pass
550 sys.exit(0) 556 sys.exit(0)
551 557
552 rev = revlist[pivot] 558 rev = revlist[pivot]
553 559
554 return (revlist[good], revlist[bad]) 560 return (revlist[minrev], revlist[maxrev])
555 561
556 562
557 def GetWebKitRevisionForChromiumRevision(rev): 563 def GetWebKitRevisionForChromiumRevision(rev):
558 """Returns the webkit revision that was in chromium's DEPS file at 564 """Returns the webkit revision that was in chromium's DEPS file at
559 chromium revision |rev|.""" 565 chromium revision |rev|."""
560 # . doesn't match newlines without re.DOTALL, so this is safe. 566 # . doesn't match newlines without re.DOTALL, so this is safe.
561 webkit_re = re.compile(r'webkit_revision.:\D*(\d+)') 567 webkit_re = re.compile(r'webkit_revision.:\D*(\d+)')
562 url = urllib.urlopen(DEPS_FILE % rev) 568 url = urllib.urlopen(DEPS_FILE % rev)
563 m = webkit_re.search(url.read()) 569 m = webkit_re.search(url.read())
564 url.close() 570 url.close()
(...skipping 66 matching lines...) Expand 10 before | Expand all | Expand 10 after
631 else: 637 else:
632 good_rev = '0.0.0.0' if opts.official_builds else 0 638 good_rev = '0.0.0.0' if opts.official_builds else 0
633 639
634 if opts.official_builds: 640 if opts.official_builds:
635 good_rev = LooseVersion(good_rev) 641 good_rev = LooseVersion(good_rev)
636 bad_rev = LooseVersion(bad_rev) 642 bad_rev = LooseVersion(bad_rev)
637 else: 643 else:
638 good_rev = int(good_rev) 644 good_rev = int(good_rev)
639 bad_rev = int(bad_rev) 645 bad_rev = int(bad_rev)
640 646
641 if good_rev > bad_rev:
642 print ('The good revision (%s) must precede the bad revision (%s).\n' %
643 (good_rev, bad_rev))
644 parser.print_help()
645 return 1
646
647 if opts.times < 1: 647 if opts.times < 1:
648 print('Number of times to run (%d) must be greater than or equal to 1.' % 648 print('Number of times to run (%d) must be greater than or equal to 1.' %
649 opts.times) 649 opts.times)
650 parser.print_help() 650 parser.print_help()
651 return 1 651 return 1
652 652
653 (last_known_good_rev, first_known_bad_rev) = Bisect( 653 (min_chromium_rev, max_chromium_rev) = Bisect(
654 opts.archive, opts.official_builds, good_rev, bad_rev, opts.times, args, 654 opts.archive, opts.official_builds, good_rev, bad_rev, opts.times, args,
655 opts.profile) 655 opts.profile)
656 656
657 # Get corresponding webkit revisions. 657 # Get corresponding webkit revisions.
658 try: 658 try:
659 last_known_good_webkit_rev = GetWebKitRevisionForChromiumRevision( 659 min_webkit_rev = GetWebKitRevisionForChromiumRevision(min_chromium_rev)
660 last_known_good_rev) 660 max_webkit_rev = GetWebKitRevisionForChromiumRevision(max_chromium_rev)
661 first_known_bad_webkit_rev = GetWebKitRevisionForChromiumRevision(
662 first_known_bad_rev)
663 except Exception, e: 661 except Exception, e:
664 # Silently ignore the failure. 662 # Silently ignore the failure.
665 last_known_good_webkit_rev, first_known_bad_webkit_rev = 0, 0 663 min_webkit_rev, max_webkit_rev = 0, 0
666 664
667 # We're done. Let the user know the results in an official manner. 665 # We're done. Let the user know the results in an official manner.
668 print DONE_MESSAGE % (str(last_known_good_rev), str(first_known_bad_rev)) 666 if good_rev > bad_rev:
669 if last_known_good_webkit_rev != first_known_bad_webkit_rev: 667 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev), str(max_chromium_rev))
668 else:
669 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev), str(max_chromium_rev))
670
671 if min_webkit_rev != max_webkit_rev:
670 print 'WEBKIT CHANGELOG URL:' 672 print 'WEBKIT CHANGELOG URL:'
671 print ' ' + WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, 673 print ' ' + WEBKIT_CHANGELOG_URL % (max_webkit_rev, min_webkit_rev)
Nico 2012/11/01 19:06:28 Should this be printed backwards too?
enne (OOO) 2012/11/02 22:26:57 This is correct.
672 last_known_good_webkit_rev)
673 print 'CHANGELOG URL:' 674 print 'CHANGELOG URL:'
674 if opts.official_builds: 675 if opts.official_builds:
675 print OFFICIAL_CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) 676 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
676 else: 677 else:
677 print ' ' + CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) 678 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
678 679
679 if __name__ == '__main__': 680 if __name__ == '__main__':
680 sys.exit(main()) 681 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