blob: 2ab9dc3f89c72f5a89c51526b20456ee0a16b788 [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]4c6fec6b2013-09-17 17:44:0816CHROMIUM_BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-snapshots'
17WEBKIT_BASE_URL = 'http://commondatastorage.googleapis.com/chromium-webkit-snapshots'
[email protected]67e0bc62009-09-03 22:06:0918
[email protected]d0149c5c2012-05-29 21:12:1119# The root URL for official builds.
[email protected]b2905832012-07-19 21:28:4320OFFICIAL_BASE_URL = 'http://master.chrome.corp.google.com/official_builds'
[email protected]d0149c5c2012-05-29 21:12:1121
[email protected]183706d92011-06-10 13:06:2222# Changelogs URL.
[email protected]07247462010-12-24 07:45:5623CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \
[email protected]ff50d1c2013-04-17 18:49:3624 'perf/dashboard/ui/changelog.html?' \
25 'url=/trunk/src&range=%d%%3A%d'
[email protected]f6a71a72009-10-08 19:55:3826
[email protected]d0149c5c2012-05-29 21:12:1127# Official Changelogs URL.
28OFFICIAL_CHANGELOG_URL = 'http://omahaproxy.appspot.com/'\
29 'changelog?old_version=%s&new_version=%s'
30
[email protected]b2fe7f22011-10-25 22:58:3131# DEPS file URL.
32DEPS_FILE= 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d'
[email protected]ff50d1c2013-04-17 18:49:3633# Blink Changelogs URL.
34BLINK_CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \
35 'perf/dashboard/ui/changelog_blink.html?' \
36 'url=/trunk&range=%d%%3A%d'
[email protected]b2fe7f22011-10-25 22:58:3137
[email protected]eadd95d2012-11-02 22:42:0938DONE_MESSAGE_GOOD_MIN = 'You are probably looking for a change made after %s ' \
39 '(known good), but no later than %s (first known bad).'
40DONE_MESSAGE_GOOD_MAX = 'You are probably looking for a change made after %s ' \
41 '(known bad), but no later than %s (first known good).'
[email protected]05ff3fd2012-04-17 23:24:0642
[email protected]67e0bc62009-09-03 22:06:0943###############################################################################
44
[email protected]4c6fec6b2013-09-17 17:44:0845import json
[email protected]67e0bc62009-09-03 22:06:0946import math
[email protected]7ad66a72009-09-04 17:52:3347import optparse
[email protected]67e0bc62009-09-03 22:06:0948import os
[email protected]d4bf3582009-09-20 00:56:3849import pipes
[email protected]67e0bc62009-09-03 22:06:0950import re
51import shutil
[email protected]afe30662011-07-30 01:05:5252import subprocess
[email protected]67e0bc62009-09-03 22:06:0953import sys
[email protected]7ad66a72009-09-04 17:52:3354import tempfile
[email protected]afe30662011-07-30 01:05:5255import threading
[email protected]67e0bc62009-09-03 22:06:0956import urllib
[email protected]d0149c5c2012-05-29 21:12:1157from distutils.version import LooseVersion
[email protected]183706d92011-06-10 13:06:2258from xml.etree import ElementTree
[email protected]bd8dcb92010-03-31 01:05:2459import zipfile
60
[email protected]cb155a82011-11-29 17:25:3461
[email protected]183706d92011-06-10 13:06:2262class PathContext(object):
63 """A PathContext is used to carry the information used to construct URLs and
64 paths when dealing with the storage server and archives."""
[email protected]4c6fec6b2013-09-17 17:44:0865 def __init__(self, base_url, platform, good_revision, bad_revision,
66 is_official, is_aura):
[email protected]183706d92011-06-10 13:06:2267 super(PathContext, self).__init__()
68 # Store off the input parameters.
[email protected]4c6fec6b2013-09-17 17:44:0869 self.base_url = base_url
[email protected]183706d92011-06-10 13:06:2270 self.platform = platform # What's passed in to the '-a/--archive' option.
71 self.good_revision = good_revision
72 self.bad_revision = bad_revision
[email protected]d0149c5c2012-05-29 21:12:1173 self.is_official = is_official
[email protected]b3b20512013-08-26 18:51:0474 self.is_aura = is_aura
[email protected]183706d92011-06-10 13:06:2275
76 # The name of the ZIP file in a revision directory on the server.
77 self.archive_name = None
78
79 # Set some internal members:
80 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
81 # _archive_extract_dir = Uncompressed directory in the archive_name file.
82 # _binary_name = The name of the executable to run.
[email protected]7aec9e82013-05-09 05:09:2383 if self.platform in ('linux', 'linux64', 'linux-arm'):
[email protected]183706d92011-06-10 13:06:2284 self._binary_name = 'chrome'
[email protected]183706d92011-06-10 13:06:2285 elif self.platform == 'mac':
[email protected]183706d92011-06-10 13:06:2286 self.archive_name = 'chrome-mac.zip'
87 self._archive_extract_dir = 'chrome-mac'
[email protected]183706d92011-06-10 13:06:2288 elif self.platform == 'win':
[email protected]183706d92011-06-10 13:06:2289 self.archive_name = 'chrome-win32.zip'
90 self._archive_extract_dir = 'chrome-win32'
91 self._binary_name = 'chrome.exe'
92 else:
[email protected]afe30662011-07-30 01:05:5293 raise Exception('Invalid platform: %s' % self.platform)
[email protected]183706d92011-06-10 13:06:2294
[email protected]d0149c5c2012-05-29 21:12:1195 if is_official:
96 if self.platform == 'linux':
[email protected]9639b002013-08-30 14:45:5297 self._listing_platform_dir = 'precise32bit/'
98 self.archive_name = 'chrome-precise32bit.zip'
99 self._archive_extract_dir = 'chrome-precise32bit'
[email protected]d0149c5c2012-05-29 21:12:11100 elif self.platform == 'linux64':
[email protected]9639b002013-08-30 14:45:52101 self._listing_platform_dir = 'precise64bit/'
102 self.archive_name = 'chrome-precise64bit.zip'
103 self._archive_extract_dir = 'chrome-precise64bit'
[email protected]d0149c5c2012-05-29 21:12:11104 elif self.platform == 'mac':
105 self._listing_platform_dir = 'mac/'
106 self._binary_name = 'Google Chrome.app/Contents/MacOS/Google Chrome'
107 elif self.platform == 'win':
[email protected]b3b20512013-08-26 18:51:04108 if self.is_aura:
109 self._listing_platform_dir = 'win-aura/'
110 else:
111 self._listing_platform_dir = 'win/'
[email protected]d0149c5c2012-05-29 21:12:11112 else:
[email protected]7aec9e82013-05-09 05:09:23113 if self.platform in ('linux', 'linux64', 'linux-arm'):
[email protected]d0149c5c2012-05-29 21:12:11114 self.archive_name = 'chrome-linux.zip'
115 self._archive_extract_dir = 'chrome-linux'
116 if self.platform == 'linux':
117 self._listing_platform_dir = 'Linux/'
118 elif self.platform == 'linux64':
119 self._listing_platform_dir = 'Linux_x64/'
[email protected]7aec9e82013-05-09 05:09:23120 elif self.platform == 'linux-arm':
121 self._listing_platform_dir = 'Linux_ARM_Cross-Compile/'
[email protected]d0149c5c2012-05-29 21:12:11122 elif self.platform == 'mac':
123 self._listing_platform_dir = 'Mac/'
124 self._binary_name = 'Chromium.app/Contents/MacOS/Chromium'
125 elif self.platform == 'win':
126 self._listing_platform_dir = 'Win/'
127
[email protected]183706d92011-06-10 13:06:22128 def GetListingURL(self, marker=None):
129 """Returns the URL for a directory listing, with an optional marker."""
130 marker_param = ''
131 if marker:
132 marker_param = '&marker=' + str(marker)
[email protected]4c6fec6b2013-09-17 17:44:08133 return self.base_url + '/?delimiter=/&prefix=' + \
134 self._listing_platform_dir + marker_param
[email protected]183706d92011-06-10 13:06:22135
136 def GetDownloadURL(self, revision):
137 """Gets the download URL for a build archive of a specific revision."""
[email protected]d0149c5c2012-05-29 21:12:11138 if self.is_official:
139 return "%s/%s/%s%s" % (
140 OFFICIAL_BASE_URL, revision, self._listing_platform_dir,
141 self.archive_name)
142 else:
[email protected]4c6fec6b2013-09-17 17:44:08143 return "%s/%s%s/%s" % (self.base_url, self._listing_platform_dir,
144 revision, self.archive_name)
[email protected]183706d92011-06-10 13:06:22145
146 def GetLastChangeURL(self):
147 """Returns a URL to the LAST_CHANGE file."""
[email protected]4c6fec6b2013-09-17 17:44:08148 return self.base_url + '/' + self._listing_platform_dir + 'LAST_CHANGE'
[email protected]183706d92011-06-10 13:06:22149
150 def GetLaunchPath(self):
151 """Returns a relative path (presumably from the archive extraction location)
152 that is used to run the executable."""
153 return os.path.join(self._archive_extract_dir, self._binary_name)
154
[email protected]b3b20512013-08-26 18:51:04155 def IsAuraBuild(self, build):
156 """Check the given build is Aura."""
157 return build.split('.')[3] == '1'
158
159 def IsASANBuild(self, build):
160 """Check the given build is ASAN build."""
161 return build.split('.')[3] == '2'
162
[email protected]afe30662011-07-30 01:05:52163 def ParseDirectoryIndex(self):
164 """Parses the Google Storage directory listing into a list of revision
[email protected]eadd95d2012-11-02 22:42:09165 numbers."""
[email protected]afe30662011-07-30 01:05:52166
167 def _FetchAndParse(url):
168 """Fetches a URL and returns a 2-Tuple of ([revisions], next-marker). If
169 next-marker is not None, then the listing is a partial listing and another
170 fetch should be performed with next-marker being the marker= GET
171 parameter."""
172 handle = urllib.urlopen(url)
173 document = ElementTree.parse(handle)
174
175 # All nodes in the tree are namespaced. Get the root's tag name to extract
176 # the namespace. Etree does namespaces as |{namespace}tag|.
177 root_tag = document.getroot().tag
178 end_ns_pos = root_tag.find('}')
179 if end_ns_pos == -1:
180 raise Exception("Could not locate end namespace for directory index")
181 namespace = root_tag[:end_ns_pos + 1]
182
183 # Find the prefix (_listing_platform_dir) and whether or not the list is
184 # truncated.
185 prefix_len = len(document.find(namespace + 'Prefix').text)
186 next_marker = None
187 is_truncated = document.find(namespace + 'IsTruncated')
188 if is_truncated is not None and is_truncated.text.lower() == 'true':
189 next_marker = document.find(namespace + 'NextMarker').text
190
191 # Get a list of all the revisions.
192 all_prefixes = document.findall(namespace + 'CommonPrefixes/' +
193 namespace + 'Prefix')
194 # The <Prefix> nodes have content of the form of
195 # |_listing_platform_dir/revision/|. Strip off the platform dir and the
196 # trailing slash to just have a number.
197 revisions = []
198 for prefix in all_prefixes:
199 revnum = prefix.text[prefix_len:-1]
200 try:
201 revnum = int(revnum)
202 revisions.append(revnum)
203 except ValueError:
204 pass
205 return (revisions, next_marker)
[email protected]9639b002013-08-30 14:45:52206
[email protected]afe30662011-07-30 01:05:52207 # Fetch the first list of revisions.
208 (revisions, next_marker) = _FetchAndParse(self.GetListingURL())
209
210 # If the result list was truncated, refetch with the next marker. Do this
211 # until an entire directory listing is done.
212 while next_marker:
213 next_url = self.GetListingURL(next_marker)
214 (new_revisions, next_marker) = _FetchAndParse(next_url)
215 revisions.extend(new_revisions)
[email protected]afe30662011-07-30 01:05:52216 return revisions
217
218 def GetRevList(self):
219 """Gets the list of revision numbers between self.good_revision and
220 self.bad_revision."""
221 # Download the revlist and filter for just the range between good and bad.
[email protected]eadd95d2012-11-02 22:42:09222 minrev = min(self.good_revision, self.bad_revision)
223 maxrev = max(self.good_revision, self.bad_revision)
[email protected]afe30662011-07-30 01:05:52224 revlist = map(int, self.ParseDirectoryIndex())
[email protected]d0149c5c2012-05-29 21:12:11225 revlist = [x for x in revlist if x >= int(minrev) and x <= int(maxrev)]
[email protected]afe30662011-07-30 01:05:52226 revlist.sort()
227 return revlist
228
[email protected]d0149c5c2012-05-29 21:12:11229 def GetOfficialBuildsList(self):
230 """Gets the list of official build numbers between self.good_revision and
231 self.bad_revision."""
232 # Download the revlist and filter for just the range between good and bad.
[email protected]eadd95d2012-11-02 22:42:09233 minrev = min(self.good_revision, self.bad_revision)
234 maxrev = max(self.good_revision, self.bad_revision)
[email protected]d0149c5c2012-05-29 21:12:11235 handle = urllib.urlopen(OFFICIAL_BASE_URL)
236 dirindex = handle.read()
237 handle.close()
238 build_numbers = re.findall(r'<a href="([0-9][0-9].*)/">', dirindex)
239 final_list = []
[email protected]d0149c5c2012-05-29 21:12:11240 i = 0
[email protected]d0149c5c2012-05-29 21:12:11241 parsed_build_numbers = [LooseVersion(x) for x in build_numbers]
242 for build_number in sorted(parsed_build_numbers):
243 path = OFFICIAL_BASE_URL + '/' + str(build_number) + '/' + \
244 self._listing_platform_dir + self.archive_name
245 i = i + 1
246 try:
247 connection = urllib.urlopen(path)
248 connection.close()
[email protected]801fb652012-07-20 20:13:50249 if build_number > maxrev:
250 break
251 if build_number >= minrev:
[email protected]b3b20512013-08-26 18:51:04252 # If we are bisecting Aura, we want to include only builds which
253 # ends with ".1".
254 if self.is_aura:
255 if self.IsAuraBuild(str(build_number)):
256 final_list.append(str(build_number))
257 # If we are bisecting only official builds (without --aura),
258 # we can not include builds which ends with '.1' or '.2' since
259 # they have different folder hierarchy inside.
260 elif (not self.IsAuraBuild(str(build_number)) and
261 not self.IsASANBuild(str(build_number))):
262 final_list.append(str(build_number))
[email protected]d0149c5c2012-05-29 21:12:11263 except urllib.HTTPError, e:
264 pass
[email protected]801fb652012-07-20 20:13:50265 return final_list
[email protected]bd8dcb92010-03-31 01:05:24266
267def UnzipFilenameToDir(filename, dir):
268 """Unzip |filename| to directory |dir|."""
[email protected]afe30662011-07-30 01:05:52269 cwd = os.getcwd()
270 if not os.path.isabs(filename):
271 filename = os.path.join(cwd, filename)
[email protected]bd8dcb92010-03-31 01:05:24272 zf = zipfile.ZipFile(filename)
273 # Make base.
[email protected]e29c08c2012-09-17 20:50:50274 if not os.path.isdir(dir):
275 os.mkdir(dir)
276 os.chdir(dir)
277 # Extract files.
278 for info in zf.infolist():
279 name = info.filename
280 if name.endswith('/'): # dir
281 if not os.path.isdir(name):
282 os.makedirs(name)
283 else: # file
284 dir = os.path.dirname(name)
285 if not os.path.isdir(dir):
286 os.makedirs(dir)
287 out = open(name, 'wb')
288 out.write(zf.read(name))
289 out.close()
290 # Set permissions. Permission info in external_attr is shifted 16 bits.
291 os.chmod(name, info.external_attr >> 16L)
292 os.chdir(cwd)
[email protected]bd8dcb92010-03-31 01:05:24293
[email protected]67e0bc62009-09-03 22:06:09294
[email protected]468a9772011-08-09 18:42:00295def FetchRevision(context, rev, filename, quit_event=None, progress_event=None):
[email protected]afe30662011-07-30 01:05:52296 """Downloads and unzips revision |rev|.
297 @param context A PathContext instance.
298 @param rev The Chromium revision number/tag to download.
299 @param filename The destination for the downloaded file.
300 @param quit_event A threading.Event which will be set by the master thread to
301 indicate that the download should be aborted.
[email protected]468a9772011-08-09 18:42:00302 @param progress_event A threading.Event which will be set by the master thread
303 to indicate that the progress of the download should be
304 displayed.
[email protected]afe30662011-07-30 01:05:52305 """
306 def ReportHook(blocknum, blocksize, totalsize):
[email protected]946be752011-10-25 23:34:21307 if quit_event and quit_event.isSet():
[email protected]d0149c5c2012-05-29 21:12:11308 raise RuntimeError("Aborting download of revision %s" % str(rev))
[email protected]946be752011-10-25 23:34:21309 if progress_event and progress_event.isSet():
[email protected]468a9772011-08-09 18:42:00310 size = blocknum * blocksize
311 if totalsize == -1: # Total size not known.
312 progress = "Received %d bytes" % size
313 else:
314 size = min(totalsize, size)
315 progress = "Received %d of %d bytes, %.2f%%" % (
316 size, totalsize, 100.0 * size / totalsize)
317 # Send a \r to let all progress messages use just one line of output.
318 sys.stdout.write("\r" + progress)
319 sys.stdout.flush()
[email protected]7ad66a72009-09-04 17:52:33320
[email protected]afe30662011-07-30 01:05:52321 download_url = context.GetDownloadURL(rev)
322 try:
323 urllib.urlretrieve(download_url, filename, ReportHook)
[email protected]946be752011-10-25 23:34:21324 if progress_event and progress_event.isSet():
[email protected]ecaba01e62011-10-26 05:33:28325 print
[email protected]afe30662011-07-30 01:05:52326 except RuntimeError, e:
327 pass
[email protected]7ad66a72009-09-04 17:52:33328
[email protected]7ad66a72009-09-04 17:52:33329
[email protected]4646a752013-07-19 22:14:34330def RunRevision(context, revision, zipfile, profile, num_runs, command, args):
[email protected]afe30662011-07-30 01:05:52331 """Given a zipped revision, unzip it and run the test."""
[email protected]d0149c5c2012-05-29 21:12:11332 print "Trying revision %s..." % str(revision)
[email protected]3ff00b72011-07-20 21:34:47333
[email protected]afe30662011-07-30 01:05:52334 # Create a temp directory and unzip the revision into it.
[email protected]7ad66a72009-09-04 17:52:33335 cwd = os.getcwd()
336 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
[email protected]afe30662011-07-30 01:05:52337 UnzipFilenameToDir(zipfile, tempdir)
[email protected]7ad66a72009-09-04 17:52:33338 os.chdir(tempdir)
[email protected]67e0bc62009-09-03 22:06:09339
[email protected]5e93cf162012-01-28 02:16:56340 # Run the build as many times as specified.
[email protected]4646a752013-07-19 22:14:34341 testargs = ['--user-data-dir=%s' % profile] + args
[email protected]d0149c5c2012-05-29 21:12:11342 # The sandbox must be run as root on Official Chrome, so bypass it.
[email protected]7aec9e82013-05-09 05:09:23343 if context.is_official and context.platform.startswith('linux'):
[email protected]d0149c5c2012-05-29 21:12:11344 testargs.append('--no-sandbox')
345
[email protected]4646a752013-07-19 22:14:34346 runcommand = []
347 for token in command.split():
348 if token == "%a":
349 runcommand.extend(testargs)
350 else:
351 runcommand.append( \
352 token.replace('%p', context.GetLaunchPath()) \
353 .replace('%s', ' '.join(testargs)))
354
[email protected]5e93cf162012-01-28 02:16:56355 for i in range(0, num_runs):
[email protected]4646a752013-07-19 22:14:34356 subproc = subprocess.Popen(runcommand,
[email protected]5e93cf162012-01-28 02:16:56357 bufsize=-1,
358 stdout=subprocess.PIPE,
359 stderr=subprocess.PIPE)
360 (stdout, stderr) = subproc.communicate()
[email protected]7ad66a72009-09-04 17:52:33361
362 os.chdir(cwd)
[email protected]7ad66a72009-09-04 17:52:33363 try:
364 shutil.rmtree(tempdir, True)
365 except Exception, e:
366 pass
[email protected]67e0bc62009-09-03 22:06:09367
[email protected]afe30662011-07-30 01:05:52368 return (subproc.returncode, stdout, stderr)
[email protected]79f14742010-03-10 01:01:57369
[email protected]cb155a82011-11-29 17:25:34370
[email protected]d0149c5c2012-05-29 21:12:11371def AskIsGoodBuild(rev, official_builds, status, stdout, stderr):
[email protected]183706d92011-06-10 13:06:22372 """Ask the user whether build |rev| is good or bad."""
[email protected]79f14742010-03-10 01:01:57373 # Loop until we get a response that we can parse.
[email protected]67e0bc62009-09-03 22:06:09374 while True:
[email protected]1d4a06242013-08-20 22:53:12375 response = raw_input('Revision %s is ' \
376 '[(g)ood/(b)ad/(r)etry/(u)nknown/(q)uit]: ' %
[email protected]53bb6342012-06-01 04:11:00377 str(rev))
[email protected]1d4a06242013-08-20 22:53:12378 if response and response in ('g', 'b', 'r', 'u'):
[email protected]53bb6342012-06-01 04:11:00379 return response
[email protected]afe30662011-07-30 01:05:52380 if response and response == 'q':
381 raise SystemExit()
[email protected]67e0bc62009-09-03 22:06:09382
[email protected]cb155a82011-11-29 17:25:34383
[email protected]53bb6342012-06-01 04:11:00384class DownloadJob(object):
385 """DownloadJob represents a task to download a given Chromium revision."""
386 def __init__(self, context, name, rev, zipfile):
387 super(DownloadJob, self).__init__()
388 # Store off the input parameters.
389 self.context = context
390 self.name = name
391 self.rev = rev
392 self.zipfile = zipfile
393 self.quit_event = threading.Event()
394 self.progress_event = threading.Event()
395
396 def Start(self):
397 """Starts the download."""
398 fetchargs = (self.context,
399 self.rev,
400 self.zipfile,
401 self.quit_event,
402 self.progress_event)
403 self.thread = threading.Thread(target=FetchRevision,
404 name=self.name,
405 args=fetchargs)
406 self.thread.start()
407
408 def Stop(self):
409 """Stops the download which must have been started previously."""
410 self.quit_event.set()
411 self.thread.join()
412 os.unlink(self.zipfile)
413
414 def WaitFor(self):
415 """Prints a message and waits for the download to complete. The download
416 must have been started previously."""
417 print "Downloading revision %s..." % str(self.rev)
418 self.progress_event.set() # Display progress of download.
419 self.thread.join()
420
421
[email protected]4c6fec6b2013-09-17 17:44:08422def Bisect(base_url,
423 platform,
[email protected]d0149c5c2012-05-29 21:12:11424 official_builds,
[email protected]b3b20512013-08-26 18:51:04425 is_aura,
[email protected]afe30662011-07-30 01:05:52426 good_rev=0,
427 bad_rev=0,
[email protected]5e93cf162012-01-28 02:16:56428 num_runs=1,
[email protected]4646a752013-07-19 22:14:34429 command="%p %a",
[email protected]60ac66e32011-07-18 16:08:25430 try_args=(),
[email protected]afe30662011-07-30 01:05:52431 profile=None,
[email protected]53bb6342012-06-01 04:11:00432 evaluate=AskIsGoodBuild):
[email protected]afe30662011-07-30 01:05:52433 """Given known good and known bad revisions, run a binary search on all
434 archived revisions to determine the last known good revision.
[email protected]60ac66e32011-07-18 16:08:25435
[email protected]afe30662011-07-30 01:05:52436 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.).
[email protected]d0149c5c2012-05-29 21:12:11437 @param official_builds Specify build type (Chromium or Official build).
[email protected]eadd95d2012-11-02 22:42:09438 @param good_rev Number/tag of the known good revision.
439 @param bad_rev Number/tag of the known bad revision.
[email protected]5e93cf162012-01-28 02:16:56440 @param num_runs Number of times to run each build for asking good/bad.
[email protected]afe30662011-07-30 01:05:52441 @param try_args A tuple of arguments to pass to the test application.
442 @param profile The name of the user profile to run with.
[email protected]53bb6342012-06-01 04:11:00443 @param evaluate A function which returns 'g' if the argument build is good,
444 'b' if it's bad or 'u' if unknown.
[email protected]afe30662011-07-30 01:05:52445
446 Threading is used to fetch Chromium revisions in the background, speeding up
447 the user's experience. For example, suppose the bounds of the search are
448 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on
449 whether revision 50 is good or bad, the next revision to check will be either
450 25 or 75. So, while revision 50 is being checked, the script will download
451 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is
452 known:
453
454 - If rev 50 is good, the download of rev 25 is cancelled, and the next test
455 is run on rev 75.
456
457 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
458 is run on rev 25.
[email protected]60ac66e32011-07-18 16:08:25459 """
460
[email protected]afe30662011-07-30 01:05:52461 if not profile:
462 profile = 'profile'
463
[email protected]4c6fec6b2013-09-17 17:44:08464 context = PathContext(base_url, platform, good_rev, bad_rev,
465 official_builds, is_aura)
[email protected]afe30662011-07-30 01:05:52466 cwd = os.getcwd()
467
[email protected]468a9772011-08-09 18:42:00468 print "Downloading list of known revisions..."
[email protected]d0149c5c2012-05-29 21:12:11469 _GetDownloadPath = lambda rev: os.path.join(cwd,
470 '%s-%s' % (str(rev), context.archive_name))
471 if official_builds:
472 revlist = context.GetOfficialBuildsList()
473 else:
474 revlist = context.GetRevList()
[email protected]afe30662011-07-30 01:05:52475
476 # Get a list of revisions to bisect across.
477 if len(revlist) < 2: # Don't have enough builds to bisect.
478 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist
479 raise RuntimeError(msg)
480
481 # Figure out our bookends and first pivot point; fetch the pivot revision.
[email protected]eadd95d2012-11-02 22:42:09482 minrev = 0
483 maxrev = len(revlist) - 1
484 pivot = maxrev / 2
[email protected]afe30662011-07-30 01:05:52485 rev = revlist[pivot]
486 zipfile = _GetDownloadPath(rev)
[email protected]eadd95d2012-11-02 22:42:09487 fetch = DownloadJob(context, 'initial_fetch', rev, zipfile)
488 fetch.Start()
489 fetch.WaitFor()
[email protected]60ac66e32011-07-18 16:08:25490
491 # Binary search time!
[email protected]eadd95d2012-11-02 22:42:09492 while fetch and fetch.zipfile and maxrev - minrev > 1:
493 if bad_rev < good_rev:
494 min_str, max_str = "bad", "good"
495 else:
496 min_str, max_str = "good", "bad"
497 print 'Bisecting range [%s (%s), %s (%s)].' % (revlist[minrev], min_str, \
498 revlist[maxrev], max_str)
499
[email protected]afe30662011-07-30 01:05:52500 # Pre-fetch next two possible pivots
501 # - down_pivot is the next revision to check if the current revision turns
502 # out to be bad.
503 # - up_pivot is the next revision to check if the current revision turns
504 # out to be good.
[email protected]eadd95d2012-11-02 22:42:09505 down_pivot = int((pivot - minrev) / 2) + minrev
[email protected]53bb6342012-06-01 04:11:00506 down_fetch = None
[email protected]eadd95d2012-11-02 22:42:09507 if down_pivot != pivot and down_pivot != minrev:
[email protected]afe30662011-07-30 01:05:52508 down_rev = revlist[down_pivot]
[email protected]53bb6342012-06-01 04:11:00509 down_fetch = DownloadJob(context, 'down_fetch', down_rev,
510 _GetDownloadPath(down_rev))
511 down_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25512
[email protected]eadd95d2012-11-02 22:42:09513 up_pivot = int((maxrev - pivot) / 2) + pivot
[email protected]53bb6342012-06-01 04:11:00514 up_fetch = None
[email protected]eadd95d2012-11-02 22:42:09515 if up_pivot != pivot and up_pivot != maxrev:
[email protected]afe30662011-07-30 01:05:52516 up_rev = revlist[up_pivot]
[email protected]53bb6342012-06-01 04:11:00517 up_fetch = DownloadJob(context, 'up_fetch', up_rev,
518 _GetDownloadPath(up_rev))
519 up_fetch.Start()
[email protected]60ac66e32011-07-18 16:08:25520
[email protected]afe30662011-07-30 01:05:52521 # Run test on the pivot revision.
[email protected]e29c08c2012-09-17 20:50:50522 status = None
523 stdout = None
524 stderr = None
525 try:
526 (status, stdout, stderr) = RunRevision(context,
527 rev,
[email protected]eadd95d2012-11-02 22:42:09528 fetch.zipfile,
[email protected]e29c08c2012-09-17 20:50:50529 profile,
530 num_runs,
[email protected]4646a752013-07-19 22:14:34531 command,
[email protected]e29c08c2012-09-17 20:50:50532 try_args)
533 except Exception, e:
534 print >>sys.stderr, e
[email protected]60ac66e32011-07-18 16:08:25535
[email protected]53bb6342012-06-01 04:11:00536 # Call the evaluate function to see if the current revision is good or bad.
[email protected]afe30662011-07-30 01:05:52537 # On that basis, kill one of the background downloads and complete the
538 # other, as described in the comments above.
539 try:
[email protected]53bb6342012-06-01 04:11:00540 answer = evaluate(rev, official_builds, status, stdout, stderr)
[email protected]eadd95d2012-11-02 22:42:09541 if answer == 'g' and good_rev < bad_rev or \
542 answer == 'b' and bad_rev < good_rev:
[email protected]1d4a06242013-08-20 22:53:12543 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09544 minrev = pivot
[email protected]53bb6342012-06-01 04:11:00545 if down_fetch:
546 down_fetch.Stop() # Kill the download of the older revision.
[email protected]1d4a06242013-08-20 22:53:12547 fetch = None
[email protected]53bb6342012-06-01 04:11:00548 if up_fetch:
549 up_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52550 pivot = up_pivot
[email protected]eadd95d2012-11-02 22:42:09551 fetch = up_fetch
552 elif answer == 'b' and good_rev < bad_rev or \
553 answer == 'g' and bad_rev < good_rev:
[email protected]1d4a06242013-08-20 22:53:12554 fetch.Stop()
[email protected]eadd95d2012-11-02 22:42:09555 maxrev = pivot
[email protected]53bb6342012-06-01 04:11:00556 if up_fetch:
557 up_fetch.Stop() # Kill the download of the newer revision.
[email protected]1d4a06242013-08-20 22:53:12558 fetch = None
[email protected]53bb6342012-06-01 04:11:00559 if down_fetch:
560 down_fetch.WaitFor()
[email protected]afe30662011-07-30 01:05:52561 pivot = down_pivot
[email protected]eadd95d2012-11-02 22:42:09562 fetch = down_fetch
[email protected]1d4a06242013-08-20 22:53:12563 elif answer == 'r':
564 pass # Retry requires no changes.
[email protected]53bb6342012-06-01 04:11:00565 elif answer == 'u':
566 # Nuke the revision from the revlist and choose a new pivot.
[email protected]1d4a06242013-08-20 22:53:12567 fetch.Stop()
[email protected]53bb6342012-06-01 04:11:00568 revlist.pop(pivot)
[email protected]eadd95d2012-11-02 22:42:09569 maxrev -= 1 # Assumes maxrev >= pivot.
[email protected]53bb6342012-06-01 04:11:00570
[email protected]eadd95d2012-11-02 22:42:09571 if maxrev - minrev > 1:
[email protected]53bb6342012-06-01 04:11:00572 # Alternate between using down_pivot or up_pivot for the new pivot
573 # point, without affecting the range. Do this instead of setting the
574 # pivot to the midpoint of the new range because adjacent revisions
575 # are likely affected by the same issue that caused the (u)nknown
576 # response.
577 if up_fetch and down_fetch:
578 fetch = [up_fetch, down_fetch][len(revlist) % 2]
579 elif up_fetch:
580 fetch = up_fetch
581 else:
582 fetch = down_fetch
583 fetch.WaitFor()
584 if fetch == up_fetch:
585 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized.
586 else:
587 pivot = down_pivot
588 zipfile = fetch.zipfile
589
590 if down_fetch and fetch != down_fetch:
591 down_fetch.Stop()
592 if up_fetch and fetch != up_fetch:
593 up_fetch.Stop()
594 else:
595 assert False, "Unexpected return value from evaluate(): " + answer
[email protected]afe30662011-07-30 01:05:52596 except SystemExit:
[email protected]468a9772011-08-09 18:42:00597 print "Cleaning up..."
[email protected]5e93cf162012-01-28 02:16:56598 for f in [_GetDownloadPath(revlist[down_pivot]),
599 _GetDownloadPath(revlist[up_pivot])]:
[email protected]afe30662011-07-30 01:05:52600 try:
601 os.unlink(f)
602 except OSError:
603 pass
604 sys.exit(0)
605
606 rev = revlist[pivot]
607
[email protected]eadd95d2012-11-02 22:42:09608 return (revlist[minrev], revlist[maxrev])
[email protected]60ac66e32011-07-18 16:08:25609
610
[email protected]4c6fec6b2013-09-17 17:44:08611def GetBlinkRevisionForChromiumRevision(self, rev):
612 """Returns the blink revision that was in REVISIONS file at
[email protected]b2fe7f22011-10-25 22:58:31613 chromium revision |rev|."""
614 # . doesn't match newlines without re.DOTALL, so this is safe.
[email protected]4c6fec6b2013-09-17 17:44:08615 file_url = "%s/%s%d/REVISIONS" % (self.base_url,
616 self._listing_platform_dir, rev)
617 url = urllib.urlopen(file_url)
618 data = json.loads(url.read())
[email protected]b2fe7f22011-10-25 22:58:31619 url.close()
[email protected]4c6fec6b2013-09-17 17:44:08620 if 'webkit_revision' in data:
621 return data['webkit_revision']
[email protected]b2fe7f22011-10-25 22:58:31622 else:
[email protected]ff50d1c2013-04-17 18:49:36623 raise Exception('Could not get blink revision for cr rev %d' % rev)
[email protected]b2fe7f22011-10-25 22:58:31624
625
[email protected]801fb652012-07-20 20:13:50626def GetChromiumRevision(url):
627 """Returns the chromium revision read from given URL."""
628 try:
629 # Location of the latest build revision number
630 return int(urllib.urlopen(url).read())
631 except Exception, e:
632 print('Could not determine latest revision. This could be bad...')
633 return 999999999
634
635
[email protected]67e0bc62009-09-03 22:06:09636def main():
[email protected]2c1d2732009-10-29 19:52:17637 usage = ('%prog [options] [-- chromium-options]\n'
[email protected]887c9182013-02-12 20:30:31638 'Perform binary search on the snapshot builds to find a minimal\n'
639 'range of revisions where a behavior change happened. The\n'
640 'behaviors are described as "good" and "bad".\n'
641 'It is NOT assumed that the behavior of the later revision is\n'
[email protected]09c58da2013-01-07 21:30:17642 'the bad one.\n'
[email protected]178aab72010-10-08 17:21:38643 '\n'
[email protected]887c9182013-02-12 20:30:31644 'Revision numbers should use\n'
645 ' Official versions (e.g. 1.0.1000.0) for official builds. (-o)\n'
646 ' SVN revisions (e.g. 123456) for chromium builds, from trunk.\n'
647 ' Use base_trunk_revision from http://omahaproxy.appspot.com/\n'
648 ' for earlier revs.\n'
649 ' Chrome\'s about: build number and omahaproxy branch_revision\n'
650 ' are incorrect, they are from branches.\n'
651 '\n'
[email protected]178aab72010-10-08 17:21:38652 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
[email protected]7ad66a72009-09-04 17:52:33653 parser = optparse.OptionParser(usage=usage)
[email protected]1a45d222009-09-19 01:58:57654 # Strangely, the default help output doesn't include the choice list.
[email protected]7aec9e82013-05-09 05:09:23655 choices = ['mac', 'win', 'linux', 'linux64', 'linux-arm']
[email protected]4082b182011-05-02 20:30:17656 # linux-chromiumos lacks a continuous archive http://crbug.com/78158
[email protected]7ad66a72009-09-04 17:52:33657 parser.add_option('-a', '--archive',
[email protected]1a45d222009-09-19 01:58:57658 choices = choices,
659 help = 'The buildbot archive to bisect [%s].' %
660 '|'.join(choices))
[email protected]d0149c5c2012-05-29 21:12:11661 parser.add_option('-o', action="store_true", dest='official_builds',
662 help = 'Bisect across official ' +
663 'Chrome builds (internal only) instead of ' +
664 'Chromium archives.')
665 parser.add_option('-b', '--bad', type = 'str',
[email protected]09c58da2013-01-07 21:30:17666 help = 'A bad revision to start bisection. ' +
667 'May be earlier or later than the good revision. ' +
668 'Default is HEAD.')
[email protected]d0149c5c2012-05-29 21:12:11669 parser.add_option('-g', '--good', type = 'str',
[email protected]09c58da2013-01-07 21:30:17670 help = 'A good revision to start bisection. ' +
671 'May be earlier or later than the bad revision. ' +
[email protected]801fb652012-07-20 20:13:50672 'Default is 0.')
[email protected]d4bf3582009-09-20 00:56:38673 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str',
674 help = 'Profile to use; this will not reset every run. ' +
[email protected]60ac66e32011-07-18 16:08:25675 'Defaults to a clean profile.', default = 'profile')
[email protected]5e93cf162012-01-28 02:16:56676 parser.add_option('-t', '--times', type = 'int',
677 help = 'Number of times to run each build before asking ' +
678 'if it\'s good or bad. Temporary profiles are reused.',
679 default = 1)
[email protected]4646a752013-07-19 22:14:34680 parser.add_option('-c', '--command', type = 'str',
681 help = 'Command to execute. %p and %a refer to Chrome ' +
682 'executable and specified extra arguments respectively. ' +
683 'Use %s to specify all extra arguments as one string. ' +
684 'Defaults to "%p %a". Note that any extra paths ' +
685 'specified should be absolute.',
686 default = '%p %a');
[email protected]4c6fec6b2013-09-17 17:44:08687 parser.add_option('-l', '--blink', action='store_true',
688 help = 'Use Blink bisect instead of Chromium. ')
[email protected]b3b20512013-08-26 18:51:04689 parser.add_option('--aura',
690 dest='aura',
691 action='store_true',
692 default=False,
693 help='Allow the script to bisect aura builds')
694
[email protected]7ad66a72009-09-04 17:52:33695 (opts, args) = parser.parse_args()
696
697 if opts.archive is None:
[email protected]178aab72010-10-08 17:21:38698 print 'Error: missing required parameter: --archive'
699 print
[email protected]7ad66a72009-09-04 17:52:33700 parser.print_help()
701 return 1
702
[email protected]b3b20512013-08-26 18:51:04703 if opts.aura:
704 if opts.archive != 'win' or not opts.official_builds:
705 print 'Error: Aura is supported only on Windows platform '\
706 'and official builds.'
707 return 1
708
[email protected]4c6fec6b2013-09-17 17:44:08709 if opts.blink:
710 base_url = WEBKIT_BASE_URL
711 else:
712 base_url = CHROMIUM_BASE_URL
713
[email protected]183706d92011-06-10 13:06:22714 # Create the context. Initialize 0 for the revisions as they are set below.
[email protected]4c6fec6b2013-09-17 17:44:08715 context = PathContext(base_url, opts.archive, 0, 0,
716 opts.official_builds, opts.aura)
[email protected]67e0bc62009-09-03 22:06:09717 # Pick a starting point, try to get HEAD for this.
[email protected]7ad66a72009-09-04 17:52:33718 if opts.bad:
719 bad_rev = opts.bad
720 else:
[email protected]801fb652012-07-20 20:13:50721 bad_rev = '999.0.0.0'
722 if not opts.official_builds:
723 bad_rev = GetChromiumRevision(context.GetLastChangeURL())
[email protected]67e0bc62009-09-03 22:06:09724
725 # Find out when we were good.
[email protected]7ad66a72009-09-04 17:52:33726 if opts.good:
727 good_rev = opts.good
728 else:
[email protected]801fb652012-07-20 20:13:50729 good_rev = '0.0.0.0' if opts.official_builds else 0
730
731 if opts.official_builds:
732 good_rev = LooseVersion(good_rev)
733 bad_rev = LooseVersion(bad_rev)
734 else:
735 good_rev = int(good_rev)
736 bad_rev = int(bad_rev)
737
[email protected]5e93cf162012-01-28 02:16:56738 if opts.times < 1:
739 print('Number of times to run (%d) must be greater than or equal to 1.' %
740 opts.times)
741 parser.print_help()
742 return 1
743
[email protected]eadd95d2012-11-02 22:42:09744 (min_chromium_rev, max_chromium_rev) = Bisect(
[email protected]4c6fec6b2013-09-17 17:44:08745 base_url, opts.archive, opts.official_builds, opts.aura, good_rev,
746 bad_rev, opts.times, opts.command, args, opts.profile)
[email protected]67e0bc62009-09-03 22:06:09747
[email protected]ff50d1c2013-04-17 18:49:36748 # Get corresponding blink revisions.
[email protected]b2fe7f22011-10-25 22:58:31749 try:
[email protected]4c6fec6b2013-09-17 17:44:08750 min_blink_rev = GetBlinkRevisionForChromiumRevision(context,
751 min_chromium_rev)
752 max_blink_rev = GetBlinkRevisionForChromiumRevision(context,
753 max_chromium_rev)
[email protected]b2fe7f22011-10-25 22:58:31754 except Exception, e:
755 # Silently ignore the failure.
[email protected]ff50d1c2013-04-17 18:49:36756 min_blink_rev, max_blink_rev = 0, 0
[email protected]b2fe7f22011-10-25 22:58:31757
[email protected]67e0bc62009-09-03 22:06:09758 # We're done. Let the user know the results in an official manner.
[email protected]eadd95d2012-11-02 22:42:09759 if good_rev > bad_rev:
760 print DONE_MESSAGE_GOOD_MAX % (str(min_chromium_rev), str(max_chromium_rev))
761 else:
762 print DONE_MESSAGE_GOOD_MIN % (str(min_chromium_rev), str(max_chromium_rev))
763
[email protected]ff50d1c2013-04-17 18:49:36764 if min_blink_rev != max_blink_rev:
765 print 'BLINK CHANGELOG URL:'
766 print ' ' + BLINK_CHANGELOG_URL % (max_blink_rev, min_blink_rev)
[email protected]d0149c5c2012-05-29 21:12:11767 print 'CHANGELOG URL:'
768 if opts.official_builds:
[email protected]eadd95d2012-11-02 22:42:09769 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
[email protected]d0149c5c2012-05-29 21:12:11770 else:
[email protected]eadd95d2012-11-02 22:42:09771 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
[email protected]cb155a82011-11-29 17:25:34772
[email protected]67e0bc62009-09-03 22:06:09773if __name__ == '__main__':
[email protected]7ad66a72009-09-04 17:52:33774 sys.exit(main())