| 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 """An interface for holding state and result of revisions in a bisect job. | 5 """An interface for holding state and result of revisions in a bisect job. |
| 6 | 6 |
| 7 When implementing support for tests other than perf, one should extend this | 7 When implementing support for tests other than perf, one should extend this |
| 8 class so that the bisect module and recipe can use it. | 8 class so that the bisect module and recipe can use it. |
| 9 | 9 |
| 10 See perf_revision_state for an example. | 10 See perf_revision_state for an example. |
| 11 """ | 11 """ |
| 12 | 12 |
| 13 import collections |
| 13 import hashlib | 14 import hashlib |
| 14 import json | 15 import json |
| 15 import math | 16 import math |
| 16 import os | 17 import os |
| 17 import tempfile | 18 import tempfile |
| 18 import re | 19 import re |
| 19 import uuid | 20 import uuid |
| 20 | 21 |
| 21 from . import depot_config | 22 from . import depot_config |
| 23 from . import exceptions |
| 22 | 24 |
| 23 # These relate to how to increase the number of repetitions during re-test | 25 # These relate to how to increase the number of repetitions during re-test |
| 24 MINIMUM_SAMPLE_SIZE = 5 | 26 MINIMUM_SAMPLE_SIZE = 5 |
| 25 INCREASE_FACTOR = 1.5 | 27 INCREASE_FACTOR = 1.5 |
| 26 | 28 |
| 27 class RevisionState(object): | 29 class RevisionState(object): |
| 28 """Abstracts the state of a single revision on a bisect job.""" | 30 """Abstracts the state of a single revision on a bisect job.""" |
| 29 | 31 |
| 30 # Possible values for the status attribute of RevisionState: | |
| 31 ( | |
| 32 NEW, # A revision_state object that has just been initialized. | |
| 33 BUILDING, # Requested a build for this revision, waiting for it. | |
| 34 TESTING, # A test job for this revision was triggered, waiting for it. | |
| 35 TESTED, # The test job completed with non-failing results. | |
| 36 FAILED, # Either the build or the test jobs failed or timed out. | |
| 37 ABORTED, # The build or test job was aborted. (For use in multi-secting). | |
| 38 SKIPPED, # A revision that was not built or tested for a special reason, | |
| 39 # such as those ranges that we know are broken, or when nudging | |
| 40 # revisions. | |
| 41 NEED_MORE_DATA, # Current number of test values is too small to establish | |
| 42 # a statistically significant difference between this | |
| 43 # revision and the revisions known to be good and bad. | |
| 44 ) = xrange(8) | |
| 45 | |
| 46 def __init__(self, bisector, commit_hash, depot_name=None, | 32 def __init__(self, bisector, commit_hash, depot_name=None, |
| 47 base_revision=None): | 33 base_revision=None): |
| 48 """Creates a new instance to track the state of a revision. | 34 """Creates a new instance to track the state of a revision. |
| 49 | 35 |
| 50 Args: | 36 Args: |
| 51 bisector (Bisector): The object performing the bisection. | 37 bisector (Bisector): The object performing the bisection. |
| 52 commit_hash (str): The hash identifying the revision to represent. | 38 commit_hash (str): The hash identifying the revision to represent. |
| 53 depot_name (str): The name of the depot as specified in DEPS. Must be a | 39 depot_name (str): The name of the depot as specified in DEPS. Must be a |
| 54 key in depot_config.DEPOT_DEPS_NAME . | 40 key in depot_config.DEPOT_DEPS_NAME . |
| 55 base_revision (RevisionState): The revision state to patch with the deps | 41 base_revision (RevisionState): The revision state to patch with the deps |
| 56 change. | 42 change. |
| 57 """ | 43 """ |
| 58 super(RevisionState, self).__init__() | 44 super(RevisionState, self).__init__() |
| 59 self.bisector = bisector | 45 self.bisector = bisector |
| 60 self._good = None | 46 self._good = None |
| 47 self.failed = False |
| 61 self.deps = None | 48 self.deps = None |
| 62 self.test_results_url = None | 49 self.test_results_url = None |
| 63 self.build_archived = False | 50 self.build_archived = False |
| 64 self.status = RevisionState.NEW | |
| 65 self.next_revision = None | 51 self.next_revision = None |
| 66 self.previous_revision = None | 52 self.previous_revision = None |
| 67 self.job_name = None | 53 self.job_name = None |
| 68 self.patch_file = None | 54 self.patch_file = None |
| 69 self.deps_revision = None | 55 self.deps_revision = None |
| 70 self.depot_name = depot_name or self.bisector.base_depot | 56 self.depot_name = depot_name or self.bisector.base_depot |
| 71 self.depot = depot_config.DEPOT_DEPS_NAME[self.depot_name] | 57 self.depot = depot_config.DEPOT_DEPS_NAME[self.depot_name] |
| 72 self.commit_hash = str(commit_hash) | 58 self.commit_hash = str(commit_hash) |
| 73 self._rev_str = None | 59 self._rev_str = None |
| 74 self.base_revision = base_revision | 60 self.base_revision = base_revision |
| 75 self.revision_overrides = {} | 61 self.revision_overrides = {} |
| 76 self.build_id = None | 62 self.build_id = None |
| 77 if self.base_revision: | 63 if self.base_revision: |
| 78 assert self.base_revision.deps_file_contents | 64 assert self.base_revision.deps_file_contents |
| 79 self.needs_patch = True | 65 self.needs_patch = True |
| 80 self.revision_overrides[self.depot['src']] = self.commit_hash | 66 self.revision_overrides[self.depot['src']] = self.commit_hash |
| 81 self.deps_patch, self.deps_file_contents = self.bisector.make_deps_patch( | 67 self.deps_patch, self.deps_file_contents = self.bisector.make_deps_patch( |
| 82 self.base_revision, self.base_revision.deps_file_contents, | 68 self.base_revision, self.base_revision.deps_file_contents, |
| 83 self.depot, self.commit_hash) | 69 self.depot, self.commit_hash) |
| 84 self.deps_sha = hashlib.sha1(self.deps_patch).hexdigest() | 70 self.deps_sha = hashlib.sha1(self.deps_patch).hexdigest() |
| 85 self.deps_sha_patch = self.bisector.make_deps_sha_file(self.deps_sha) | 71 self.deps_sha_patch = self.bisector.make_deps_sha_file(self.deps_sha) |
| 86 self.deps = dict(base_revision.deps) | 72 self.deps = dict(base_revision.deps) |
| 87 self.deps[self.depot_name] = self.commit_hash | 73 self.deps[self.depot_name] = self.commit_hash |
| 88 else: | 74 else: |
| 89 self.needs_patch = False | 75 self.needs_patch = False |
| 90 self.build_url = self.bisector.get_platform_gs_prefix() + self._gs_suffix() | 76 self.build_url = self.bisector.get_platform_gs_prefix() + self._gs_suffix() |
| 91 self.values = [] | 77 self.valueset_paths = [] |
| 92 self.mean_value = None | 78 self.chartjson_paths = [] |
| 93 self.overall_return_code = None | 79 self.debug_values = [] |
| 80 self.return_codes = [] |
| 81 self.mean = None |
| 94 self.std_dev = None | 82 self.std_dev = None |
| 95 self._test_config = None | 83 self._test_config = None |
| 96 | 84 |
| 97 if self.bisector.test_type == 'perf': | 85 if self.bisector.test_type == 'perf': |
| 98 self.repeat_count = MINIMUM_SAMPLE_SIZE | 86 self.repeat_count = MINIMUM_SAMPLE_SIZE |
| 99 else: | 87 else: |
| 100 self.repeat_count = self.bisector.bisect_config.get( | 88 self.repeat_count = self.bisector.bisect_config.get( |
| 101 'repeat_count', MINIMUM_SAMPLE_SIZE) | 89 'repeat_count', MINIMUM_SAMPLE_SIZE) |
| 102 | 90 |
| 103 @property | 91 @property |
| 104 def tested(self): | |
| 105 return self.status in (RevisionState.TESTED,) | |
| 106 | |
| 107 @property | |
| 108 def in_progress(self): | |
| 109 return self.status in (RevisionState.BUILDING, RevisionState.TESTING, | |
| 110 RevisionState.NEED_MORE_DATA) | |
| 111 | |
| 112 @property | |
| 113 def failed(self): | |
| 114 return self.status == RevisionState.FAILED | |
| 115 | |
| 116 @property | |
| 117 def aborted(self): | |
| 118 return self.status == RevisionState.ABORTED | |
| 119 | |
| 120 @property | |
| 121 def good(self): | 92 def good(self): |
| 122 return self._good == True | 93 return self._good == True |
| 123 | 94 |
| 124 @property | 95 @property |
| 125 def bad(self): | 96 def bad(self): |
| 126 return self._good == False | 97 return self._good == False |
| 127 | 98 |
| 128 @good.setter | 99 @good.setter |
| 129 def good(self, value): | 100 def good(self, value): |
| 130 self._good = value | 101 self._good = value |
| 131 | 102 |
| 132 @bad.setter | 103 @bad.setter |
| 133 def bad(self, value): | 104 def bad(self, value): |
| 134 self._good = not value | 105 self._good = not value |
| 135 | 106 |
| 107 @property |
| 108 def test_run_count(self): |
| 109 return max( |
| 110 len(self.valueset_paths), |
| 111 len(self.chartjson_paths), |
| 112 len(self.return_codes)) |
| 113 |
| 136 def start_job(self): | 114 def start_job(self): |
| 137 """Starts a build, or a test job if the build is available.""" | 115 api = self.bisector.api |
| 138 if self.status == RevisionState.NEW and not self._is_build_archived(): | 116 try: |
| 139 self._request_build() | 117 if not self._is_build_archived(): |
| 140 self.status = RevisionState.BUILDING | 118 self._request_build() |
| 141 return | 119 with api.m.step.nest('Waiting for build'): |
| 120 while not self._is_build_archived(): |
| 121 api.m.python.inline( |
| 122 'sleeping', |
| 123 """ |
| 124 import sys |
| 125 import time |
| 126 time.sleep(20*60) |
| 127 sys.exit(0) |
| 128 """) |
| 129 if self._is_build_failed(): |
| 130 self.failed = True |
| 131 return |
| 142 | 132 |
| 143 if self._is_build_archived() and self.status in ( | |
| 144 RevisionState.NEW, RevisionState.BUILDING, | |
| 145 RevisionState.NEED_MORE_DATA): | |
| 146 self._do_test() | 133 self._do_test() |
| 147 self.status = RevisionState.TESTING | 134 # TODO(robertocn): Add a test to remove this CL |
| 135 while not self._check_revision_good(): # pragma: no cover |
| 136 min(self, self.bisector.lkgr, self.bisector.fkbr, |
| 137 key=lambda(x): x.test_run_count)._do_test() |
| 138 |
| 139 except exceptions.UntestableRevisionException: |
| 140 self.failed = True |
| 148 | 141 |
| 149 def deps_change(self): | 142 def deps_change(self): |
| 150 """Uses `git show` to see if a given commit contains a DEPS change.""" | 143 """Uses `git show` to see if a given commit contains a DEPS change.""" |
| 151 # Avoid checking DEPS changes for dependency repo revisions. | 144 # Avoid checking DEPS changes for dependency repo revisions. |
| 152 # crbug.com/580681 | 145 # crbug.com/580681 |
| 153 if self.needs_patch: # pragma: no cover | 146 if self.needs_patch: # pragma: no cover |
| 154 return False | 147 return False |
| 155 api = self.bisector.api | 148 api = self.bisector.api |
| 156 working_dir = api.working_dir | 149 working_dir = api.working_dir |
| 157 cwd = working_dir.join( | 150 cwd = working_dir.join( |
| 158 depot_config.DEPOT_DEPS_NAME[self.depot_name]['src']) | 151 depot_config.DEPOT_DEPS_NAME[self.depot_name]['src']) |
| 159 name = 'Checking DEPS for ' + self.commit_hash | 152 name = 'Checking DEPS for ' + self.commit_hash |
| 160 step_result = api.m.git( | 153 step_result = api.m.git( |
| 161 'show', '--name-only', '--pretty=format:', | 154 'show', '--name-only', '--pretty=format:', |
| 162 self.commit_hash, cwd=cwd, stdout=api.m.raw_io.output(), name=name) | 155 self.commit_hash, cwd=cwd, stdout=api.m.raw_io.output(), name=name, |
| 163 if self.bisector.dummy_builds and not self.commit_hash.startswith('dcdc'): | 156 step_test_data=lambda: api._test_data['deps_change'][self.commit_hash] |
| 164 return False | 157 ) |
| 165 if 'DEPS' in step_result.stdout.splitlines(): # pragma: no cover | 158 if 'DEPS' in step_result.stdout.splitlines(): # pragma: no cover |
| 166 return True | 159 return True |
| 167 return False # pragma: no cover | 160 return False # pragma: no cover |
| 168 | 161 |
| 169 def _gen_deps_local_scope(self): | 162 def _gen_deps_local_scope(self): |
| 170 """Defines the Var and From functions in a dict for calling exec. | 163 """Defines the Var and From functions in a dict for calling exec. |
| 171 | 164 |
| 172 This is needed for executing the DEPS file. | 165 This is needed for executing the DEPS file. |
| 173 """ | 166 """ |
| 174 deps_data = { | 167 deps_data = { |
| 175 'Var': lambda _: deps_data['vars'][_], | 168 'Var': lambda _: deps_data['vars'][_], |
| 176 'From': lambda *args: None, | 169 'From': lambda *args: None, |
| 177 } | 170 } |
| 178 return deps_data | 171 return deps_data |
| 179 | 172 |
| 180 def _read_content(self, url, file_name, branch): # pragma: no cover | 173 def _read_content(self, url, file_name, branch): # pragma: no cover |
| 181 """Uses gitiles recipe module to download and read file contents.""" | 174 """Uses gitiles recipe module to download and read file contents.""" |
| 182 try: | 175 try: |
| 183 return self.bisector.api.m.gitiles.download_file( | 176 return self.bisector.api.m.gitiles.download_file( |
| 184 repository_url=url, file_path=file_name, branch=branch) | 177 repository_url=url, file_path=file_name, branch=branch) |
| 185 except TypeError: | 178 except TypeError: |
| 186 print 'Could not read content for %s/%s/%s' % (url, file_name, branch) | 179 print 'Could not read content for %s/%s/%s' % (url, file_name, branch) |
| 187 return None | 180 return None |
| 188 | 181 |
| 189 def read_deps(self, recipe_tester_name): | 182 def read_deps(self, recipe_tester_name): |
| 190 """Sets the dependencies for this revision from the contents of DEPS.""" | 183 """Sets the dependencies for this revision from the contents of DEPS.""" |
| 191 api = self.bisector.api | 184 api = self.bisector.api |
| 192 if self.deps: | |
| 193 return | |
| 194 if self.bisector.internal_bisect: # pragma: no cover | 185 if self.bisector.internal_bisect: # pragma: no cover |
| 195 self.deps_file_contents = self._read_content( | 186 self.deps_file_contents = self._read_content( |
| 196 depot_config.DEPOT_DEPS_NAME[self.depot_name]['url'], | 187 depot_config.DEPOT_DEPS_NAME[self.depot_name]['url'], |
| 197 depot_config.DEPOT_DEPS_NAME[self.depot_name]['deps_file'], | 188 depot_config.DEPOT_DEPS_NAME[self.depot_name]['deps_file'], |
| 198 self.commit_hash) | 189 self.commit_hash) |
| 199 # On April 5th, 2016 .DEPS.git was changed to DEPS on android-chrome repo, | 190 # On April 5th, 2016 .DEPS.git was changed to DEPS on android-chrome repo, |
| 200 # we are doing this in order to support both deps files. | 191 # we are doing this in order to support both deps files. |
| 201 if not self.deps_file_contents: | 192 if not self.deps_file_contents: |
| 202 self.deps_file_contents = self._read_content( | 193 self.deps_file_contents = self._read_content( |
| 203 depot_config.DEPOT_DEPS_NAME[self.depot_name]['url'], | 194 depot_config.DEPOT_DEPS_NAME[self.depot_name]['url'], |
| 204 depot_config.DEPS_FILENAME, | 195 depot_config.DEPS_FILENAME, |
| 205 self.commit_hash) | 196 self.commit_hash) |
| 206 else: | 197 else: |
| 207 step_result = api.m.python( | 198 step_result = api.m.python( |
| 208 'fetch file %s:%s' % (self.commit_hash, depot_config.DEPS_FILENAME), | 199 'fetch file %s:%s' % (self.commit_hash, depot_config.DEPS_FILENAME), |
| 209 api.resource('fetch_file.py'), | 200 api.resource('fetch_file.py'), |
| 210 [depot_config.DEPS_FILENAME, '--commit', self.commit_hash], | 201 [depot_config.DEPS_FILENAME, '--commit', self.commit_hash], |
| 211 stdout=api.m.raw_io.output()) | 202 stdout=api.m.raw_io.output(), |
| 203 step_test_data=lambda: api._test_data['deps'][self.commit_hash] |
| 204 ) |
| 212 self.deps_file_contents = step_result.stdout | 205 self.deps_file_contents = step_result.stdout |
| 213 try: | 206 try: |
| 214 deps_data = self._gen_deps_local_scope() | 207 deps_data = self._gen_deps_local_scope() |
| 215 exec(self.deps_file_contents or 'deps = {}', {}, deps_data) | 208 exec (self.deps_file_contents or 'deps = {}') in {}, deps_data |
| 216 deps_data = deps_data['deps'] | 209 deps_data = deps_data['deps'] |
| 217 except ImportError: # pragma: no cover | 210 except ImportError: # pragma: no cover |
| 218 # TODO(robertocn): Implement manual parsing of DEPS when exec fails. | 211 # TODO(robertocn): Implement manual parsing of DEPS when exec fails. |
| 219 raise NotImplementedError('Path not implemented to manually parse DEPS') | 212 raise NotImplementedError('Path not implemented to manually parse DEPS') |
| 220 | 213 |
| 221 revision_regex = re.compile('.git@(?P<revision>[a-fA-F0-9]+)') | 214 revision_regex = re.compile('.git@(?P<revision>[a-fA-F0-9]+)') |
| 222 results = {} | 215 results = {} |
| 223 for depot_name, depot_data in depot_config.DEPOT_DEPS_NAME.iteritems(): | 216 for depot_name, depot_data in depot_config.DEPOT_DEPS_NAME.iteritems(): |
| 224 if (depot_data.get('platform') and | 217 if (depot_data.get('platform') and |
| 225 depot_data.get('platform') not in recipe_tester_name.lower()): | 218 depot_data.get('platform') not in recipe_tester_name.lower()): |
| (...skipping 10 matching lines...) Expand all Loading... |
| 236 else: # pragma: no cover | 229 else: # pragma: no cover |
| 237 warning_text = ('Could not parse revision for %s while bisecting ' | 230 warning_text = ('Could not parse revision for %s while bisecting ' |
| 238 '%s' % (depot_name, self.depot)) | 231 '%s' % (depot_name, self.depot)) |
| 239 if warning_text not in self.bisector.warnings: | 232 if warning_text not in self.bisector.warnings: |
| 240 self.bisector.warnings.append(warning_text) | 233 self.bisector.warnings.append(warning_text) |
| 241 else: | 234 else: |
| 242 results[depot_name] = None | 235 results[depot_name] = None |
| 243 self.deps = results | 236 self.deps = results |
| 244 return | 237 return |
| 245 | 238 |
| 246 def update_status(self): | |
| 247 """Checks on the pending jobs and updates status accordingly. | |
| 248 | |
| 249 This method will check for the build to complete and then trigger the test, | |
| 250 or will wait for the test as appropriate. | |
| 251 | |
| 252 To wait for the test we try to get the buildbot job url from GS, and if | |
| 253 available, we query the status of such job. | |
| 254 """ | |
| 255 if self.status == RevisionState.BUILDING: | |
| 256 if self._is_build_archived(): | |
| 257 self.start_job() | |
| 258 elif self._is_build_failed(): # pragma: no cover | |
| 259 self.status = RevisionState.FAILED | |
| 260 elif (self.status in (RevisionState.TESTING, RevisionState.NEED_MORE_DATA) | |
| 261 and self._results_available()): | |
| 262 # If we have already decided whether the revision is good or bad we | |
| 263 # shouldn't check again | |
| 264 check_revision_goodness = not(self.good or self.bad) | |
| 265 self._read_test_results( | |
| 266 check_revision_goodness=check_revision_goodness) | |
| 267 # We assume _read_test_results may have changed the status to a broken | |
| 268 # state such as FAILED or ABORTED. | |
| 269 if self.status in (RevisionState.TESTING, RevisionState.NEED_MORE_DATA): | |
| 270 self.status = RevisionState.TESTED | |
| 271 | |
| 272 def _is_build_archived(self): | 239 def _is_build_archived(self): |
| 273 """Checks if the revision is already built and archived.""" | 240 """Checks if the revision is already built and archived.""" |
| 274 if not self.build_archived: | 241 if not self.build_archived: |
| 275 api = self.bisector.api | 242 api = self.bisector.api |
| 276 self.build_archived = api.gsutil_file_exists(self.build_url) | 243 self.build_archived = api.gsutil_file_exists( |
| 277 | 244 self.build_url, |
| 278 if self.bisector.dummy_builds: | 245 step_test_data=lambda: api._test_data.get( |
| 279 self.build_archived = self.in_progress | 246 'gsutil_exists', {}).get(self.commit_hash).pop() |
| 247 if api._test_data.get('gsutil_exists', {}).get(self.commit_hash) |
| 248 else collections.namedtuple('retcode_attr', ['retcode'])(0)) |
| 280 | 249 |
| 281 return self.build_archived | 250 return self.build_archived |
| 282 | 251 |
| 283 def _is_build_failed(self): | 252 def _is_build_failed(self): |
| 284 api = self.bisector.api | 253 api = self.bisector.api |
| 285 result = api.m.buildbucket.get_build( | 254 result = api.m.buildbucket.get_build( |
| 286 self.build_id, | 255 self.build_id, |
| 287 api.m.service_account.get_json_path(api.SERVICE_ACCOUNT), | 256 api.m.service_account.get_json_path(api.SERVICE_ACCOUNT), |
| 288 step_test_data=lambda: api.m.json.test_api.output_stream( | 257 step_test_data=lambda: api.test_api.buildbot_job_status_mock( |
| 289 {'build': {'result': 'SUCCESS', 'status': 'COMPLETED'}} | 258 api._test_data.get('build_status', {}).get(self.commit_hash, []))) |
| 290 )) | |
| 291 return (result.stdout['build']['status'] == 'COMPLETED' and | 259 return (result.stdout['build']['status'] == 'COMPLETED' and |
| 292 result.stdout['build'].get('result') != 'SUCCESS') | 260 result.stdout['build'].get('result') != 'SUCCESS') |
| 293 | 261 |
| 294 def _results_available(self): | |
| 295 """Checks if the results for the test job have been uploaded.""" | |
| 296 api = self.bisector.api | |
| 297 result = api.gsutil_file_exists(self.test_results_url) | |
| 298 if self.bisector.dummy_builds: | |
| 299 return self.in_progress | |
| 300 return result # pragma: no cover | |
| 301 | |
| 302 def _gs_suffix(self): | 262 def _gs_suffix(self): |
| 303 """Provides the expected right half of the build filename. | 263 """Provides the expected right half of the build filename. |
| 304 | 264 |
| 305 This takes into account whether the build has a deps patch. | 265 This takes into account whether the build has a deps patch. |
| 306 """ | 266 """ |
| 307 top_revision = self | 267 top_revision = self |
| 308 while top_revision.base_revision: | 268 while top_revision.base_revision: |
| 309 top_revision = top_revision.base_revision | 269 top_revision = top_revision.base_revision |
| 310 name_parts = [top_revision.commit_hash] | 270 name_parts = [top_revision.commit_hash] |
| 311 if self.needs_patch: | 271 if self.needs_patch: |
| 312 name_parts.append(self.deps_sha) | 272 name_parts.append(self.deps_sha) |
| 313 return '%s.zip' % '_'.join(name_parts) | 273 return '%s.zip' % '_'.join(name_parts) |
| 314 | 274 |
| 315 def _read_test_results(self, check_revision_goodness=True): | 275 def _read_test_results(self, results): |
| 316 """Gets the test results from GS and checks if the rev is good or bad.""" | 276 # Results will be a dictionary containing path to chartjsons, paths to |
| 317 test_results = self._get_test_results() | 277 # valueset, list of return codes. |
| 318 # Results will contain the keys 'results' and 'output' where output is the | 278 self.return_codes.extend(results.get('retcodes', [])) |
| 319 # stdout of the command, and 'results' is itself a dict with the key | 279 if results.get('errors'): # pragma: no cover |
| 320 # 'values' unless the test failed, in which case 'results' will contain | 280 self.failed = True |
| 321 # the 'error' key explaining the type of error. | 281 if 'MISSING_METRIC' in results.get('errors'): |
| 322 results = test_results['results'] | |
| 323 if results.get('errors'): | |
| 324 self.status = RevisionState.FAILED | |
| 325 if 'MISSING_METRIC' in results.get('errors'): # pragma: no cover | |
| 326 self.bisector.surface_result('MISSING_METRIC') | 282 self.bisector.surface_result('MISSING_METRIC') |
| 327 return | 283 raise exceptions.UntestableRevisionException(results['errors']) |
| 328 self.values += results['values'] | 284 elif self.bisector.is_return_code_mode(): |
| 329 api = self.bisector.api | 285 assert len(results['retcodes']) |
| 330 if test_results.get('retcodes') and test_results['retcodes'][-1] != 0 and ( | |
| 331 api.m.chromium.c.TARGET_PLATFORM == 'android'): #pragma: no cover | |
| 332 api.m.chromium_android.device_status() | |
| 333 current_connected_devices = api.m.chromium_android.devices | |
| 334 current_device = api.m.bisect_tester.device_to_test | |
| 335 if current_device not in current_connected_devices: | |
| 336 # We need to manually raise step failure here because we are catching | |
| 337 # them further down the line to enable return_code bisects and bisecting | |
| 338 # on benchmarks that are a little flaky. | |
| 339 raise api.m.step.StepFailure('Test device disconnected.') | |
| 340 if self.bisector.is_return_code_mode(): | |
| 341 retcodes = test_results['retcodes'] | |
| 342 self.overall_return_code = 0 if all(v == 0 for v in retcodes) else 1 | |
| 343 # Keeping mean_value for compatibility with dashboard. | |
| 344 # TODO(robertocn): refactor mean_value, specially when uploading results | |
| 345 # to dashboard. | |
| 346 self.mean_value = self.overall_return_code | |
| 347 elif self.values: | |
| 348 api = self.bisector.api | |
| 349 self.mean_value = api.m.math_utils.mean(self.values) | |
| 350 self.std_dev = api.m.math_utils.standard_deviation(self.values) | |
| 351 # Values were not found, but the test did not otherwise fail. | |
| 352 else: | 286 else: |
| 353 self.status = RevisionState.FAILED | 287 self.valueset_paths.extend(results.get('valueset_paths')) |
| 354 self.bisector.surface_result('MISSING_METRIC') | 288 self.chartjson_paths.extend(results.get('chartjson_paths')) |
| 355 return | 289 if results.get('retcodes') and 0 not in results['retcodes']: |
| 356 # If we have already decided on the goodness of this revision, we shouldn't | 290 raise exceptions.UntestableRevisionException( |
| 357 # recheck it. | 291 'got non-zero return code on all runs for ' + self.commit_hash) |
| 358 if self.good or self.bad: | |
| 359 check_revision_goodness = False | |
| 360 # We cannot test the goodness of the initial rev range. | |
| 361 if (self.bisector.good_rev != self and self.bisector.bad_rev != self and | |
| 362 check_revision_goodness): | |
| 363 if self._check_revision_good(): | |
| 364 self.good = True | |
| 365 else: | |
| 366 self.bad = True | |
| 367 | 292 |
| 368 def _request_build(self): | 293 def _request_build(self): |
| 369 """Posts a request to buildbot to build this revision and archive it.""" | 294 """Posts a request to buildbot to build this revision and archive it.""" |
| 370 api = self.bisector.api | 295 api = self.bisector.api |
| 371 bot_name = self.bisector.get_builder_bot_for_this_platform() | 296 bot_name = self.bisector.get_builder_bot_for_this_platform() |
| 372 | 297 |
| 373 # To allow multiple nested levels, we go to the topmost revision. | 298 # To allow multiple nested levels, we go to the topmost revision. |
| 374 top_revision = self | 299 top_revision = self |
| 375 while top_revision.base_revision: | 300 while top_revision.base_revision: |
| 376 top_revision = top_revision.base_revision | 301 top_revision = top_revision.base_revision |
| (...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 424 self._test_config = result | 349 self._test_config = result |
| 425 return result | 350 return result |
| 426 | 351 |
| 427 def _do_test(self): | 352 def _do_test(self): |
| 428 """Triggers tests for a revision, either locally or via try job. | 353 """Triggers tests for a revision, either locally or via try job. |
| 429 | 354 |
| 430 If local testing is enabled (i.e. director/tester merged) then | 355 If local testing is enabled (i.e. director/tester merged) then |
| 431 the test will be run on the same machine. Otherwise, this posts | 356 the test will be run on the same machine. Otherwise, this posts |
| 432 a request to buildbot to download and perf-test this build. | 357 a request to buildbot to download and perf-test this build. |
| 433 """ | 358 """ |
| 434 if self.bisector.bisect_config.get('dummy_job_names'): | 359 if self.test_run_count: # pragma: no cover |
| 435 self.job_name = self.commit_hash + '-test' | 360 self.repeat_count = max(MINIMUM_SAMPLE_SIZE, math.ceil( |
| 436 else: # pragma: no cover | 361 self.test_run_count * 1.5)) - self.test_run_count |
| 437 self.job_name = uuid.uuid4().hex | 362 |
| 438 api = self.bisector.api | 363 api = self.bisector.api |
| 439 # Stores revision map for different repos eg, android-chrome, src, v8 etc. | 364 # Stores revision map for different repos eg, android-chrome, src, v8 etc. |
| 440 revision_ladder = {} | 365 revision_ladder = {} |
| 441 top_revision = self | 366 top_revision = self |
| 442 revision_ladder[top_revision.depot_name] = top_revision.commit_hash | 367 revision_ladder[top_revision.depot_name] = top_revision.commit_hash |
| 443 while top_revision.base_revision: # pragma: no cover | 368 while top_revision.base_revision: # pragma: no cover |
| 444 revision_ladder[top_revision.depot_name] = top_revision.commit_hash | 369 revision_ladder[top_revision.depot_name] = top_revision.commit_hash |
| 445 top_revision = top_revision.base_revision | 370 top_revision = top_revision.base_revision |
| 446 perf_test_properties = { | 371 perf_test_properties = { |
| 447 'builder_name': self.bisector.get_perf_tester_name(), | |
| 448 'properties': { | 372 'properties': { |
| 449 'revision': top_revision.commit_hash, | 373 'revision': top_revision.commit_hash, |
| 450 'parent_got_revision': top_revision.commit_hash, | 374 'parent_got_revision': top_revision.commit_hash, |
| 451 'parent_build_archive_url': self.build_url, | 375 'parent_build_archive_url': self.build_url, |
| 452 'bisect_config': self._get_bisect_config_for_tester(), | 376 'bisect_config': self._get_bisect_config_for_tester(), |
| 453 'job_name': self.job_name, | |
| 454 'revision_ladder': revision_ladder, | 377 'revision_ladder': revision_ladder, |
| 455 }, | 378 }, |
| 456 } | 379 } |
| 457 self.test_results_url = (self.bisector.api.GS_RESULTS_URL + | 380 skip_download = self.bisector.last_tested_revision == self |
| 458 self.job_name + '.results') | 381 self.bisector.last_tested_revision = self |
| 459 if (api.m.bisect_tester.local_test_enabled() or | 382 overrides = perf_test_properties['properties'] |
| 460 self.bisector.internal_bisect): # pragma: no cover | |
| 461 skip_download = self.bisector.last_tested_revision == self | |
| 462 self.bisector.last_tested_revision = self | |
| 463 overrides = perf_test_properties['properties'] | |
| 464 api.run_local_test_run(overrides, skip_download=skip_download) | |
| 465 else: | |
| 466 step_name = 'Triggering test job for ' + self.commit_hash | |
| 467 api.m.trigger(perf_test_properties, name=step_name) | |
| 468 | 383 |
| 469 def retest(self): # pragma: no cover | 384 def run_test_step_test_data(): |
| 470 # We need at least 5 samples for applying Mann-Whitney U test | 385 """Returns a single step data object when called. |
| 471 # with P < 0.01, two-tailed . | |
| 472 target_sample_size = max(5, math.ceil(len(self.values) * 1.5)) | |
| 473 self.status = RevisionState.NEED_MORE_DATA | |
| 474 self.repeat_count = target_sample_size - len(self.values) | |
| 475 self.start_job() | |
| 476 self.bisector.wait_for(self) | |
| 477 | 386 |
| 478 def _get_test_results(self): | 387 These are expected to be populated by the test_api. |
| 479 """Tries to get the results of a test job from cloud storage.""" | 388 """ |
| 480 api = self.bisector.api | 389 if api._test_data['run_results'].get(self.commit_hash): |
| 481 try: | 390 return api._test_data['run_results'][self.commit_hash].pop(0) |
| 482 stdout = api.m.raw_io.output() | 391 return api._test_data['run_results']['default'] |
| 483 name = 'Get test results for build ' + self.commit_hash | |
| 484 step_result = api.m.gsutil.cat(self.test_results_url, stdout=stdout, | |
| 485 name=name) | |
| 486 if not step_result.stdout: | |
| 487 raise api.m.step.StepFailure('Test for build %s failed' % | |
| 488 self.revision_string()) | |
| 489 except api.m.step.StepFailure as sf: # pragma: no cover | |
| 490 self.bisector.surface_result('TEST_FAILURE') | |
| 491 return {'results': {'errors': str(sf)}} | |
| 492 else: | |
| 493 return json.loads(step_result.stdout) | |
| 494 | 392 |
| 495 def _check_revision_good(self): | 393 self._read_test_results(api.run_local_test_run( |
| 394 overrides, skip_download=skip_download, |
| 395 step_test_data=run_test_step_test_data |
| 396 )) |
| 397 |
| 398 def _check_revision_good(self): # pragma: no cover |
| 496 """Determines if a revision is good or bad. | 399 """Determines if a revision is good or bad. |
| 497 | 400 |
| 498 Iteratively increment the sample size of the revision being tested, the last | 401 Returns: |
| 499 known good revision, and the first known bad revision until a relationship | 402 True if the revision is either good or bad, False if it cannot be |
| 500 of significant difference can be established betweeb the results of the | 403 determined from the available data. |
| 501 revision being tested and one of the other two. | 404 """ |
| 405 # Do not reclassify revisions. Important for reference range. |
| 406 if self.good or self.bad: |
| 407 return True |
| 502 | 408 |
| 503 If the results do not converge towards finding a significant difference in | |
| 504 either direction, this is expected to timeout eventually. This scenario | |
| 505 should be rather rare, since it is expected that the fkbr and lkgr are | |
| 506 significantly different as a precondition. | |
| 507 | |
| 508 Returns: | |
| 509 True if the results of testing this revision are significantly different | |
| 510 from those of testing the earliest known bad revision. | |
| 511 False if they are instead significantly different form those of testing | |
| 512 the latest knwon good revision. | |
| 513 """ | |
| 514 lkgr = self.bisector.lkgr | 409 lkgr = self.bisector.lkgr |
| 515 fkbr = self.bisector.fkbr | 410 fkbr = self.bisector.fkbr |
| 411 if self.bisector.is_return_code_mode(): |
| 412 if self.overall_return_code == lkgr.overall_return_code: |
| 413 self.good = True |
| 414 else: |
| 415 self.bad = True |
| 416 return True |
| 417 diff_from_good = self.bisector.compare_revisions(self, lkgr) |
| 418 diff_from_bad = self.bisector.compare_revisions(self, fkbr) |
| 419 if diff_from_good == False and diff_from_bad == False: |
| 420 # We have reached the max number of samples and have not established |
| 421 # difference, give up. |
| 422 raise exceptions.InconclusiveBisectException() |
| 423 if diff_from_good and diff_from_bad: |
| 424 # Multiple regressions. |
| 425 # For now, proceed bisecting the biggest difference of the means. |
| 426 dist_from_good = abs(self.mean - lkgr.mean) |
| 427 dist_from_bad = abs(self.mean - fkbr.mean) |
| 428 if dist_from_good > dist_from_bad: |
| 429 # TODO(robertocn): Add way to handle the secondary regression |
| 430 #self.bisector.handle_secondary_regression(self, fkbr) |
| 431 self.bad = True |
| 432 return True |
| 433 else: |
| 434 #self.bisector.handle_secondary_regression(lkgr, self) |
| 435 self.good = True |
| 436 return True |
| 437 if diff_from_good: |
| 438 self.bad = True |
| 439 return True |
| 440 elif diff_from_bad: |
| 441 self.good = True |
| 442 return True |
| 443 return False |
| 516 | 444 |
| 517 if self.bisector.is_return_code_mode(): | |
| 518 return self.overall_return_code == lkgr.overall_return_code | |
| 519 | |
| 520 while True: | |
| 521 diff_from_good = self.bisector.significantly_different( | |
| 522 lkgr.values[:len(fkbr.values)], self.values) | |
| 523 diff_from_bad = self.bisector.significantly_different( | |
| 524 fkbr.values[:len(lkgr.values)], self.values) | |
| 525 | |
| 526 if diff_from_good and diff_from_bad: | |
| 527 # Multiple regressions. | |
| 528 # For now, proceed bisecting the biggest difference of the means. | |
| 529 dist_from_good = abs(self.mean_value - lkgr.mean_value) | |
| 530 dist_from_bad = abs(self.mean_value - fkbr.mean_value) | |
| 531 if dist_from_good > dist_from_bad: | |
| 532 # TODO(robertocn): Add way to handle the secondary regression | |
| 533 #self.bisector.handle_secondary_regression(self, fkbr) | |
| 534 return False | |
| 535 else: | |
| 536 #self.bisector.handle_secondary_regression(lkgr, self) | |
| 537 return True | |
| 538 | |
| 539 if diff_from_good or diff_from_bad: # pragma: no cover | |
| 540 return diff_from_bad | |
| 541 | |
| 542 self._next_retest() # pragma: no cover | |
| 543 | 445 |
| 544 def revision_string(self): | 446 def revision_string(self): |
| 545 if self._rev_str: | 447 if self._rev_str: |
| 546 return self._rev_str | 448 return self._rev_str |
| 547 result = '' | 449 result = '' |
| 548 if self.base_revision: # pragma: no cover | 450 if self.base_revision: # pragma: no cover |
| 549 result += self.base_revision.revision_string() + ',' | 451 result += self.base_revision.revision_string() + ',' |
| 550 commit = self.commit_hash[:10] | 452 commit = self.commit_hash[:10] |
| 551 if self.depot_name == 'chromium': | 453 if self.depot_name == 'chromium': |
| 552 try: | 454 try: |
| 553 commit = str(self.bisector.api.m.commit_position | 455 commit = str(self.bisector.api.m.commit_position |
| 554 .chromium_commit_position_from_hash(self.commit_hash)) | 456 .chromium_commit_position_from_hash(self.commit_hash)) |
| 555 except self.bisector.api.m.step.StepFailure: | 457 except self.bisector.api.m.step.StepFailure: |
| 556 pass # Failure to resolve a commit position is no reason to break. | 458 pass # Failure to resolve a commit position is no reason to break. |
| 557 result += '%s@%s' % (self.depot_name, commit) | 459 result += '%s@%s' % (self.depot_name, commit) |
| 558 self._rev_str = result | 460 self._rev_str = result |
| 559 return self._rev_str | 461 return self._rev_str |
| 560 | 462 |
| 561 def _next_retest(self): # pragma: no cover | 463 def __repr__(self): # pragma: no cover |
| 562 """Chooses one of current, lkgr, fkbr to retest. | 464 if not self.test_run_count: |
| 465 return ('RevisionState(rev=%s), values=[]' % self.revision_string()) |
| 466 if self.bisector.is_return_code_mode(): |
| 467 return ('RevisionState(rev=%s, mean=%r, overall_return_code=%r, ' |
| 468 'std_dev=%r)') % (self.revision_string(), self.mean, |
| 469 self.overall_return_code, self.std_dev) |
| 470 return ('RevisionState(rev=%s, mean_value=%r, std_dev=%r)' % ( |
| 471 self.revision_string(), self.mean, self.std_dev)) |
| 563 | 472 |
| 564 Look for the smallest sample and retest that. If the last tested revision | 473 @property |
| 565 is tied for the smallest sample, use that to take advantage of the fact | 474 def overall_return_code(self): |
| 566 that it is already downloaded and unzipped. | 475 if self.bisector.is_return_code_mode(): |
| 567 """ | 476 if self.return_codes: |
| 568 next_revision_to_test = min(self.bisector.lkgr, self, self.bisector.fkbr, | 477 if max(self.return_codes): |
| 569 key=lambda x: len(x.values)) | 478 return 1 |
| 570 if (len(self.bisector.last_tested_revision.values) == | 479 return 0 |
| 571 next_revision_to_test.values): | 480 raise ValueError('overall_return_code needs non-empty sample' |
| 572 self.bisector.last_tested_revision.retest() | 481 ) # pragma: no cover |
| 573 else: | 482 raise ValueError('overall_return_code only applies to return_code bisects' |
| 574 next_revision_to_test.retest() | 483 ) # pragma: no cover |
| 575 | |
| 576 def __repr__(self): | |
| 577 if self.overall_return_code is not None: | |
| 578 return ('RevisionState(rev=%s, values=%r, overall_return_code=%r, ' | |
| 579 'std_dev=%r)') % (self.revision_string(), self.values, | |
| 580 self.overall_return_code, self.std_dev) | |
| 581 return ('RevisionState(rev=%s, values=%r, mean_value=%r, std_dev=%r)' % ( | |
| 582 self.revision_string(), self.values, self.mean_value, self.std_dev)) | |
| OLD | NEW |