blob: a2bd6f7a2354800317eefba3aeb1c303e919c18d [file] [log] [blame]
[email protected]cb155a82011-11-29 17:25:341#!/usr/bin/env python
[email protected]5e93cf162012-01-28 02:16:562# Copyright (c) 2012 The Chromium Authors. All rights reserved.
[email protected]67e0bc62009-09-03 22:06:093# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Snapshot Build Bisect Tool
7
[email protected]7ad66a72009-09-04 17:52:338This script bisects a snapshot archive using binary search. It starts at
[email protected]67e0bc62009-09-03 22:06:099a bad revision (it will try to guess HEAD) and asks for a last known-good
10revision. It will then binary search across this revision range by downloading,
11unzipping, and opening Chromium for you. After testing the specific revision,
12it will ask you whether it is good or bad before continuing the search.
[email protected]67e0bc62009-09-03 22:06:0913"""
14
[email protected]183706d92011-06-10 13:06:2215# The root URL for storage.
[email protected]60ac66e32011-07-18 16:08:2516BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-snapshots'
[email protected]67e0bc62009-09-03 22:06:0917
[email protected]d0149c5c2012-05-29 21:12:1118# The root URL for official builds.
[email protected]b2905832012-07-19 21:28:4319OFFICIAL_BASE_URL = 'http://master.chrome.corp.google.com/official_builds'
[email protected]d0149c5c2012-05-29 21:12:1120
[email protected]183706d92011-06-10 13:06:2221# Changelogs URL.
[email protected]07247462010-12-24 07:45:5622CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \
[email protected]ff50d1c2013-04-17 18:49:3623 'perf/dashboard/ui/changelog.html?' \
24 'url=/trunk/src&range=%d%%3A%d'
[email protected]f6a71a72009-10-08 19:55:3825
[email protected]d0149c5c2012-05-29 21:12:1126# Official Changelogs URL.
27OFFICIAL_CHANGELOG_URL = 'http://omahaproxy.appspot.com/'\
28 'changelog?old_version=%s&new_version=%s'
29
[email protected]b2fe7f22011-10-25 22:58:3130# DEPS file URL.
31DEPS_FILE= 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d'
[email protected]ff50d1c2013-04-17 18:49:3632# Blink Changelogs URL.
33BLINK_CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \
34 'perf/dashboard/ui/changelog_blink.html?' \
35 'url=/trunk&range=%d%%3A%d'
[email protected]b2fe7f22011-10-25 22:58:3136
[email protected]eadd95d2012-11-02 22:42:0937DONE_MESSAGE_GOOD_MIN = 'You are probably looking for a change made after %s ' \
38 '(known good), but no later than %s (first known bad).'
39DONE_MESSAGE_GOOD_MAX = 'You are probably looking for a change made after %s ' \
40 '(known bad), but no later than %s (first known good).'
[email protected]05ff3fd2012-04-17 23:24:0641
[email protected]67e0bc62009-09-03 22:06:0942###############################################################################
43
44import math
[email protected]7ad66a72009-09-04 17:52:3345import optparse
[email protected]67e0bc62009-09-03 22:06:0946import os
[email protected]d4bf3582009-09-20 00:56:3847import pipes
[email protected]67e0bc62009-09-03 22:06:0948import re
49import shutil
[email protected]afe30662011-07-30 01:05:5250import subprocess
[email protected]67e0bc62009-09-03 22:06:0951import sys
[email protected]7ad66a72009-09-04 17:52:3352import tempfile
[email protected]afe30662011-07-30 01:05:5253import threading
[email protected]67e0bc62009-09-03 22:06:0954import urllib
[email protected]d0149c5c2012-05-29 21:12:1155from distutils.version import LooseVersion
[email protected]183706d92011-06-10 13:06:2256from xml.etree import ElementTree
[email protected]bd8dcb92010-03-31 01:05:2457import zipfile
58
[email protected]cb155a82011-11-29 17:25:3459
[email protected]183706d92011-06-10 13:06:2260class PathContext(object):
61 """A PathContext is used to carry the information used to construct URLs and
62 paths when dealing with the storage server and archives."""
[email protected]d0149c5c2012-05-29 21:12:1163 def __init__(self, platform, good_revision, bad_revision, is_official):
[email protected]183706d92011-06-10 13:06:2264 super(PathContext, self).__init__()
65 # Store off the input parameters.
66 self.platform = platform # What's passed in to the '-a/--archive' option.
67 self.good_revision = good_revision
68 self.bad_revision = bad_revision
[email protected]d0149c5c2012-05-29 21:12:1169 self.is_official = is_official
[email protected]183706d92011-06-10 13:06:2270
71 # The name of the ZIP file in a revision directory on the server.
72 self.archive_name = None
73
74 # Set some internal members:
75 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
76 # _archive_extract_dir = Uncompressed directory in the archive_name file.
77 # _binary_name = The name of the executable to run.
[email protected]1960edd2011-07-01 16:53:5278 if self.platform == 'linux' or self.platform == 'linux64':
[email protected]183706d92011-06-10 13:06:2279 self._binary_name = 'chrome'
[email protected]183706d92011-06-10 13:06:2280 elif self.platform == 'mac':
[email protected]183706d92011-06-10 13:06:2281 self.archive_name = 'chrome-mac.zip'
82 self._archive_extract_dir = 'chrome-mac'
[email protected]183706d92011-06-10 13:06:2283 elif self.platform == 'win':
[email protected]183706d92011-06-10 13:06:2284 self.archive_name = 'chrome-win32.zip'
85 self._archive_extract_dir = 'chrome-win32'
86 self._binary_name = 'chrome.exe'
87 else:
[email protected]afe30662011-07-30 01:05:5288 raise Exception('Invalid platform: %s' % self.platform)
[email protected]183706d92011-06-10 13:06:2289
[email protected]d0149c5c2012-05-29 21:12:1190 if is_official:
91 if self.platform == 'linux':
92 self._listing_platform_dir = 'lucid32bit/'
93 self.archive_name = 'chrome-lucid32bit.zip'
94 self._archive_extract_dir = 'chrome-lucid32bit'
95 elif self.platform == 'linux64':
96 self._listing_platform_dir = 'lucid64bit/'
97 self.archive_name = 'chrome-lucid64bit.zip'
98 self._archive_extract_dir = 'chrome-lucid64bit'
99 elif self.platform == 'mac':
100 self._listing_platform_dir = 'mac/'
101 self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome'
102 elif self.platform == 'win':
103 self._listing_platform_dir = 'win/'
104 else:
105 if self.platform == 'linux' or self.platform == 'linux64':
106 self.archive_name = 'chrome-linux.zip'
107 self._archive_extract_dir = 'chrome-linux'
108 if self.platform == 'linux':
109 self._listing_platform_dir = 'Linux/'
110 elif self.platform == 'linux64':
111 self._listing_platform_dir = 'Linux_x64/'
112 elif self.platform == 'mac':
113 self._listing_platform_dir = 'Mac/'
114 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium'
115 elif self.platform == 'win':
116 self._listing_platform_dir = 'Win/'
117
[email protected]183706d92011-06-10 13:06:22118 def GetListingURL(self, marker=None):
119 """Returns the URL for a directory listing, with an optional marker."""
120 marker_param = ''
121 if marker:
122 marker_param = '&marker=' + str(marker)
123 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \
124 marker_param
125
126 def GetDownloadURL(self, revision):
127 """Gets the download URL for a build archive of a specific revision."""
[email protected]d0149c5c2012-05-29 21:12:11128 if self.is_official:
129 return "%s/%s/%s%s" % (
130 OFFICIAL_BASE_URL, revision, self._listing_platform_dir,
131 self.archive_name)
132 else:
133 return "%s/%s%s/%s" % (
134 BASE_URL, self._listing_platform_dir, revision, self.archive_name)
[email protected]183706d92011-06-10 13:06:22135
136 def GetLastChangeURL(self):
137 """Returns a URL to the LAST_CHANGE file."""
138 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE'
139
140 def GetLaunchPath(self):
141 """Returns a relative path (presumably from the archive extraction location)
142 that is used to run the executable."""
143 return os.path.join(self._archive_extract_dir, self._binary_name)
144
[email protected]afe30662011-07-30 01:05:52145 def ParseDirectoryIndex(self):
146 """Parses the Google Storage directory listing into a list of revision
[email protected]eadd95d2012-11-02 22:42:09147 numbers."""
[email protected]afe30662011-07-30 01:05:52148
149 def _FetchAndParse(url):
150 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If
151 next-marker is not None, then the listing is a partial listing and another
152 fetch should be performed with next-marker being the marker= GET
153 parameter."""
154 handle = urllib.urlopen(url)
155 document = ElementTree.parse(handle)
156
157 # All nodes in the tree are namespaced. Get the root's tag name to extract
158 # the namespace. Etree does namespaces as |{namespace}tag|.
159 root_tag = document.getroot().tag
160 end_ns_pos = root_tag.find('}')
161 if end_ns_pos == -1:
162 raise Exception("Could not locate end namespace for directory index")
163 namespace = root_tag[:end_ns_pos + 1]
164
165 # Find the prefix (_listing_platform_dir) and whether or not the list is
166 # truncated.
167 prefix_len = len(document.find(namespace + 'Prefix').text)
168 next_marker = None
169 is_truncated = document.find(namespace + 'IsTruncated')
170 if is_truncated is not None and is_truncated.text.lower() == 'true':
171 next_marker = document.find(namespace + 'NextMarker').text
172
173 # Get a list of all the revisions.
174 all_prefixes = document.findall(namespace + 'CommonPrefixes/' +
175 namespace + 'Prefix')
176 # The <Prefix> nodes have content of the form of
177 # |_listing_platform_dir/revision/|. Strip off the platform dir and the
178 # trailing slash to just have a number.
179 revisions = []
180 for prefix in all_prefixes:
181 revnum = prefix.text[prefix_len:-1]
182 try:
183 revnum = int(revnum)
184 revisions.append(revnum)
185 except ValueError:
186 pass
187 return (revisions, next_marker)
[email protected]d0149c5c2012-05-29 21:12:11188
[email protected]afe30662011-07-30 01:05:52189 # Fetch the first list of revisions.
190 (revisions, next_marker) = _FetchAndParse(self.GetListingURL())
191
192 # If the result list was truncated, refetch with the next marker. Do this
193 # until an entire directory listing is done.
194 while next_marker:
195 next_url = self.GetListingURL(next_marker)
196 (new_revisions, next_marker) = _FetchAndParse(next_url)
197 revisions.extend(new_revisions)
[email protected]afe30662011-07-30 01:05:52198 return revisions
199
200 def GetRevList(self):
201 """Gets the list of revision numbers between self.good_revision and
202 self.bad_revision."""
203 # Download the revlist and filter for just the range between good and bad.
[email protected]eadd95d2012-11-02 22:42:09204 minrev = min(self.good_revision, self.bad_revision)
205 maxrev = max(self.good_revision, self.bad_revision)
[email protected]afe30662011-07-30 01:05:52206 revlist = map(int, self.ParseDirectoryIndex())
[email protected]d0149c5c2012-05-29 21:12:11207 revlist = [x for x in revlist if x >= int(minrev) and x <= int(maxrev)]
[email protected]afe30662011-07-30 01:05:52208 revlist.sort()
209 return revlist
210
[email protected]d0149c5c2012-05-29 21:12:11211 def GetOfficialBuildsList(self):
212 """Gets the list of official build numbers between self.good_revision and
213 self.bad_revision."""
214 # Download the revlist and filter for just the range between good and bad.
[email protected]eadd95d2012-11-02 22:42:09215 minrev = min(self.good_revision, self.bad_revision)
216 maxrev = max(self.good_revision, self.bad_revision)
[email protected]d0149c5c2012-05-29 21:12:11217 handle = urllib.urlopen(OFFICIAL_BASE_URL)
218 dirindex = handle.read()
219 handle.close()
220 build_numbers = re.findall(r'<a href="([0-9][0-9].*)/">', dirindex)
221 final_list = []
[email protected]d0149c5c2012-05-29 21:12:11222 i = 0
[email protected]d0149c5c2012-05-29 21:12:11223 parsed_build_numbers = [LooseVersion(x) for x in build_numbers]
224 for build_number in sorted(parsed_build_numbers):
225 path = OFFICIAL_BASE_URL + '/' + str(build_number) + '/' + \
226 self._listing_platform_dir + self.archive_name
227 i = i + 1
228 try:
229 connection = urllib.urlopen(path)
230 connection.close()
[email protected]801fb652012-07-20 20:13:50231 if build_number > maxrev:
232 break
233 if build_number >= minrev:
234 final_list.append(str(build_number))
[email protected]d0149c5c2012-05-29 21:12:11235 except urllib.HTTPError, e:
236 pass
[email protected]801fb652012-07-20 20:13:50237 return final_list
[email protected]bd8dcb92010-03-31 01:05:24238
239def UnzipFilenameToDir(filename, dir):
240 """Unzip |filename| to directory |dir|."""
[email protected]afe30662011-07-30 01:05:52241 cwd = os.getcwd()
242 if not os.path.isabs(filename):
243 filename = os.path.join(cwd, filename)
[email protected]bd8dcb92010-03-31 01:05:24244 zf = zipfile.ZipFile(filename)
245 # Make base.
[email protected]e29c08c2012-09-17 20:50:50246 if not os.path.isdir(dir):
247 os.mkdir(dir)
248 os.chdir(dir)
249 # Extract files.
250 for info in zf.infolist():
251 name = info.filename
252 if name.endswith('/'): # dir
253 if not os.path.isdir(name):
254 os.makedirs(name)
255 else: # file
256 dir = os.path.dirname(name)
257 if not os.path.isdir(dir):
258 os.makedirs(dir)
259 out = open(name, 'wb')
260 out.write(zf.read(name))
261 out.close()
262 # Set permissions. Permission info in external_attr is shifted 16 bits.
263 os.chmod(name, info.external_attr >> 16L)
264 os.chdir(cwd)
[email protected]bd8dcb92010-03-31 01:05:24265
[email protected]67e0bc62009-09-03 22:06:09266
[email protected]468a9772011-08-09 18:42:00267def FetchRevision(context, rev, filename, quit_event=None, progress_event=None):
[email protected]afe30662011-07-30 01:05:52268 """Downloads and unzips revision |rev|.
269 @param context A PathContext instance.
270 @param rev The Chromium revision number/tag to download.
271 @param filename The destination for the downloaded file.
272 @param quit_event A threading.Event which will be set by the master thread to
273 indicate that the download should be aborted.
[email protected]468a9772011-08-09 18:42:00274 @param progress_event A threading.Event which will be set by the master thread
275 to indicate that the progress of the download should be
276 displayed.
[email protected]afe30662011-07-30 01:05:52277 """
278 def ReportHook(blocknum, blocksize, totalsize):
[email protected]946be752011-10-25 23:34:21279 if quit_event and quit_event.isSet():
[email protected]d0149c5c2012-05-29 21:12:11280 raise RuntimeError("Aborting download of revision %s" % str(rev))
[email protected]946be752011-10-25 23:34:21281 if progress_event and progress_event.isSet():
[email protected]468a9772011-08-09 18:42:00282 size = blocknum * blocksize
283 if totalsize == -1: # Total size not known.
284 progress = "Received %d bytes" % size
285 else:
286 size = min(totalsize, size)
287 progress = "Received %d of %d bytes, %.2f%%" % (
288 size, totalsize, 100.0 * size / totalsize)
289 # Send a \r to let all progress messages use just one line of output.
290 sys.stdout.write("\r" + progress)
291 sys.stdout.flush()
[email protected]7ad66a72009-09-04 17:52:33292
[email protected]afe30662011-07-30 01:05:52293 download_url = context.GetDownloadURL(rev)
294 try:
295 urllib.urlretrieve(download_url, filename, ReportHook)
[email protected]946be752011-10-25 23:34:21296 if progress_event and progress_event.isSet():
[email protected]ecaba01e62011-10-26 05:33:28297 print
[email protected]afe30662011-07-30 01:05:52298 except RuntimeError, e:
299 pass
[email protected]7ad66a72009-09-04 17:52:33300
[email protected]7ad66a72009-09-04 17:52:33301
[email protected]5e93cf162012-01-28 02:16:56302def RunRevision(context, revision, zipfile, profile, num_runs, args):
[email protected]afe30662011-07-30 01:05:52303 """Given a zipped revision, unzip it and run the test."""
[email protected]d0149c5c2012-05-29 21:12:11304 print "Trying revision %s..." % str(revision)
[email protected]3ff00b72011-07-20 21:34:47305
[email protected]afe30662011-07-30 01:05:52306 # Create a temp directory and unzip the revision into it.
[email protected]7ad66a72009-09-04 17:52:33307 cwd = os.getcwd()
308 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
[email protected]afe30662011-07-30 01:05:52309 UnzipFilenameToDir(zipfile, tempdir)
[email protected]7ad66a72009-09-04 17:52:33310 os.chdir(tempdir)
[email protected]67e0bc62009-09-03 22:06:09311
[email protected]5e93cf162012-01-28 02:16:56312 # Run the build as many times as specified.
[email protected]afe30662011-07-30 01:05:52313 testargs = [context.GetLaunchPath(), '--user-data-dir=%s' % profile] + args
[email protected]d0149c5c2012-05-29 21:12:11314 # The sandbox must be run as root on Official Chrome, so bypass it.
315 if context.is_official and (context.platform == 'linux' or
316 context.platform == 'linux64'):
317 testargs.append('--no-sandbox')
318
[email protected]5e93cf162012-01-28 02:16:56319 for i in range(0, num_runs):
320 subproc = subprocess.Popen(testargs,
321 bufsize=-1,
322 stdout=subprocess.PIPE,
323 stderr=subprocess.PIPE)
324 (stdout, stderr) = subproc.communicate()
[email protected]7ad66a72009-09-04 17:52:33325
326 os.chdir(cwd)
[email protected]7ad66a72009-09-04 17:52:33327 try:
328 shutil.rmtree(tempdir, True)
329 except Exception, e:
330 pass
[email protected]67e0bc62009-09-03 22:06:09331
[email protected]afe30662011-07-30 01:05:52332 return (subproc.returncode, stdout, stderr)
[email protected]79f14742010-03-10 01:01:57333
[email protected]cb155a82011-11-29 17:25:34334
[email protected]d0149c5c2012-05-29 21:12:11335def AskIsGoodBuild(rev, official_builds, status, stdout, stderr):
[email protected]183706d92011-06-10 13:06:22336 """Ask the user whether build |rev| is good or bad."""
[email protected]79f14742010-03-10 01:01:57337 # Loop until we get a response that we can parse.
[email protected]67e0bc62009-09-03 22:06:09338 while True:
[email protected]53bb6342012-06-01 04:11:00339 response = raw_input('Revision %s is [(g)ood/(b)ad/(u)nknown/(q)uit]: ' %
340 str(rev))
341 if response and response in ('g', 'b', 'u'):
342 return response
[email protected]afe30662011-07-30 01:05:52343 if response and response == 'q':
344 raise SystemExit()
[email protected]67e0bc62009-09-03 22:06:09345
[email protected]cb155a82011-11-29 17:25:34346
[email protected]53bb6342012-06-01 04:11:00347class DownloadJob(object):
348 """DownloadJob represents a task to download a given Chromium revision."""
349 def __init__(self, context, name, rev, zipfile):
350 super(DownloadJob, self).__init__()
351 # Store off the input parameters.
352 self.context = context
353 self.name = name
354 self.rev = rev
355 self.zipfile = zipfile
356 self.quit_event = threading.Event()
357 self.progress_event = threading.Event()
358
359 def Start(self):
360 """Starts the download."""
361 fetchargs = (self.context,
362 self.rev,
363 self.zipfile,
364 self.quit_event,
365 self.progress_event)
366 self.thread = threading.Thread(target=FetchRevision,
367 name=self.name,
368 args=fetchargs)
369 self.thread.start()
370
371 def Stop(self):
372 """Stops the download which must have been started previously."""
373 self.quit_event.set()
374 self.thread.join()
375 os.unlink(self.zipfile)
376
377 def WaitFor(self):
378 """Prints a message and waits for the download to complete. The download
379 must have been started previously."""
380 print "Downloading revision %s..." % str(self.rev)
381 self.progress_event.set() # Display progress of download.
382 self.thread.join()
383
384
[email protected]afe30662011-07-30 01:05:52385def Bisect(platform,
[email protected]d0149c5c2012-05-29 21:12:11386 official_builds,
[email protected]afe30662011-07-30 01:05:52387 good_rev=0,
388 bad_rev=0,
[email protected]5e93cf162012-01-28 02:16:56389 num_runs=1,
[email protected]60ac66e32011-07-18 16:08:25390 try_args=(),
[email protected]afe30662011-07-30 01:05:52391 profile=None,
[email protected]53bb6342012-06-01 04:11:00392 evaluate=AskIsGoodBuild):
[email protected]afe30662011-07-30 01:05:52393 """Given known good and known bad revisions, run a binary search on all
394 archived revisions to determine the last known good revision.
[email protected]60ac66e32011-07-18 16:08:25395
[email protected]afe30662011-07-30 01:05:52396 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.).
[email protected]d0149c5c2012-05-29 21:12:11397 @param official_builds Specify build type (Chromium or Official build).
[email protected]eadd95d2012-11-02 22:42:09398 @param good_rev Number/tag of the known good revision.
399 @param bad_rev Number/tag of the known bad revision.
[email protected]5e93cf162012-01-28 02:16:56400 @param num_runs Number of times to run each build for asking good/bad.
[email protected]afe30662011-07-30 01:05:52401 @param try_args A tuple of arguments to pass to the test application.
402 @param profile The name of the user profile to run with.
[email protected]53bb6342012-06-01 04:11:00403 @param evaluate A function which returns 'g' if the argument build is good,
404 'b' if it's bad or 'u' if unknown.
[email protected]afe30662011-07-30 01:05:52405
406 Threading is used to fetch Chromium revisions in the background, speeding up
407 the user's experience. For example, suppose the bounds of the search are
408 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on
409 whether revision 50 is good or bad, the next revision to check will be either
410 25 or 75. So, while revision 50 is being checked, the script will download
411 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is
412 known:
413
414 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
415 is run on rev 75.
416
417 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
418 is run on rev 25.
[email protected]60ac66e32011-07-18 16:08:25419 """
420
[email protected]afe30662011-07-30 01:05:52421 if not profile:
422 profile = 'profile'
423
[email protected]d0149c5c2012-05-29 21:12:11424 context = PathContext(platform, good_rev, bad_rev, official_builds)
[email protected]afe30662011-07-30 01:05:52425 cwd = os.getcwd()
426
[email protected]d0149c5c2012-05-29 21:12:11427
[email protected]afe30662011-07-30 01:05:52428
[email protected]468a9772011-08-09 18:42:00429 print "Downloading list of known revisions..."
[email protected]d0149c5c2012-05-29 21:12:11430 _GetDownloadPath = lambda rev: os.path.join(cwd,
431 '%s-%s' % (str(rev), context.archive_name))
432 if official_builds:
433 revlist = context.GetOfficialBuildsList()
434 else:
435 revlist = context.GetRevList()
[email protected]afe30662011-07-30 01:05:52436
437 # Get a list of revisions to bisect across.
438 if len(revlist) < 2: # Don't have enough builds to bisect.
439 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist
440 raise RuntimeError(msg)
441
442 # Figure out our bookends and first pivot point; fetch the pivot revision.
[email protected]eadd95d2012-11-02 22:42:09443 minrev = 0
444 maxrev = len(revlist) - 1
445 pivot = maxrev / 2
[email protected]afe30662011-07-30 01:05:52446 rev = revlist[pivot]
447 zipfile = _GetDownloadPath(rev)
[email protected]eadd95d2012-11-02 22:42:09448 fetch = DownloadJob(context, 'initial_fetch', rev, zipfile)
449 fetch.Start()
450 fetch.WaitFor()
[email protected]60ac66e32011-07-18 16:08:25451
452 # Binary search time!
[email protected]eadd95d2012-11-02 22:42:09453 while fetch and fetch.zipfile and maxrev - minrev > 1:
454 if bad_rev < good_rev:
455 min_str, max_str = "bad", "good"
456 else:
457 min_str, max_str = "good", "bad"
458 print 'Bisecting range [%s (%s), %s (%s)].' % (revlist[minrev], min_str, \
459 revlist[maxrev], max_str)
460
[email protected]afe30662011-07-30 01:05:52461 # Pre-fetch next two possible pivots
462 # - down_pivot is the next revision to check if the current revision turns
463 # out to be bad.
464 # - up_pivot is the next revision to check if the current revision turns
465 # out to be good.
[email protected]eadd95d2012-11-02 22:42:09466 down_pivot = int((pivot - minrev) / 2) + minrev
[email protected]53bb6342012-06-01 04:11:00467 down_fetch = None
[email protected]eadd95d2012-11-02 22:42:09468 if down_pivot != pivot and down_pivot != minrev:
[email protected]afe30662011-07-30 01:05:52469 down_rev = revlist[down_pivot]
[email protected]53bb6342012-06-01 04:11:00470 down_fetch = DownloadJob(context, 'down_fetch', down_rev,
471 _GetDownloadPath(down_rev))
472 down_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25473
[email protected]eadd95d2012-11-02 22:42:09474 up_pivot = int((maxrev - pivot) / 2) + pivot
[email protected]53bb6342012-06-01 04:11:00475 up_fetch = None
[email protected]eadd95d2012-11-02 22:42:09476 if up_pivot != pivot and up_pivot != maxrev:
[email protected]afe30662011-07-30 01:05:52477 up_rev = revlist[up_pivot]
[email protected]53bb6342012-06-01 04:11:00478 up_fetch = DownloadJob(context, 'up_fetch', up_rev,
479 _GetDownloadPath(up_rev))
480 up_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25481
[email protected]afe30662011-07-30 01:05:52482 # Run test on the pivot revision.
[email protected]e29c08c2012-09-17 20:50:50483 status = None
484 stdout = None
485 stderr = None
486 try:
487 (status, stdout, stderr) = RunRevision(context,
488 rev,
[email protected]eadd95d2012-11-02 22:42:09489 fetch.zipfile,
[email protected]e29c08c2012-09-17 20:50:50490 profile,
491 num_runs,
492 try_args)
493 except Exception, e:
494 print >>sys.stderr, e
[email protected]eadd95d2012-11-02 22:42:09495 fetch.Stop()
496 fetch = None
[email protected]60ac66e32011-07-18 16:08:25497
[email protected]53bb6342012-06-01 04:11:00498 # Call the evaluate function to see if the current revision is good or bad.
[email protected]afe30662011-07-30 01:05:52499 # On that basis, kill one of the background downloads and complete the
500 # other, as described in the comments above.
501 try:
[email protected]53bb6342012-06-01 04:11:00502 answer = evaluate(rev, official_builds, status, stdout, stderr)
[email protected]eadd95d2012-11-02 22:42:09503 if answer == 'g' and good_rev < bad_rev or \
504 answer == 'b' and bad_rev < good_rev:
505 minrev = pivot
[email protected]53bb6342012-06-01 04:11:00506 if down_fetch:
507 down_fetch.Stop() # Kill the download of the older revision.
508 if up_fetch:
509 up_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52510 pivot = up_pivot
[email protected]eadd95d2012-11-02 22:42:09511 fetch = up_fetch
512 elif answer == 'b' and good_rev < bad_rev or \
513 answer == 'g' and bad_rev < good_rev:
514 maxrev = pivot
[email protected]53bb6342012-06-01 04:11:00515 if up_fetch:
516 up_fetch.Stop() # Kill the download of the newer revision.
517 if down_fetch:
518 down_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52519 pivot = down_pivot
[email protected]eadd95d2012-11-02 22:42:09520 fetch = down_fetch
[email protected]53bb6342012-06-01 04:11:00521 elif answer == 'u':
522 # Nuke the revision from the revlist and choose a new pivot.
523 revlist.pop(pivot)
[email protected]eadd95d2012-11-02 22:42:09524 maxrev -= 1 # Assumes maxrev >= pivot.
[email protected]53bb6342012-06-01 04:11:00525
[email protected]eadd95d2012-11-02 22:42:09526 if maxrev - minrev > 1:
[email protected]53bb6342012-06-01 04:11:00527 # Alternate between using down_pivot or up_pivot for the new pivot
528 # point, without affecting the range. Do this instead of setting the
529 # pivot to the midpoint of the new range because adjacent revisions
530 # are likely affected by the same issue that caused the (u)nknown
531 # response.
532 if up_fetch and down_fetch:
533 fetch = [up_fetch, down_fetch][len(revlist) % 2]
534 elif up_fetch:
535 fetch = up_fetch
536 else:
537 fetch = down_fetch
538 fetch.WaitFor()
539 if fetch == up_fetch:
540 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized.
541 else:
542 pivot = down_pivot
543 zipfile = fetch.zipfile
544
545 if down_fetch and fetch != down_fetch:
546 down_fetch.Stop()
547 if up_fetch and fetch != up_fetch:
548 up_fetch.Stop()
549 else:
550 assert False, "Unexpected return value from evaluate(): " + answer
[email protected]afe30662011-07-30 01:05:52551 except SystemExit:
[email protected]468a9772011-08-09 18:42:00552 print "Cleaning up..."
[email protected]5e93cf162012-01-28 02:16:56553 for f in [_GetDownloadPath(revlist[down_pivot]),
554 _GetDownloadPath(revlist[up_pivot])]:
[email protected]afe30662011-07-30 01:05:52555 try:
556 os.unlink(f)
557 except OSError:
558 pass
559 sys.exit(0)
560
561 rev = revlist[pivot]
562
[email protected]eadd95d2012-11-02 22:42:09563 return (revlist[minrev], revlist[maxrev])
[email protected]60ac66e32011-07-18 16:08:25564
565
[email protected]ff50d1c2013-04-17 18:49:36566def GetBlinkRevisionForChromiumRevision(rev):
567 """Returns the blink revision that was in chromium's DEPS file at
[email protected]b2fe7f22011-10-25 22:58:31568 chromium revision |rev|."""
569 # . doesn't match newlines without re.DOTALL, so this is safe.
[email protected]ff50d1c2013-04-17 18:49:36570 blink_re = re.compile(r'webkit_revision.:\D*(\d+)')
[email protected]b2fe7f22011-10-25 22:58:31571 url = urllib.urlopen(DEPS_FILE % rev)
[email protected]ff50d1c2013-04-17 18:49:36572 m = blink_re.search(url.read())
[email protected]b2fe7f22011-10-25 22:58:31573 url.close()
574 if m:
575 return int(m.group(1))
576 else:
[email protected]ff50d1c2013-04-17 18:49:36577 raise Exception('Could not get blink revision for cr rev %d' % rev)
[email protected]b2fe7f22011-10-25 22:58:31578
579
[email protected]801fb652012-07-20 20:13:50580def GetChromiumRevision(url):
581 """Returns the chromium revision read from given URL."""
582 try:
583 # Location of the latest build revision number
584 return int(urllib.urlopen(url).read())
585 except Exception, e:
586 print('Could not determine latest revision. This could be bad...')
587 return 999999999
588
589
[email protected]67e0bc62009-09-03 22:06:09590def main():
[email protected]2c1d2732009-10-29 19:52:17591 usage = ('%prog [options] [-- chromium-options]\n'
[email protected]887c9182013-02-12 20:30:31592 'Perform binary search on the snapshot builds to find a minimal\n'
593 'range of revisions where a behavior change happened. The\n'
594 'behaviors are described as "good" and "bad".\n'
595 'It is NOT assumed that the behavior of the later revision is\n'
[email protected]09c58da2013-01-07 21:30:17596 'the bad one.\n'
[email protected]178aab72010-10-08 17:21:38597 '\n'
[email protected]887c9182013-02-12 20:30:31598 'Revision numbers should use\n'
599 ' Official versions (e.g. 1.0.1000.0) for official builds. (-o)\n'
600 ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n'
601 ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n'
602 ' for earlier revs.\n'
603 ' Chrome\'s about: build number and omahaproxy branch_revision\n'
604 ' are incorrect, they are from branches.\n'
605 '\n'
[email protected]178aab72010-10-08 17:21:38606 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
[email protected]7ad66a72009-09-04 17:52:33607 parser = optparse.OptionParser(usage=usage)
[email protected]1a45d222009-09-19 01:58:57608 # Strangely, the default help output doesn't include the choice list.
[email protected]20105cf2011-05-10 18:16:45609 choices = ['mac', 'win', 'linux', 'linux64']
[email protected]4082b182011-05-02 20:30:17610 # linux-chromiumos lacks a continuous archive http://crbug.com/78158
[email protected]7ad66a72009-09-04 17:52:33611 parser.add_option('-a', '--archive',
[email protected]1a45d222009-09-19 01:58:57612 choices = choices,
613 help = 'The buildbot archive to bisect [%s].' %
614 '|'.join(choices))
[email protected]d0149c5c2012-05-29 21:12:11615 parser.add_option('-o', action="store_true", dest='official_builds',
616 help = 'Bisect across official ' +
617 'Chrome builds (internal only) instead of ' +
618 'Chromium archives.')
619 parser.add_option('-b', '--bad', type = 'str',
[email protected]09c58da2013-01-07 21:30:17620 help = 'A bad revision to start bisection. ' +
621 'May be earlier or later than the good revision. ' +
622 'Default is HEAD.')
[email protected]d0149c5c2012-05-29 21:12:11623 parser.add_option('-g', '--good', type = 'str',
[email protected]09c58da2013-01-07 21:30:17624 help = 'A good revision to start bisection. ' +
625 'May be earlier or later than the bad revision. ' +
[email protected]801fb652012-07-20 20:13:50626 'Default is 0.')
[email protected]d4bf3582009-09-20 00:56:38627 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str',
628 help = 'Profile to use; this will not reset every run. ' +
[email protected]60ac66e32011-07-18 16:08:25629 'Defaults to a clean profile.', default = 'profile')
[email protected]5e93cf162012-01-28 02:16:56630 parser.add_option('-t', '--times', type = 'int',
631 help = 'Number of times to run each build before asking ' +
632 'if it\'s good or bad. Temporary profiles are reused.',
633 default = 1)
[email protected]7ad66a72009-09-04 17:52:33634 (opts, args) = parser.parse_args()
635
636 if opts.archive is None:
[email protected]178aab72010-10-08 17:21:38637 print 'Error: missing required parameter: --archive'
638 print
[email protected]7ad66a72009-09-04 17:52:33639 parser.print_help()
640 return 1
641
[email protected]183706d92011-06-10 13:06:22642 # Create the context. Initialize 0 for the revisions as they are set below.
[email protected]d0149c5c2012-05-29 21:12:11643 context = PathContext(opts.archive, 0, 0, opts.official_builds)
[email protected]67e0bc62009-09-03 22:06:09644 # Pick a starting point, try to get HEAD for this.
[email protected]7ad66a72009-09-04 17:52:33645 if opts.bad:
646 bad_rev = opts.bad
647 else:
[email protected]801fb652012-07-20 20:13:50648 bad_rev = '999.0.0.0'
649 if not opts.official_builds:
650 bad_rev = GetChromiumRevision(context.GetLastChangeURL())
[email protected]67e0bc62009-09-03 22:06:09651
652 # Find out when we were good.
[email protected]7ad66a72009-09-04 17:52:33653 if opts.good:
654 good_rev = opts.good
655 else:
[email protected]801fb652012-07-20 20:13:50656 good_rev = '0.0.0.0' if opts.official_builds else 0
657
658 if opts.official_builds:
659 good_rev = LooseVersion(good_rev)
660 bad_rev = LooseVersion(bad_rev)
661 else:
662 good_rev = int(good_rev)
663 bad_rev = int(bad_rev)
664
[email protected]5e93cf162012-01-28 02:16:56665 if opts.times < 1:
666 print('Number of times to run (%d) must be greater than or equal to 1.' %
667 opts.times)
668 parser.print_help()
669 return 1
670
[email protected]eadd95d2012-11-02 22:42:09671 (min_chromium_rev, max_chromium_rev) = Bisect(
[email protected]d0149c5c2012-05-29 21:12:11672 opts.archive, opts.official_builds, good_rev, bad_rev, opts.times, args,
673 opts.profile)
[email protected]67e0bc62009-09-03 22:06:09674
[email protected]ff50d1c2013-04-17 18:49:36675 # Get corresponding blink revisions.
[email protected]b2fe7f22011-10-25 22:58:31676 try:
[email protected]ff50d1c2013-04-17 18:49:36677 min_blink_rev = GetBlinkRevisionForChromiumRevision(min_chromium_rev)
678 max_blink_rev = GetBlinkRevisionForChromiumRevision(max_chromium_rev)
[email protected]b2fe7f22011-10-25 22:58:31679 except Exception, e:
680 # Silently ignore the failure.
[email protected]ff50d1c2013-04-17 18:49:36681 min_blink_rev, max_blink_rev = 0, 0
[email protected]b2fe7f22011-10-25 22:58:31682
[email protected]67e0bc62009-09-03 22:06:09683 # We're done. Let the user know the results in an official manner.
[email protected]eadd95d2012-11-02 22:42:09684 if good_rev > bad_rev:
685 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev), str(max_chromium_rev))
686 else:
687 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev), str(max_chromium_rev))
688
[email protected]ff50d1c2013-04-17 18:49:36689 if min_blink_rev != max_blink_rev:
690 print 'BLINK CHANGELOG URL:'
691 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev)
[email protected]d0149c5c2012-05-29 21:12:11692 print 'CHANGELOG URL:'
693 if opts.official_builds:
[email protected]eadd95d2012-11-02 22:42:09694 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
[email protected]d0149c5c2012-05-29 21:12:11695 else:
[email protected]eadd95d2012-11-02 22:42:09696 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
[email protected]cb155a82011-11-29 17:25:34697
[email protected]67e0bc62009-09-03 22:06:09698if __name__ == '__main__':
[email protected]7ad66a72009-09-04 17:52:33699 sys.exit(main())