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 data['webkit_revision']: |
Robert Sesek
2013/09/17 13:45:35
if 'webkit_revision' in data: is a safer way to do
| |
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 webkit revision for cr rev %d' % rev) |
Robert Sesek
2013/09/17 13:45:35
I'd keep this as blink, since that's what the opti
| |
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: |
628 print('Could not determine latest revision. This could be bad...') | 632 print('Could not determine latest revision. This could be bad...') |
629 return 999999999 | 633 return 999999999 |
(...skipping 43 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 | |
Robert Sesek
2013/09/17 13:45:35
nit: remove extra blank line
| |
714 | |
703 # Create the context. Initialize 0 for the revisions as they are set below. | 715 # 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) | 716 context = PathContext(base_url, opts.archive, 0, 0, |
717 opts.official_builds, opts.aura) | |
705 # Pick a starting point, try to get HEAD for this. | 718 # Pick a starting point, try to get HEAD for this. |
706 if opts.bad: | 719 if opts.bad: |
707 bad_rev = opts.bad | 720 bad_rev = opts.bad |
708 else: | 721 else: |
709 bad_rev = '999.0.0.0' | 722 bad_rev = '999.0.0.0' |
710 if not opts.official_builds: | 723 if not opts.official_builds: |
711 bad_rev = GetChromiumRevision(context.GetLastChangeURL()) | 724 bad_rev = GetChromiumRevision(context.GetLastChangeURL()) |
712 | 725 |
713 # Find out when we were good. | 726 # Find out when we were good. |
714 if opts.good: | 727 if opts.good: |
715 good_rev = opts.good | 728 good_rev = opts.good |
716 else: | 729 else: |
717 good_rev = '0.0.0.0' if opts.official_builds else 0 | 730 good_rev = '0.0.0.0' if opts.official_builds else 0 |
718 | 731 |
719 if opts.official_builds: | 732 if opts.official_builds: |
720 good_rev = LooseVersion(good_rev) | 733 good_rev = LooseVersion(good_rev) |
721 bad_rev = LooseVersion(bad_rev) | 734 bad_rev = LooseVersion(bad_rev) |
722 else: | 735 else: |
723 good_rev = int(good_rev) | 736 good_rev = int(good_rev) |
724 bad_rev = int(bad_rev) | 737 bad_rev = int(bad_rev) |
725 | 738 |
726 if opts.times < 1: | 739 if opts.times < 1: |
727 print('Number of times to run (%d) must be greater than or equal to 1.' % | 740 print('Number of times to run (%d) must be greater than or equal to 1.' % |
728 opts.times) | 741 opts.times) |
729 parser.print_help() | 742 parser.print_help() |
730 return 1 | 743 return 1 |
731 | 744 |
732 (min_chromium_rev, max_chromium_rev) = Bisect( | 745 (min_chromium_rev, max_chromium_rev) = Bisect( |
733 opts.archive, opts.official_builds, opts.aura, good_rev, bad_rev, | 746 base_url, opts.archive, opts.official_builds, opts.aura, good_rev, |
734 opts.times, opts.command, args, opts.profile) | 747 bad_rev, opts.times, opts.command, args, opts.profile) |
735 | 748 |
736 # Get corresponding blink revisions. | 749 # Get corresponding blink revisions. |
737 try: | 750 try: |
738 min_blink_rev = GetBlinkRevisionForChromiumRevision(min_chromium_rev) | 751 min_blink_rev = GetBlinkRevisionForChromiumRevision(context, |
739 max_blink_rev = GetBlinkRevisionForChromiumRevision(max_chromium_rev) | 752 min_chromium_rev) |
753 max_blink_rev = GetBlinkRevisionForChromiumRevision(context, | |
754 max_chromium_rev) | |
740 except Exception, e: | 755 except Exception, e: |
741 # Silently ignore the failure. | 756 # Silently ignore the failure. |
742 min_blink_rev, max_blink_rev = 0, 0 | 757 min_blink_rev, max_blink_rev = 0, 0 |
743 | 758 |
744 # We're done. Let the user know the results in an official manner. | 759 # We're done. Let the user know the results in an official manner. |
745 if good_rev > bad_rev: | 760 if good_rev > bad_rev: |
746 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev), str(max_chromium_rev)) | 761 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev), str(max_chromium_rev)) |
747 else: | 762 else: |
748 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev), str(max_chromium_rev)) | 763 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev), str(max_chromium_rev)) |
749 | 764 |
750 if min_blink_rev != max_blink_rev: | 765 if min_blink_rev != max_blink_rev: |
751 print 'BLINK CHANGELOG URL:' | 766 print 'BLINK CHANGELOG URL:' |
752 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev) | 767 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev) |
753 print 'CHANGELOG URL:' | 768 print 'CHANGELOG URL:' |
754 if opts.official_builds: | 769 if opts.official_builds: |
755 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) | 770 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) |
756 else: | 771 else: |
757 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) | 772 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) |
758 | 773 |
759 if __name__ == '__main__': | 774 if __name__ == '__main__': |
760 sys.exit(main()) | 775 sys.exit(main()) |
OLD | NEW |