| OLD | NEW |
| 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, |
| 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 CHROMIUM_BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-sn
apshots' |
| 17 WEBKIT_BASE_URL = 'http://commondatastorage.googleapis.com/chromium-webkit-snaps
hots' |
| 17 | 18 |
| 18 # The root URL for official builds. | 19 # The root URL for official builds. |
| 19 OFFICIAL_BASE_URL = 'http://master.chrome.corp.google.com/official_builds' | 20 OFFICIAL_BASE_URL = 'http://master.chrome.corp.google.com/official_builds' |
| 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?' \ | 24 'perf/dashboard/ui/changelog.html?' \ |
| 24 'url=/trunk/src&range=%d%%3A%d' | 25 'url=/trunk/src&range=%d%%3A%d' |
| 25 | 26 |
| 26 # Official Changelogs URL. | 27 # Official Changelogs URL. |
| 27 OFFICIAL_CHANGELOG_URL = 'http://omahaproxy.appspot.com/'\ | 28 OFFICIAL_CHANGELOG_URL = 'http://omahaproxy.appspot.com/'\ |
| 28 'changelog?old_version=%s&new_version=%s' | 29 'changelog?old_version=%s&new_version=%s' |
| 29 | 30 |
| 30 # DEPS file URL. | 31 # DEPS file URL. |
| 31 DEPS_FILE= 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d' | 32 DEPS_FILE= 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d' |
| 32 # Blink Changelogs URL. | 33 # Blink Changelogs URL. |
| 33 BLINK_CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ | 34 BLINK_CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ |
| 34 'perf/dashboard/ui/changelog_blink.html?' \ | 35 'perf/dashboard/ui/changelog_blink.html?' \ |
| 35 'url=/trunk&range=%d%%3A%d' | 36 'url=/trunk&range=%d%%3A%d' |
| 36 | 37 |
| 37 DONE_MESSAGE_GOOD_MIN = 'You are probably looking for a change made after %s ' \ | 38 DONE_MESSAGE_GOOD_MIN = 'You are probably looking for a change made after %s ' \ |
| 38 '(known good), but no later than %s (first known bad).' | 39 '(known good), but no later than %s (first known bad).' |
| 39 DONE_MESSAGE_GOOD_MAX = 'You are probably looking for a change made after %s ' \ | 40 DONE_MESSAGE_GOOD_MAX = 'You are probably looking for a change made after %s ' \ |
| 40 '(known bad), but no later than %s (first known good).' | 41 '(known bad), but no later than %s (first known good).' |
| 41 | 42 |
| 42 ############################################################################### | 43 ############################################################################### |
| 43 | 44 |
| 45 import json |
| 44 import math | 46 import math |
| 45 import optparse | 47 import optparse |
| 46 import os | 48 import os |
| 47 import pipes | 49 import pipes |
| 48 import re | 50 import re |
| 49 import shutil | 51 import shutil |
| 50 import subprocess | 52 import subprocess |
| 51 import sys | 53 import sys |
| 52 import tempfile | 54 import tempfile |
| 53 import threading | 55 import threading |
| 54 import urllib | 56 import urllib |
| 55 from distutils.version import LooseVersion | 57 from distutils.version import LooseVersion |
| 56 from xml.etree import ElementTree | 58 from xml.etree import ElementTree |
| 57 import zipfile | 59 import zipfile |
| 58 | 60 |
| 59 | 61 |
| 60 class PathContext(object): | 62 class PathContext(object): |
| 61 """A PathContext is used to carry the information used to construct URLs and | 63 """A PathContext is used to carry the information used to construct URLs and |
| 62 paths when dealing with the storage server and archives.""" | 64 paths when dealing with the storage server and archives.""" |
| 63 def __init__(self, platform, good_revision, bad_revision, is_official, | 65 def __init__(self, base_url, platform, good_revision, bad_revision, |
| 64 is_aura): | 66 is_official, is_aura): |
| 65 super(PathContext, self).__init__() | 67 super(PathContext, self).__init__() |
| 66 # Store off the input parameters. | 68 # Store off the input parameters. |
| 69 self.base_url = base_url |
| 67 self.platform = platform # What's passed in to the '-a/--archive' option. | 70 self.platform = platform # What's passed in to the '-a/--archive' option. |
| 68 self.good_revision = good_revision | 71 self.good_revision = good_revision |
| 69 self.bad_revision = bad_revision | 72 self.bad_revision = bad_revision |
| 70 self.is_official = is_official | 73 self.is_official = is_official |
| 71 self.is_aura = is_aura | 74 self.is_aura = is_aura |
| 72 | 75 |
| 73 # The name of the ZIP file in a revision directory on the server. | 76 # The name of the ZIP file in a revision directory on the server. |
| 74 self.archive_name = None | 77 self.archive_name = None |
| 75 | 78 |
| 76 # Set some internal members: | 79 # Set some internal members: |
| (...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 120 self._listing_platform_dir = 'Mac/' | 123 self._listing_platform_dir = 'Mac/' |
| 121 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' | 124 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium' |
| 122 elif self.platform == 'win': | 125 elif self.platform == 'win': |
| 123 self._listing_platform_dir = 'Win/' | 126 self._listing_platform_dir = 'Win/' |
| 124 | 127 |
| 125 def GetListingURL(self, marker=None): | 128 def GetListingURL(self, marker=None): |
| 126 """Returns the URL for a directory listing, with an optional marker.""" | 129 """Returns the URL for a directory listing, with an optional marker.""" |
| 127 marker_param = '' | 130 marker_param = '' |
| 128 if marker: | 131 if marker: |
| 129 marker_param = '&marker=' + str(marker) | 132 marker_param = '&marker=' + str(marker) |
| 130 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ | 133 return self.base_url + '/?delimiter=/&prefix=' + \ |
| 131 marker_param | 134 self._listing_platform_dir + marker_param |
| 132 | 135 |
| 133 def GetDownloadURL(self, revision): | 136 def GetDownloadURL(self, revision): |
| 134 """Gets the download URL for a build archive of a specific revision.""" | 137 """Gets the download URL for a build archive of a specific revision.""" |
| 135 if self.is_official: | 138 if self.is_official: |
| 136 return "%s/%s/%s%s" % ( | 139 return "%s/%s/%s%s" % ( |
| 137 OFFICIAL_BASE_URL, revision, self._listing_platform_dir, | 140 OFFICIAL_BASE_URL, revision, self._listing_platform_dir, |
| 138 self.archive_name) | 141 self.archive_name) |
| 139 else: | 142 else: |
| 140 return "%s/%s%s/%s" % ( | 143 return "%s/%s%s/%s" % (self.base_url, self._listing_platform_dir, |
| 141 BASE_URL, self._listing_platform_dir, revision, self.archive_name) | 144 revision, self.archive_name) |
| 142 | 145 |
| 143 def GetLastChangeURL(self): | 146 def GetLastChangeURL(self): |
| 144 """Returns a URL to the LAST_CHANGE file.""" | 147 """Returns a URL to the LAST_CHANGE file.""" |
| 145 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' | 148 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE' |
| 146 | 149 |
| 147 def GetLaunchPath(self): | 150 def GetLaunchPath(self): |
| 148 """Returns a relative path (presumably from the archive extraction location) | 151 """Returns a relative path (presumably from the archive extraction location) |
| 149 that is used to run the executable.""" | 152 that is used to run the executable.""" |
| 150 return os.path.join(self._archive_extract_dir, self._binary_name) | 153 return os.path.join(self._archive_extract_dir, self._binary_name) |
| 151 | 154 |
| 152 def IsAuraBuild(self, build): | 155 def IsAuraBuild(self, build): |
| 153 """Check the given build is Aura.""" | 156 """Check the given build is Aura.""" |
| 154 return build.split('.')[3] == '1' | 157 return build.split('.')[3] == '1' |
| 155 | 158 |
| (...skipping 253 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 409 os.unlink(self.zipfile) | 412 os.unlink(self.zipfile) |
| 410 | 413 |
| 411 def WaitFor(self): | 414 def WaitFor(self): |
| 412 """Prints a message and waits for the download to complete. The download | 415 """Prints a message and waits for the download to complete. The download |
| 413 must have been started previously.""" | 416 must have been started previously.""" |
| 414 print "Downloading revision %s..." % str(self.rev) | 417 print "Downloading revision %s..." % str(self.rev) |
| 415 self.progress_event.set() # Display progress of download. | 418 self.progress_event.set() # Display progress of download. |
| 416 self.thread.join() | 419 self.thread.join() |
| 417 | 420 |
| 418 | 421 |
| 419 def Bisect(platform, | 422 def Bisect(base_url, |
| 423 platform, |
| 420 official_builds, | 424 official_builds, |
| 421 is_aura, | 425 is_aura, |
| 422 good_rev=0, | 426 good_rev=0, |
| 423 bad_rev=0, | 427 bad_rev=0, |
| 424 num_runs=1, | 428 num_runs=1, |
| 425 command="%p %a", | 429 command="%p %a", |
| 426 try_args=(), | 430 try_args=(), |
| 427 profile=None, | 431 profile=None, |
| 428 evaluate=AskIsGoodBuild): | 432 evaluate=AskIsGoodBuild): |
| 429 """Given known good and known bad revisions, run a binary search on all | 433 """Given known good and known bad revisions, run a binary search on all |
| (...skipping 20 matching lines...) Expand all Loading... |
| 450 - If rev 50 is good, the download of rev 25 is cancelled, and the next test | 454 - If rev 50 is good, the download of rev 25 is cancelled, and the next test |
| 451 is run on rev 75. | 455 is run on rev 75. |
| 452 | 456 |
| 453 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test | 457 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test |
| 454 is run on rev 25. | 458 is run on rev 25. |
| 455 """ | 459 """ |
| 456 | 460 |
| 457 if not profile: | 461 if not profile: |
| 458 profile = 'profile' | 462 profile = 'profile' |
| 459 | 463 |
| 460 context = PathContext(platform, good_rev, bad_rev, official_builds, is_aura) | 464 context = PathContext(base_url, platform, good_rev, bad_rev, |
| 465 official_builds, is_aura) |
| 461 cwd = os.getcwd() | 466 cwd = os.getcwd() |
| 462 | 467 |
| 463 | |
| 464 | |
| 465 print "Downloading list of known revisions..." | 468 print "Downloading list of known revisions..." |
| 466 _GetDownloadPath = lambda rev: os.path.join(cwd, | 469 _GetDownloadPath = lambda rev: os.path.join(cwd, |
| 467 '%s-%s' % (str(rev), context.archive_name)) | 470 '%s-%s' % (str(rev), context.archive_name)) |
| 468 if official_builds: | 471 if official_builds: |
| 469 revlist = context.GetOfficialBuildsList() | 472 revlist = context.GetOfficialBuildsList() |
| 470 else: | 473 else: |
| 471 revlist = context.GetRevList() | 474 revlist = context.GetRevList() |
| 472 | 475 |
| 473 # Get a list of revisions to bisect across. | 476 # Get a list of revisions to bisect across. |
| 474 if len(revlist) < 2: # Don't have enough builds to bisect. | 477 if len(revlist) < 2: # Don't have enough builds to bisect. |
| (...skipping 123 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 598 os.unlink(f) | 601 os.unlink(f) |
| 599 except OSError: | 602 except OSError: |
| 600 pass | 603 pass |
| 601 sys.exit(0) | 604 sys.exit(0) |
| 602 | 605 |
| 603 rev = revlist[pivot] | 606 rev = revlist[pivot] |
| 604 | 607 |
| 605 return (revlist[minrev], revlist[maxrev]) | 608 return (revlist[minrev], revlist[maxrev]) |
| 606 | 609 |
| 607 | 610 |
| 608 def GetBlinkRevisionForChromiumRevision(rev): | 611 def GetBlinkRevisionForChromiumRevision(self, rev): |
| 609 """Returns the blink revision that was in chromium's DEPS file at | 612 """Returns the blink revision that was in REVISIONS file at |
| 610 chromium revision |rev|.""" | 613 chromium revision |rev|.""" |
| 611 # . doesn't match newlines without re.DOTALL, so this is safe. | 614 # . doesn't match newlines without re.DOTALL, so this is safe. |
| 612 blink_re = re.compile(r'webkit_revision.:\D*(\d+)') | 615 file_url = "%s/%s%d/REVISIONS" % (self.base_url, |
| 613 url = urllib.urlopen(DEPS_FILE % rev) | 616 self._listing_platform_dir, rev) |
| 614 m = blink_re.search(url.read()) | 617 url = urllib.urlopen(file_url) |
| 618 data = json.loads(url.read()) |
| 615 url.close() | 619 url.close() |
| 616 if m: | 620 if 'webkit_revision' in data: |
| 617 return int(m.group(1)) | 621 return data['webkit_revision'] |
| 618 else: | 622 else: |
| 619 raise Exception('Could not get blink revision for cr rev %d' % rev) | 623 raise Exception('Could not get blink revision for cr rev %d' % rev) |
| 620 | 624 |
| 621 | 625 |
| 622 def GetChromiumRevision(url): | 626 def GetChromiumRevision(url): |
| 623 """Returns the chromium revision read from given URL.""" | 627 """Returns the chromium revision read from given URL.""" |
| 624 try: | 628 try: |
| 625 # Location of the latest build revision number | 629 # Location of the latest build revision number |
| 626 return int(urllib.urlopen(url).read()) | 630 return int(urllib.urlopen(url).read()) |
| 627 except Exception, e: | 631 except Exception, e: |
| (...skipping 45 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 673 help = 'Number of times to run each build before asking ' + | 677 help = 'Number of times to run each build before asking ' + |
| 674 'if it\'s good or bad. Temporary profiles are reused.', | 678 'if it\'s good or bad. Temporary profiles are reused.', |
| 675 default = 1) | 679 default = 1) |
| 676 parser.add_option('-c', '--command', type = 'str', | 680 parser.add_option('-c', '--command', type = 'str', |
| 677 help = 'Command to execute. %p and %a refer to Chrome ' + | 681 help = 'Command to execute. %p and %a refer to Chrome ' + |
| 678 'executable and specified extra arguments respectively. ' + | 682 'executable and specified extra arguments respectively. ' + |
| 679 'Use %s to specify all extra arguments as one string. ' + | 683 'Use %s to specify all extra arguments as one string. ' + |
| 680 'Defaults to "%p %a". Note that any extra paths ' + | 684 'Defaults to "%p %a". Note that any extra paths ' + |
| 681 'specified should be absolute.', | 685 'specified should be absolute.', |
| 682 default = '%p %a'); | 686 default = '%p %a'); |
| 687 parser.add_option('-l', '--blink', action='store_true', |
| 688 help = 'Use Blink bisect instead of Chromium. ') |
| 683 parser.add_option('--aura', | 689 parser.add_option('--aura', |
| 684 dest='aura', | 690 dest='aura', |
| 685 action='store_true', | 691 action='store_true', |
| 686 default=False, | 692 default=False, |
| 687 help='Allow the script to bisect aura builds') | 693 help='Allow the script to bisect aura builds') |
| 688 | 694 |
| 689 (opts, args) = parser.parse_args() | 695 (opts, args) = parser.parse_args() |
| 690 | 696 |
| 691 if opts.archive is None: | 697 if opts.archive is None: |
| 692 print 'Error: missing required parameter: --archive' | 698 print 'Error: missing required parameter: --archive' |
| 693 print | 699 print |
| 694 parser.print_help() | 700 parser.print_help() |
| 695 return 1 | 701 return 1 |
| 696 | 702 |
| 697 if opts.aura: | 703 if opts.aura: |
| 698 if opts.archive != 'win' or not opts.official_builds: | 704 if opts.archive != 'win' or not opts.official_builds: |
| 699 print 'Error: Aura is supported only on Windows platform '\ | 705 print 'Error: Aura is supported only on Windows platform '\ |
| 700 'and official builds.' | 706 'and official builds.' |
| 701 return 1 | 707 return 1 |
| 702 | 708 |
| 709 if opts.blink: |
| 710 base_url = WEBKIT_BASE_URL |
| 711 else: |
| 712 base_url = CHROMIUM_BASE_URL |
| 713 |
| 703 # Create the context. Initialize 0 for the revisions as they are set below. | 714 # Create the context. Initialize 0 for the revisions as they are set below. |
| 704 context = PathContext(opts.archive, 0, 0, opts.official_builds, opts.aura) | 715 context = PathContext(base_url, opts.archive, 0, 0, |
| 716 opts.official_builds, opts.aura) |
| 705 # Pick a starting point, try to get HEAD for this. | 717 # Pick a starting point, try to get HEAD for this. |
| 706 if opts.bad: | 718 if opts.bad: |
| 707 bad_rev = opts.bad | 719 bad_rev = opts.bad |
| 708 else: | 720 else: |
| 709 bad_rev = '999.0.0.0' | 721 bad_rev = '999.0.0.0' |
| 710 if not opts.official_builds: | 722 if not opts.official_builds: |
| 711 bad_rev = GetChromiumRevision(context.GetLastChangeURL()) | 723 bad_rev = GetChromiumRevision(context.GetLastChangeURL()) |
| 712 | 724 |
| 713 # Find out when we were good. | 725 # Find out when we were good. |
| 714 if opts.good: | 726 if opts.good: |
| 715 good_rev = opts.good | 727 good_rev = opts.good |
| 716 else: | 728 else: |
| 717 good_rev = '0.0.0.0' if opts.official_builds else 0 | 729 good_rev = '0.0.0.0' if opts.official_builds else 0 |
| 718 | 730 |
| 719 if opts.official_builds: | 731 if opts.official_builds: |
| 720 good_rev = LooseVersion(good_rev) | 732 good_rev = LooseVersion(good_rev) |
| 721 bad_rev = LooseVersion(bad_rev) | 733 bad_rev = LooseVersion(bad_rev) |
| 722 else: | 734 else: |
| 723 good_rev = int(good_rev) | 735 good_rev = int(good_rev) |
| 724 bad_rev = int(bad_rev) | 736 bad_rev = int(bad_rev) |
| 725 | 737 |
| 726 if opts.times < 1: | 738 if opts.times < 1: |
| 727 print('Number of times to run (%d) must be greater than or equal to 1.' % | 739 print('Number of times to run (%d) must be greater than or equal to 1.' % |
| 728 opts.times) | 740 opts.times) |
| 729 parser.print_help() | 741 parser.print_help() |
| 730 return 1 | 742 return 1 |
| 731 | 743 |
| 732 (min_chromium_rev, max_chromium_rev) = Bisect( | 744 (min_chromium_rev, max_chromium_rev) = Bisect( |
| 733 opts.archive, opts.official_builds, opts.aura, good_rev, bad_rev, | 745 base_url, opts.archive, opts.official_builds, opts.aura, good_rev, |
| 734 opts.times, opts.command, args, opts.profile) | 746 bad_rev, opts.times, opts.command, args, opts.profile) |
| 735 | 747 |
| 736 # Get corresponding blink revisions. | 748 # Get corresponding blink revisions. |
| 737 try: | 749 try: |
| 738 min_blink_rev = GetBlinkRevisionForChromiumRevision(min_chromium_rev) | 750 min_blink_rev = GetBlinkRevisionForChromiumRevision(context, |
| 739 max_blink_rev = GetBlinkRevisionForChromiumRevision(max_chromium_rev) | 751 min_chromium_rev) |
| 752 max_blink_rev = GetBlinkRevisionForChromiumRevision(context, |
| 753 max_chromium_rev) |
| 740 except Exception, e: | 754 except Exception, e: |
| 741 # Silently ignore the failure. | 755 # Silently ignore the failure. |
| 742 min_blink_rev, max_blink_rev = 0, 0 | 756 min_blink_rev, max_blink_rev = 0, 0 |
| 743 | 757 |
| 744 # We're done. Let the user know the results in an official manner. | 758 # We're done. Let the user know the results in an official manner. |
| 745 if good_rev > bad_rev: | 759 if good_rev > bad_rev: |
| 746 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev), str(max_chromium_rev)) | 760 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev), str(max_chromium_rev)) |
| 747 else: | 761 else: |
| 748 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev), str(max_chromium_rev)) | 762 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev), str(max_chromium_rev)) |
| 749 | 763 |
| 750 if min_blink_rev != max_blink_rev: | 764 if min_blink_rev != max_blink_rev: |
| 751 print 'BLINK CHANGELOG URL:' | 765 print 'BLINK CHANGELOG URL:' |
| 752 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev) | 766 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev) |
| 753 print 'CHANGELOG URL:' | 767 print 'CHANGELOG URL:' |
| 754 if opts.official_builds: | 768 if opts.official_builds: |
| 755 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) | 769 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) |
| 756 else: | 770 else: |
| 757 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) | 771 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) |
| 758 | 772 |
| 759 if __name__ == '__main__': | 773 if __name__ == '__main__': |
| 760 sys.exit(main()) | 774 sys.exit(main()) |
| OLD | NEW |