OLD | NEW |
---|---|
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 Loading... | |
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 Loading... | |
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 Loading... | |
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 Loading... | |
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()) |
OLD | NEW |