Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(760)

Side by Side Diff: tools/bisect-builds.py

Issue 6788015: bisect-builds.py: Use the continuous archive, rather than the snapshots. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Created 9 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Snapshot Build Bisect Tool 6 """Snapshot Build Bisect Tool
7 7
8 This script bisects a snapshot archive using binary search. It starts at 8 This script bisects a snapshot archive using binary search. It starts at
9 a bad revision (it will try to guess HEAD) and asks for a last known-good 9 a bad revision (it will try to guess HEAD) and asks for a last known-good
10 revision. It will then binary search across this revision range by downloading, 10 revision. It will then binary search across this revision range by downloading,
11 unzipping, and opening Chromium for you. After testing the specific revision, 11 unzipping, and opening Chromium for you. After testing the specific revision,
12 it will ask you whether it is good or bad before continuing the search. 12 it will ask you whether it is good or bad before continuing the search.
13 """ 13 """
14 14
15 # Base URL to download snapshots from. 15 # Base URL to download snapshots from.
16 BUILD_BASE_URL = 'http://build.chromium.org/f/chromium/snapshots/' 16 BUILD_BASE_URL = 'http://build.chromium.org/f/chromium/continuous/'
17
18 # The index file that lists all the builds. This lives in BUILD_BASE_URL.
19 BUILD_INDEX_FILE = 'all_builds.txt'
17 20
18 # The type (platform) of the build archive. This is what's passed in to the 21 # The type (platform) of the build archive. This is what's passed in to the
19 # '-a/--archive' option. 22 # '-a/--archive' option.
20 BUILD_ARCHIVE_TYPE = '' 23 BUILD_ARCHIVE_TYPE = ''
21 24
22 # The selected archive to bisect. 25 # The location of the builds. Format this with a (date, revision) tuple, which
23 BUILD_ARCHIVE_DIR = '' 26 # can be obtained through ParseIndexLine().
24 27 BUILD_ARCHIVE_URL = '/%s/%d/'
25 # The location of the builds.
26 BUILD_ARCHIVE_URL = '/%d/'
27 28
28 # Name of the build archive. 29 # Name of the build archive.
29 BUILD_ZIP_NAME = '' 30 BUILD_ZIP_NAME = ''
30 31
31 # Directory name inside the archive. 32 # Directory name inside the archive.
32 BUILD_DIR_NAME = '' 33 BUILD_DIR_NAME = ''
33 34
34 # Name of the executable. 35 # Name of the executable.
35 BUILD_EXE_NAME = '' 36 BUILD_EXE_NAME = ''
36 37
(...skipping 10 matching lines...) Expand all
47 import optparse 48 import optparse
48 import os 49 import os
49 import pipes 50 import pipes
50 import re 51 import re
51 import shutil 52 import shutil
52 import sys 53 import sys
53 import tempfile 54 import tempfile
54 import urllib 55 import urllib
55 import zipfile 56 import zipfile
56 57
57
58 def UnzipFilenameToDir(filename, dir): 58 def UnzipFilenameToDir(filename, dir):
59 """Unzip |filename| to directory |dir|.""" 59 """Unzip |filename| to directory |dir|."""
60 zf = zipfile.ZipFile(filename) 60 zf = zipfile.ZipFile(filename)
61 # Make base. 61 # Make base.
62 pushd = os.getcwd() 62 pushd = os.getcwd()
63 try: 63 try:
64 if not os.path.isdir(dir): 64 if not os.path.isdir(dir):
65 os.mkdir(dir) 65 os.mkdir(dir)
66 os.chdir(dir) 66 os.chdir(dir)
67 # Extract files. 67 # Extract files.
68 for info in zf.infolist(): 68 for info in zf.infolist():
69 name = info.filename 69 name = info.filename
70 if name.endswith('/'): # dir 70 if name.endswith('/'): # dir
71 if not os.path.isdir(name): 71 if not os.path.isdir(name):
72 os.makedirs(name) 72 os.makedirs(name)
73 else: # file 73 else: # file
74 dir = os.path.dirname(name) 74 dir = os.path.dirname(name)
75 if not os.path.isdir(dir): 75 if not os.path.isdir(dir):
76 os.makedirs(dir) 76 os.makedirs(dir)
77 out = open(name, 'wb') 77 out = open(name, 'wb')
78 out.write(zf.read(name)) 78 out.write(zf.read(name))
79 out.close() 79 out.close()
80 # Set permissions. Permission info in external_attr is shifted 16 bits. 80 # Set permissions. Permission info in external_attr is shifted 16 bits.
81 os.chmod(name, info.external_attr >> 16L) 81 os.chmod(name, info.external_attr >> 16L)
82 os.chdir(pushd) 82 os.chdir(pushd)
83 except Exception, e: 83 except Exception, e:
84 print >>sys.stderr, e 84 print >>sys.stderr, e
85 sys.exit(1) 85 sys.exit(1)
86 86
Evan Martin 2011/04/14 19:03:56 fwiw, double-newline after function bodies is reco
Robert Sesek 2011/04/29 20:25:44 Done.
87
88 def SetArchiveVars(archive): 87 def SetArchiveVars(archive):
89 """Set a bunch of global variables appropriate for the specified archive.""" 88 """Set a bunch of global variables appropriate for the specified archive."""
90 global BUILD_ARCHIVE_TYPE 89 global BUILD_ARCHIVE_TYPE
91 global BUILD_ARCHIVE_DIR
92 global BUILD_ZIP_NAME 90 global BUILD_ZIP_NAME
93 global BUILD_DIR_NAME 91 global BUILD_DIR_NAME
94 global BUILD_EXE_NAME 92 global BUILD_EXE_NAME
95 global BUILD_BASE_URL 93 global BUILD_BASE_URL
96 94
97 BUILD_ARCHIVE_TYPE = archive 95 BUILD_ARCHIVE_TYPE = archive
98 BUILD_ARCHIVE_DIR = 'chromium-rel-' + BUILD_ARCHIVE_TYPE
99 96
100 if BUILD_ARCHIVE_TYPE in ('linux', 'linux-64', 'linux-chromiumos'): 97 if BUILD_ARCHIVE_TYPE in ('linux', 'linux64', 'linux-chromiumos'):
101 BUILD_ZIP_NAME = 'chrome-linux.zip' 98 BUILD_ZIP_NAME = 'chrome-linux.zip'
102 BUILD_DIR_NAME = 'chrome-linux' 99 BUILD_DIR_NAME = 'chrome-linux'
103 BUILD_EXE_NAME = 'chrome' 100 BUILD_EXE_NAME = 'chrome'
104 elif BUILD_ARCHIVE_TYPE in ('mac'): 101 elif BUILD_ARCHIVE_TYPE in ('mac'):
105 BUILD_ZIP_NAME = 'chrome-mac.zip' 102 BUILD_ZIP_NAME = 'chrome-mac.zip'
106 BUILD_DIR_NAME = 'chrome-mac' 103 BUILD_DIR_NAME = 'chrome-mac'
107 BUILD_EXE_NAME = 'Chromium.app/Contents/MacOS/Chromium' 104 BUILD_EXE_NAME = 'Chromium.app/Contents/MacOS/Chromium'
108 elif BUILD_ARCHIVE_TYPE in ('xp'): 105 elif BUILD_ARCHIVE_TYPE in ('xp'):
109 BUILD_ZIP_NAME = 'chrome-win32.zip' 106 BUILD_ZIP_NAME = 'chrome-win32.zip'
110 BUILD_DIR_NAME = 'chrome-win32' 107 BUILD_DIR_NAME = 'chrome-win32'
111 BUILD_EXE_NAME = 'chrome.exe' 108 BUILD_EXE_NAME = 'chrome.exe'
112 109
113 BUILD_BASE_URL += BUILD_ARCHIVE_DIR 110 def ParseDirectoryIndex(url):
111 """Parses the all_builds.txt index file. The format of this file is:
112 mac/2011-02-16/75130
113 mac/2011-02-16/75218
114 mac/2011-02-16/75226
115 mac/2011-02-16/75234
116 mac/2011-02-16/75184
117 This function will return a list of DATE/REVISION strings for the platform
118 specified by BUILD_ARCHIVE_TYPE.
119 """
120 handle = urllib.urlopen(url)
121 dirindex = handle.readlines()
122 handle.close()
114 123
115 def ParseDirectoryIndex(url): 124 # Only return values for the specified platform.
116 """Parses the HTML directory listing into a list of revision numbers.""" 125 archtype = BUILD_ARCHIVE_TYPE
117 handle = urllib.urlopen(url) 126 dirindex = filter(lambda l: l.startswith(archtype), dirindex)
118 dirindex = handle.read() 127
119 handle.close() 128 # Remove the newline separator and the platform token.
120 return re.findall(r'<a href="([0-9]*)/">\1/</a>', dirindex) 129 dirindex = map(lambda l: l[len(archtype) + 1:].strip(), dirindex)
130 dirindex.sort()
131 return dirindex
132
133 def ParseIndexLine(iline):
134 """Takes an index line returned by ParseDirectoryIndex() and returns a
135 2-tuple of (date, revision). |date| is a string and |revision| is an int."""
136 split = iline.split('/')
137 assert(len(split) == 2)
138 return (split[0], int(split[1]))
139
140 def GetRevision(iline):
141 """Takes an index line, parses it, and returns the revision."""
142 return ParseIndexLine(iline)[1]
121 143
122 def GetRevList(good, bad): 144 def GetRevList(good, bad):
123 """Gets the list of revision numbers between |good| and |bad|.""" 145 """Gets the list of revision numbers between |good| and |bad|."""
124 # Download the main revlist. 146 # Download the main revlist.
125 revlist = ParseDirectoryIndex(BUILD_BASE_URL) 147 revlist = ParseDirectoryIndex(BUILD_BASE_URL + BUILD_INDEX_FILE)
126 revlist = map(int, revlist) 148
127 revlist = filter(lambda r: range(good, bad).__contains__(int(r)), revlist) 149 revrange = range(good, bad)
150 revlist = filter(lambda r: revrange.__contains__(GetRevision(r)),
Evan Martin 2011/04/14 19:03:56 GetRevision(r) in revrange ?
Robert Sesek 2011/04/29 20:25:44 Done.
151 revlist)
128 revlist.sort() 152 revlist.sort()
129 return revlist 153 return revlist
130 154
131 def TryRevision(rev, profile, args): 155 def TryRevision(iline, profile, args):
132 """Downloads revision |rev|, unzips it, and opens it for the user to test. 156 """Downloads revision from |iline|, unzips it, and opens it for the user to
133 |profile| is the profile to use.""" 157 test. |profile| is the profile to use."""
134 # Do this in a temp dir so we don't collide with user files. 158 # Do this in a temp dir so we don't collide with user files.
135 cwd = os.getcwd() 159 cwd = os.getcwd()
136 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') 160 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
137 os.chdir(tempdir) 161 os.chdir(tempdir)
138 162
139 # Download the file. 163 # Download the file.
140 download_url = BUILD_BASE_URL + (BUILD_ARCHIVE_URL % rev) + BUILD_ZIP_NAME 164 download_url = BUILD_BASE_URL + BUILD_ARCHIVE_TYPE + \
141 def _Reporthook(blocknum, blocksize, totalsize): 165 (BUILD_ARCHIVE_URL % ParseIndexLine(iline)) + BUILD_ZIP_NAME
166 def _ReportHook(blocknum, blocksize, totalsize):
142 size = blocknum * blocksize 167 size = blocknum * blocksize
143 if totalsize == -1: # Total size not known. 168 if totalsize == -1: # Total size not known.
144 progress = "Received %d bytes" % size 169 progress = "Received %d bytes" % size
145 else: 170 else:
146 size = min(totalsize, size) 171 size = min(totalsize, size)
147 progress = "Received %d of %d bytes, %.2f%%" % ( 172 progress = "Received %d of %d bytes, %.2f%%" % (
148 size, totalsize, 100.0 * size / totalsize) 173 size, totalsize, 100.0 * size / totalsize)
149 # Send a \r to let all progress messages use just one line of output. 174 # Send a \r to let all progress messages use just one line of output.
150 sys.stdout.write("\r" + progress) 175 sys.stdout.write("\r" + progress)
151 sys.stdout.flush() 176 sys.stdout.flush()
152 try: 177 try:
153 print 'Fetching ' + download_url 178 print 'Fetching ' + download_url
154 urllib.urlretrieve(download_url, BUILD_ZIP_NAME, _Reporthook) 179 urllib.urlretrieve(download_url, BUILD_ZIP_NAME, _ReportHook)
155 print 180 print
156 except Exception, e: 181 except Exception, e:
157 print('Could not retrieve the download. Sorry.') 182 print('Could not retrieve the download. Sorry.')
158 sys.exit(-1) 183 sys.exit(-1)
159 184
160 # Unzip the file. 185 # Unzip the file.
161 print 'Unzipping ...' 186 print 'Unzipping ...'
162 UnzipFilenameToDir(BUILD_ZIP_NAME, os.curdir) 187 UnzipFilenameToDir(BUILD_ZIP_NAME, os.curdir)
163 188
164 # Tell the system to open the app. 189 # Tell the system to open the app.
165 args = ['--user-data-dir=%s' % profile] + args 190 args = ['--user-data-dir=%s' % profile] + args
166 flags = ' '.join(map(pipes.quote, args)) 191 flags = ' '.join(map(pipes.quote, args))
167 exe = os.path.join(os.getcwd(), BUILD_DIR_NAME, BUILD_EXE_NAME) 192 exe = os.path.join(os.getcwd(), BUILD_DIR_NAME, BUILD_EXE_NAME)
168 cmd = '%s %s' % (exe, flags) 193 cmd = '%s %s' % (exe, flags)
169 print 'Running %s' % cmd 194 print 'Running %s' % cmd
170 os.system(cmd) 195 os.system(cmd)
171 196
172 os.chdir(cwd) 197 os.chdir(cwd)
173 print 'Cleaning temp dir ...' 198 print 'Cleaning temp dir ...'
174 try: 199 try:
175 shutil.rmtree(tempdir, True) 200 shutil.rmtree(tempdir, True)
176 except Exception, e: 201 except Exception, e:
177 pass 202 pass
178 203
179 204 def AskIsGoodBuild(iline):
180 def AskIsGoodBuild(rev): 205 """Ask the user whether build from index line |iline| is good or bad."""
181 """Ask the user whether build |rev| is good or bad."""
182 # Loop until we get a response that we can parse. 206 # Loop until we get a response that we can parse.
183 while True: 207 while True:
184 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) 208 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % GetRevision(iline))
185 if response and response in ('g', 'b'): 209 if response and response in ('g', 'b'):
186 return response == 'g' 210 return response == 'g'
187 211
188 def main(): 212 def main():
189 usage = ('%prog [options] [-- chromium-options]\n' 213 usage = ('%prog [options] [-- chromium-options]\n'
190 'Perform binary search on the snapshot builds.\n' 214 'Perform binary search on the snapshot builds.\n'
191 '\n' 215 '\n'
192 'Tip: add "-- --no-first-run" to bypass the first run prompts.') 216 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
193 parser = optparse.OptionParser(usage=usage) 217 parser = optparse.OptionParser(usage=usage)
194 # Strangely, the default help output doesn't include the choice list. 218 # Strangely, the default help output doesn't include the choice list.
195 choices = ['mac', 'xp', 'linux', 'linux-64', 'linux-chromiumos'] 219 choices = ['mac', 'xp', 'linux', 'linux64']#, 'linux-chromiumos'] http://crbug .com/78158
Evan Martin 2011/04/14 19:03:56 typo here, extra pasted url
Robert Sesek 2011/04/29 20:25:44 No, it's actually a bug that chromiumos doesn't ha
196 parser.add_option('-a', '--archive', 220 parser.add_option('-a', '--archive',
197 choices = choices, 221 choices = choices,
198 help = 'The buildbot archive to bisect [%s].' % 222 help = 'The buildbot archive to bisect [%s].' %
199 '|'.join(choices)) 223 '|'.join(choices))
200 parser.add_option('-b', '--bad', type = 'int', 224 parser.add_option('-b', '--bad', type = 'int',
201 help = 'The bad revision to bisect to.') 225 help = 'The bad revision to bisect to.')
202 parser.add_option('-g', '--good', type = 'int', 226 parser.add_option('-g', '--good', type = 'int',
203 help = 'The last known good revision to bisect from.') 227 help = 'The last known good revision to bisect from.')
204 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', 228 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str',
205 help = 'Profile to use; this will not reset every run. ' + 229 help = 'Profile to use; this will not reset every run. ' +
(...skipping 14 matching lines...) Expand all
220 244
221 SetArchiveVars(opts.archive) 245 SetArchiveVars(opts.archive)
222 246
223 # Pick a starting point, try to get HEAD for this. 247 # Pick a starting point, try to get HEAD for this.
224 if opts.bad: 248 if opts.bad:
225 bad_rev = opts.bad 249 bad_rev = opts.bad
226 else: 250 else:
227 bad_rev = 0 251 bad_rev = 0
228 try: 252 try:
229 # Location of the latest build revision number 253 # Location of the latest build revision number
230 BUILD_LATEST_URL = '%s/LATEST' % (BUILD_BASE_URL) 254 BUILD_LATEST_URL = '%s/LATEST/REVISION' % (BUILD_BASE_URL)
231 nh = urllib.urlopen(BUILD_LATEST_URL) 255 nh = urllib.urlopen(BUILD_LATEST_URL)
232 latest = int(nh.read()) 256 latest = int(nh.read())
233 nh.close() 257 nh.close()
234 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest) 258 bad_rev = raw_input('Bad revision [HEAD:%d]: ' % latest)
235 if (bad_rev == ''): 259 if (bad_rev == ''):
236 bad_rev = latest 260 bad_rev = latest
237 bad_rev = int(bad_rev) 261 bad_rev = int(bad_rev)
238 except Exception, e: 262 except Exception, e:
239 print('Could not determine latest revision. This could be bad...') 263 print('Could not determine latest revision. This could be bad...')
240 bad_rev = int(raw_input('Bad revision: ')) 264 bad_rev = int(raw_input('Bad revision: '))
(...skipping 24 matching lines...) Expand all
265 last_known_good_rev = revlist[good] 289 last_known_good_rev = revlist[good]
266 290
267 # Binary search time! 291 # Binary search time!
268 while good < bad: 292 while good < bad:
269 candidates = revlist[good:bad] 293 candidates = revlist[good:bad]
270 num_poss = len(candidates) 294 num_poss = len(candidates)
271 if num_poss > 10: 295 if num_poss > 10:
272 print('%d candidates. %d tries left.' % 296 print('%d candidates. %d tries left.' %
273 (num_poss, round(math.log(num_poss, 2)))) 297 (num_poss, round(math.log(num_poss, 2))))
274 else: 298 else:
275 print('Candidates: %s' % revlist[good:bad]) 299 print('Candidates: %s' % map(GetRevision, revlist[good:bad]))
276 300
277 # Cut the problem in half... 301 # Cut the problem in half...
278 test = int((bad - good) / 2) + good 302 test = int((bad - good) / 2) + good
279 test_rev = revlist[test] 303 test_rev = revlist[test]
280 304
281 # Let the user give this rev a spin (in her own profile, if she wants). 305 # Let the user give this rev a spin (in her own profile, if she wants).
282 profile = opts.profile 306 profile = opts.profile
283 if not profile: 307 if not profile:
284 profile = 'profile' # In a temp dir. 308 profile = 'profile' # In a temp dir.
285 TryRevision(test_rev, profile, args) 309 TryRevision(test_rev, profile, args)
286 if AskIsGoodBuild(test_rev): 310 if AskIsGoodBuild(test_rev):
287 last_known_good_rev = revlist[good] 311 last_known_good_rev = revlist[good]
288 good = test + 1 312 good = test + 1
289 else: 313 else:
290 bad = test 314 bad = test
291 315
292 # We're done. Let the user know the results in an official manner. 316 # We're done. Let the user know the results in an official manner.
293 print('You are probably looking for build %d.' % revlist[bad]) 317 bad_revision = GetRevision(revlist[bad])
318 print('You are probably looking for build %d.' % bad_revision)
294 print('CHANGELOG URL:') 319 print('CHANGELOG URL:')
295 print(CHANGELOG_URL % (last_known_good_rev, revlist[bad])) 320 print(CHANGELOG_URL % (GetRevision(last_known_good_rev), bad_revision))
296 print('Built at revision:') 321 print('Built at revision:')
297 print(BUILD_VIEWVC_URL % revlist[bad]) 322 print(BUILD_VIEWVC_URL % bad_revision)
298 323
299 if __name__ == '__main__': 324 if __name__ == '__main__':
300 sys.exit(main()) 325 sys.exit(main())
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698