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.') | |
qyearsley
2016/01/17 22:49:19
Note: these are moved from bisect_results.py
| |
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(self.get_result(), halt_on_failure) |
498 """ | |
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 | 510 |
505 def get_revision_to_eval(self): | 511 def get_revision_to_eval(self): |
506 """Gets the next RevistionState object in the candidate range. | 512 """Gets the next RevistionState object in the candidate range. |
507 | 513 |
508 Returns: | 514 Returns: |
509 The next Revision object in a list. | 515 The next Revision object in a list. |
510 """ | 516 """ |
511 self._update_candidate_range() | 517 self._update_candidate_range() |
512 candidate_range = [revision for revision in | 518 candidate_range = [revision for revision in |
513 self.revisions[self.lkgr.list_index + 1: | 519 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): | 791 def surface_result(self, result_string): |
786 assert result_string in VALID_RESULT_CODES | 792 assert result_string in VALID_RESULT_CODES |
787 prefix = 'B4T_' # To avoid collision. Stands for bisect (abbr. `a la i18n). | 793 prefix = 'B4T_' # To avoid collision. Stands for bisect (abbr. `a la i18n). |
788 result_code = prefix + result_string | 794 result_code = prefix + result_string |
789 assert len(result_code) <= 20 | 795 assert len(result_code) <= 20 |
790 if result_code not in self.result_codes: | 796 if result_code not in self.result_codes: |
791 self.result_codes.add(result_code) | 797 self.result_codes.add(result_code) |
792 properties = self.api.m.step.active_result.presentation.properties | 798 properties = self.api.m.step.active_result.presentation.properties |
793 properties['extra_result_code'] = sorted(self.result_codes) | 799 properties['extra_result_code'] = sorted(self.result_codes) |
794 | 800 |
801 def get_result(self): | |
802 """Returns the results as a jsonable object.""" | |
803 config = self.bisect_config | |
804 results_confidence = 0 | |
805 if self.culprit: | |
806 results_confidence = self.api.m.math_utils.confidence_score( | |
807 self.lkgr.values, self.fkbr.values) | |
808 | |
809 if self.failed: | |
810 status = 'failed' | |
811 elif self.bisect_over: | |
812 status = 'completed' | |
813 else: | |
814 status = 'started' | |
815 | |
816 fail_reason = None | |
817 if self.failed_initial_confidence: | |
818 fail_reason = _FAILED_INITIAL_CONFIDENCE_ABORT_REASON | |
819 elif self.failed_direction: | |
820 fail_reason = _DIRECTION_OF_IMPROVEMENT_ABORT_REASON | |
821 return { | |
822 'try_job_id': config.get('try_job_id'), | |
823 'bug_id': config.get('bug_id'), | |
824 'status': status, | |
825 'buildbot_log_url': self._get_build_url(), | |
826 'bisect_bot': self.get_perf_tester_name(), | |
827 'command': config['command'], | |
828 'test_type': config['test_type'], | |
829 'metric': config['metric'], | |
830 'change': self.relative_change, | |
831 'score': results_confidence, | |
832 'good_revision': self.good_rev.commit_hash, | |
833 'bad_revision': self.bad_rev.commit_hash, | |
834 'warnings': self.warnings, | |
835 'fail_reason': fail_reason, | |
836 'culprit_data': self._culprit_data(), | |
837 'revision_data': self._revision_data() | |
838 } | |
839 | |
840 def _culprit_data(self): | |
841 culprit = self.culprit | |
842 api = self.api | |
843 if not culprit: | |
844 return None | |
845 culprit_cl_hash = culprit.deps_revision or culprit.commit_hash | |
846 culprit_info = api.query_revision_info( | |
847 culprit_cl_hash, culprit.depot_name) | |
848 | |
849 return { | |
850 'subject': culprit_info['subject'], | |
851 'author': culprit_info['author'], | |
852 'email': culprit_info['email'], | |
853 'cl_date': culprit_info['date'], | |
854 'commit_info': culprit_info['body'], | |
855 'revisions_links': [], | |
856 'cl': culprit.deps_revision or culprit.commit_hash | |
857 } | |
858 | |
859 def _revision_data(self): | |
860 revision_rows = [] | |
861 for r in self.revisions: | |
862 if r.tested or r.aborted: | |
863 revision_rows.append({ | |
864 'depot_name': r.depot_name, | |
865 'deps_revision': r.deps_revision, | |
866 'commit_pos': r.commit_pos, | |
867 'mean_value': r.mean_value, | |
868 'std_dev': r.std_dev, | |
869 'values': r.values, | |
870 'result': 'good' if r.good else 'bad' if r.bad else 'unknown', | |
871 }) | |
872 return revision_rows | |
873 | |
874 def _get_build_url(self): | |
875 properties = self.api.m.properties | |
876 bot_url = properties.get('buildbotURL', | |
877 'http://build.chromium.org/p/chromium/') | |
878 builder_name = urllib.quote(properties.get('buildername', '')) | |
879 builder_number = str(properties.get('buildnumber', '')) | |
880 return '%sbuilders/%s/builds/%s' % (bot_url, builder_name, builder_number) | |
OLD | NEW |