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 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', None) |
|
qyearsley
2015/03/10 23:20:48
The default value None is redundant if bisect_conf
RobertoCN
2015/03/13 20:55:59
Done.
| |
| 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): Hash of the diff that patches DEPS. |
|
qyearsley
2015/03/10 23:20:48
Specifically, a SHA1 hash of the diff that patches
RobertoCN
2015/03/13 20:55:59
Done.
| |
| 82 | |
| 83 Returns: | |
| 84 A string containing a git diff. | |
| 73 """ | 85 """ |
| 74 if revision_to_expand is not None: | 86 return _DEPS_SHA_PATCH % {'deps_sha': deps_sha} |
| 75 # TODO: Implement this path (insert revisions when deps change) | 87 |
| 76 raise NotImplementedError() # pragma: no cover | 88 def _git_intern_file(self, file_contents, cwd, commit_hash): |
| 89 """Writes a file to the git database and produces its git hash. | |
| 90 | |
| 91 Args: | |
| 92 file_contents (str): The contents of the file to be hashed and interned. | |
| 93 cwd (recipe_config_types.Path): Path to the checkout whose repository the | |
| 94 file is to be written to. | |
| 95 commit_hash (str): An identifier for the step. | |
| 96 Returns: | |
|
qyearsley
2015/03/10 23:20:48
Blank line between the Args section and Returns se
RobertoCN
2015/03/13 20:55:59
Done.
| |
| 97 A string containing the hash of the interned object. | |
| 98 """ | |
| 99 cmd = 'hash-object -t blob -w --stdin'.split(' ') | |
| 100 stdin = self.api.m.raw_io.input(file_contents) | |
| 101 stdout = self.api.m.raw_io.output() | |
| 102 step_name = 'Hashing modified DEPS file with revision ' + commit_hash | |
| 103 step_result = self.api.m.git(*cmd, cwd=cwd, stdin=stdin, stdout=stdout, | |
| 104 name=step_name) | |
| 105 hash_string = step_result.stdout.splitlines()[0] | |
| 106 try: | |
| 107 if hash_string: | |
| 108 int(hash_string, 16) | |
| 109 return hash_string | |
| 110 except ValueError: | |
| 111 pass | |
| 112 | |
| 113 raise self.api.m.step.StepFailure('Git did not output a valid hash for the ' | |
| 114 'interned file.') | |
| 115 | |
| 116 def _gen_diff_patch(self, git_object_a, git_object_b, src_alias, dst_alias, | |
| 117 cwd, deps_rev): | |
| 118 """Produces a git diff patch. | |
| 119 | |
| 120 Args: | |
| 121 git_object_a, git_object_b (str): Tree-ish git object identifiers. | |
| 122 src_alias, dst_alias (str): labels to replace the tree-ish identifiers on | |
| 123 the resulting diff patch. | |
|
qyearsley
2015/03/10 23:20:48
Optional: You could also put each of these on thei
RobertoCN
2015/03/13 20:55:58
Done.
| |
| 124 cwd (recipe_config_types.Path): Path to the checkout whose repo contains | |
| 125 the objects to be compared. | |
| 126 deps_rev (str): Deps revision to identify the patch generating step. | |
| 127 | |
| 128 Returns: | |
| 129 A string containing the diff patch as produced by the 'git diff' command. | |
| 130 """ | |
| 131 cmd = 'diff %s %s --src-prefix=IAMSRC: --dst-prefix=IAMDST:' | |
|
qyearsley
2015/03/10 23:20:48
Possible explanation comment that could be added:
RobertoCN
2015/03/13 20:55:58
Done.
| |
| 132 cmd %= (git_object_a, git_object_b) | |
| 133 cmd = cmd.split(' ') | |
| 134 stdout = self.api.m.raw_io.output() | |
| 135 step_name = 'Generating patch for %s to %s' % (git_object_a, deps_rev) | |
| 136 step_result = self.api.m.git(*cmd, cwd=cwd, stdout=stdout, name=step_name) | |
| 137 patch_text = step_result.stdout | |
| 138 src_string = 'IAMSRC:' + git_object_a | |
| 139 dst_string = 'IAMDST:' + git_object_b | |
| 140 patch_text = patch_text.replace(src_string, src_alias) | |
| 141 patch_text = patch_text.replace(dst_string, dst_alias) | |
| 142 return patch_text | |
| 143 | |
| 144 def make_deps_patch(self, base_revision, base_file_contents, | |
| 145 depot, new_commit_hash): | |
| 146 """Make a diff patch that updates a specific dependency revision. | |
| 147 | |
| 148 Args: | |
| 149 base_revision (RevisionState): The revision for which the DEPS file is to | |
| 150 be patched. | |
| 151 base_file_contents (str): The contents of the original DEPS file. | |
| 152 depot (str): The dependency to modify. | |
| 153 new_commit_hash (str): The revision to put in place of the old one. | |
| 154 | |
| 155 Returns: | |
| 156 A pair containing the git diff patch that updates DEPS, and the | |
| 157 full text of the modified DEPS file, both as strings. | |
| 158 """ | |
| 159 original_contents = str(base_file_contents) | |
| 160 patched_contents = str(original_contents) | |
| 161 | |
| 162 # Modify DEPS | |
| 163 deps_var = depot['deps_var'] | |
| 164 deps_item_regexp = re.compile( | |
| 165 r'(?<=["\']%s["\']: ["\'])([a-fA-F0-9]+)(?=["\'])' % deps_var, | |
| 166 re.MULTILINE) | |
| 167 if not re.search(deps_item_regexp, original_contents): | |
| 168 raise self.api.m.step.StepFailure('DEPS file does not contain entry for ' | |
| 169 + deps_var) | |
| 170 patched_contents = re.sub(deps_item_regexp, new_commit_hash, | |
| 171 original_contents) | |
| 172 | |
| 173 interned_deps_hash = self._git_intern_file(patched_contents, | |
| 174 self.api.m.path['checkout'], | |
| 175 new_commit_hash) | |
| 176 patch_text = self._gen_diff_patch(base_revision.commit_hash + ':DEPS', | |
| 177 interned_deps_hash, 'DEPS', 'DEPS', | |
| 178 cwd=self.api.m.path['checkout'], | |
| 179 deps_rev=new_commit_hash) | |
| 180 return patch_text, patched_contents | |
| 181 | |
| 182 def _get_rev_range_for_depot(self, depot_name, min_rev, max_rev, | |
| 183 base_revision): | |
| 184 results = [] | |
| 185 depot = depot_config.DEPOT_DEPS_NAME[depot_name] | |
| 186 depot_path = self.api.m.path['slave_build'].join(depot['src']) | |
| 187 step_name = ('Expanding revision range for revision %s on depot %s' | |
| 188 % (max_rev, depot_name)) | |
| 189 step_result = self.api.m.git('log', '--format=%H', min_rev + '...' + | |
| 190 max_rev, stdout=self.api.m.raw_io.output(), | |
| 191 cwd=depot_path, name=step_name) | |
| 192 # We skip the first revision in the list as it is max_rev | |
| 193 new_revisions = step_result.stdout.splitlines()[1:] | |
| 194 for revision in new_revisions: | |
| 195 results.append(self.revision_class(None, self, | |
| 196 base_revision=base_revision, | |
| 197 deps_revision=revision, | |
| 198 dependency_depot_name=depot_name, | |
| 199 depot=depot)) | |
| 200 results.reverse() | |
| 201 return results | |
| 202 | |
| 203 def _expand_revision_range(self): | |
| 204 """Populates the revisions attribute. | |
| 205 | |
| 206 After running this method, self.revisions should contain all the chromium | |
| 207 revisions between the good and bad revisions. | |
| 208 """ | |
| 77 rev_list = self._commit_pos_range( | 209 rev_list = self._commit_pos_range( |
| 78 self.good_rev.commit_pos, self.bad_rev.commit_pos) | 210 self.good_rev.commit_pos, self.bad_rev.commit_pos) |
| 79 intermediate_revs = [self.revision_class(str(x), self) for x in rev_list] | 211 intermediate_revs = [self.revision_class(str(x), self) for x in rev_list] |
| 80 self.revisions = [self.good_rev] + intermediate_revs + [self.bad_rev] | 212 self.revisions = [self.good_rev] + intermediate_revs + [self.bad_rev] |
| 213 self._update_revision_list_indexes() | |
| 214 | |
| 215 def _expand_deps_revisions(self, revision_to_expand): | |
| 216 """Populates the revisions attribute with additional deps revisions. | |
| 217 | |
| 218 Inserts the revisions from the external repos in the appropriate place. | |
| 219 | |
| 220 Args: | |
| 221 revision_to_expand: A revision where there is a deps change. | |
| 222 | |
| 223 Returns: | |
| 224 A boolean indicating whether any revisions were inserted. | |
| 225 """ | |
| 226 # TODO(robertocn): Review variable names in this function. They are | |
| 227 # potentially confusing. | |
| 228 assert revision_to_expand is not None | |
| 229 try: | |
| 230 min_revision = revision_to_expand.previous_revision | |
| 231 max_revision = revision_to_expand | |
| 232 min_revision.read_deps() # Parses DEPS file and sets the .deps property. | |
| 233 max_revision.read_deps() # Ditto. | |
| 234 for depot_name in depot_config.DEPOT_DEPS_NAME.keys(): | |
| 235 if depot_name in min_revision.deps and depot_name in max_revision.deps: | |
| 236 dep_revision_min = min_revision.deps[depot_name] | |
| 237 dep_revision_max = max_revision.deps[depot_name] | |
| 238 if (dep_revision_min and dep_revision_max and | |
| 239 dep_revision_min != dep_revision_max): | |
| 240 rev_list = self._get_rev_range_for_depot(depot_name, | |
| 241 dep_revision_min, | |
| 242 dep_revision_max, | |
| 243 min_revision) | |
| 244 new_revisions = self.revisions[:max_revision.list_index] | |
| 245 new_revisions += rev_list | |
| 246 new_revisions += self.revisions[max_revision.list_index:] | |
| 247 self.revisions = new_revisions | |
| 248 self._update_revision_list_indexes() | |
| 249 return True | |
| 250 except RuntimeError: | |
| 251 warning_text = ('Could not expand dependency revisions for ' + | |
| 252 revision_to_expand.revision_string) | |
| 253 if warning_text not in self.warnings: | |
| 254 self.warnings.append(warning_text) | |
| 255 return False | |
| 256 | |
| 257 | |
| 258 def _update_revision_list_indexes(self): | |
| 259 """Sets list_index, next and previous properties for each revision.""" | |
| 81 for i, rev in enumerate(self.revisions): | 260 for i, rev in enumerate(self.revisions): |
| 82 rev.list_index = i | 261 rev.list_index = i |
| 83 for i in xrange(len(self.revisions)): | 262 for i in xrange(len(self.revisions)): |
| 84 if i: | 263 if i: |
| 85 self.revisions[i].previous_revision = self.revisions[i - 1] | 264 self.revisions[i].previous_revision = self.revisions[i - 1] |
| 86 if i < len(self.revisions) - 1: | 265 if i < len(self.revisions) - 1: |
| 87 self.revisions[i].next_revision = self.revisions[i + 1] | 266 self.revisions[i].next_revision = self.revisions[i + 1] |
| 88 | 267 |
| 89 def check_improvement_direction(self): # pragma: no cover | 268 def check_improvement_direction(self): # pragma: no cover |
| 90 """Verifies that the change from 'good' to 'bad' is in the right direction. | 269 """Verifies that the change from 'good' to 'bad' is in the right direction. |
| 91 | 270 |
| 92 The change between the test results obtained for the given 'good' and 'bad' | 271 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` | 272 '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 | 273 `improvement_direction` attribute is positive if a larger number is |
| 95 smaller number is considered better. | 274 considered better, and negative if a smaller number is considered better. |
| 96 """ | 275 """ |
| 97 direction = self.improvement_direction | 276 direction = self.improvement_direction |
| 98 if direction is None: | 277 if direction is None: |
| 99 return True | 278 return True |
| 100 good = self.good_rev.mean_value | 279 good = self.good_rev.mean_value |
| 101 bad = self.bad_rev.mean_value | 280 bad = self.bad_rev.mean_value |
| 102 if ((bad > good and direction > 0) or | 281 if ((bad > good and direction > 0) or |
| 103 (bad < good and direction < 0)): | 282 (bad < good and direction < 0)): |
| 104 self._set_failed_direction_results() | 283 self._set_failed_direction_results() |
| 105 return False | 284 return False |
| (...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 179 def check_bisect_finished(self, revision): | 358 def check_bisect_finished(self, revision): |
| 180 """Checks if this revision completes the bisection process. | 359 """Checks if this revision completes the bisection process. |
| 181 | 360 |
| 182 In this case 'finished' refers to finding one revision considered 'good' | 361 In this case 'finished' refers to finding one revision considered 'good' |
| 183 immediately preceding a revision considered 'bad' where the 'bad' revision | 362 immediately preceding a revision considered 'bad' where the 'bad' revision |
| 184 does not contain a deps change. | 363 does not contain a deps change. |
| 185 """ | 364 """ |
| 186 if (revision.bad and revision.previous_revision and | 365 if (revision.bad and revision.previous_revision and |
| 187 revision.previous_revision.good): # pragma: no cover | 366 revision.previous_revision.good): # pragma: no cover |
| 188 if revision.deps_change(): | 367 if revision.deps_change(): |
| 189 self._expand_revision_range(revision) | 368 more_revisions = self._expand_deps_revisions(revision) |
| 190 return False | 369 return not more_revisions |
| 191 self.culprit = revision | 370 self.culprit = revision |
| 192 return True | 371 return True |
| 193 if (revision.good and revision.next_revision and | 372 if (revision.good and revision.next_revision and |
| 194 revision.next_revision.bad): | 373 revision.next_revision.bad): |
| 195 if revision.next_revision.deps_change(): # pragma: no cover | 374 if revision.next_revision.deps_change(): |
| 196 self._expand_revision_range(revision.next_revision) | 375 more_revisions = self._expand_deps_revisions(revision.next_revision) |
| 197 return False | 376 return not more_revisions |
| 198 self.culprit = revision.next_revision | 377 self.culprit = revision.next_revision |
| 199 return True | 378 return True |
| 200 return False | 379 return False |
| 201 | 380 |
| 202 def wait_for_all(self, revision_list): | 381 def wait_for_all(self, revision_list): |
| 203 """Waits for all revisions in list to finish.""" | 382 """Waits for all revisions in list to finish.""" |
| 204 while any([r.in_progress for r in revision_list]): | 383 while any([r.in_progress for r in revision_list]): |
| 205 self.wait_for_any(revision_list) | 384 self.wait_for_any(revision_list) |
| 206 for revision in revision_list: | 385 for revision in revision_list: |
| 207 revision.update_status() | 386 revision.update_status() |
| (...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 271 return 'linux_perf_tester' | 450 return 'linux_perf_tester' |
| 272 | 451 |
| 273 def get_builder_bot_for_this_platform(self): | 452 def get_builder_bot_for_this_platform(self): |
| 274 # TODO: Actually look at the current platform. | 453 # TODO: Actually look at the current platform. |
| 275 return 'linux_perf_bisect_builder' | 454 return 'linux_perf_bisect_builder' |
| 276 | 455 |
| 277 def get_platform_gs_prefix(self): | 456 def get_platform_gs_prefix(self): |
| 278 # TODO: Actually check the current platform | 457 # TODO: Actually check the current platform |
| 279 return 'gs://chrome-perf/Linux Builder/full-build-linux_' | 458 return 'gs://chrome-perf/Linux Builder/full-build-linux_' |
| 280 | 459 |
| OLD | NEW |