| OLD | NEW |
| 1 # Copyright 2015 The Chromium Authors. All rights reserved. | 1 # Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 # Use of this source code is governed by a BSD-style license that can be | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 import json | 5 import json |
| 6 import re | 6 import re |
| 7 import urllib |
| 7 | 8 |
| 8 from . import bisect_results | |
| 9 from . import depot_config | 9 from . import depot_config |
| 10 from . import revision_state | 10 from . import revision_state |
| 11 | 11 |
| 12 _DEPS_SHA_PATCH = """ | 12 _DEPS_SHA_PATCH = """ |
| 13 diff --git DEPS.sha DEPS.sha | 13 diff --git DEPS.sha DEPS.sha |
| 14 new file mode 100644 | 14 new file mode 100644 |
| 15 --- /dev/null | 15 --- /dev/null |
| 16 +++ DEPS.sha | 16 +++ DEPS.sha |
| 17 @@ -0,0 +1 @@ | 17 @@ -0,0 +1 @@ |
| 18 +%(deps_sha)s | 18 +%(deps_sha)s |
| (...skipping 16 matching lines...) Expand all Loading... |
| 35 'LO_INIT_CONF', # Bisect aborted early for lack of confidence. | 35 'LO_INIT_CONF', # Bisect aborted early for lack of confidence. |
| 36 'MISSING_METRIC', # The metric was not found in the test text/json output. | 36 'MISSING_METRIC', # The metric was not found in the test text/json output. |
| 37 'LO_FINAL_CONF', # The bisect completed without a culprit. | 37 'LO_FINAL_CONF', # The bisect completed without a culprit. |
| 38 ) | 38 ) |
| 39 | 39 |
| 40 # When we look for the next revision to build, we search nearby revisions | 40 # When we look for the next revision to build, we search nearby revisions |
| 41 # looking for a revision that's already been archived. Since we don't want | 41 # looking for a revision that's already been archived. Since we don't want |
| 42 # to move *too* far from the original revision, we'll cap the search at 25%. | 42 # to move *too* far from the original revision, we'll cap the search at 25%. |
| 43 DEFAULT_SEARCH_RANGE_PERCENTAGE = 0.25 | 43 DEFAULT_SEARCH_RANGE_PERCENTAGE = 0.25 |
| 44 | 44 |
| 45 _FAILED_INITIAL_CONFIDENCE_ABORT_REASON = ( |
| 46 'The metric values for the initial "good" and "bad" revisions ' |
| 47 'do not represent a clear regression.') |
| 48 |
| 49 _DIRECTION_OF_IMPROVEMENT_ABORT_REASON = ( |
| 50 'The metric values for the initial "good" and "bad" revisions match the ' |
| 51 'expected direction of improvement. Thus, likely represent an improvement ' |
| 52 'and not a regression.') |
| 53 |
| 45 | 54 |
| 46 class Bisector(object): | 55 class Bisector(object): |
| 47 """This class abstracts an ongoing bisect (or n-sect) job.""" | 56 """This class abstracts an ongoing bisect (or n-sect) job.""" |
| 48 | 57 |
| 49 def __init__(self, api, bisect_config, revision_class, init_revisions=True): | 58 def __init__(self, api, bisect_config, revision_class, init_revisions=True): |
| 50 """Initializes the state of a new bisect job from a dictionary. | 59 """Initializes the state of a new bisect job from a dictionary. |
| 51 | 60 |
| 52 Note that the initial good_rev and bad_rev MUST resolve to a commit position | 61 Note that the initial good_rev and bad_rev MUST resolve to a commit position |
| 53 in the chromium repo. | 62 in the chromium repo. |
| 54 """ | 63 """ |
| (...skipping 402 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 457 result += 'bisector.fkbr: %r\n\n' % self.fkbr | 466 result += 'bisector.fkbr: %r\n\n' % self.fkbr |
| 458 result += self._revision_value_table() | 467 result += self._revision_value_table() |
| 459 if (self.lkgr and self.lkgr.values and self.fkbr and self.fkbr.values): | 468 if (self.lkgr and self.lkgr.values and self.fkbr and self.fkbr.values): |
| 460 result += '\n' + self._t_test_results() | 469 result += '\n' + self._t_test_results() |
| 461 return result | 470 return result |
| 462 | 471 |
| 463 def _revision_value_table(self): | 472 def _revision_value_table(self): |
| 464 """Returns a string table showing revisions and their values.""" | 473 """Returns a string table showing revisions and their values.""" |
| 465 header = [['Revision', 'Values']] | 474 header = [['Revision', 'Values']] |
| 466 rows = [[str(r.commit_pos), str(r.values)] for r in self.revisions] | 475 rows = [[str(r.commit_pos), str(r.values)] for r in self.revisions] |
| 467 return bisect_results.pretty_table(header + rows) | 476 return self._pretty_table(header + rows) |
| 477 |
| 478 def _pretty_table(self, data): |
| 479 results = [] |
| 480 for row in data: |
| 481 results.append('%-15s' * len(row) % tuple(row)) |
| 482 return '\n'.join(results) |
| 468 | 483 |
| 469 def _t_test_results(self): | 484 def _t_test_results(self): |
| 470 """Returns a string showing t-test results for lkgr and fkbr.""" | 485 """Returns a string showing t-test results for lkgr and fkbr.""" |
| 471 t, df, p = self.api.m.math_utils.welchs_t_test( | 486 t, df, p = self.api.m.math_utils.welchs_t_test( |
| 472 self.lkgr.values, self.fkbr.values) | 487 self.lkgr.values, self.fkbr.values) |
| 473 lines = [ | 488 lines = [ |
| 474 'LKGR values: %r' % self.lkgr.values, | 489 'LKGR values: %r' % self.lkgr.values, |
| 475 'FKBR values: %r' % self.fkbr.values, | 490 'FKBR values: %r' % self.fkbr.values, |
| 476 't-statistic: %r' % t, | 491 't-statistic: %r' % t, |
| 477 'deg. of freedom: %r' % df, | 492 'deg. of freedom: %r' % df, |
| 478 'p-value: %r' % p, | 493 'p-value: %r' % p, |
| 479 'Confidence score: %r' % (100 * (1 - p)) | 494 'Confidence score: %r' % (100 * (1 - p)) |
| 480 ] | 495 ] |
| 481 return '\n'.join(lines) | 496 return '\n'.join(lines) |
| 482 | 497 |
| 483 def partial_results(self): | |
| 484 return bisect_results.BisectResults(self, partial=True).as_string() | |
| 485 | |
| 486 def print_result_debug_info(self): | 498 def print_result_debug_info(self): |
| 487 """Prints extra debug info at the end of the bisect process.""" | 499 """Prints extra debug info at the end of the bisect process.""" |
| 488 lines = self._results_debug_message().splitlines() | 500 lines = self._results_debug_message().splitlines() |
| 489 # If we emit a null step then add a log to it, the log should be kept | 501 # If we emit a null step then add a log to it, the log should be kept |
| 490 # longer than 7 days (which is often needed to debug some issues). | 502 # longer than 7 days (which is often needed to debug some issues). |
| 491 self.api.m.step('Debug Info', []) | 503 self.api.m.step('Debug Info', []) |
| 492 self.api.m.step.active_result.presentation.logs['Debug Info'] = lines | 504 self.api.m.step.active_result.presentation.logs['Debug Info'] = lines |
| 493 | 505 |
| 494 def print_result(self): | 506 def post_result(self, halt_on_failure=False): |
| 495 results = bisect_results.BisectResults(self).as_string() | 507 """Posts bisect results to Perf Dashboard.""" |
| 496 self.api.m.python.inline( | 508 self.api.m.perf_dashboard.set_default_config() |
| 497 'Results', | 509 self.api.m.perf_dashboard.post_bisect_results( |
| 498 """ | 510 self.get_result(), halt_on_failure) |
| 499 import shutil | |
| 500 import sys | |
| 501 shutil.copyfileobj(open(sys.argv[1]), sys.stdout) | |
| 502 """, | |
| 503 args=[self.api.m.raw_io.input(data=results)]) | |
| 504 | 511 |
| 505 def get_revision_to_eval(self): | 512 def get_revision_to_eval(self): |
| 506 """Gets the next RevistionState object in the candidate range. | 513 """Gets the next RevistionState object in the candidate range. |
| 507 | 514 |
| 508 Returns: | 515 Returns: |
| 509 The next Revision object in a list. | 516 The next Revision object in a list. |
| 510 """ | 517 """ |
| 511 self._update_candidate_range() | 518 self._update_candidate_range() |
| 512 candidate_range = [revision for revision in | 519 candidate_range = [revision for revision in |
| 513 self.revisions[self.lkgr.list_index + 1: | 520 self.revisions[self.lkgr.list_index + 1: |
| (...skipping 271 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 785 def surface_result(self, result_string): | 792 def surface_result(self, result_string): |
| 786 assert result_string in VALID_RESULT_CODES | 793 assert result_string in VALID_RESULT_CODES |
| 787 prefix = 'B4T_' # To avoid collision. Stands for bisect (abbr. `a la i18n). | 794 prefix = 'B4T_' # To avoid collision. Stands for bisect (abbr. `a la i18n). |
| 788 result_code = prefix + result_string | 795 result_code = prefix + result_string |
| 789 assert len(result_code) <= 20 | 796 assert len(result_code) <= 20 |
| 790 if result_code not in self.result_codes: | 797 if result_code not in self.result_codes: |
| 791 self.result_codes.add(result_code) | 798 self.result_codes.add(result_code) |
| 792 properties = self.api.m.step.active_result.presentation.properties | 799 properties = self.api.m.step.active_result.presentation.properties |
| 793 properties['extra_result_code'] = sorted(self.result_codes) | 800 properties['extra_result_code'] = sorted(self.result_codes) |
| 794 | 801 |
| 802 def get_result(self): |
| 803 """Returns the results as a jsonable object.""" |
| 804 config = self.bisect_config |
| 805 results_confidence = 0 |
| 806 if self.culprit: |
| 807 results_confidence = self.api.m.math_utils.confidence_score( |
| 808 self.lkgr.values, self.fkbr.values) |
| 809 |
| 810 if self.failed: |
| 811 status = 'failed' |
| 812 elif self.bisect_over: |
| 813 status = 'completed' |
| 814 else: |
| 815 status = 'started' |
| 816 |
| 817 fail_reason = None |
| 818 if self.failed_initial_confidence: |
| 819 fail_reason = _FAILED_INITIAL_CONFIDENCE_ABORT_REASON |
| 820 elif self.failed_direction: |
| 821 fail_reason = _DIRECTION_OF_IMPROVEMENT_ABORT_REASON |
| 822 return { |
| 823 'try_job_id': config.get('try_job_id'), |
| 824 'bug_id': config.get('bug_id'), |
| 825 'status': status, |
| 826 'buildbot_log_url': self._get_build_url(), |
| 827 'bisect_bot': self.get_perf_tester_name(), |
| 828 'command': config['command'], |
| 829 'test_type': config['test_type'], |
| 830 'metric': config['metric'], |
| 831 'change': self.relative_change, |
| 832 'score': results_confidence, |
| 833 'good_revision': self.good_rev.commit_hash, |
| 834 'bad_revision': self.bad_rev.commit_hash, |
| 835 'warnings': self.warnings, |
| 836 'fail_reason': fail_reason, |
| 837 'culprit_data': self._culprit_data(), |
| 838 'revision_data': self._revision_data() |
| 839 } |
| 840 |
| 841 def _culprit_data(self): |
| 842 culprit = self.culprit |
| 843 api = self.api |
| 844 if not culprit: |
| 845 return None |
| 846 culprit_cl_hash = culprit.deps_revision or culprit.commit_hash |
| 847 culprit_info = api.query_revision_info( |
| 848 culprit_cl_hash, culprit.depot_name) |
| 849 |
| 850 return { |
| 851 'subject': culprit_info['subject'], |
| 852 'author': culprit_info['author'], |
| 853 'email': culprit_info['email'], |
| 854 'cl_date': culprit_info['date'], |
| 855 'commit_info': culprit_info['body'], |
| 856 'revisions_links': [], |
| 857 'cl': culprit.deps_revision or culprit.commit_hash |
| 858 } |
| 859 |
| 860 def _revision_data(self): |
| 861 revision_rows = [] |
| 862 for r in self.revisions: |
| 863 if r.tested or r.aborted: |
| 864 revision_rows.append({ |
| 865 'depot_name': r.depot_name, |
| 866 'deps_revision': r.deps_revision, |
| 867 'commit_pos': r.commit_pos, |
| 868 'mean_value': r.mean_value, |
| 869 'std_dev': r.std_dev, |
| 870 'values': r.values, |
| 871 'result': 'good' if r.good else 'bad' if r.bad else 'unknown', |
| 872 }) |
| 873 return revision_rows |
| 874 |
| 875 def _get_build_url(self): |
| 876 properties = self.api.m.properties |
| 877 bot_url = properties.get('buildbotURL', |
| 878 'http://build.chromium.org/p/chromium/') |
| 879 builder_name = urllib.quote(properties.get('buildername', '')) |
| 880 builder_number = str(properties.get('buildnumber', '')) |
| 881 return '%sbuilders/%s/builds/%s' % (bot_url, builder_name, builder_number) |
| OLD | NEW |