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

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

Issue 7329005: Add programmatic interface (Closed) Base URL: http://git.chromium.org/git/chromium.git@trunk
Patch Set: Added documentation Created 9 years, 5 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) 2011 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 # The root URL for storage. 15 # The root URL for storage.
16 BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-continuous' 16 BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-snapshots'
17 17
18 # URL to the ViewVC commit page. 18 # URL to the ViewVC commit page.
19 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d' 19 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d'
20 20
21 # Changelogs URL. 21 # Changelogs URL.
22 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ 22 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \
23 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' 23 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d'
24 24
25 ############################################################################### 25 ###############################################################################
26 26
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after
77 def GetListingURL(self, marker=None): 77 def GetListingURL(self, marker=None):
78 """Returns the URL for a directory listing, with an optional marker.""" 78 """Returns the URL for a directory listing, with an optional marker."""
79 marker_param = '' 79 marker_param = ''
80 if marker: 80 if marker:
81 marker_param = '&marker=' + str(marker) 81 marker_param = '&marker=' + str(marker)
82 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ 82 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \
83 marker_param 83 marker_param
84 84
85 def GetDownloadURL(self, revision): 85 def GetDownloadURL(self, revision):
86 """Gets the download URL for a build archive of a specific revision.""" 86 """Gets the download URL for a build archive of a specific revision."""
87 return BASE_URL + '/' + self._listing_platform_dir + str(revision) + '/' + \ 87 return "%s/%s%d/%s" % (
88 self.archive_name 88 BASE_URL, self._listing_platform_dir, revision, self.archive_name)
89 89
90 def GetLastChangeURL(self): 90 def GetLastChangeURL(self):
91 """Returns a URL to the LAST_CHANGE file.""" 91 """Returns a URL to the LAST_CHANGE file."""
92 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' 92 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE'
93 93
94 def GetLaunchPath(self): 94 def GetLaunchPath(self):
95 """Returns a relative path (presumably from the archive extraction location) 95 """Returns a relative path (presumably from the archive extraction location)
96 that is used to run the executable.""" 96 that is used to run the executable."""
97 return os.path.join(self._archive_extract_dir, self._binary_name) 97 return os.path.join(self._archive_extract_dir, self._binary_name)
98 98
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after
142 # All nodes in the tree are namespaced. Get the root's tag name to extract 142 # All nodes in the tree are namespaced. Get the root's tag name to extract
143 # the namespace. Etree does namespaces as |{namespace}tag|. 143 # the namespace. Etree does namespaces as |{namespace}tag|.
144 root_tag = document.getroot().tag 144 root_tag = document.getroot().tag
145 end_ns_pos = root_tag.find('}') 145 end_ns_pos = root_tag.find('}')
146 if end_ns_pos == -1: 146 if end_ns_pos == -1:
147 raise Exception("Could not locate end namespace for directory index") 147 raise Exception("Could not locate end namespace for directory index")
148 namespace = root_tag[:end_ns_pos + 1] 148 namespace = root_tag[:end_ns_pos + 1]
149 149
150 # Find the prefix (_listing_platform_dir) and whether or not the list is 150 # Find the prefix (_listing_platform_dir) and whether or not the list is
151 # truncated. 151 # truncated.
152 prefix = document.find(namespace + 'Prefix').text 152 prefix_len = len(document.find(namespace + 'Prefix').text)
153 next_marker = None 153 next_marker = None
154 is_truncated = document.find(namespace + 'IsTruncated') 154 is_truncated = document.find(namespace + 'IsTruncated')
155 if is_truncated is not None and is_truncated.text.lower() == 'true': 155 if is_truncated is not None and is_truncated.text.lower() == 'true':
156 next_marker = document.find(namespace + 'NextMarker').text 156 next_marker = document.find(namespace + 'NextMarker').text
157 157
158 # Get a list of all the revisions. 158 # Get a list of all the revisions.
159 all_prefixes = document.findall(namespace + 'CommonPrefixes/' + 159 all_prefixes = document.findall(namespace + 'CommonPrefixes/' +
160 namespace + 'Prefix') 160 namespace + 'Prefix')
161 # The <Prefix> nodes have content of the form of 161 # The <Prefix> nodes have content of the form of
162 # |_listing_platform_dir/revision/|. Strip off the platform dir and the 162 # |_listing_platform_dir/revision/|. Strip off the platform dir and the
163 # trailing slash to just have a number. 163 # trailing slash to just have a number.
164 revisions = map(lambda x: x.text[len(prefix):-1], all_prefixes) 164 revisions = []
165 for prefix in all_prefixes:
166 revnum = prefix.text[prefix_len:-1]
167 try:
168 revnum = int(revnum)
169 revisions.append(revnum)
170 except ValueError:
171 pass
165 return (revisions, next_marker) 172 return (revisions, next_marker)
166 173
167 # Fetch the first list of revisions. 174 # Fetch the first list of revisions.
168 (revisions, next_marker) = _FetchAndParse(context.GetListingURL()) 175 (revisions, next_marker) = _FetchAndParse(context.GetListingURL())
169 # If the result list was truncated, refetch with the next marker. Do this 176 # If the result list was truncated, refetch with the next marker. Do this
170 # until an entire directory listing is done. 177 # until an entire directory listing is done.
171 while next_marker: 178 while next_marker:
172 (new_revisions, next_marker) = _FetchAndParse( 179 (new_revisions, next_marker) = _FetchAndParse(
173 context.GetListingURL(next_marker)) 180 context.GetListingURL(next_marker))
174 revisions.extend(new_revisions) 181 revisions.extend(new_revisions)
(...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after
237 244
238 def AskIsGoodBuild(rev): 245 def AskIsGoodBuild(rev):
239 """Ask the user whether build |rev| is good or bad.""" 246 """Ask the user whether build |rev| is good or bad."""
240 # Loop until we get a response that we can parse. 247 # Loop until we get a response that we can parse.
241 while True: 248 while True:
242 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) 249 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev))
243 if response and response in ('g', 'b'): 250 if response and response in ('g', 'b'):
244 return response == 'g' 251 return response == 'g'
245 252
246 253
254 def Bisect(good,
255 bad,
256 revlist,
257 context,
258 try_args=(),
259 profile='profile',
260 predicate=AskIsGoodBuild):
261 """Tries to find the exact commit where a regression was introduced by
262 running a binary search on all archived builds in a given revision range.
263
264 @param good The index in revlist of the last known good revision
Robert Sesek 2011/07/18 15:40:33 nit: each of these should end with a full-stop
265 @param bad The index in revlist of the first known bad revision
266 @param revlist A list of chromium revision numbers to check
267 @param context A PathContext object
268 @param try_args A tuple of arguments to pass to the predicate function
269 @param profile The user profile with which to run chromium
270 @param predicate A predicate function which returns True iff the argument
271 chromium revision is good
272 """
273
274 last_known_good_rev = revlist[good]
275 first_known_bad_rev = revlist[bad]
276
277 # Binary search time!
278 while good < bad:
279 candidates = revlist[good:bad]
280 num_poss = len(candidates)
281 if num_poss > 10:
282 print('%d candidates. %d tries left.' %
283 (num_poss, round(math.log(num_poss, 2))))
284 else:
285 print('Candidates: %s' % revlist[good:bad])
286
287 # Cut the problem in half...
288 test = int((bad - good) / 2) + good
289 test_rev = revlist[test]
290
291 # Let the user give this rev a spin (in her own profile, if she wants).
292 TryRevision(context, test_rev, profile, try_args)
293 if predicate(test_rev):
294 last_known_good_rev = revlist[good]
295 good = test + 1
296 else:
297 bad = test
298
299 return (last_known_good_rev, first_known_bad_rev)
300
301
247 def main(): 302 def main():
248 usage = ('%prog [options] [-- chromium-options]\n' 303 usage = ('%prog [options] [-- chromium-options]\n'
249 'Perform binary search on the snapshot builds.\n' 304 'Perform binary search on the snapshot builds.\n'
250 '\n' 305 '\n'
251 'Tip: add "-- --no-first-run" to bypass the first run prompts.') 306 'Tip: add "-- --no-first-run" to bypass the first run prompts.')
252 parser = optparse.OptionParser(usage=usage) 307 parser = optparse.OptionParser(usage=usage)
253 # Strangely, the default help output doesn't include the choice list. 308 # Strangely, the default help output doesn't include the choice list.
254 choices = ['mac', 'win', 'linux', 'linux64'] 309 choices = ['mac', 'win', 'linux', 'linux64']
255 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 310 # linux-chromiumos lacks a continuous archive http://crbug.com/78158
256 parser.add_option('-a', '--archive', 311 parser.add_option('-a', '--archive',
257 choices = choices, 312 choices = choices,
258 help = 'The buildbot archive to bisect [%s].' % 313 help = 'The buildbot archive to bisect [%s].' %
259 '|'.join(choices)) 314 '|'.join(choices))
260 parser.add_option('-b', '--bad', type = 'int', 315 parser.add_option('-b', '--bad', type = 'int',
261 help = 'The bad revision to bisect to.') 316 help = 'The bad revision to bisect to.')
262 parser.add_option('-g', '--good', type = 'int', 317 parser.add_option('-g', '--good', type = 'int',
263 help = 'The last known good revision to bisect from.') 318 help = 'The last known good revision to bisect from.')
264 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', 319 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str',
265 help = 'Profile to use; this will not reset every run. ' + 320 help = 'Profile to use; this will not reset every run. ' +
266 'Defaults to a clean profile.') 321 'Defaults to a clean profile.', default = 'profile')
267 (opts, args) = parser.parse_args() 322 (opts, args) = parser.parse_args()
268 323
269 if opts.archive is None: 324 if opts.archive is None:
270 print 'Error: missing required parameter: --archive' 325 print 'Error: missing required parameter: --archive'
271 print 326 print
272 parser.print_help() 327 parser.print_help()
273 return 1 328 return 1
274 329
275 if opts.bad and opts.good and (opts.good > opts.bad): 330 if opts.bad and opts.good and (opts.good > opts.bad):
276 print ('The good revision (%d) must precede the bad revision (%d).\n' % 331 print ('The good revision (%d) must precede the bad revision (%d).\n' %
(...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after
319 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist 374 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist
320 sys.exit(1) 375 sys.exit(1)
321 376
322 # If we don't have a |good_rev|, set it to be the first revision possible. 377 # If we don't have a |good_rev|, set it to be the first revision possible.
323 if good_rev == 0: 378 if good_rev == 0:
324 good_rev = revlist[0] 379 good_rev = revlist[0]
325 380
326 # These are indexes of |revlist|. 381 # These are indexes of |revlist|.
327 good = 0 382 good = 0
328 bad = len(revlist) - 1 383 bad = len(revlist) - 1
329 last_known_good_rev = revlist[good]
330 384
331 # Binary search time! 385 (last_known_good_rev, first_known_bad_rev) = Bisect(
332 while good < bad: 386 good, bad, revlist, context, args, opts.profile)
333 candidates = revlist[good:bad]
334 num_poss = len(candidates)
335 if num_poss > 10:
336 print('%d candidates. %d tries left.' %
337 (num_poss, round(math.log(num_poss, 2))))
338 else:
339 print('Candidates: %s' % revlist[good:bad])
340
341 # Cut the problem in half...
342 test = int((bad - good) / 2) + good
343 test_rev = revlist[test]
344
345 # Let the user give this rev a spin (in her own profile, if she wants).
346 profile = opts.profile
347 if not profile:
348 profile = 'profile' # In a temp dir.
349 TryRevision(context, test_rev, profile, args)
350 if AskIsGoodBuild(test_rev):
351 last_known_good_rev = revlist[good]
352 good = test + 1
353 else:
354 bad = test
355 387
356 # We're done. Let the user know the results in an official manner. 388 # We're done. Let the user know the results in an official manner.
357 print('You are probably looking for build %d.' % revlist[bad]) 389 print('You are probably looking for build %d.' % first_known_bad_rev)
358 print('CHANGELOG URL:') 390 print('CHANGELOG URL:')
359 print(CHANGELOG_URL % (last_known_good_rev, revlist[bad])) 391 print(CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev))
360 print('Built at revision:') 392 print('Built at revision:')
361 print(BUILD_VIEWVC_URL % revlist[bad]) 393 print(BUILD_VIEWVC_URL % first_known_bad_rev)
362 394
363 if __name__ == '__main__': 395 if __name__ == '__main__':
364 sys.exit(main()) 396 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