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
4 # found in the LICENSE file.
6 """Snapshot Build Bisect Tool
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
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,
12 it will ask you whether it is good or bad before continuing the search.
15 # The base URL for stored build archives.
16 CHROMIUM_BASE_URL = ('http://commondatastorage.googleapis.com'
17 '/chromium-browser-snapshots')
18 WEBKIT_BASE_URL = ('http://commondatastorage.googleapis.com'
19 '/chromium-webkit-snapshots')
20 ASAN_BASE_URL = ('http://commondatastorage.googleapis.com'
21 '/chromium-browser-asan')
23 # The base URL for official builds.
24 OFFICIAL_BASE_URL = 'http://master.chrome.corp.google.com/official_builds'
26 # URL template for viewing changelogs between revisions.
27 CHANGELOG_URL = ('http://build.chromium.org'
28 '/f/chromium/perf/dashboard/ui/changelog.html'
29 '?url=/trunk/src&range=%d%%3A%d')
31 # URL template for viewing changelogs between official versions.
32 OFFICIAL_CHANGELOG_URL = ('http://omahaproxy.appspot.com/changelog'
33 '?old_version=%s&new_version=%s')
36 DEPS_FILE = 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d'
38 # Blink changelogs URL.
39 BLINK_CHANGELOG_URL = ('http://build.chromium.org'
40 '/f/chromium/perf/dashboard/ui/changelog_blink.html'
41 '?url=/trunk&range=%d%%3A%d')
43 DONE_MESSAGE_GOOD_MIN = ('You are probably looking for a change made after %s ('
44 'known good), but no later than %s (first known bad).')
45 DONE_MESSAGE_GOOD_MAX = ('You are probably looking for a change made after %s ('
46 'known bad), but no later than %s (first known good).')
48 CHROMIUM_GITHASH_TO_SVN_URL = (
49 'https://chromium.googlesource.com/chromium/src/+/%s?format=json')
51 BLINK_GITHASH_TO_SVN_URL = (
52 'https://chromium.googlesource.com/chromium/blink/+/%s?format=json')
54 GITHASH_TO_SVN_URL = {
55 'chromium': CHROMIUM_GITHASH_TO_SVN_URL,
56 'blink': BLINK_GITHASH_TO_SVN_URL,
59 # Search pattern to be matched in the JSON output from
60 # CHROMIUM_GITHASH_TO_SVN_URL to get the chromium revision (svn revision).
61 CHROMIUM_SEARCH_PATTERN = (
62 r'.*git-svn-id: svn://svn.chromium.org/chrome/trunk/src@(\d+) ')
64 # Search pattern to be matched in the json output from
65 # BLINK_GITHASH_TO_SVN_URL to get the blink revision (svn revision).
66 BLINK_SEARCH_PATTERN = (
67 r'.*git-svn-id: svn://svn.chromium.org/blink/trunk@(\d+) ')
70 'chromium': CHROMIUM_SEARCH_PATTERN,
71 'blink': BLINK_SEARCH_PATTERN,
74 ###############################################################################
87 from distutils.version import LooseVersion
88 from xml.etree import ElementTree
92 class PathContext(object):
93 """A PathContext is used to carry the information used to construct URLs and
94 paths when dealing with the storage server and archives."""
95 def __init__(self, base_url, platform, good_revision, bad_revision,
96 is_official, is_asan, use_local_repo, flash_path = None,
98 super(PathContext, self).__init__()
99 # Store off the input parameters.
100 self.base_url = base_url
101 self.platform = platform # What's passed in to the '-a/--archive' option.
102 self.good_revision = good_revision
103 self.bad_revision = bad_revision
104 self.is_official = is_official
105 self.is_asan = is_asan
106 self.build_type = 'release'
107 self.flash_path = flash_path
108 # Dictionary which stores svn revision number as key and it's
109 # corresponding git hash as value. This data is populated in
110 # _FetchAndParse and used later in GetDownloadURL while downloading
112 self.githash_svn_dict = {}
113 self.pdf_path = pdf_path
115 # The name of the ZIP file in a revision directory on the server.
116 self.archive_name = None
118 # If the script is run from a local Chromium checkout,
119 # "--use-local-repo" option can be used to make the script run faster.
120 # It uses "git svn find-rev <SHA1>" command to convert git hash to svn
122 self.use_local_repo = use_local_repo
124 # Set some internal members:
125 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
126 # _archive_extract_dir = Uncompressed directory in the archive_name file.
127 # _binary_name = The name of the executable to run.
128 if self.platform in ('linux', 'linux64', 'linux-arm'):
129 self._binary_name = 'chrome'
130 elif self.platform == 'mac':
131 self.archive_name = 'chrome-mac.zip'
132 self._archive_extract_dir = 'chrome-mac'
133 elif self.platform == 'win':
134 self.archive_name = 'chrome-win32.zip'
135 self._archive_extract_dir = 'chrome-win32'
136 self._binary_name = 'chrome.exe'
138 raise Exception('Invalid platform: %s' % self.platform)
141 if self.platform == 'linux':
142 self._listing_platform_dir = 'precise32bit/'
143 self.archive_name = 'chrome-precise32bit.zip'
144 self._archive_extract_dir = 'chrome-precise32bit'
145 elif self.platform == 'linux64':
146 self._listing_platform_dir = 'precise64bit/'
147 self.archive_name = 'chrome-precise64bit.zip'
148 self._archive_extract_dir = 'chrome-precise64bit'
149 elif self.platform == 'mac':
150 self._listing_platform_dir = 'mac/'
151 self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome'
152 elif self.platform == 'win':
153 self._listing_platform_dir = 'win/'
155 if self.platform in ('linux', 'linux64', 'linux-arm'):
156 self.archive_name = 'chrome-linux.zip'
157 self._archive_extract_dir = 'chrome-linux'
158 if self.platform == 'linux':
159 self._listing_platform_dir = 'Linux/'
160 elif self.platform == 'linux64':
161 self._listing_platform_dir = 'Linux_x64/'
162 elif self.platform == 'linux-arm':
163 self._listing_platform_dir = 'Linux_ARM_Cross-Compile/'
164 elif self.platform == 'mac':
165 self._listing_platform_dir = 'Mac/'
166 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium'
167 elif self.platform == 'win':
168 self._listing_platform_dir = 'Win/'
170 def GetASANPlatformDir(self):
171 """ASAN builds are in directories like "linux-release", or have filenames
172 like "asan-win32-release-277079.zip". This aligns to our platform names
173 except in the case of Windows where they use "win32" instead of "win"."""
174 if self.platform == 'win':
179 def GetListingURL(self, marker=None):
180 """Returns the URL for a directory listing, with an optional marker."""
183 marker_param = '&marker=' + str(marker)
185 prefix = '%s-%s' % (self.GetASANPlatformDir(), self.build_type)
186 return self.base_url + '/?delimiter=&prefix=' + prefix + marker_param
188 return (self.base_url + '/?delimiter=/&prefix=' +
189 self._listing_platform_dir + marker_param)
191 def GetDownloadURL(self, revision):
192 """Gets the download URL for a build archive of a specific revision."""
194 return '%s/%s-%s/%s-%d.zip' % (
195 ASAN_BASE_URL, self.GetASANPlatformDir(), self.build_type,
196 self.GetASANBaseName(), revision)
198 return '%s/%s/%s%s' % (
199 OFFICIAL_BASE_URL, revision, self._listing_platform_dir,
202 if str(revision) in self.githash_svn_dict:
203 revision = self.githash_svn_dict[str(revision)]
204 return '%s/%s%s/%s' % (self.base_url, self._listing_platform_dir,
205 revision, self.archive_name)
207 def GetLastChangeURL(self):
208 """Returns a URL to the LAST_CHANGE file."""
209 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE'
211 def GetASANBaseName(self):
212 """Returns the base name of the ASAN zip file."""
213 if 'linux' in self.platform:
214 return 'asan-symbolized-%s-%s' % (self.GetASANPlatformDir(),
217 return 'asan-%s-%s' % (self.GetASANPlatformDir(), self.build_type)
219 def GetLaunchPath(self, revision):
220 """Returns a relative path (presumably from the archive extraction location)
221 that is used to run the executable."""
223 extract_dir = '%s-%d' % (self.GetASANBaseName(), revision)
225 extract_dir = self._archive_extract_dir
226 return os.path.join(extract_dir, self._binary_name)
228 def ParseDirectoryIndex(self):
229 """Parses the Google Storage directory listing into a list of revision
232 def _FetchAndParse(url):
233 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If
234 next-marker is not None, then the listing is a partial listing and another
235 fetch should be performed with next-marker being the marker= GET
237 handle = urllib.urlopen(url)
238 document = ElementTree.parse(handle)
240 # All nodes in the tree are namespaced. Get the root's tag name to extract
241 # the namespace. Etree does namespaces as |{namespace}tag|.
242 root_tag = document.getroot().tag
243 end_ns_pos = root_tag.find('}')
245 raise Exception('Could not locate end namespace for directory index')
246 namespace = root_tag[:end_ns_pos + 1]
248 # Find the prefix (_listing_platform_dir) and whether or not the list is
250 prefix_len = len(document.find(namespace + 'Prefix').text)
252 is_truncated = document.find(namespace + 'IsTruncated')
253 if is_truncated is not None and is_truncated.text.lower() == 'true':
254 next_marker = document.find(namespace + 'NextMarker').text
255 # Get a list of all the revisions.
257 githash_svn_dict = {}
259 asan_regex = re.compile(r'.*%s-(\d+)\.zip$' % (self.GetASANBaseName()))
260 # Non ASAN builds are in a <revision> directory. The ASAN builds are
262 all_prefixes = document.findall(namespace + 'Contents/' +
264 for prefix in all_prefixes:
265 m = asan_regex.match(prefix.text)
268 revisions.append(int(m.group(1)))
272 all_prefixes = document.findall(namespace + 'CommonPrefixes/' +
273 namespace + 'Prefix')
274 # The <Prefix> nodes have content of the form of
275 # |_listing_platform_dir/revision/|. Strip off the platform dir and the
276 # trailing slash to just have a number.
277 for prefix in all_prefixes:
278 revnum = prefix.text[prefix_len:-1]
280 if not revnum.isdigit():
282 revnum = self.GetSVNRevisionFromGitHash(git_hash)
283 githash_svn_dict[revnum] = git_hash
284 if revnum is not None:
286 revisions.append(revnum)
289 return (revisions, next_marker, githash_svn_dict)
291 # Fetch the first list of revisions.
292 (revisions, next_marker, self.githash_svn_dict) = _FetchAndParse(
293 self.GetListingURL())
294 # If the result list was truncated, refetch with the next marker. Do this
295 # until an entire directory listing is done.
297 next_url = self.GetListingURL(next_marker)
298 (new_revisions, next_marker, new_dict) = _FetchAndParse(next_url)
299 revisions.extend(new_revisions)
300 self.githash_svn_dict.update(new_dict)
303 def _GetSVNRevisionFromGitHashWithoutGitCheckout(self, git_sha1, depot):
304 json_url = GITHASH_TO_SVN_URL[depot] % git_sha1
305 response = urllib.urlopen(json_url)
306 if response.getcode() == 200:
308 data = json.loads(response.read()[4:])
310 print 'ValueError for JSON URL: %s' % json_url
314 if 'message' in data:
315 message = data['message'].split('\n')
316 message = [line for line in message if line.strip()]
317 search_pattern = re.compile(SEARCH_PATTERN[depot])
318 result = search_pattern.search(message[len(message)-1])
320 return result.group(1)
321 print 'Failed to get svn revision number for %s' % git_sha1
324 def _GetSVNRevisionFromGitHashFromGitCheckout(self, git_sha1, depot):
325 def _RunGit(command, path):
326 command = ['git'] + command
328 original_path = os.getcwd()
330 shell = sys.platform.startswith('win')
331 proc = subprocess.Popen(command, shell=shell, stdout=subprocess.PIPE,
332 stderr=subprocess.PIPE)
333 (output, _) = proc.communicate()
336 os.chdir(original_path)
337 return (output, proc.returncode)
341 path = os.path.join(os.getcwd(), 'third_party', 'WebKit')
342 if os.path.basename(os.getcwd()) == 'src':
343 command = ['svn', 'find-rev', git_sha1]
344 (git_output, return_code) = _RunGit(command, path)
346 return git_output.strip('\n')
349 print ('Script should be run from src folder. ' +
350 'Eg: python tools/bisect-builds.py -g 280588 -b 280590' +
351 '--archive linux64 --use-local-repo')
354 def GetSVNRevisionFromGitHash(self, git_sha1, depot='chromium'):
355 if not self.use_local_repo:
356 return self._GetSVNRevisionFromGitHashWithoutGitCheckout(git_sha1, depot)
358 return self._GetSVNRevisionFromGitHashFromGitCheckout(git_sha1, depot)
360 def GetRevList(self):
361 """Gets the list of revision numbers between self.good_revision and
362 self.bad_revision."""
363 # Download the revlist and filter for just the range between good and bad.
364 minrev = min(self.good_revision, self.bad_revision)
365 maxrev = max(self.good_revision, self.bad_revision)
366 revlist_all = map(int, self.ParseDirectoryIndex())
368 revlist = [x for x in revlist_all if x >= int(minrev) and x <= int(maxrev)]
371 # Set good and bad revisions to be legit revisions.
373 if self.good_revision < self.bad_revision:
374 self.good_revision = revlist[0]
375 self.bad_revision = revlist[-1]
377 self.bad_revision = revlist[0]
378 self.good_revision = revlist[-1]
380 # Fix chromium rev so that the deps blink revision matches REVISIONS file.
381 if self.base_url == WEBKIT_BASE_URL:
383 self.good_revision = FixChromiumRevForBlink(revlist,
387 self.bad_revision = FixChromiumRevForBlink(revlist,
393 def GetOfficialBuildsList(self):
394 """Gets the list of official build numbers between self.good_revision and
395 self.bad_revision."""
396 # Download the revlist and filter for just the range between good and bad.
397 minrev = min(self.good_revision, self.bad_revision)
398 maxrev = max(self.good_revision, self.bad_revision)
399 handle = urllib.urlopen(OFFICIAL_BASE_URL)
400 dirindex = handle.read()
402 build_numbers = re.findall(r'<a href="([0-9][0-9].*)/">', dirindex)
405 parsed_build_numbers = [LooseVersion(x) for x in build_numbers]
406 for build_number in sorted(parsed_build_numbers):
407 path = (OFFICIAL_BASE_URL + '/' + str(build_number) + '/' +
408 self._listing_platform_dir + self.archive_name)
411 connection = urllib.urlopen(path)
413 if build_number > maxrev:
415 if build_number >= minrev:
416 final_list.append(str(build_number))
417 except urllib.HTTPError:
421 def UnzipFilenameToDir(filename, directory):
422 """Unzip |filename| to |directory|."""
424 if not os.path.isabs(filename):
425 filename = os.path.join(cwd, filename)
426 zf = zipfile.ZipFile(filename)
428 if not os.path.isdir(directory):
432 for info in zf.infolist():
434 if name.endswith('/'): # dir
435 if not os.path.isdir(name):
438 directory = os.path.dirname(name)
439 if not os.path.isdir(directory):
440 os.makedirs(directory)
441 out = open(name, 'wb')
442 out.write(zf.read(name))
444 # Set permissions. Permission info in external_attr is shifted 16 bits.
445 os.chmod(name, info.external_attr >> 16L)
449 def FetchRevision(context, rev, filename, quit_event=None, progress_event=None):
450 """Downloads and unzips revision |rev|.
451 @param context A PathContext instance.
452 @param rev The Chromium revision number/tag to download.
453 @param filename The destination for the downloaded file.
454 @param quit_event A threading.Event which will be set by the master thread to
455 indicate that the download should be aborted.
456 @param progress_event A threading.Event which will be set by the master thread
457 to indicate that the progress of the download should be
460 def ReportHook(blocknum, blocksize, totalsize):
461 if quit_event and quit_event.isSet():
462 raise RuntimeError('Aborting download of revision %s' % str(rev))
463 if progress_event and progress_event.isSet():
464 size = blocknum * blocksize
465 if totalsize == -1: # Total size not known.
466 progress = 'Received %d bytes' % size
468 size = min(totalsize, size)
469 progress = 'Received %d of %d bytes, %.2f%%' % (
470 size, totalsize, 100.0 * size / totalsize)
471 # Send a \r to let all progress messages use just one line of output.
472 sys.stdout.write('\r' + progress)
475 download_url = context.GetDownloadURL(rev)
477 urllib.urlretrieve(download_url, filename, ReportHook)
478 if progress_event and progress_event.isSet():
484 def RunRevision(context, revision, zip_file, profile, num_runs, command, args):
485 """Given a zipped revision, unzip it and run the test."""
486 print 'Trying revision %s...' % str(revision)
488 # Create a temp directory and unzip the revision into it.
490 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
491 UnzipFilenameToDir(zip_file, tempdir)
494 # Run the build as many times as specified.
495 testargs = ['--user-data-dir=%s' % profile] + args
496 # The sandbox must be run as root on Official Chrome, so bypass it.
497 if ((context.is_official or context.flash_path or context.pdf_path) and
498 context.platform.startswith('linux')):
499 testargs.append('--no-sandbox')
500 if context.flash_path:
501 testargs.append('--ppapi-flash-path=%s' % context.flash_path)
502 # We have to pass a large enough Flash version, which currently needs not
503 # be correct. Instead of requiring the user of the script to figure out and
504 # pass the correct version we just spoof it.
505 testargs.append('--ppapi-flash-version=99.9.999.999')
507 # TODO(vitalybuka): Remove in the future. See crbug.com/395687.
509 shutil.copy(context.pdf_path,
510 os.path.dirname(context.GetLaunchPath(revision)))
511 testargs.append('--enable-print-preview')
514 for token in shlex.split(command):
516 runcommand.extend(testargs)
519 token.replace('%p', os.path.abspath(context.GetLaunchPath(revision))).
520 replace('%s', ' '.join(testargs)))
523 for _ in range(num_runs):
524 subproc = subprocess.Popen(runcommand,
526 stdout=subprocess.PIPE,
527 stderr=subprocess.PIPE)
528 (stdout, stderr) = subproc.communicate()
529 results.append((subproc.returncode, stdout, stderr))
533 shutil.rmtree(tempdir, True)
537 for (returncode, stdout, stderr) in results:
539 return (returncode, stdout, stderr)
543 # The arguments official_builds, status, stdout and stderr are unused.
544 # They are present here because this function is passed to Bisect which then
545 # calls it with 5 arguments.
546 # pylint: disable=W0613
547 def AskIsGoodBuild(rev, official_builds, status, stdout, stderr):
548 """Asks the user whether build |rev| is good or bad."""
549 # Loop until we get a response that we can parse.
551 response = raw_input('Revision %s is '
552 '[(g)ood/(b)ad/(r)etry/(u)nknown/(q)uit]: ' %
554 if response and response in ('g', 'b', 'r', 'u'):
556 if response and response == 'q':
560 def IsGoodASANBuild(rev, official_builds, status, stdout, stderr):
561 """Determine if an ASAN build |rev| is good or bad
563 Will examine stderr looking for the error message emitted by ASAN. If not
564 found then will fallback to asking the user."""
567 for line in stderr.splitlines():
569 if line.find('ERROR: AddressSanitizer:') != -1:
572 print 'Revision %d determined to be bad.' % rev
574 return AskIsGoodBuild(rev, official_builds, status, stdout, stderr)
576 class DownloadJob(object):
577 """DownloadJob represents a task to download a given Chromium revision."""
579 def __init__(self, context, name, rev, zip_file):
580 super(DownloadJob, self).__init__()
581 # Store off the input parameters.
582 self.context = context
585 self.zip_file = zip_file
586 self.quit_event = threading.Event()
587 self.progress_event = threading.Event()
591 """Starts the download."""
592 fetchargs = (self.context,
597 self.thread = threading.Thread(target=FetchRevision,
603 """Stops the download which must have been started previously."""
604 assert self.thread, 'DownloadJob must be started before Stop is called.'
605 self.quit_event.set()
607 os.unlink(self.zip_file)
610 """Prints a message and waits for the download to complete. The download
611 must have been started previously."""
612 assert self.thread, 'DownloadJob must be started before WaitFor is called.'
613 print 'Downloading revision %s...' % str(self.rev)
614 self.progress_event.set() # Display progress of download.
624 evaluate=AskIsGoodBuild):
625 """Given known good and known bad revisions, run a binary search on all
626 archived revisions to determine the last known good revision.
628 @param context PathContext object initialized with user provided parameters.
629 @param num_runs Number of times to run each build for asking good/bad.
630 @param try_args A tuple of arguments to pass to the test application.
631 @param profile The name of the user profile to run with.
632 @param interactive If it is false, use command exit code for good or bad
633 judgment of the argument build.
634 @param evaluate A function which returns 'g' if the argument build is good,
635 'b' if it's bad or 'u' if unknown.
637 Threading is used to fetch Chromium revisions in the background, speeding up
638 the user's experience. For example, suppose the bounds of the search are
639 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on
640 whether revision 50 is good or bad, the next revision to check will be either
641 25 or 75. So, while revision 50 is being checked, the script will download
642 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is
645 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
648 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
655 good_rev = context.good_revision
656 bad_rev = context.bad_revision
659 print 'Downloading list of known revisions...',
660 if not context.use_local_repo:
661 print '(use --use-local-repo for speed if you have a local checkout)'
664 _GetDownloadPath = lambda rev: os.path.join(cwd,
665 '%s-%s' % (str(rev), context.archive_name))
666 if context.is_official:
667 revlist = context.GetOfficialBuildsList()
669 revlist = context.GetRevList()
671 # Get a list of revisions to bisect across.
672 if len(revlist) < 2: # Don't have enough builds to bisect.
673 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist
674 raise RuntimeError(msg)
676 # Figure out our bookends and first pivot point; fetch the pivot revision.
678 maxrev = len(revlist) - 1
681 zip_file = _GetDownloadPath(rev)
682 fetch = DownloadJob(context, 'initial_fetch', rev, zip_file)
686 # Binary search time!
687 while fetch and fetch.zip_file and maxrev - minrev > 1:
688 if bad_rev < good_rev:
689 min_str, max_str = 'bad', 'good'
691 min_str, max_str = 'good', 'bad'
692 print 'Bisecting range [%s (%s), %s (%s)].' % (revlist[minrev], min_str,
693 revlist[maxrev], max_str)
695 # Pre-fetch next two possible pivots
696 # - down_pivot is the next revision to check if the current revision turns
698 # - up_pivot is the next revision to check if the current revision turns
700 down_pivot = int((pivot - minrev) / 2) + minrev
702 if down_pivot != pivot and down_pivot != minrev:
703 down_rev = revlist[down_pivot]
704 down_fetch = DownloadJob(context, 'down_fetch', down_rev,
705 _GetDownloadPath(down_rev))
708 up_pivot = int((maxrev - pivot) / 2) + pivot
710 if up_pivot != pivot and up_pivot != maxrev:
711 up_rev = revlist[up_pivot]
712 up_fetch = DownloadJob(context, 'up_fetch', up_rev,
713 _GetDownloadPath(up_rev))
716 # Run test on the pivot revision.
721 (status, stdout, stderr) = RunRevision(context,
729 print >> sys.stderr, e
731 # Call the evaluate function to see if the current revision is good or bad.
732 # On that basis, kill one of the background downloads and complete the
733 # other, as described in the comments above.
738 print 'Bad revision: %s' % rev
741 print 'Good revision: %s' % rev
743 answer = evaluate(rev, context.is_official, status, stdout, stderr)
744 if ((answer == 'g' and good_rev < bad_rev)
745 or (answer == 'b' and bad_rev < good_rev)):
749 down_fetch.Stop() # Kill the download of the older revision.
755 elif ((answer == 'b' and good_rev < bad_rev)
756 or (answer == 'g' and bad_rev < good_rev)):
760 up_fetch.Stop() # Kill the download of the newer revision.
767 pass # Retry requires no changes.
769 # Nuke the revision from the revlist and choose a new pivot.
772 maxrev -= 1 # Assumes maxrev >= pivot.
774 if maxrev - minrev > 1:
775 # Alternate between using down_pivot or up_pivot for the new pivot
776 # point, without affecting the range. Do this instead of setting the
777 # pivot to the midpoint of the new range because adjacent revisions
778 # are likely affected by the same issue that caused the (u)nknown
780 if up_fetch and down_fetch:
781 fetch = [up_fetch, down_fetch][len(revlist) % 2]
787 if fetch == up_fetch:
788 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized.
791 zip_file = fetch.zip_file
793 if down_fetch and fetch != down_fetch:
795 if up_fetch and fetch != up_fetch:
798 assert False, 'Unexpected return value from evaluate(): ' + answer
800 print 'Cleaning up...'
801 for f in [_GetDownloadPath(revlist[down_pivot]),
802 _GetDownloadPath(revlist[up_pivot])]:
811 return (revlist[minrev], revlist[maxrev], context)
814 def GetBlinkDEPSRevisionForChromiumRevision(rev):
815 """Returns the blink revision that was in REVISIONS file at
816 chromium revision |rev|."""
817 # . doesn't match newlines without re.DOTALL, so this is safe.
818 blink_re = re.compile(r'webkit_revision\D*(\d+)')
819 url = urllib.urlopen(DEPS_FILE % rev)
820 m = blink_re.search(url.read())
823 return int(m.group(1))
825 raise Exception('Could not get Blink revision for Chromium rev %d' % rev)
828 def GetBlinkRevisionForChromiumRevision(context, rev):
829 """Returns the blink revision that was in REVISIONS file at
830 chromium revision |rev|."""
831 def _IsRevisionNumber(revision):
832 if isinstance(revision, int):
835 return revision.isdigit()
836 if str(rev) in context.githash_svn_dict:
837 rev = context.githash_svn_dict[str(rev)]
838 file_url = '%s/%s%s/REVISIONS' % (context.base_url,
839 context._listing_platform_dir, rev)
840 url = urllib.urlopen(file_url)
841 if url.getcode() == 200:
843 data = json.loads(url.read())
845 print 'ValueError for JSON URL: %s' % file_url
850 if 'webkit_revision' in data:
851 blink_rev = data['webkit_revision']
852 if not _IsRevisionNumber(blink_rev):
853 blink_rev = int(context.GetSVNRevisionFromGitHash(blink_rev, 'blink'))
856 raise Exception('Could not get blink revision for cr rev %d' % rev)
859 def FixChromiumRevForBlink(revisions_final, revisions, self, rev):
860 """Returns the chromium revision that has the correct blink revision
861 for blink bisect, DEPS and REVISIONS file might not match since
862 blink snapshots point to tip of tree blink.
863 Note: The revisions_final variable might get modified to include
864 additional revisions."""
865 blink_deps_rev = GetBlinkDEPSRevisionForChromiumRevision(rev)
867 while (GetBlinkRevisionForChromiumRevision(self, rev) > blink_deps_rev):
868 idx = revisions.index(rev)
870 rev = revisions[idx-1]
871 if rev not in revisions_final:
872 revisions_final.insert(0, rev)
874 revisions_final.sort()
878 def GetChromiumRevision(context, url):
879 """Returns the chromium revision read from given URL."""
881 # Location of the latest build revision number
882 latest_revision = urllib.urlopen(url).read()
883 if latest_revision.isdigit():
884 return int(latest_revision)
885 return context.GetSVNRevisionFromGitHash(latest_revision)
887 print 'Could not determine latest revision. This could be bad...'
892 usage = ('%prog [options] [-- chromium-options]\n'
893 'Perform binary search on the snapshot builds to find a minimal\n'
894 'range of revisions where a behavior change happened. The\n'
895 'behaviors are described as "good" and "bad".\n'
896 'It is NOT assumed that the behavior of the later revision is\n'
899 'Revision numbers should use\n'
900 ' Official versions (e.g. 1.0.1000.0) for official builds. (-o)\n'
901 ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n'
902 ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n'
903 ' for earlier revs.\n'
904 ' Chrome\'s about: build number and omahaproxy branch_revision\n'
905 ' are incorrect, they are from branches.\n'
907 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
908 parser = optparse.OptionParser(usage=usage)
909 # Strangely, the default help output doesn't include the choice list.
910 choices = ['mac', 'win', 'linux', 'linux64', 'linux-arm']
911 # linux-chromiumos lacks a continuous archive http://crbug.com/78158
912 parser.add_option('-a', '--archive',
914 help='The buildbot archive to bisect [%s].' %
916 parser.add_option('-o',
918 dest='official_builds',
919 help='Bisect across official Chrome builds (internal '
920 'only) instead of Chromium archives.')
921 parser.add_option('-b', '--bad',
923 help='A bad revision to start bisection. '
924 'May be earlier or later than the good revision. '
926 parser.add_option('-f', '--flash_path',
928 help='Absolute path to a recent Adobe Pepper Flash '
929 'binary to be used in this bisection (e.g. '
930 'on Windows C:\...\pepflashplayer.dll and on Linux '
931 '/opt/google/chrome/PepperFlash/'
932 'libpepflashplayer.so).')
933 parser.add_option('-d', '--pdf_path',
935 help='Absolute path to a recent PDF plugin '
936 'binary to be used in this bisection (e.g. '
937 'on Windows C:\...\pdf.dll and on Linux '
938 '/opt/google/chrome/libpdf.so). Option also enables '
940 parser.add_option('-g', '--good',
942 help='A good revision to start bisection. ' +
943 'May be earlier or later than the bad revision. ' +
945 parser.add_option('-p', '--profile', '--user-data-dir',
948 help='Profile to use; this will not reset every run. '
949 'Defaults to a clean profile.')
950 parser.add_option('-t', '--times',
953 help='Number of times to run each build before asking '
954 'if it\'s good or bad. Temporary profiles are reused.')
955 parser.add_option('-c', '--command',
958 help='Command to execute. %p and %a refer to Chrome '
959 'executable and specified extra arguments '
960 'respectively. Use %s to specify all extra arguments '
961 'as one string. Defaults to "%p %a". Note that any '
962 'extra paths specified should be absolute.')
963 parser.add_option('-l', '--blink',
965 help='Use Blink bisect instead of Chromium. ')
966 parser.add_option('', '--not-interactive',
969 help='Use command exit code to tell good/bad revision.')
970 parser.add_option('--asan',
974 help='Allow the script to bisect ASAN builds')
975 parser.add_option('--use-local-repo',
976 dest='use_local_repo',
979 help='Allow the script to convert git SHA1 to SVN '
980 'revision using "git svn find-rev <SHA1>" '
981 'command from a Chromium checkout.')
983 (opts, args) = parser.parse_args()
985 if opts.archive is None:
986 print 'Error: missing required parameter: --archive'
992 supported_platforms = ['linux', 'mac', 'win']
993 if opts.archive not in supported_platforms:
994 print 'Error: ASAN bisecting only supported on these platforms: [%s].' % (
995 '|'.join(supported_platforms))
997 if opts.official_builds:
998 print 'Error: Do not yet support bisecting official ASAN builds.'
1002 base_url = ASAN_BASE_URL
1004 base_url = WEBKIT_BASE_URL
1006 base_url = CHROMIUM_BASE_URL
1008 # Create the context. Initialize 0 for the revisions as they are set below.
1009 context = PathContext(base_url, opts.archive, opts.good, opts.bad,
1010 opts.official_builds, opts.asan, opts.use_local_repo,
1011 opts.flash_path, opts.pdf_path)
1012 # Pick a starting point, try to get HEAD for this.
1014 context.bad_revision = '999.0.0.0'
1015 context.bad_revision = GetChromiumRevision(
1016 context, context.GetLastChangeURL())
1018 # Find out when we were good.
1020 context.good_revision = '0.0.0.0' if opts.official_builds else 0
1023 msg = 'Could not find Flash binary at %s' % opts.flash_path
1024 assert os.path.exists(opts.flash_path), msg
1027 msg = 'Could not find PDF binary at %s' % opts.pdf_path
1028 assert os.path.exists(opts.pdf_path), msg
1030 if opts.official_builds:
1031 context.good_revision = LooseVersion(context.good_revision)
1032 context.bad_revision = LooseVersion(context.bad_revision)
1034 context.good_revision = int(context.good_revision)
1035 context.bad_revision = int(context.bad_revision)
1038 print('Number of times to run (%d) must be greater than or equal to 1.' %
1044 evaluator = IsGoodASANBuild
1046 evaluator = AskIsGoodBuild
1048 # Save these revision numbers to compare when showing the changelog URL
1050 good_rev = context.good_revision
1051 bad_rev = context.bad_revision
1053 (min_chromium_rev, max_chromium_rev, context) = Bisect(
1054 context, opts.times, opts.command, args, opts.profile,
1055 not opts.not_interactive, evaluator)
1057 # Get corresponding blink revisions.
1059 min_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1061 max_blink_rev = GetBlinkRevisionForChromiumRevision(context,
1064 # Silently ignore the failure.
1065 min_blink_rev, max_blink_rev = 0, 0
1068 # We're done. Let the user know the results in an official manner.
1069 if good_rev > bad_rev:
1070 print DONE_MESSAGE_GOOD_MAX % (str(min_blink_rev), str(max_blink_rev))
1072 print DONE_MESSAGE_GOOD_MIN % (str(min_blink_rev), str(max_blink_rev))
1074 print 'BLINK CHANGELOG URL:'
1075 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev)
1078 # We're done. Let the user know the results in an official manner.
1079 if good_rev > bad_rev:
1080 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev),
1081 str(max_chromium_rev))
1083 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev),
1084 str(max_chromium_rev))
1085 if min_blink_rev != max_blink_rev:
1086 print ('NOTE: There is a Blink roll in the range, '
1087 'you might also want to do a Blink bisect.')
1089 print 'CHANGELOG URL:'
1090 if opts.official_builds:
1091 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
1093 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
1096 if __name__ == '__main__':