Chromium Code Reviews| 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 time | 7 import time |
| 8 import urllib | 8 import urllib |
| 9 | 9 |
| 10 from . import config_validation | 10 from . import config_validation |
| (...skipping 28 matching lines...) Expand all Loading... | |
| 39 'LO_FINAL_CONF', # The bisect completed without a culprit. | 39 'LO_FINAL_CONF', # The bisect completed without a culprit. |
| 40 ) | 40 ) |
| 41 | 41 |
| 42 # When we look for the next revision to build, we search nearby revisions | 42 # When we look for the next revision to build, we search nearby revisions |
| 43 # looking for a revision that's already been archived. Since we don't want | 43 # looking for a revision that's already been archived. Since we don't want |
| 44 # to move *too* far from the original revision, we'll cap the search at 25%. | 44 # to move *too* far from the original revision, we'll cap the search at 25%. |
| 45 DEFAULT_SEARCH_RANGE_PERCENTAGE = 0.25 | 45 DEFAULT_SEARCH_RANGE_PERCENTAGE = 0.25 |
| 46 | 46 |
| 47 # How long to re-test the initial good-bad range for until significant | 47 # How long to re-test the initial good-bad range for until significant |
| 48 # difference is established. | 48 # difference is established. |
| 49 REGRESSION_CHECK_TIMEOUT = 20 * 60 * 60 # 20 hours. A build times out after 24. | 49 REGRESSION_CHECK_TIMEOUT = 2 * 60 * 60 |
|
eakuefner
2016/09/09 20:35:42
Why? Comment?
RobertoCN
2016/09/13 22:11:40
I don't know. I'll set it back to what it was. A l
| |
| 50 # If we reach this number of samples on the reference range and have not | |
| 51 # achieved statistical significance, bail. | |
| 52 MAX_REQUIRED_SAMPLES = 15 | |
| 53 | |
| 54 # Significance level to use for determining difference between revisions via | |
| 55 # hypothesis testing. | |
| 56 SIGNIFICANCE_LEVEL = 0.01 | |
| 57 | 50 |
| 58 _FAILED_INITIAL_CONFIDENCE_ABORT_REASON = ( | 51 _FAILED_INITIAL_CONFIDENCE_ABORT_REASON = ( |
| 59 'The metric values for the initial "good" and "bad" revisions ' | 52 'The metric values for the initial "good" and "bad" revisions ' |
| 60 'do not represent a clear regression.') | 53 'do not represent a clear regression.') |
| 61 | 54 |
| 62 _DIRECTION_OF_IMPROVEMENT_ABORT_REASON = ( | 55 _DIRECTION_OF_IMPROVEMENT_ABORT_REASON = ( |
| 63 'The metric values for the initial "good" and "bad" revisions match the ' | 56 'The metric values for the initial "good" and "bad" revisions match the ' |
| 64 'expected direction of improvement. Thus, likely represent an improvement ' | 57 'expected direction of improvement. Thus, likely represent an improvement ' |
| 65 'and not a regression.') | 58 'and not a regression.') |
| 66 | 59 |
| 67 | 60 |
| 68 class Bisector(object): | 61 class Bisector(object): |
| 69 """This class abstracts an ongoing bisect (or n-sect) job.""" | 62 """This class abstracts an ongoing bisect (or n-sect) job.""" |
| 70 | 63 |
| 64 SIGNIFICANTLY_DIFFERENT = True | |
|
prasadv
2016/09/09 18:52:30
I think declaring enum here would be better.
RobertoCN
2016/09/13 22:11:40
Done.
| |
| 65 NOT_SIGNIFICANTLY_DIFFERENT = False | |
| 66 NEED_MORE_DATA = 'needMoreData' | |
| 67 | |
| 71 def __init__(self, api, bisect_config, revision_class, init_revisions=True, | 68 def __init__(self, api, bisect_config, revision_class, init_revisions=True, |
| 72 **flags): | 69 **flags): |
| 73 """Initializes the state of a new bisect job from a dictionary. | 70 """Initializes the state of a new bisect job from a dictionary. |
| 74 | 71 |
| 75 Note that the initial good_rev and bad_rev MUST resolve to a commit position | 72 Note that the initial good_rev and bad_rev MUST resolve to a commit position |
| 76 in the chromium repo. | 73 in the chromium repo. |
| 77 """ | 74 """ |
| 78 super(Bisector, self).__init__() | 75 super(Bisector, self).__init__() |
| 79 self.flags = flags | 76 self.flags = flags |
| 80 self._api = api | 77 self._api = api |
| (...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 148 | 145 |
| 149 Returns: | 146 Returns: |
| 150 A 40-digit git commit hash string. | 147 A 40-digit git commit hash string. |
| 151 """ | 148 """ |
| 152 if self._is_sha1(rev): # pragma: no cover | 149 if self._is_sha1(rev): # pragma: no cover |
| 153 return rev | 150 return rev |
| 154 if rev.isdigit(): | 151 if rev.isdigit(): |
| 155 commit_position = self._api.m.commit_position.construct( | 152 commit_position = self._api.m.commit_position.construct( |
| 156 branch='refs/heads/master', value=rev) | 153 branch='refs/heads/master', value=rev) |
| 157 try: | 154 try: |
| 158 return self._api.m.crrev.to_commit_hash(commit_position) | 155 return self._api.m.crrev.to_commit_hash( |
| 156 commit_position, | |
| 157 step_test_data=lambda: self.api._test_data['hash_cp_map'][rev]) | |
| 159 except self.api.m.step.StepFailure: # pragma: no cover | 158 except self.api.m.step.StepFailure: # pragma: no cover |
| 160 self.surface_result('BAD_REV') | 159 self.surface_result('BAD_REV') |
| 161 raise | 160 raise |
| 162 self.surface_result('BAD_REV') # pragma: no cover | 161 self.surface_result('BAD_REV') # pragma: no cover |
| 163 raise self.api.m.step.StepFailure( | 162 raise self.api.m.step.StepFailure( |
| 164 'Invalid input revision: %r' % (rev,)) # pragma: no cover | 163 'Invalid input revision: %r' % (rev,)) # pragma: no cover |
| 165 | 164 |
| 166 @staticmethod | 165 @staticmethod |
| 167 def _is_sha1(s): | 166 def _is_sha1(s): |
| 168 return bool(re.match('^[0-9A-Fa-f]{40}$', s)) | 167 return bool(re.match('^[0-9A-Fa-f]{40}$', s)) |
| 169 | 168 |
| 170 def significantly_different( | 169 def compare_revisions(self, revision_a, revision_b): |
| 171 self, list_a, list_b, | 170 """ |
| 172 significance_level=SIGNIFICANCE_LEVEL): # pragma: no cover | 171 Returns: |
| 173 """Uses an external script to run hypothesis testing with scipy. | 172 SIGNIFICANTLY_DIFFERENT if the samples are significantly different. |
| 173 NEED_MORE_DATA if there is not enough data to tell. | |
| 174 NOT_SIGNIFICANTLY_DIFFERENT if if there's enough data but still can't | |
| 175 tell the samples apart. | |
| 176 """ | |
| 177 output_format = 'chartjson' | |
| 178 values_a = revision_a.chartjson_paths | |
| 179 values_b = revision_b.chartjson_paths | |
| 180 if revision_a.valueset_paths and revision_b.valueset_paths: | |
| 181 output_format = 'valueset' | |
|
prasadv
2016/09/09 18:52:30
shouldn't we re-assign values_a and values_b here?
RobertoCN
2016/09/13 22:11:40
Nice catch!
Done.
| |
| 174 | 182 |
| 175 The reason why we need an external script is that scipy is not available to | 183 result = self.api.stat_compare( |
| 176 the default python installed in all platforms. We instead rely on an | 184 values_a, |
| 177 anaconda environment to provide those packages. | 185 values_b, |
| 186 self.bisect_config['metric'], | |
| 187 output_format=output_format, | |
| 188 step_test_data=lambda: self.api.test_api.compare_samples_data( | |
| 189 self.api._test_data.get('revision_data'), revision_a, revision_b)) | |
| 178 | 190 |
| 179 Args: | 191 revision_a.debug_values = result['sample_a']['debug_values'] |
| 180 list_a, list_b: Two lists representing samples to be compared. | 192 revision_b.debug_values = result['sample_b']['debug_values'] |
| 181 significance_level: Self-describing. As a decimal fraction. | |
| 182 | 193 |
| 183 Returns: | 194 if result['result'] == self.NEED_MORE_DATA: |
| 184 A boolean indicating whether the null hypothesis ~(that the lists are | 195 return self.NEED_MORE_DATA |
| 185 samples from the same population) can be rejected at the specified | 196 elif result['result']: |
| 186 significance level. | 197 return self.SIGNIFICANTLY_DIFFERENT |
| 187 """ | 198 # TODO(robertocn): Remove this pragma and cover this line. |
| 188 step_result = self.api.m.python( | 199 else: # pragma: no cover |
| 189 'Checking sample difference', | 200 return self.NOT_SIGNIFICANTLY_DIFFERENT |
| 190 self.api.resource('significantly_different.py'), | |
| 191 [json.dumps(list_a), json.dumps(list_b), str(significance_level)], | |
| 192 stdout=self.api.m.json.output()) | |
| 193 results = step_result.stdout | |
| 194 if results is None: | |
| 195 assert self.dummy_builds | |
| 196 return True | |
| 197 significantly_different = results['significantly_different'] | |
| 198 step_result.presentation.logs[str(significantly_different)] = [ | |
| 199 'See json.output for details'] | |
| 200 return significantly_different | |
| 201 | 201 |
| 202 def config_step(self): | 202 def config_step(self): |
| 203 """Yields a step that prints the bisect config.""" | 203 """Yields a step that prints the bisect config.""" |
| 204 api = self.api | 204 api = self.api |
| 205 | 205 |
| 206 # bisect_config may come as a FrozenDict (which is not serializable). | 206 # bisect_config may come as a FrozenDict (which is not serializable). |
| 207 bisect_config = dict(self.bisect_config) | 207 bisect_config = dict(self.bisect_config) |
| 208 | 208 |
| 209 def fix_windows_backslashes(s): | 209 def fix_windows_backslashes(s): |
| 210 backslash_regex = re.compile(r'(?<!\\)\\(?!\\)') | 210 backslash_regex = re.compile(r'(?<!\\)\\(?!\\)') |
| (...skipping 17 matching lines...) Expand all Loading... | |
| 228 except config_validation.ValidationFail as error: | 228 except config_validation.ValidationFail as error: |
| 229 self.surface_result('BAD_CONFIG') | 229 self.surface_result('BAD_CONFIG') |
| 230 self.api.m.halt(error.message) | 230 self.api.m.halt(error.message) |
| 231 raise self.api.m.step.StepFailure(error.message) | 231 raise self.api.m.step.StepFailure(error.message) |
| 232 | 232 |
| 233 @property | 233 @property |
| 234 def api(self): | 234 def api(self): |
| 235 return self._api | 235 return self._api |
| 236 | 236 |
| 237 def compute_relative_change(self): | 237 def compute_relative_change(self): |
| 238 old_value = float(self.good_rev.mean_value) | 238 old_value = float(self.good_rev.mean or 0) |
| 239 new_value = float(self.bad_rev.mean_value) | 239 new_value = float(self.bad_rev.mean or 0) |
| 240 | 240 |
| 241 if new_value and not old_value: # pragma: no cover | 241 if new_value and not old_value: # pragma: no cover |
| 242 self.relative_change = ZERO_TO_NON_ZERO | 242 self.relative_change = ZERO_TO_NON_ZERO |
| 243 return | 243 return |
| 244 | 244 |
| 245 rel_change = self.api.m.math_utils.relative_change(old_value, new_value) | 245 rel_change = self.api.m.math_utils.relative_change(old_value, new_value) |
| 246 self.relative_change = '%.2f%%' % (100 * rel_change) | 246 self.relative_change = '%.2f%%' % (100 * rel_change) |
| 247 | 247 |
| 248 def make_deps_sha_file(self, deps_sha): | 248 def make_deps_sha_file(self, deps_sha): |
| 249 """Make a diff patch that creates DEPS.sha. | 249 """Make a diff patch that creates DEPS.sha. |
| (...skipping 16 matching lines...) Expand all Loading... | |
| 266 file is to be written to. | 266 file is to be written to. |
| 267 commit_hash (str): An identifier for the step. | 267 commit_hash (str): An identifier for the step. |
| 268 | 268 |
| 269 Returns: | 269 Returns: |
| 270 A string containing the hash of the interned object. | 270 A string containing the hash of the interned object. |
| 271 """ | 271 """ |
| 272 cmd = 'hash-object -t blob -w --stdin'.split(' ') | 272 cmd = 'hash-object -t blob -w --stdin'.split(' ') |
| 273 stdin = self.api.m.raw_io.input(file_contents) | 273 stdin = self.api.m.raw_io.input(file_contents) |
| 274 stdout = self.api.m.raw_io.output() | 274 stdout = self.api.m.raw_io.output() |
| 275 step_name = 'Hashing modified DEPS file with revision ' + commit_hash | 275 step_name = 'Hashing modified DEPS file with revision ' + commit_hash |
| 276 step_result = self.api.m.git(*cmd, cwd=cwd, stdin=stdin, stdout=stdout, | 276 step_result = self.api.m.git( |
| 277 name=step_name) | 277 *cmd, cwd=cwd, stdin=stdin, stdout=stdout, name=step_name, |
| 278 step_test_data=lambda: | |
| 279 self.api.m.raw_io.test_api.stream_output(commit_hash)) | |
| 278 hash_string = step_result.stdout.splitlines()[0] | 280 hash_string = step_result.stdout.splitlines()[0] |
| 279 try: | 281 try: |
| 280 if hash_string: | 282 if hash_string: |
| 281 int(hash_string, 16) | 283 int(hash_string, 16) |
| 282 return hash_string | 284 return hash_string |
| 283 except ValueError: # pragma: no cover | 285 except ValueError: # pragma: no cover |
| 284 reason = 'Git did not output a valid hash for the interned file.' | 286 reason = 'Git did not output a valid hash for the interned file.' |
| 285 self.api.m.halt(reason) | 287 self.api.m.halt(reason) |
| 286 raise self.api.m.step.StepFailure(reason) | 288 raise self.api.m.step.StepFailure(reason) |
| 287 | 289 |
| 288 def _gen_diff_patch(self, git_object_a, git_object_b, src_alias, dst_alias, | 290 def _gen_diff_patch(self, git_object_a, git_object_b, src_alias, dst_alias, |
|
prasadv
2016/09/09 18:52:30
Do we still need this? Since we are using buildbuc
RobertoCN
2016/09/13 22:11:41
Removed it. Now, we'll hash self.revision_string()
| |
| 289 cwd, deps_rev): | 291 cwd, deps_rev): |
| 290 """Produces a git diff patch. | 292 """Produces a git diff patch. |
| 291 | 293 |
| 292 Args: | 294 Args: |
| 293 git_object_a (str): Tree-ish git object identifier. | 295 git_object_a (str): Tree-ish git object identifier. |
| 294 git_object_b (str): Another tree-ish git object identifier. | 296 git_object_b (str): Another tree-ish git object identifier. |
| 295 src_alias (str): Label to replace the tree-ish identifier on | 297 src_alias (str): Label to replace the tree-ish identifier on |
| 296 the resulting diff patch. (git_object_a) | 298 the resulting diff patch. (git_object_a) |
| 297 dst_alias (str): Same as above for (git_object_b) | 299 dst_alias (str): Same as above for (git_object_b) |
| 298 cwd (recipe_config_types.Path): Path to the checkout whose repo contains | 300 cwd (recipe_config_types.Path): Path to the checkout whose repo contains |
| 299 the objects to be compared. | 301 the objects to be compared. |
| 300 deps_rev (str): Deps revision to identify the patch generating step. | 302 deps_rev (str): Deps revision to identify the patch generating step. |
| 301 | 303 |
| 302 Returns: | 304 Returns: |
| 303 A string containing the diff patch as produced by the 'git diff' command. | 305 A string containing the diff patch as produced by the 'git diff' command. |
| 304 """ | 306 """ |
| 305 # The prefixes used in the command below are used to find and replace the | 307 # The prefixes used in the command below are used to find and replace the |
| 306 # tree-ish git object id's on the diff output more easily. | 308 # tree-ish git object id's on the diff output more easily. |
| 307 cmd = 'diff %s %s --src-prefix=IAMSRC: --dst-prefix=IAMDST:' | 309 cmd = 'diff %s %s --src-prefix=IAMSRC: --dst-prefix=IAMDST:' |
| 308 cmd %= (git_object_a, git_object_b) | 310 cmd %= (git_object_a, git_object_b) |
| 309 cmd = cmd.split(' ') | 311 cmd = cmd.split(' ') |
| 310 stdout = self.api.m.raw_io.output() | 312 stdout = self.api.m.raw_io.output() |
| 311 step_name = 'Generating patch for %s to %s' % (git_object_a, deps_rev) | 313 step_name = 'Generating patch for %s to %s' % (git_object_a, deps_rev) |
| 312 step_result = self.api.m.git(*cmd, cwd=cwd, stdout=stdout, name=step_name) | 314 step_result = self.api.m.git( |
| 315 *cmd, cwd=cwd, stdout=stdout, name=step_name, | |
| 316 step_test_data=lambda: self.api._test_data['diff_patch']) | |
| 313 patch_text = step_result.stdout | 317 patch_text = step_result.stdout |
| 314 src_string = 'IAMSRC:' + git_object_a | 318 src_string = 'IAMSRC:' + git_object_a |
| 315 dst_string = 'IAMDST:' + git_object_b | 319 dst_string = 'IAMDST:' + git_object_b |
| 316 patch_text = patch_text.replace(src_string, src_alias) | 320 patch_text = patch_text.replace(src_string, src_alias) |
| 317 patch_text = patch_text.replace(dst_string, dst_alias) | 321 patch_text = patch_text.replace(dst_string, dst_alias) |
| 318 return patch_text | 322 return patch_text |
| 319 | 323 |
| 320 def make_deps_patch(self, base_revision, base_file_contents, | 324 def make_deps_patch(self, base_revision, base_file_contents, |
| 321 depot, new_commit_hash): | 325 depot, new_commit_hash): |
| 322 """Make a diff patch that updates a specific dependency revision. | 326 """Make a diff patch that updates a specific dependency revision. |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 365 interned_deps_hash, deps_file, deps_file, | 369 interned_deps_hash, deps_file, deps_file, |
| 366 cwd=cwd, | 370 cwd=cwd, |
| 367 deps_rev=new_commit_hash) | 371 deps_rev=new_commit_hash) |
| 368 return patch_text, patched_contents | 372 return patch_text, patched_contents |
| 369 | 373 |
| 370 def _expand_initial_revision_range(self): | 374 def _expand_initial_revision_range(self): |
| 371 """Sets the initial contents of |self.revisions|.""" | 375 """Sets the initial contents of |self.revisions|.""" |
| 372 with self.api.m.step.nest('Expanding revision range'): | 376 with self.api.m.step.nest('Expanding revision range'): |
| 373 good_hash = self.good_rev.commit_hash | 377 good_hash = self.good_rev.commit_hash |
| 374 bad_hash = self.bad_rev.commit_hash | 378 bad_hash = self.bad_rev.commit_hash |
| 379 depot = self.good_rev.depot_name | |
| 375 step_name = 'for revisions %s:%s' % (good_hash, bad_hash) | 380 step_name = 'for revisions %s:%s' % (good_hash, bad_hash) |
| 376 revisions = self._revision_range( | 381 revisions = self._revision_range( |
| 377 start=good_hash, | 382 start=good_hash, |
| 378 end=bad_hash, | 383 end=bad_hash, |
| 379 depot_name=self.base_depot, | 384 depot_name=self.base_depot, |
| 380 step_name=step_name, | 385 step_name=step_name, |
| 381 exclude_end=True) | 386 exclude_end=True, |
| 387 step_test_data=lambda: self.api._test_data['revision_list'][depot] | |
| 388 ) | |
| 382 self.revisions = [self.good_rev] + revisions + [self.bad_rev] | 389 self.revisions = [self.good_rev] + revisions + [self.bad_rev] |
| 383 self._update_revision_list_indexes() | 390 self._update_revision_list_indexes() |
| 384 | 391 |
| 385 def _revision_range(self, start, end, depot_name, base_revision=None, | 392 def _revision_range(self, start, end, depot_name, base_revision=None, |
| 386 step_name=None, exclude_end=False): | 393 step_name=None, exclude_end=False, **kwargs): |
| 387 """Returns a list of RevisionState objects between |start| and |end|. | 394 """Returns a list of RevisionState objects between |start| and |end|. |
| 388 | 395 |
| 389 When expanding the initial revision range we want to exclude the last | 396 When expanding the initial revision range we want to exclude the last |
| 390 revision, since both good and bad have already been created and tested. | 397 revision, since both good and bad have already been created and tested. |
| 391 When bisecting into a roll on the other hand, we want to include the last | 398 When bisecting into a roll on the other hand, we want to include the last |
| 392 revision in the roll, because although the code should be equivalent to | 399 revision in the roll, because although the code should be equivalent to |
| 393 the roll, we want to blame the right culprit and not the roll. | 400 the roll, we want to blame the right culprit and not the roll. |
| 394 | 401 |
| 395 Args: | 402 Args: |
| 396 start (str): Start commit hash. | 403 start (str): Start commit hash. |
| 397 end (str): End commit hash. | 404 end (str): End commit hash. |
| 398 depot_name (str): Short string name of repo, e.g. chromium or v8. | 405 depot_name (str): Short string name of repo, e.g. chromium or v8. |
| 399 base_revision (str): Base revision in the downstream repo (e.g. chromium). | 406 base_revision (str): Base revision in the downstream repo (e.g. chromium). |
| 400 step_name (str): Optional step name. | 407 step_name (str): Optional step name. |
| 401 exclude_end (bool): Whether to exclude the last revision in the range, | 408 exclude_end (bool): Whether to exclude the last revision in the range, |
| 402 i.e. the revision given as end. | 409 i.e. the revision given as end. |
| 403 | 410 |
| 404 Returns: | 411 Returns: |
| 405 A list of RevisionState objects. | 412 A list of RevisionState objects. |
| 406 """ | 413 """ |
| 407 if self.internal_bisect: # pragma: no cover | 414 if self.internal_bisect: # pragma: no cover |
| 408 return self._revision_range_with_gitiles( | 415 return self._revision_range_with_gitiles( |
| 409 start, end, depot_name, base_revision, step_name) | 416 start, end, depot_name, base_revision, step_name) |
| 410 try: | 417 try: |
| 411 step_result = self.api.m.python( | 418 step_result = self.api.m.python( |
| 412 step_name, | 419 step_name, |
| 413 self.api.resource('fetch_intervening_revisions.py'), | 420 self.api.resource('fetch_intervening_revisions.py'), |
| 414 [start, end, depot_config.DEPOT_DEPS_NAME[depot_name]['url']], | 421 [start, end, depot_config.DEPOT_DEPS_NAME[depot_name]['url']], |
| 415 stdout=self.api.m.json.output()) | 422 stdout=self.api.m.json.output(), **kwargs) |
| 416 except self.api.m.step.StepFailure: # pragma: no cover | 423 except self.api.m.step.StepFailure: # pragma: no cover |
| 417 self.surface_result('BAD_REV') | 424 self.surface_result('BAD_REV') |
| 418 raise | 425 raise |
| 419 revisions = [] | 426 revisions = [] |
| 420 revision_hashes = step_result.stdout | 427 revision_hashes = step_result.stdout |
| 421 if exclude_end: | 428 if exclude_end: |
| 422 revision_hashes = revision_hashes[:-1] | 429 revision_hashes = revision_hashes[:-1] |
| 423 for commit_hash, _ in revision_hashes: | 430 for commit_hash, _ in revision_hashes: |
| 424 revisions.append(self.revision_class( | 431 revisions.append(self.revision_class( |
| 425 bisector=self, | 432 bisector=self, |
| (...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 514 dep_revision_max = max_revision.deps[depot_name] | 521 dep_revision_max = max_revision.deps[depot_name] |
| 515 if (dep_revision_min and dep_revision_max and | 522 if (dep_revision_min and dep_revision_max and |
| 516 dep_revision_min != dep_revision_max): | 523 dep_revision_min != dep_revision_max): |
| 517 step_name = ('Expanding revision range for revision %s' | 524 step_name = ('Expanding revision range for revision %s' |
| 518 ' on depot %s' % (dep_revision_max, depot_name)) | 525 ' on depot %s' % (dep_revision_max, depot_name)) |
| 519 rev_list = self._revision_range( | 526 rev_list = self._revision_range( |
| 520 start=dep_revision_min, | 527 start=dep_revision_min, |
| 521 end=dep_revision_max, | 528 end=dep_revision_max, |
| 522 depot_name=depot_name, | 529 depot_name=depot_name, |
| 523 base_revision=min_revision, | 530 base_revision=min_revision, |
| 524 step_name=step_name) | 531 step_name=step_name, |
| 532 step_test_data=lambda: | |
| 533 self.api._test_data['revision_list'][depot_name]) | |
| 525 new_revisions = self.revisions[:max_revision.list_index] | 534 new_revisions = self.revisions[:max_revision.list_index] |
| 526 new_revisions += rev_list | 535 new_revisions += rev_list |
| 527 new_revisions += self.revisions[max_revision.list_index:] | 536 new_revisions += self.revisions[max_revision.list_index:] |
| 528 self.revisions = new_revisions | 537 self.revisions = new_revisions |
| 529 self._update_revision_list_indexes() | 538 self._update_revision_list_indexes() |
| 530 return True | 539 return True |
| 531 except RuntimeError: # pragma: no cover | 540 except RuntimeError: # pragma: no cover |
| 532 warning_text = ('Could not expand dependency revisions for ' + | 541 warning_text = ('Could not expand dependency revisions for ' + |
| 533 revision_to_expand.commit_hash) | 542 revision_to_expand.commit_hash) |
| 534 self.surface_result('BAD_REV') | 543 self.surface_result('BAD_REV') |
| (...skipping 16 matching lines...) Expand all Loading... | |
| 551 | 560 |
| 552 The change between the test results obtained for the given 'good' and | 561 The change between the test results obtained for the given 'good' and |
| 553 'bad' revisions is expected to be considered a regression. The | 562 'bad' revisions is expected to be considered a regression. The |
| 554 `improvement_direction` attribute is positive if a larger number is | 563 `improvement_direction` attribute is positive if a larger number is |
| 555 considered better, and negative if a smaller number is considered better. | 564 considered better, and negative if a smaller number is considered better. |
| 556 | 565 |
| 557 Returns: | 566 Returns: |
| 558 True if the check passes (i.e. no problem), False if the change is not | 567 True if the check passes (i.e. no problem), False if the change is not |
| 559 a regression according to the improvement direction. | 568 a regression according to the improvement direction. |
| 560 """ | 569 """ |
| 561 good = self.good_rev.mean_value | 570 good = self.good_rev.mean |
| 562 bad = self.bad_rev.mean_value | 571 bad = self.bad_rev.mean |
| 563 | 572 |
| 564 if self.is_return_code_mode(): | 573 if self.is_return_code_mode(): |
| 565 return True | 574 return True |
| 566 | 575 |
| 567 direction = self.improvement_direction | 576 direction = self.improvement_direction |
| 568 if direction is None: | 577 if direction is None: |
| 569 return True | 578 return True |
| 570 if (bad > good and direction > 0) or (bad < good and direction < 0): | 579 if (bad > good and direction > 0) or (bad < good and direction < 0): |
| 571 self._set_failed_direction_results() | 580 self._set_failed_direction_results() |
| 572 return False | 581 return False |
| (...skipping 14 matching lines...) Expand all Loading... | |
| 587 """Checks that the initial range presents a clear enough regression. | 596 """Checks that the initial range presents a clear enough regression. |
| 588 | 597 |
| 589 We ensure that the good and bad revisions produce significantly different | 598 We ensure that the good and bad revisions produce significantly different |
| 590 results, increasing the sample size until MAX_REQUIRED_SAMPLES is reached | 599 results, increasing the sample size until MAX_REQUIRED_SAMPLES is reached |
| 591 or REGRESSION_CHECK_TIMEOUT seconds have elapsed. | 600 or REGRESSION_CHECK_TIMEOUT seconds have elapsed. |
| 592 | 601 |
| 593 Returns: True if the revisions produced results that differ from each | 602 Returns: True if the revisions produced results that differ from each |
| 594 other in a statistically significant manner. False if such difference could | 603 other in a statistically significant manner. False if such difference could |
| 595 not be established in the time or sample size allowed. | 604 not be established in the time or sample size allowed. |
| 596 """ | 605 """ |
| 597 if self.test_type == 'return_code': | 606 if self.is_return_code_mode(): |
| 598 return (self.good_rev.overall_return_code != | 607 return (self.good_rev.overall_return_code != |
| 599 self.bad_rev.overall_return_code) | 608 self.bad_rev.overall_return_code) |
| 600 | 609 |
| 601 if self.bypass_stats_check: | 610 if self.bypass_stats_check: |
| 602 dummy_result = self.good_rev.values != self.bad_rev.values | 611 self.compare_revisions(self.good_rev, self.bad_rev) |
| 612 dummy_result = self.good_rev.mean != self.bad_rev.mean | |
| 603 if not dummy_result: | 613 if not dummy_result: |
| 604 self._set_insufficient_confidence_warning() | 614 self._set_insufficient_confidence_warning() |
| 605 return dummy_result | 615 return dummy_result |
| 606 | 616 |
| 617 # TODO(robertocn): This step should not be necessary in some cases. | |
| 607 with self.api.m.step.nest('Re-testing reference range'): | 618 with self.api.m.step.nest('Re-testing reference range'): |
| 608 expiration_time = time.time() + REGRESSION_CHECK_TIMEOUT | 619 expiration_time = time.time() + REGRESSION_CHECK_TIMEOUT |
| 609 while time.time() < expiration_time: | 620 while time.time() < expiration_time: |
| 610 if len(self.good_rev.values) >= 5 and len(self.bad_rev.values) >= 5: | 621 if (self.good_rev.test_run_count >= 5 |
| 611 if self.significantly_different(self.good_rev.values, | 622 and self.bad_rev.test_run_count >= 5): |
| 612 self.bad_rev.values): | 623 if self.compare_revisions(self.good_rev, self.bad_rev): |
| 613 return True | 624 return True |
| 614 if len(self.good_rev.values) == len(self.bad_rev.values): | 625 if self.good_rev.test_run_count == self.bad_rev.test_run_count: |
| 615 revision_to_retest = self.last_tested_revision | 626 revision_to_retest = self.last_tested_revision |
| 616 else: | 627 else: |
| 617 revision_to_retest = min(self.good_rev, self.bad_rev, | 628 revision_to_retest = min(self.good_rev, self.bad_rev, |
| 618 key=lambda x: len(x.values)) | 629 key=lambda x: x.test_run_count) |
| 619 if len(revision_to_retest.values) < MAX_REQUIRED_SAMPLES: | 630 revision_to_retest._do_test() |
| 620 revision_to_retest.retest() | 631 |
| 621 else: | |
| 622 break | |
| 623 self._set_insufficient_confidence_warning() | 632 self._set_insufficient_confidence_warning() |
| 624 return False | 633 return False |
| 625 | 634 |
| 626 | 635 |
| 627 def get_exception(self): | 636 def get_exception(self): |
| 628 raise NotImplementedError() # pragma: no cover | 637 raise NotImplementedError() # pragma: no cover |
| 629 # TODO: should return an exception with the details of the failure. | 638 # TODO: should return an exception with the details of the failure. |
| 630 | 639 |
| 631 def _set_insufficient_confidence_warning( | 640 def _set_insufficient_confidence_warning( |
| 632 self): # pragma: no cover | 641 self): # pragma: no cover |
| 633 """Adds a warning about the lack of initial regression confidence.""" | 642 """Adds a warning about the lack of initial regression confidence.""" |
| 634 self.failed_initial_confidence = True | 643 self.failed_initial_confidence = True |
| 635 self.surface_result('LO_INIT_CONF') | 644 self.surface_result('LO_INIT_CONF') |
| 636 self.warnings.append( | 645 self.warnings.append( |
| 637 'Bisect failed to reproduce the regression with enough confidence.') | 646 'Bisect failed to reproduce the regression with enough confidence.') |
| 638 | 647 |
| 639 def _results_debug_message(self): | 648 def _results_debug_message(self): |
| 640 """Returns a string with values used to debug a bisect result.""" | 649 """Returns a string with values used to debug a bisect result.""" |
| 641 result = 'bisector.lkgr: %r\n' % self.lkgr | 650 result = 'bisector.lkgr: %r\n' % self.lkgr |
| 642 result += 'bisector.fkbr: %r\n\n' % self.fkbr | 651 result += 'bisector.fkbr: %r\n\n' % self.fkbr |
| 643 result += self._revision_value_table() | 652 result += self._revision_value_table() |
| 644 if (self.lkgr and self.lkgr.values and self.fkbr and self.fkbr.values): | 653 if (self.lkgr and self.lkgr.test_run_count and self.fkbr and |
| 645 result += '\n' + self._t_test_results() | 654 self.fkbr.test_run_count): |
| 655 result += '\n' + '\n'.join([ | |
| 656 'LKGR values: %r' % list(self.lkgr.debug_values), | |
| 657 'FKBR values: %r' % list(self.fkbr.debug_values), | |
| 658 ]) | |
| 646 return result | 659 return result |
| 647 | 660 |
| 648 def _revision_value_table(self): | 661 def _revision_value_table(self): |
| 649 """Returns a string table showing revisions and their values.""" | 662 """Returns a string table showing revisions and their values.""" |
| 650 header = [['Revision', 'Values']] | 663 header = [['Revision', 'Values']] |
| 651 rows = [[r.revision_string(), str(r.values)] for r in self.revisions] | 664 rows = [[r.revision_string(), str(r.debug_values)] for r in self.revisions] |
| 652 return self._pretty_table(header + rows) | 665 return self._pretty_table(header + rows) |
| 653 | 666 |
| 654 def _pretty_table(self, data): | 667 def _pretty_table(self, data): |
| 655 results = [] | 668 results = [] |
| 656 for row in data: | 669 for row in data: |
| 657 results.append('%-15s' * len(row) % tuple(row)) | 670 results.append('%-15s' * len(row) % tuple(row)) |
| 658 return '\n'.join(results) | 671 return '\n'.join(results) |
| 659 | 672 |
| 660 def _t_test_results(self): | |
| 661 """Returns a string showing t-test results for lkgr and fkbr.""" | |
| 662 t, df, p = self.api.m.math_utils.welchs_t_test( | |
| 663 self.lkgr.values, self.fkbr.values) | |
| 664 lines = [ | |
| 665 'LKGR values: %r' % self.lkgr.values, | |
| 666 'FKBR values: %r' % self.fkbr.values, | |
| 667 't-statistic: %r' % t, | |
| 668 'deg. of freedom: %r' % df, | |
| 669 'p-value: %r' % p, | |
| 670 'Confidence score: %r' % (100 * (1 - p)) | |
| 671 ] | |
| 672 return '\n'.join(lines) | |
| 673 | |
| 674 def print_result_debug_info(self): | 673 def print_result_debug_info(self): |
| 675 """Prints extra debug info at the end of the bisect process.""" | 674 """Prints extra debug info at the end of the bisect process.""" |
| 676 lines = self._results_debug_message().splitlines() | 675 lines = self._results_debug_message().splitlines() |
| 677 # If we emit a null step then add a log to it, the log should be kept | 676 # If we emit a null step then add a log to it, the log should be kept |
| 678 # longer than 7 days (which is often needed to debug some issues). | 677 # longer than 7 days (which is often needed to debug some issues). |
| 679 self.api.m.step('Debug Info', []) | 678 self.api.m.step('Debug Info', []) |
| 680 self.api.m.step.active_result.presentation.logs['Debug Info'] = lines | 679 self.api.m.step.active_result.presentation.logs['Debug Info'] = lines |
| 681 | 680 |
| 682 def post_result(self, halt_on_failure=False): | 681 def post_result(self, halt_on_failure=False): |
| 683 """Posts bisect results to Perf Dashboard.""" | 682 """Posts bisect results to Perf Dashboard.""" |
| 684 self.api.m.perf_dashboard.set_default_config() | 683 self.api.m.perf_dashboard.set_default_config() |
| 685 self.api.m.perf_dashboard.post_bisect_results( | 684 self.api.m.perf_dashboard.post_bisect_results( |
| 686 self.get_result(), halt_on_failure) | 685 self.get_result(), halt_on_failure) |
| 687 | 686 |
| 688 def get_revision_to_eval(self): | 687 def get_revision_to_eval(self): |
| 689 """Gets the next RevisionState object in the candidate range. | 688 """Gets the next RevisionState object in the candidate range. |
| 690 | 689 |
| 691 Returns: | 690 Returns: |
| 692 The next Revision object in a list. | 691 The next Revision object in a list. |
| 693 """ | 692 """ |
| 694 self._update_candidate_range() | 693 self._update_candidate_range() |
| 695 candidate_range = [revision for revision in | 694 candidate_range = [revision for revision in |
| 696 self.revisions[self.lkgr.list_index + 1: | 695 self.revisions[self.lkgr.list_index + 1: |
| 697 self.fkbr.list_index] | 696 self.fkbr.list_index] |
| 698 if not revision.tested and not revision.failed] | 697 if not revision.failed] |
| 699 if len(candidate_range) == 1: | 698 if len(candidate_range) == 1: |
| 700 return candidate_range[0] | 699 return candidate_range[0] |
| 701 if len(candidate_range) == 0: | 700 if len(candidate_range) == 0: |
| 702 return None | 701 return None |
| 703 | 702 |
| 704 default_revision = candidate_range[len(candidate_range) / 2] | 703 default_revision = candidate_range[len(candidate_range) / 2] |
| 705 | 704 |
| 706 with self.api.m.step.nest( | 705 with self.api.m.step.nest( |
| 707 'Wiggling revision ' + default_revision.revision_string()): | 706 'Wiggling revision ' + default_revision.revision_string()): |
| 708 # We'll search up to 25% of the range (in either direction) to try and | 707 # We'll search up to 25% of the range (in either direction) to try and |
| (...skipping 22 matching lines...) Expand all Loading... | |
| 731 return False | 730 return False |
| 732 | 731 |
| 733 def check_bisect_finished(self, revision): | 732 def check_bisect_finished(self, revision): |
| 734 """Checks if this revision completes the bisection process. | 733 """Checks if this revision completes the bisection process. |
| 735 | 734 |
| 736 In this case 'finished' refers to finding one revision considered 'good' | 735 In this case 'finished' refers to finding one revision considered 'good' |
| 737 immediately preceding a revision considered 'bad' where the 'bad' revision | 736 immediately preceding a revision considered 'bad' where the 'bad' revision |
| 738 does not contain a DEPS change. | 737 does not contain a DEPS change. |
| 739 """ | 738 """ |
| 740 if (revision.bad and revision.previous_revision and | 739 if (revision.bad and revision.previous_revision and |
| 741 revision.previous_revision.good): # pragma: no cover | 740 revision.previous_revision.good): |
| 742 if revision.deps_change() and self._expand_deps_revisions(revision): | 741 if revision.deps_change() and self._expand_deps_revisions(revision): |
| 743 return False | 742 return False |
| 744 self.culprit = revision | 743 self.culprit = revision |
| 745 return True | 744 return True |
| 746 if (revision.good and revision.next_revision and | 745 if (revision.good and revision.next_revision and |
| 747 revision.next_revision.bad): | 746 revision.next_revision.bad): |
| 748 if (revision.next_revision.deps_change() | 747 if (revision.next_revision.deps_change() |
| 749 and self._expand_deps_revisions(revision.next_revision)): | 748 and self._expand_deps_revisions(revision.next_revision)): |
| 750 return False | 749 return False |
| 751 self.culprit = revision.next_revision | 750 self.culprit = revision.next_revision |
| 752 return True | 751 return True |
| 753 return False | 752 # We'll never get here because revision adjacency is checked before this |
| 754 | 753 # function is called. |
| 755 def wait_for_all(self, revision_list): | 754 assert False # pragma: no cover |
| 756 """Waits for all revisions in list to finish.""" | |
| 757 for r in revision_list: | |
| 758 self.wait_for(r) | |
| 759 | |
| 760 def wait_for(self, revision, nest_check=True): | |
| 761 """Waits for the revision to finish its job.""" | |
| 762 if nest_check and not self.flags.get( | |
| 763 'do_not_nest_wait_for_revision'): # pragma: no cover | |
| 764 with self.api.m.step.nest('Waiting for ' + revision.revision_string()): | |
| 765 return self.wait_for(revision, nest_check=False) | |
| 766 while True: | |
| 767 revision.update_status() | |
| 768 if revision.in_progress: | |
| 769 self.api.m.python.inline( | |
| 770 'sleeping', | |
| 771 """ | |
| 772 import sys | |
| 773 import time | |
| 774 time.sleep(20*60) | |
| 775 sys.exit(0) | |
| 776 """) | |
| 777 else: | |
| 778 break | |
| 779 | 755 |
| 780 def _update_candidate_range(self): | 756 def _update_candidate_range(self): |
| 781 """Updates lkgr and fkbr (last known good/first known bad) revisions. | 757 """Updates lkgr and fkbr (last known good/first known bad) revisions. |
| 782 | 758 |
| 783 lkgr and fkbr are 'pointers' to the appropriate RevisionState objects in | 759 lkgr and fkbr are 'pointers' to the appropriate RevisionState objects in |
| 784 bisectors.revisions.""" | 760 bisectors.revisions.""" |
| 785 for r in self.revisions: | 761 for r in self.revisions: |
| 786 if r.tested: | 762 if r.test_run_count: |
| 787 if r.good: | 763 if r.good: |
| 788 self.lkgr = r | 764 self.lkgr = r |
| 789 elif r.bad: | 765 elif r.bad: |
| 790 self.fkbr = r | 766 self.fkbr = r |
| 791 break | 767 break |
| 792 assert self.lkgr and self.fkbr | 768 assert self.lkgr and self.fkbr |
| 793 | 769 |
| 794 def get_perf_tester_name(self): | 770 def get_perf_tester_name(self): |
| 795 """Gets the name of the tester bot (on tryserver.chromium.perf) to use. | 771 """Gets the name of the tester bot (on tryserver.chromium.perf) to use. |
| 796 | 772 |
| (...skipping 17 matching lines...) Expand all Loading... | |
| 814 | 790 |
| 815 # TODO(prasadv): Refactor this code to remove hard coded values. | 791 # TODO(prasadv): Refactor this code to remove hard coded values. |
| 816 bot_name = self.get_perf_tester_name() | 792 bot_name = self.get_perf_tester_name() |
| 817 if 'win' in bot_name: | 793 if 'win' in bot_name: |
| 818 if any(b in bot_name for b in ['x64', 'gpu']): | 794 if any(b in bot_name for b in ['x64', 'gpu']): |
| 819 return 'winx64_bisect_builder' | 795 return 'winx64_bisect_builder' |
| 820 return 'win_perf_bisect_builder' | 796 return 'win_perf_bisect_builder' |
| 821 | 797 |
| 822 # TODO(prasadv): Refactor this code to remove hard coded values and use | 798 # TODO(prasadv): Refactor this code to remove hard coded values and use |
| 823 # target_bit from the bot config. crbug.com/640287 | 799 # target_bit from the bot config. crbug.com/640287 |
| 824 if 'android' in bot_name: | 800 if 'android' in bot_name: # pragma: no cover |
| 825 if any(b in bot_name for b in ['arm64', 'nexus9', 'nexus5X']): | 801 if any(b in bot_name for b in ['arm64', 'nexus9', 'nexus5X']): |
| 826 return 'android_arm64_perf_bisect_builder' | 802 return 'android_arm64_perf_bisect_builder' |
| 827 return 'android_perf_bisect_builder' | 803 return 'android_perf_bisect_builder' |
| 828 | 804 |
| 829 if 'mac' in bot_name: | 805 if 'mac' in bot_name: |
| 830 return 'mac_perf_bisect_builder' | 806 return 'mac_perf_bisect_builder' |
| 831 | 807 |
| 832 return 'linux_perf_bisect_builder' | 808 return 'linux_perf_bisect_builder' |
| 833 | 809 |
| 834 def get_platform_gs_prefix(self): | 810 def get_platform_gs_prefix(self): |
| 835 """Returns the prefix of a GS URL where a build can be found. | 811 """Returns the prefix of a GS URL where a build can be found. |
| 836 | 812 |
| 837 This prefix includes the schema, bucket, directory and beginning | 813 This prefix includes the schema, bucket, directory and beginning |
| 838 of filename. It is joined together with the part of the filename | 814 of filename. It is joined together with the part of the filename |
| 839 that includes the revision and the file extension to form the | 815 that includes the revision and the file extension to form the |
| 840 full GS URL. | 816 full GS URL. |
| 841 """ | 817 """ |
| 842 if self.api.buildurl_gs_prefix: # pragma: no cover | 818 if self.api.buildurl_gs_prefix: # pragma: no cover |
| 843 return self.api.buildurl_gs_prefix | 819 return self.api.buildurl_gs_prefix |
| 844 | 820 |
| 845 # TODO(prasadv): Refactor this code to remove hard coded values. | 821 # TODO(prasadv): Refactor this code to remove hard coded values. |
| 846 bot_name = self.get_perf_tester_name() | 822 bot_name = self.get_perf_tester_name() |
| 847 if 'win' in bot_name: | 823 if 'win' in bot_name: |
| 848 if any(b in bot_name for b in ['x64', 'gpu']): | 824 if any(b in bot_name for b in ['x64', 'gpu']): |
| 849 return 'gs://chrome-perf/Win x64 Builder/full-build-win32_' | 825 return 'gs://chrome-perf/Win x64 Builder/full-build-win32_' |
| 850 return 'gs://chrome-perf/Win Builder/full-build-win32_' | 826 return 'gs://chrome-perf/Win Builder/full-build-win32_' |
| 851 | 827 |
| 852 # TODO(prasadv): Refactor this code to remove hard coded values and use | 828 # TODO(prasadv): Refactor this code to remove hard coded values and use |
| 853 # target_bit from the bot config. crbug.com/640287 | 829 # target_bit from the bot config. crbug.com/640287 |
| 854 if 'android' in bot_name: | 830 if 'android' in bot_name: #pragma: no cover |
| 855 if any(b in bot_name for b in ['arm64', 'nexus9', 'nexus5X']): | 831 if any(b in bot_name for b in ['arm64', 'nexus9', 'nexus5X']): |
| 856 return 'gs://chrome-perf/Android arm64 Builder/full-build-linux_' | 832 return 'gs://chrome-perf/Android arm64 Builder/full-build-linux_' |
| 857 return 'gs://chrome-perf/Android Builder/full-build-linux_' | 833 return 'gs://chrome-perf/Android Builder/full-build-linux_' |
| 858 | 834 |
| 859 if 'mac' in bot_name: | 835 if 'mac' in bot_name: |
| 860 return 'gs://chrome-perf/Mac Builder/full-build-mac_' | 836 return 'gs://chrome-perf/Mac Builder/full-build-mac_' |
| 861 | 837 |
| 862 return 'gs://chrome-perf/Linux Builder/full-build-linux_' | 838 return 'gs://chrome-perf/Linux Builder/full-build-linux_' |
| 863 | 839 |
| 864 def ensure_sync_master_branch(self): | 840 def ensure_sync_master_branch(self): |
| 865 """Make sure the local master is in sync with the fetched origin/master. | 841 """Make sure the local master is in sync with the fetched origin/master. |
| 866 | 842 |
| 867 We have seen on several occasions that the local master branch gets reset | 843 We have seen on several occasions that the local master branch gets reset |
| 868 to previous revisions and also detached head states. Running this should | 844 to previous revisions and also detached head states. Running this should |
| 869 take care of either situation. | 845 take care of either situation. |
| 870 """ | 846 """ |
| 871 # TODO(robertocn): Investigate what causes the states mentioned in the | 847 # TODO(robertocn): Investigate what causes the states mentioned in the |
| 872 # docstring in the first place. | 848 # docstring in the first place. |
| 873 self.api.m.git('update-ref', 'refs/heads/master', | 849 self.api.m.git('update-ref', 'refs/heads/master', |
| 874 'refs/remotes/origin/master') | 850 'refs/remotes/origin/master') |
| 875 self.api.m.git('checkout', 'master', cwd=self.api.m.path['checkout']) | 851 self.api.m.git('checkout', 'master', cwd=self.api.m.path['checkout']) |
| 876 | 852 |
| 877 def is_return_code_mode(self): | 853 def is_return_code_mode(self): |
| 878 """Checks whether this is a bisect on the test's exit code.""" | 854 """Checks whether this is a bisect on the test's exit code.""" |
| 879 return self.bisect_config.get('test_type') == 'return_code' | 855 return self.test_type == 'return_code' |
| 880 | 856 |
| 881 def surface_result(self, result_string): | 857 def surface_result(self, result_string): |
| 882 assert result_string in VALID_RESULT_CODES | 858 assert result_string in VALID_RESULT_CODES |
| 883 prefix = 'B4T_' # To avoid collision. Stands for bisect (abbr. `a la i18n). | 859 prefix = 'B4T_' # To avoid collision. Stands for bisect (abbr. `a la i18n). |
| 884 result_code = prefix + result_string | 860 result_code = prefix + result_string |
| 885 assert len(result_code) <= 20 | 861 assert len(result_code) <= 20 |
| 886 if result_code not in self.result_codes: | 862 if result_code not in self.result_codes: |
| 887 self.result_codes.add(result_code) | 863 self.result_codes.add(result_code) |
| 888 properties = self.api.m.step.active_result.presentation.properties | 864 properties = self.api.m.step.active_result.presentation.properties |
| 889 properties['extra_result_code'] = sorted(self.result_codes) | 865 properties['extra_result_code'] = sorted(self.result_codes) |
| 890 | 866 |
| 891 def get_result(self): | 867 def get_result(self): |
| 892 """Returns the results as a jsonable object.""" | 868 """Returns the results as a jsonable object.""" |
| 893 config = self.bisect_config | 869 config = self.bisect_config |
| 894 results_confidence = 0 | |
| 895 if self.culprit: | |
| 896 results_confidence = self.api.m.math_utils.confidence_score( | |
| 897 self.lkgr.values, self.fkbr.values) | |
| 898 | 870 |
| 899 if self.failed: | 871 if self.failed: |
| 900 status = 'failed' | 872 status = 'failed' |
| 901 elif self.bisect_over: | 873 elif self.bisect_over: |
| 902 status = 'completed' | 874 status = 'completed' |
| 903 else: | 875 else: |
| 904 status = 'started' | 876 status = 'started' |
| 905 | 877 |
| 906 aborted_reason = None | 878 aborted_reason = None |
| 907 if self.failed_initial_confidence: | 879 if self.failed_initial_confidence: |
| 908 aborted_reason = _FAILED_INITIAL_CONFIDENCE_ABORT_REASON | 880 aborted_reason = _FAILED_INITIAL_CONFIDENCE_ABORT_REASON |
| 909 elif self.failed_direction: | 881 elif self.failed_direction: |
| 910 aborted_reason = _DIRECTION_OF_IMPROVEMENT_ABORT_REASON | 882 aborted_reason = _DIRECTION_OF_IMPROVEMENT_ABORT_REASON |
| 911 return { | 883 return { |
| 912 'try_job_id': config.get('try_job_id'), | 884 'try_job_id': config.get('try_job_id'), |
| 913 'bug_id': config.get('bug_id'), | 885 'bug_id': config.get('bug_id'), |
| 914 'status': status, | 886 'status': status, |
| 915 'buildbot_log_url': self._get_build_url(), | 887 'buildbot_log_url': self._get_build_url(), |
| 916 'bisect_bot': self.get_perf_tester_name(), | 888 'bisect_bot': self.get_perf_tester_name(), |
| 917 'command': config['command'], | 889 'command': config['command'], |
| 918 'test_type': config['test_type'], | 890 'test_type': config['test_type'], |
| 919 'metric': config['metric'], | 891 'metric': config.get('metric'), |
| 920 'change': self.relative_change, | 892 'change': self.relative_change, |
| 921 'score': results_confidence, | |
| 922 'good_revision': self.good_rev.commit_hash, | 893 'good_revision': self.good_rev.commit_hash, |
| 923 'bad_revision': self.bad_rev.commit_hash, | 894 'bad_revision': self.bad_rev.commit_hash, |
| 924 'warnings': self.warnings, | 895 'warnings': self.warnings, |
| 925 'aborted_reason': aborted_reason, | 896 'aborted_reason': aborted_reason, |
| 926 'culprit_data': self._culprit_data(), | 897 'culprit_data': self._culprit_data(), |
| 927 'revision_data': self._revision_data() | 898 'revision_data': self._revision_data() |
| 928 } | 899 } |
| 929 | 900 |
| 930 def _culprit_data(self): | 901 def _culprit_data(self): |
| 931 culprit = self.culprit | 902 culprit = self.culprit |
| (...skipping 11 matching lines...) Expand all Loading... | |
| 943 'email': culprit_info['email'], | 914 'email': culprit_info['email'], |
| 944 'cl_date': culprit_info['date'], | 915 'cl_date': culprit_info['date'], |
| 945 'commit_info': culprit_info['body'], | 916 'commit_info': culprit_info['body'], |
| 946 'revisions_links': [], | 917 'revisions_links': [], |
| 947 'cl': culprit.commit_hash | 918 'cl': culprit.commit_hash |
| 948 } | 919 } |
| 949 | 920 |
| 950 def _revision_data(self): | 921 def _revision_data(self): |
| 951 revision_rows = [] | 922 revision_rows = [] |
| 952 for r in self.revisions: | 923 for r in self.revisions: |
| 953 if r.tested or r.aborted: | 924 if r.test_run_count: |
| 954 revision_rows.append({ | 925 revision_rows.append({ |
| 955 'depot_name': r.depot_name, | 926 'depot_name': r.depot_name, |
| 956 'commit_hash': r.commit_hash, | 927 'commit_hash': r.commit_hash, |
| 957 'revision_string': r.revision_string(), | 928 'revision_string': r.revision_string(), |
| 958 'mean_value': r.mean_value, | 929 'mean_value': r.mean, |
| 959 'std_dev': r.std_dev, | 930 'std_dev': r.std_dev, |
| 960 'values': r.values, | 931 'values': r.debug_values, |
| 961 'result': 'good' if r.good else 'bad' if r.bad else 'unknown', | 932 'result': 'good' if r.good else 'bad' if r.bad else 'unknown', |
| 962 }) | 933 }) |
| 963 return revision_rows | 934 return revision_rows |
| 964 | 935 |
| 965 def _get_build_url(self): | 936 def _get_build_url(self): |
| 966 properties = self.api.m.properties | 937 properties = self.api.m.properties |
| 967 bot_url = properties.get('buildbotURL', | 938 bot_url = properties.get('buildbotURL', |
| 968 'http://build.chromium.org/p/chromium/') | 939 'http://build.chromium.org/p/chromium/') |
| 969 builder_name = urllib.quote(properties.get('buildername', '')) | 940 builder_name = urllib.quote(properties.get('buildername', '')) |
| 970 builder_number = str(properties.get('buildnumber', '')) | 941 builder_number = str(properties.get('buildnumber', '')) |
| 971 return '%sbuilders/%s/builds/%s' % (bot_url, builder_name, builder_number) | 942 return '%sbuilders/%s/builds/%s' % (bot_url, builder_name, builder_number) |
| OLD | NEW |