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