OLD | NEW |
---|---|
1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2012 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, |
(...skipping 23 matching lines...) Expand all Loading... | |
34 | 34 |
35 DONE_MESSAGE = 'You are probably looking for a change made after ' \ | 35 DONE_MESSAGE = 'You are probably looking for a change made after ' \ |
36 '%s (known good), but no later than %s (first known bad).' | 36 '%s (known good), but no later than %s (first known bad).' |
37 | 37 |
38 ############################################################################### | 38 ############################################################################### |
39 | 39 |
40 import math | 40 import math |
41 import optparse | 41 import optparse |
42 import os | 42 import os |
43 import pipes | 43 import pipes |
44 import random | |
44 import re | 45 import re |
45 import shutil | 46 import shutil |
46 import subprocess | 47 import subprocess |
47 import sys | 48 import sys |
48 import tempfile | 49 import tempfile |
49 import threading | 50 import threading |
50 import urllib | 51 import urllib |
51 from distutils.version import LooseVersion | 52 from distutils.version import LooseVersion |
52 from xml.etree import ElementTree | 53 from xml.etree import ElementTree |
53 import zipfile | 54 import zipfile |
(...skipping 280 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
334 except Exception, e: | 335 except Exception, e: |
335 pass | 336 pass |
336 | 337 |
337 return (subproc.returncode, stdout, stderr) | 338 return (subproc.returncode, stdout, stderr) |
338 | 339 |
339 | 340 |
340 def AskIsGoodBuild(rev, official_builds, status, stdout, stderr): | 341 def AskIsGoodBuild(rev, official_builds, status, stdout, stderr): |
341 """Ask the user whether build |rev| is good or bad.""" | 342 """Ask the user whether build |rev| is good or bad.""" |
342 # Loop until we get a response that we can parse. | 343 # Loop until we get a response that we can parse. |
343 while True: | 344 while True: |
344 response = raw_input('Revision %s is [(g)ood/(b)ad/(q)uit]: ' % str(rev)) | 345 response = raw_input('Revision %s is [(g)ood/(b)ad/(u)nknown/(q)uit]: ' % |
345 if response and response in ('g', 'b'): | 346 str(rev)) |
346 return response == 'g' | 347 if response and response in ('g', 'b', 'u'): |
348 return response | |
347 if response and response == 'q': | 349 if response and response == 'q': |
348 raise SystemExit() | 350 raise SystemExit() |
349 | 351 |
350 | 352 |
353 class DownloadJob(object): | |
354 """DownloadJob represents a task to download a given Chromium revision.""" | |
355 def __init__(self, context, name, rev, zipfile): | |
356 super(DownloadJob, self).__init__() | |
357 # Store off the input parameters. | |
358 self.context = context | |
359 self.name = name | |
360 self.rev = rev | |
361 self.zipfile = zipfile | |
362 self.quit_event = threading.Event() | |
363 self.progress_event = threading.Event() | |
364 | |
365 def Start(self): | |
366 """Starts the download.""" | |
367 fetchargs = (self.context, | |
368 self.rev, | |
369 self.zipfile, | |
370 self.quit_event, | |
371 self.progress_event) | |
372 self.thread = threading.Thread(target=FetchRevision, | |
373 name=self.name, | |
374 args=fetchargs) | |
375 self.thread.start() | |
376 | |
377 def Stop(self): | |
378 """Stops the download which must have been started previously.""" | |
379 self.quit_event.set() | |
380 self.thread.join() | |
381 os.unlink(self.zipfile) | |
382 | |
383 def WaitFor(self): | |
384 """Prints a message and waits for the download to complete. The download | |
385 must have been started previously.""" | |
386 print "Downloading revision %s..." % str(self.rev) | |
387 self.progress_event.set() # Display progress of download. | |
388 self.thread.join() | |
389 | |
390 | |
351 def Bisect(platform, | 391 def Bisect(platform, |
352 official_builds, | 392 official_builds, |
353 good_rev=0, | 393 good_rev=0, |
354 bad_rev=0, | 394 bad_rev=0, |
355 num_runs=1, | 395 num_runs=1, |
356 try_args=(), | 396 try_args=(), |
357 profile=None, | 397 profile=None, |
358 predicate=AskIsGoodBuild): | 398 evaluate=AskIsGoodBuild): |
359 """Given known good and known bad revisions, run a binary search on all | 399 """Given known good and known bad revisions, run a binary search on all |
360 archived revisions to determine the last known good revision. | 400 archived revisions to determine the last known good revision. |
361 | 401 |
362 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). | 402 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). |
363 @param official_builds Specify build type (Chromium or Official build). | 403 @param official_builds Specify build type (Chromium or Official build). |
364 @param good_rev Number/tag of the last known good revision. | 404 @param good_rev Number/tag of the last known good revision. |
365 @param bad_rev Number/tag of the first known bad revision. | 405 @param bad_rev Number/tag of the first known bad revision. |
366 @param num_runs Number of times to run each build for asking good/bad. | 406 @param num_runs Number of times to run each build for asking good/bad. |
367 @param try_args A tuple of arguments to pass to the test application. | 407 @param try_args A tuple of arguments to pass to the test application. |
368 @param profile The name of the user profile to run with. | 408 @param profile The name of the user profile to run with. |
369 @param predicate A predicate function which returns True iff the argument | 409 @param evaluate A function which returns 'g' if the argument Chromium |
Robert Sesek
2012/05/31 19:46:10
Since this can now bisect official builds, just sa
Alexei Svitkine (slow)
2012/05/31 20:08:15
Done.
| |
370 chromium revision is good. | 410 revision is good, 'b' if it's bad or 'u' if unknown. |
371 | 411 |
372 Threading is used to fetch Chromium revisions in the background, speeding up | 412 Threading is used to fetch Chromium revisions in the background, speeding up |
373 the user's experience. For example, suppose the bounds of the search are | 413 the user's experience. For example, suppose the bounds of the search are |
374 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on | 414 good_rev=0, bad_rev=100. The first revision to be checked is 50. Depending on |
375 whether revision 50 is good or bad, the next revision to check will be either | 415 whether revision 50 is good or bad, the next revision to check will be either |
376 25 or 75. So, while revision 50 is being checked, the script will download | 416 25 or 75. So, while revision 50 is being checked, the script will download |
377 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is | 417 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is |
378 known: | 418 known: |
379 | 419 |
380 - If rev 50 is good, the download of rev 25 is cancelled, and the next test | 420 - If rev 50 is good, the download of rev 25 is cancelled, and the next test |
(...skipping 23 matching lines...) Expand all Loading... | |
404 if len(revlist) < 2: # Don't have enough builds to bisect. | 444 if len(revlist) < 2: # Don't have enough builds to bisect. |
405 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist | 445 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist |
406 raise RuntimeError(msg) | 446 raise RuntimeError(msg) |
407 | 447 |
408 # Figure out our bookends and first pivot point; fetch the pivot revision. | 448 # Figure out our bookends and first pivot point; fetch the pivot revision. |
409 good = 0 | 449 good = 0 |
410 bad = len(revlist) - 1 | 450 bad = len(revlist) - 1 |
411 pivot = bad / 2 | 451 pivot = bad / 2 |
412 rev = revlist[pivot] | 452 rev = revlist[pivot] |
413 zipfile = _GetDownloadPath(rev) | 453 zipfile = _GetDownloadPath(rev) |
414 progress_event = threading.Event() | 454 base_fetch = DownloadJob(context, 'base_fetch', rev, zipfile) |
415 progress_event.set() | 455 base_fetch.Start() |
416 print "Downloading revision %s..." % str(rev) | 456 base_fetch.WaitFor() |
417 FetchRevision(context, rev, zipfile, | |
418 quit_event=None, progress_event=progress_event) | |
419 | 457 |
420 # Binary search time! | 458 # Binary search time! |
421 while zipfile and bad - good > 1: | 459 while zipfile and bad - good > 1: |
422 # Pre-fetch next two possible pivots | 460 # Pre-fetch next two possible pivots |
423 # - down_pivot is the next revision to check if the current revision turns | 461 # - down_pivot is the next revision to check if the current revision turns |
424 # out to be bad. | 462 # out to be bad. |
425 # - up_pivot is the next revision to check if the current revision turns | 463 # - up_pivot is the next revision to check if the current revision turns |
426 # out to be good. | 464 # out to be good. |
427 down_pivot = int((pivot - good) / 2) + good | 465 down_pivot = int((pivot - good) / 2) + good |
428 down_thread = None | 466 down_fetch = None |
429 if down_pivot != pivot and down_pivot != good: | 467 if down_pivot != pivot and down_pivot != good: |
430 down_rev = revlist[down_pivot] | 468 down_rev = revlist[down_pivot] |
431 down_zipfile = _GetDownloadPath(down_rev) | 469 down_fetch = DownloadJob(context, 'down_fetch', down_rev, |
432 down_quit_event = threading.Event() | 470 _GetDownloadPath(down_rev)) |
433 down_progress_event = threading.Event() | 471 down_fetch.Start() |
434 fetchargs = (context, | |
435 down_rev, | |
436 down_zipfile, | |
437 down_quit_event, | |
438 down_progress_event) | |
439 down_thread = threading.Thread(target=FetchRevision, | |
440 name='down_fetch', | |
441 args=fetchargs) | |
442 down_thread.start() | |
443 | 472 |
444 up_pivot = int((bad - pivot) / 2) + pivot | 473 up_pivot = int((bad - pivot) / 2) + pivot |
445 up_thread = None | 474 up_fetch = None |
446 if up_pivot != pivot and up_pivot != bad: | 475 if up_pivot != pivot and up_pivot != bad: |
447 up_rev = revlist[up_pivot] | 476 up_rev = revlist[up_pivot] |
448 up_zipfile = _GetDownloadPath(up_rev) | 477 up_fetch = DownloadJob(context, 'up_fetch', up_rev, |
449 up_quit_event = threading.Event() | 478 _GetDownloadPath(up_rev)) |
450 up_progress_event = threading.Event() | 479 up_fetch.Start() |
451 fetchargs = (context, | |
452 up_rev, | |
453 up_zipfile, | |
454 up_quit_event, | |
455 up_progress_event) | |
456 up_thread = threading.Thread(target=FetchRevision, | |
457 name='up_fetch', | |
458 args=fetchargs) | |
459 up_thread.start() | |
460 | 480 |
461 # Run test on the pivot revision. | 481 # Run test on the pivot revision. |
462 (status, stdout, stderr) = RunRevision(context, | 482 (status, stdout, stderr) = RunRevision(context, |
463 rev, | 483 rev, |
464 zipfile, | 484 zipfile, |
465 profile, | 485 profile, |
466 num_runs, | 486 num_runs, |
467 try_args) | 487 try_args) |
468 os.unlink(zipfile) | 488 os.unlink(zipfile) |
469 zipfile = None | 489 zipfile = None |
470 | 490 |
471 # Call the predicate function to see if the current revision is good or bad. | 491 # Call the evaluate function to see if the current revision is good or bad. |
472 # On that basis, kill one of the background downloads and complete the | 492 # On that basis, kill one of the background downloads and complete the |
473 # other, as described in the comments above. | 493 # other, as described in the comments above. |
474 try: | 494 try: |
475 if predicate(rev, official_builds, status, stdout, stderr): | 495 answer = evaluate(rev, official_builds, status, stdout, stderr) |
496 if answer == 'g': | |
476 good = pivot | 497 good = pivot |
477 if down_thread: | 498 if down_fetch: |
478 down_quit_event.set() # Kill the download of older revision. | 499 down_fetch.Stop() # Kill the download of the older revision. |
479 down_thread.join() | 500 if up_fetch: |
480 os.unlink(down_zipfile) | 501 up_fetch.WaitFor() |
481 if up_thread: | |
482 print "Downloading revision %s..." % str(up_rev) | |
483 up_progress_event.set() # Display progress of download. | |
484 up_thread.join() # Wait for newer revision to finish downloading. | |
485 pivot = up_pivot | 502 pivot = up_pivot |
486 zipfile = up_zipfile | 503 zipfile = up_fetch.zipfile |
487 else: | 504 elif answer == 'b': |
488 bad = pivot | 505 bad = pivot |
489 if up_thread: | 506 if up_fetch: |
490 up_quit_event.set() # Kill download of newer revision. | 507 up_fetch.Stop() # Kill the download of the newer revision. |
491 up_thread.join() | 508 if down_fetch: |
492 os.unlink(up_zipfile) | 509 down_fetch.WaitFor() |
493 if down_thread: | |
494 print "Downloading revision %s..." % str(down_rev) | |
495 down_progress_event.set() # Display progress of download. | |
496 down_thread.join() # Wait for older revision to finish downloading. | |
497 pivot = down_pivot | 510 pivot = down_pivot |
498 zipfile = down_zipfile | 511 zipfile = down_fetch.zipfile |
512 else: # answer == 'u' | |
Robert Sesek
2012/05/31 19:46:10
I'd just make this an elseif and then have an else
Alexei Svitkine (slow)
2012/05/31 20:08:15
Done.
| |
513 # Nuke the revision from the revlist and choose a new pivot. | |
514 revlist.pop(pivot) | |
515 bad -= 1 # Assumes bad >= pivot. | |
516 | |
517 fetch = None | |
518 if bad - good > 1: | |
519 # Randomly use down_pivot or up_pivot without affecting the range. | |
520 # Do this instead of setting the pivot to the midpoint of the new | |
521 # range because adjacent revisions are likely affected by the same | |
522 # issue that caused the (u)nknown response. | |
523 if up_fetch and down_fetch: | |
524 fetch = random.choice([up_fetch, down_fetch]) | |
Robert Sesek
2012/05/31 19:46:10
Any reason to do this instead of [up_fetch, down_f
Alexei Svitkine (slow)
2012/05/31 20:08:15
I like len(revlist), since this will alternate bet
| |
525 elif up_fetch: | |
526 fetch = up_fetch | |
527 else: | |
528 fetch = down_fetch | |
529 if fetch: | |
Robert Sesek
2012/05/31 19:46:10
When would |fetch| be None still?
Alexei Svitkine (slow)
2012/05/31 20:08:15
If |down_fetch| is None, which I assumed could hap
Alexei Svitkine (slow)
2012/05/31 20:26:46
So, this condition could happen if there are only
Alexei Svitkine (slow)
2012/05/31 20:32:52
Done.
| |
530 fetch.WaitFor() | |
531 if fetch == up_fetch: | |
532 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized. | |
533 else: | |
534 pivot = down_pivot | |
535 zipfile = fetch.zipfile | |
536 | |
537 if down_fetch and fetch != down_fetch: | |
538 down_fetch.Stop() | |
539 if up_fetch and fetch != up_fetch: | |
540 up_fetch.Stop() | |
499 except SystemExit: | 541 except SystemExit: |
500 print "Cleaning up..." | 542 print "Cleaning up..." |
501 for f in [_GetDownloadPath(revlist[down_pivot]), | 543 for f in [_GetDownloadPath(revlist[down_pivot]), |
502 _GetDownloadPath(revlist[up_pivot])]: | 544 _GetDownloadPath(revlist[up_pivot])]: |
503 try: | 545 try: |
504 os.unlink(f) | 546 os.unlink(f) |
505 except OSError: | 547 except OSError: |
506 pass | 548 pass |
507 sys.exit(0) | 549 sys.exit(0) |
508 | 550 |
(...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
630 print ' ' + WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, | 672 print ' ' + WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, |
631 last_known_good_webkit_rev) | 673 last_known_good_webkit_rev) |
632 print 'CHANGELOG URL:' | 674 print 'CHANGELOG URL:' |
633 if opts.official_builds: | 675 if opts.official_builds: |
634 print OFFICIAL_CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) | 676 print OFFICIAL_CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) |
635 else: | 677 else: |
636 print ' ' + CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) | 678 print ' ' + CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) |
637 | 679 |
638 if __name__ == '__main__': | 680 if __name__ == '__main__': |
639 sys.exit(main()) | 681 sys.exit(main()) |
OLD | NEW |