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 323 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
334 except Exception, e: | 334 except Exception, e: |
335 pass | 335 pass |
336 | 336 |
337 return (subproc.returncode, stdout, stderr) | 337 return (subproc.returncode, stdout, stderr) |
338 | 338 |
339 | 339 |
340 def AskIsGoodBuild(rev, official_builds, status, stdout, stderr): | 340 def AskIsGoodBuild(rev, official_builds, status, stdout, stderr): |
341 """Ask the user whether build |rev| is good or bad.""" | 341 """Ask the user whether build |rev| is good or bad.""" |
342 # Loop until we get a response that we can parse. | 342 # Loop until we get a response that we can parse. |
343 while True: | 343 while True: |
344 response = raw_input('Revision %s is [(g)ood/(b)ad/(q)uit]: ' % str(rev)) | 344 response = raw_input('Revision %s is [(g)ood/(b)ad/(u)nknown/(q)uit]: ' % |
345 if response and response in ('g', 'b'): | 345 str(rev)) |
346 return response == 'g' | 346 if response and response in ('g', 'b', 'u'): |
| 347 return response |
347 if response and response == 'q': | 348 if response and response == 'q': |
348 raise SystemExit() | 349 raise SystemExit() |
349 | 350 |
350 | 351 |
| 352 class DownloadJob(object): |
| 353 """DownloadJob represents a task to download a given Chromium revision.""" |
| 354 def __init__(self, context, name, rev, zipfile): |
| 355 super(DownloadJob, self).__init__() |
| 356 # Store off the input parameters. |
| 357 self.context = context |
| 358 self.name = name |
| 359 self.rev = rev |
| 360 self.zipfile = zipfile |
| 361 self.quit_event = threading.Event() |
| 362 self.progress_event = threading.Event() |
| 363 |
| 364 def Start(self): |
| 365 """Starts the download.""" |
| 366 fetchargs = (self.context, |
| 367 self.rev, |
| 368 self.zipfile, |
| 369 self.quit_event, |
| 370 self.progress_event) |
| 371 self.thread = threading.Thread(target=FetchRevision, |
| 372 name=self.name, |
| 373 args=fetchargs) |
| 374 self.thread.start() |
| 375 |
| 376 def Stop(self): |
| 377 """Stops the download which must have been started previously.""" |
| 378 self.quit_event.set() |
| 379 self.thread.join() |
| 380 os.unlink(self.zipfile) |
| 381 |
| 382 def WaitFor(self): |
| 383 """Prints a message and waits for the download to complete. The download |
| 384 must have been started previously.""" |
| 385 print "Downloading revision %s..." % str(self.rev) |
| 386 self.progress_event.set() # Display progress of download. |
| 387 self.thread.join() |
| 388 |
| 389 |
351 def Bisect(platform, | 390 def Bisect(platform, |
352 official_builds, | 391 official_builds, |
353 good_rev=0, | 392 good_rev=0, |
354 bad_rev=0, | 393 bad_rev=0, |
355 num_runs=1, | 394 num_runs=1, |
356 try_args=(), | 395 try_args=(), |
357 profile=None, | 396 profile=None, |
358 predicate=AskIsGoodBuild): | 397 evaluate=AskIsGoodBuild): |
359 """Given known good and known bad revisions, run a binary search on all | 398 """Given known good and known bad revisions, run a binary search on all |
360 archived revisions to determine the last known good revision. | 399 archived revisions to determine the last known good revision. |
361 | 400 |
362 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). | 401 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). |
363 @param official_builds Specify build type (Chromium or Official build). | 402 @param official_builds Specify build type (Chromium or Official build). |
364 @param good_rev Number/tag of the last known good revision. | 403 @param good_rev Number/tag of the last known good revision. |
365 @param bad_rev Number/tag of the first known bad revision. | 404 @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. | 405 @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. | 406 @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. | 407 @param profile The name of the user profile to run with. |
369 @param predicate A predicate function which returns True iff the argument | 408 @param evaluate A function which returns 'g' if the argument build is good, |
370 chromium revision is good. | 409 'b' if it's bad or 'u' if unknown. |
371 | 410 |
372 Threading is used to fetch Chromium revisions in the background, speeding up | 411 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 | 412 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 | 413 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 | 414 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 | 415 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 | 416 revisions 25 and 75 in the background. Once the good/bad verdict on rev 50 is |
378 known: | 417 known: |
379 | 418 |
380 - If rev 50 is good, the download of rev 25 is cancelled, and the next test | 419 - 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. | 443 if len(revlist) < 2: # Don't have enough builds to bisect. |
405 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist | 444 msg = 'We don\'t have enough builds to bisect. revlist: %s' % revlist |
406 raise RuntimeError(msg) | 445 raise RuntimeError(msg) |
407 | 446 |
408 # Figure out our bookends and first pivot point; fetch the pivot revision. | 447 # Figure out our bookends and first pivot point; fetch the pivot revision. |
409 good = 0 | 448 good = 0 |
410 bad = len(revlist) - 1 | 449 bad = len(revlist) - 1 |
411 pivot = bad / 2 | 450 pivot = bad / 2 |
412 rev = revlist[pivot] | 451 rev = revlist[pivot] |
413 zipfile = _GetDownloadPath(rev) | 452 zipfile = _GetDownloadPath(rev) |
414 progress_event = threading.Event() | 453 initial_fetch = DownloadJob(context, 'initial_fetch', rev, zipfile) |
415 progress_event.set() | 454 initial_fetch.Start() |
416 print "Downloading revision %s..." % str(rev) | 455 initial_fetch.WaitFor() |
417 FetchRevision(context, rev, zipfile, | |
418 quit_event=None, progress_event=progress_event) | |
419 | 456 |
420 # Binary search time! | 457 # Binary search time! |
421 while zipfile and bad - good > 1: | 458 while zipfile and bad - good > 1: |
422 # Pre-fetch next two possible pivots | 459 # Pre-fetch next two possible pivots |
423 # - down_pivot is the next revision to check if the current revision turns | 460 # - down_pivot is the next revision to check if the current revision turns |
424 # out to be bad. | 461 # out to be bad. |
425 # - up_pivot is the next revision to check if the current revision turns | 462 # - up_pivot is the next revision to check if the current revision turns |
426 # out to be good. | 463 # out to be good. |
427 down_pivot = int((pivot - good) / 2) + good | 464 down_pivot = int((pivot - good) / 2) + good |
428 down_thread = None | 465 down_fetch = None |
429 if down_pivot != pivot and down_pivot != good: | 466 if down_pivot != pivot and down_pivot != good: |
430 down_rev = revlist[down_pivot] | 467 down_rev = revlist[down_pivot] |
431 down_zipfile = _GetDownloadPath(down_rev) | 468 down_fetch = DownloadJob(context, 'down_fetch', down_rev, |
432 down_quit_event = threading.Event() | 469 _GetDownloadPath(down_rev)) |
433 down_progress_event = threading.Event() | 470 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 | 471 |
444 up_pivot = int((bad - pivot) / 2) + pivot | 472 up_pivot = int((bad - pivot) / 2) + pivot |
445 up_thread = None | 473 up_fetch = None |
446 if up_pivot != pivot and up_pivot != bad: | 474 if up_pivot != pivot and up_pivot != bad: |
447 up_rev = revlist[up_pivot] | 475 up_rev = revlist[up_pivot] |
448 up_zipfile = _GetDownloadPath(up_rev) | 476 up_fetch = DownloadJob(context, 'up_fetch', up_rev, |
449 up_quit_event = threading.Event() | 477 _GetDownloadPath(up_rev)) |
450 up_progress_event = threading.Event() | 478 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 | 479 |
461 # Run test on the pivot revision. | 480 # Run test on the pivot revision. |
462 (status, stdout, stderr) = RunRevision(context, | 481 (status, stdout, stderr) = RunRevision(context, |
463 rev, | 482 rev, |
464 zipfile, | 483 zipfile, |
465 profile, | 484 profile, |
466 num_runs, | 485 num_runs, |
467 try_args) | 486 try_args) |
468 os.unlink(zipfile) | 487 os.unlink(zipfile) |
469 zipfile = None | 488 zipfile = None |
470 | 489 |
471 # Call the predicate function to see if the current revision is good or bad. | 490 # 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 | 491 # On that basis, kill one of the background downloads and complete the |
473 # other, as described in the comments above. | 492 # other, as described in the comments above. |
474 try: | 493 try: |
475 if predicate(rev, official_builds, status, stdout, stderr): | 494 answer = evaluate(rev, official_builds, status, stdout, stderr) |
| 495 if answer == 'g': |
476 good = pivot | 496 good = pivot |
477 if down_thread: | 497 if down_fetch: |
478 down_quit_event.set() # Kill the download of older revision. | 498 down_fetch.Stop() # Kill the download of the older revision. |
479 down_thread.join() | 499 if up_fetch: |
480 os.unlink(down_zipfile) | 500 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 | 501 pivot = up_pivot |
486 zipfile = up_zipfile | 502 zipfile = up_fetch.zipfile |
| 503 elif answer == 'b': |
| 504 bad = pivot |
| 505 if up_fetch: |
| 506 up_fetch.Stop() # Kill the download of the newer revision. |
| 507 if down_fetch: |
| 508 down_fetch.WaitFor() |
| 509 pivot = down_pivot |
| 510 zipfile = down_fetch.zipfile |
| 511 elif answer == 'u': |
| 512 # Nuke the revision from the revlist and choose a new pivot. |
| 513 revlist.pop(pivot) |
| 514 bad -= 1 # Assumes bad >= pivot. |
| 515 |
| 516 fetch = None |
| 517 if bad - good > 1: |
| 518 # Alternate between using down_pivot or up_pivot for the new pivot |
| 519 # point, without affecting the range. Do this instead of setting the |
| 520 # pivot to the midpoint of the new range because adjacent revisions |
| 521 # are likely affected by the same issue that caused the (u)nknown |
| 522 # response. |
| 523 if up_fetch and down_fetch: |
| 524 fetch = [up_fetch, down_fetch][len(revlist) % 2] |
| 525 elif up_fetch: |
| 526 fetch = up_fetch |
| 527 else: |
| 528 fetch = down_fetch |
| 529 fetch.WaitFor() |
| 530 if fetch == up_fetch: |
| 531 pivot = up_pivot - 1 # Subtracts 1 because revlist was resized. |
| 532 else: |
| 533 pivot = down_pivot |
| 534 zipfile = fetch.zipfile |
| 535 |
| 536 if down_fetch and fetch != down_fetch: |
| 537 down_fetch.Stop() |
| 538 if up_fetch and fetch != up_fetch: |
| 539 up_fetch.Stop() |
487 else: | 540 else: |
488 bad = pivot | 541 assert False, "Unexpected return value from evaluate(): " + answer |
489 if up_thread: | |
490 up_quit_event.set() # Kill download of newer revision. | |
491 up_thread.join() | |
492 os.unlink(up_zipfile) | |
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 | |
498 zipfile = down_zipfile | |
499 except SystemExit: | 542 except SystemExit: |
500 print "Cleaning up..." | 543 print "Cleaning up..." |
501 for f in [_GetDownloadPath(revlist[down_pivot]), | 544 for f in [_GetDownloadPath(revlist[down_pivot]), |
502 _GetDownloadPath(revlist[up_pivot])]: | 545 _GetDownloadPath(revlist[up_pivot])]: |
503 try: | 546 try: |
504 os.unlink(f) | 547 os.unlink(f) |
505 except OSError: | 548 except OSError: |
506 pass | 549 pass |
507 sys.exit(0) | 550 sys.exit(0) |
508 | 551 |
(...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
630 print ' ' + WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, | 673 print ' ' + WEBKIT_CHANGELOG_URL % (first_known_bad_webkit_rev, |
631 last_known_good_webkit_rev) | 674 last_known_good_webkit_rev) |
632 print 'CHANGELOG URL:' | 675 print 'CHANGELOG URL:' |
633 if opts.official_builds: | 676 if opts.official_builds: |
634 print OFFICIAL_CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) | 677 print OFFICIAL_CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) |
635 else: | 678 else: |
636 print ' ' + CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) | 679 print ' ' + CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev) |
637 | 680 |
638 if __name__ == '__main__': | 681 if __name__ == '__main__': |
639 sys.exit(main()) | 682 sys.exit(main()) |
OLD | NEW |