| 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 re |
| 6 |
| 5 from . import bisect_results | 7 from . import bisect_results |
| 8 from . import depot_config |
| 9 |
| 10 _DEPS_SHA_PATCH = """ |
| 11 diff --git DEPS.sha DEPS.sha |
| 12 new file mode 100644 |
| 13 --- /dev/null |
| 14 +++ DEPS.sha |
| 15 @@ -0,0 +1 @@ |
| 16 +%(deps_sha)s |
| 17 """ |
| 18 |
| 6 | 19 |
| 7 class Bisector(object): | 20 class Bisector(object): |
| 8 """This class abstracts an ongoing bisect (or n-sect) job.""" | 21 """This class abstracts an ongoing bisect (or n-sect) job.""" |
| 9 | 22 |
| 10 def __init__(self, api, bisect_config, revision_class): | 23 def __init__(self, api, bisect_config, revision_class): |
| 11 """Initializes the state of a new bisect job from a dictionary. | 24 """Initializes the state of a new bisect job from a dictionary. |
| 12 | 25 |
| 13 Note that the initial good_rev and bad_rev MUST resolve to a commit position | 26 Note that the initial good_rev and bad_rev MUST resolve to a commit position |
| 14 in the chromium repo. | 27 in the chromium repo. |
| 15 """ | 28 """ |
| 16 super(Bisector, self).__init__() | 29 super(Bisector, self).__init__() |
| 17 self._api = api | 30 self._api = api |
| 18 self.bisect_config = bisect_config | 31 self.bisect_config = bisect_config |
| 19 self.revision_class = revision_class | 32 self.revision_class = revision_class |
| 20 | 33 |
| 21 # Test-only properties. | 34 # Test-only properties. |
| 22 # TODO: Replace these with proper mod_test_data | 35 # TODO: Replace these with proper mod_test_data |
| 23 self.dummy_regression_confidence = bisect_config.get( | 36 self.dummy_regression_confidence = bisect_config.get( |
| 24 'dummy_regression_confidence', None) | 37 'dummy_regression_confidence') |
| 25 self.dummy_builds = bisect_config.get('dummy_builds', False) | 38 self.dummy_builds = bisect_config.get('dummy_builds', False) |
| 26 | 39 |
| 27 # Loading configuration items | 40 # Loading configuration items |
| 28 self.test_type = bisect_config.get('test_type', 'perf') | 41 self.test_type = bisect_config.get('test_type', 'perf') |
| 29 self.improvement_direction = int(bisect_config.get( | 42 self.improvement_direction = int(bisect_config.get( |
| 30 'improvement_direction', 0)) or None | 43 'improvement_direction', 0)) or None |
| 31 | 44 |
| 32 self.required_regression_confidence = bisect_config.get( | 45 self.required_regression_confidence = bisect_config.get( |
| 33 'required_regression_confidence', 95) | 46 'required_regression_confidence', 95) |
| 34 | 47 |
| (...skipping 19 matching lines...) Expand all Loading... |
| 54 @property | 67 @property |
| 55 def api(self): | 68 def api(self): |
| 56 return self._api | 69 return self._api |
| 57 | 70 |
| 58 @staticmethod | 71 @staticmethod |
| 59 def _commit_pos_range(a, b): | 72 def _commit_pos_range(a, b): |
| 60 """Given 2 commit positions, returns a list with the ones between.""" | 73 """Given 2 commit positions, returns a list with the ones between.""" |
| 61 a, b = sorted(map(int, [a, b])) | 74 a, b = sorted(map(int, [a, b])) |
| 62 return xrange(a + 1, b) | 75 return xrange(a + 1, b) |
| 63 | 76 |
| 64 def _expand_revision_range(self, revision_to_expand=None): | 77 def make_deps_sha_file(self, deps_sha): |
| 65 """This method populates the revisions attribute. | 78 """Make a diff patch that creates DEPS.sha. |
| 66 | |
| 67 After running method self.revisions should contain all the revisions | |
| 68 between the good and bad revisions. If given a `revision_to_expand`, it'll | |
| 69 insert the revisions from the external repos in the appropriate place. | |
| 70 | 79 |
| 71 Args: | 80 Args: |
| 72 revision_to_expand: A revision where there is a deps change. | 81 deps_sha (str): The hex digest of a SHA1 hash of the diff that patches |
| 82 DEPS. |
| 83 |
| 84 Returns: |
| 85 A string containing a git diff. |
| 73 """ | 86 """ |
| 74 if revision_to_expand is not None: | 87 return _DEPS_SHA_PATCH % {'deps_sha': deps_sha} |
| 75 # TODO: Implement this path (insert revisions when deps change) | 88 |
| 76 raise NotImplementedError() # pragma: no cover | 89 def _git_intern_file(self, file_contents, cwd, commit_hash): |
| 90 """Writes a file to the git database and produces its git hash. |
| 91 |
| 92 Args: |
| 93 file_contents (str): The contents of the file to be hashed and interned. |
| 94 cwd (recipe_config_types.Path): Path to the checkout whose repository the |
| 95 file is to be written to. |
| 96 commit_hash (str): An identifier for the step. |
| 97 |
| 98 Returns: |
| 99 A string containing the hash of the interned object. |
| 100 """ |
| 101 cmd = 'hash-object -t blob -w --stdin'.split(' ') |
| 102 stdin = self.api.m.raw_io.input(file_contents) |
| 103 stdout = self.api.m.raw_io.output() |
| 104 step_name = 'Hashing modified DEPS file with revision ' + commit_hash |
| 105 step_result = self.api.m.git(*cmd, cwd=cwd, stdin=stdin, stdout=stdout, |
| 106 name=step_name) |
| 107 hash_string = step_result.stdout.splitlines()[0] |
| 108 try: |
| 109 if hash_string: |
| 110 int(hash_string, 16) |
| 111 return hash_string |
| 112 except ValueError: |
| 113 pass |
| 114 |
| 115 raise self.api.m.step.StepFailure('Git did not output a valid hash for the ' |
| 116 'interned file.') |
| 117 |
| 118 def _gen_diff_patch(self, git_object_a, git_object_b, src_alias, dst_alias, |
| 119 cwd, deps_rev): |
| 120 """Produces a git diff patch. |
| 121 |
| 122 Args: |
| 123 git_object_a (str): Tree-ish git object identifier. |
| 124 git_object_b (str): Another tree-ish git object identifier. |
| 125 src_alias (str): Label to replace the tree-ish identifier on |
| 126 the resulting diff patch. (git_object_a) |
| 127 dst_alias (str): Same as above for (git_object_b) |
| 128 cwd (recipe_config_types.Path): Path to the checkout whose repo contains |
| 129 the objects to be compared. |
| 130 deps_rev (str): Deps revision to identify the patch generating step. |
| 131 |
| 132 Returns: |
| 133 A string containing the diff patch as produced by the 'git diff' command. |
| 134 """ |
| 135 # The prefixes used in the command below are used to find and replace the |
| 136 # tree-ish git object id's on the diff output more easily. |
| 137 cmd = 'diff %s %s --src-prefix=IAMSRC: --dst-prefix=IAMDST:' |
| 138 cmd %= (git_object_a, git_object_b) |
| 139 cmd = cmd.split(' ') |
| 140 stdout = self.api.m.raw_io.output() |
| 141 step_name = 'Generating patch for %s to %s' % (git_object_a, deps_rev) |
| 142 step_result = self.api.m.git(*cmd, cwd=cwd, stdout=stdout, name=step_name) |
| 143 patch_text = step_result.stdout |
| 144 src_string = 'IAMSRC:' + git_object_a |
| 145 dst_string = 'IAMDST:' + git_object_b |
| 146 patch_text = patch_text.replace(src_string, src_alias) |
| 147 patch_text = patch_text.replace(dst_string, dst_alias) |
| 148 return patch_text |
| 149 |
| 150 def make_deps_patch(self, base_revision, base_file_contents, |
| 151 depot, new_commit_hash): |
| 152 """Make a diff patch that updates a specific dependency revision. |
| 153 |
| 154 Args: |
| 155 base_revision (RevisionState): The revision for which the DEPS file is to |
| 156 be patched. |
| 157 base_file_contents (str): The contents of the original DEPS file. |
| 158 depot (str): The dependency to modify. |
| 159 new_commit_hash (str): The revision to put in place of the old one. |
| 160 |
| 161 Returns: |
| 162 A pair containing the git diff patch that updates DEPS, and the |
| 163 full text of the modified DEPS file, both as strings. |
| 164 """ |
| 165 original_contents = str(base_file_contents) |
| 166 patched_contents = str(original_contents) |
| 167 |
| 168 # Modify DEPS |
| 169 deps_var = depot['deps_var'] |
| 170 deps_item_regexp = re.compile( |
| 171 r'(?<=["\']%s["\']: ["\'])([a-fA-F0-9]+)(?=["\'])' % deps_var, |
| 172 re.MULTILINE) |
| 173 if not re.search(deps_item_regexp, original_contents): |
| 174 raise self.api.m.step.StepFailure('DEPS file does not contain entry for ' |
| 175 + deps_var) |
| 176 patched_contents = re.sub(deps_item_regexp, new_commit_hash, |
| 177 original_contents) |
| 178 |
| 179 interned_deps_hash = self._git_intern_file(patched_contents, |
| 180 self.api.m.path['checkout'], |
| 181 new_commit_hash) |
| 182 patch_text = self._gen_diff_patch(base_revision.commit_hash + ':DEPS', |
| 183 interned_deps_hash, 'DEPS', 'DEPS', |
| 184 cwd=self.api.m.path['checkout'], |
| 185 deps_rev=new_commit_hash) |
| 186 return patch_text, patched_contents |
| 187 |
| 188 def _get_rev_range_for_depot(self, depot_name, min_rev, max_rev, |
| 189 base_revision): |
| 190 results = [] |
| 191 depot = depot_config.DEPOT_DEPS_NAME[depot_name] |
| 192 depot_path = self.api.m.path['slave_build'].join(depot['src']) |
| 193 step_name = ('Expanding revision range for revision %s on depot %s' |
| 194 % (max_rev, depot_name)) |
| 195 step_result = self.api.m.git('log', '--format=%H', min_rev + '...' + |
| 196 max_rev, stdout=self.api.m.raw_io.output(), |
| 197 cwd=depot_path, name=step_name) |
| 198 # We skip the first revision in the list as it is max_rev |
| 199 new_revisions = step_result.stdout.splitlines()[1:] |
| 200 for revision in new_revisions: |
| 201 results.append(self.revision_class(None, self, |
| 202 base_revision=base_revision, |
| 203 deps_revision=revision, |
| 204 dependency_depot_name=depot_name, |
| 205 depot=depot)) |
| 206 results.reverse() |
| 207 return results |
| 208 |
| 209 def _expand_revision_range(self): |
| 210 """Populates the revisions attribute. |
| 211 |
| 212 After running this method, self.revisions should contain all the chromium |
| 213 revisions between the good and bad revisions. |
| 214 """ |
| 77 rev_list = self._commit_pos_range( | 215 rev_list = self._commit_pos_range( |
| 78 self.good_rev.commit_pos, self.bad_rev.commit_pos) | 216 self.good_rev.commit_pos, self.bad_rev.commit_pos) |
| 79 intermediate_revs = [self.revision_class(str(x), self) for x in rev_list] | 217 intermediate_revs = [self.revision_class(str(x), self) for x in rev_list] |
| 80 self.revisions = [self.good_rev] + intermediate_revs + [self.bad_rev] | 218 self.revisions = [self.good_rev] + intermediate_revs + [self.bad_rev] |
| 219 self._update_revision_list_indexes() |
| 220 |
| 221 def _expand_deps_revisions(self, revision_to_expand): |
| 222 """Populates the revisions attribute with additional deps revisions. |
| 223 |
| 224 Inserts the revisions from the external repos in the appropriate place. |
| 225 |
| 226 Args: |
| 227 revision_to_expand: A revision where there is a deps change. |
| 228 |
| 229 Returns: |
| 230 A boolean indicating whether any revisions were inserted. |
| 231 """ |
| 232 # TODO(robertocn): Review variable names in this function. They are |
| 233 # potentially confusing. |
| 234 assert revision_to_expand is not None |
| 235 try: |
| 236 min_revision = revision_to_expand.previous_revision |
| 237 max_revision = revision_to_expand |
| 238 min_revision.read_deps() # Parses DEPS file and sets the .deps property. |
| 239 max_revision.read_deps() # Ditto. |
| 240 for depot_name in depot_config.DEPOT_DEPS_NAME.keys(): |
| 241 if depot_name in min_revision.deps and depot_name in max_revision.deps: |
| 242 dep_revision_min = min_revision.deps[depot_name] |
| 243 dep_revision_max = max_revision.deps[depot_name] |
| 244 if (dep_revision_min and dep_revision_max and |
| 245 dep_revision_min != dep_revision_max): |
| 246 rev_list = self._get_rev_range_for_depot(depot_name, |
| 247 dep_revision_min, |
| 248 dep_revision_max, |
| 249 min_revision) |
| 250 new_revisions = self.revisions[:max_revision.list_index] |
| 251 new_revisions += rev_list |
| 252 new_revisions += self.revisions[max_revision.list_index:] |
| 253 self.revisions = new_revisions |
| 254 self._update_revision_list_indexes() |
| 255 return True |
| 256 except RuntimeError: |
| 257 warning_text = ('Could not expand dependency revisions for ' + |
| 258 revision_to_expand.revision_string) |
| 259 if warning_text not in self.warnings: |
| 260 self.warnings.append(warning_text) |
| 261 return False |
| 262 |
| 263 |
| 264 def _update_revision_list_indexes(self): |
| 265 """Sets list_index, next and previous properties for each revision.""" |
| 81 for i, rev in enumerate(self.revisions): | 266 for i, rev in enumerate(self.revisions): |
| 82 rev.list_index = i | 267 rev.list_index = i |
| 83 for i in xrange(len(self.revisions)): | 268 for i in xrange(len(self.revisions)): |
| 84 if i: | 269 if i: |
| 85 self.revisions[i].previous_revision = self.revisions[i - 1] | 270 self.revisions[i].previous_revision = self.revisions[i - 1] |
| 86 if i < len(self.revisions) - 1: | 271 if i < len(self.revisions) - 1: |
| 87 self.revisions[i].next_revision = self.revisions[i + 1] | 272 self.revisions[i].next_revision = self.revisions[i + 1] |
| 88 | 273 |
| 89 def check_improvement_direction(self): # pragma: no cover | 274 def check_improvement_direction(self): # pragma: no cover |
| 90 """Verifies that the change from 'good' to 'bad' is in the right direction. | 275 """Verifies that the change from 'good' to 'bad' is in the right direction. |
| 91 | 276 |
| 92 The change between the test results obtained for the given 'good' and 'bad' | 277 The change between the test results obtained for the given 'good' and |
| 93 revisions is expected to be considered a regression. The `improvement_direct
ion` | 278 'bad' revisions is expected to be considered a regression. The |
| 94 attribute is positive if a larger number is considered better, and negative
if a | 279 `improvement_direction` attribute is positive if a larger number is |
| 95 smaller number is considered better. | 280 considered better, and negative if a smaller number is considered better. |
| 96 """ | 281 """ |
| 97 direction = self.improvement_direction | 282 direction = self.improvement_direction |
| 98 if direction is None: | 283 if direction is None: |
| 99 return True | 284 return True |
| 100 good = self.good_rev.mean_value | 285 good = self.good_rev.mean_value |
| 101 bad = self.bad_rev.mean_value | 286 bad = self.bad_rev.mean_value |
| 102 if ((bad > good and direction > 0) or | 287 if ((bad > good and direction > 0) or |
| 103 (bad < good and direction < 0)): | 288 (bad < good and direction < 0)): |
| 104 self._set_failed_direction_results() | 289 self._set_failed_direction_results() |
| 105 return False | 290 return False |
| (...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 179 def check_bisect_finished(self, revision): | 364 def check_bisect_finished(self, revision): |
| 180 """Checks if this revision completes the bisection process. | 365 """Checks if this revision completes the bisection process. |
| 181 | 366 |
| 182 In this case 'finished' refers to finding one revision considered 'good' | 367 In this case 'finished' refers to finding one revision considered 'good' |
| 183 immediately preceding a revision considered 'bad' where the 'bad' revision | 368 immediately preceding a revision considered 'bad' where the 'bad' revision |
| 184 does not contain a deps change. | 369 does not contain a deps change. |
| 185 """ | 370 """ |
| 186 if (revision.bad and revision.previous_revision and | 371 if (revision.bad and revision.previous_revision and |
| 187 revision.previous_revision.good): # pragma: no cover | 372 revision.previous_revision.good): # pragma: no cover |
| 188 if revision.deps_change(): | 373 if revision.deps_change(): |
| 189 self._expand_revision_range(revision) | 374 more_revisions = self._expand_deps_revisions(revision) |
| 190 return False | 375 return not more_revisions |
| 191 self.culprit = revision | 376 self.culprit = revision |
| 192 return True | 377 return True |
| 193 if (revision.good and revision.next_revision and | 378 if (revision.good and revision.next_revision and |
| 194 revision.next_revision.bad): | 379 revision.next_revision.bad): |
| 195 if revision.next_revision.deps_change(): # pragma: no cover | 380 if revision.next_revision.deps_change(): |
| 196 self._expand_revision_range(revision.next_revision) | 381 more_revisions = self._expand_deps_revisions(revision.next_revision) |
| 197 return False | 382 return not more_revisions |
| 198 self.culprit = revision.next_revision | 383 self.culprit = revision.next_revision |
| 199 return True | 384 return True |
| 200 return False | 385 return False |
| 201 | 386 |
| 202 def wait_for_all(self, revision_list): | 387 def wait_for_all(self, revision_list): |
| 203 """Waits for all revisions in list to finish.""" | 388 """Waits for all revisions in list to finish.""" |
| 204 while any([r.in_progress for r in revision_list]): | 389 while any([r.in_progress for r in revision_list]): |
| 205 self.wait_for_any(revision_list) | 390 self.wait_for_any(revision_list) |
| 206 for revision in revision_list: | 391 for revision in revision_list: |
| 207 revision.update_status() | 392 revision.update_status() |
| (...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 271 return 'linux_perf_tester' | 456 return 'linux_perf_tester' |
| 272 | 457 |
| 273 def get_builder_bot_for_this_platform(self): | 458 def get_builder_bot_for_this_platform(self): |
| 274 # TODO: Actually look at the current platform. | 459 # TODO: Actually look at the current platform. |
| 275 return 'linux_perf_bisect_builder' | 460 return 'linux_perf_bisect_builder' |
| 276 | 461 |
| 277 def get_platform_gs_prefix(self): | 462 def get_platform_gs_prefix(self): |
| 278 # TODO: Actually check the current platform | 463 # TODO: Actually check the current platform |
| 279 return 'gs://chrome-perf/Linux Builder/full-build-linux_' | 464 return 'gs://chrome-perf/Linux Builder/full-build-linux_' |
| 280 | 465 |
| OLD | NEW |