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 = """diff --git DEPS.sha DEPS.sha | |
|
qyearsley
2015/02/22 20:37:54
Would it make sense to mark this as private with a
RobertoCN
2015/02/24 20:01:13
Done.
| |
| 11 new file mode 100644 | |
| 12 --- /dev/null | |
| 13 +++ DEPS.sha | |
| 14 @@ -0,0 +1 @@ | |
| 15 +%(deps_sha)s | |
| 16 """ | |
| 17 | |
| 6 | 18 |
| 7 class Bisector(object): | 19 class Bisector(object): |
| 8 """This class abstracts an ongoing bisect (or n-sect) job.""" | 20 """This class abstracts an ongoing bisect (or n-sect) job.""" |
| 9 | 21 |
| 10 def __init__(self, api, bisect_config, revision_class): | 22 def __init__(self, api, bisect_config, revision_class): |
| 11 """Initializes the state of a new bisect job from a dictionary. | 23 """Initializes the state of a new bisect job from a dictionary. |
| 12 | 24 |
| 13 Note that the initial good_rev and bad_rev MUST resolve to a commit position | 25 Note that the initial good_rev and bad_rev MUST resolve to a commit position |
| 14 in the chromium repo. | 26 in the chromium repo. |
| 15 """ | 27 """ |
| 16 super(Bisector, self).__init__() | 28 super(Bisector, self).__init__() |
| 17 self._api = api | 29 self._api = api |
| 18 self.bisect_config = bisect_config | 30 self.bisect_config = bisect_config |
| 19 self.revision_class = revision_class | 31 self.revision_class = revision_class |
| 20 | 32 |
| 21 # Test-only properties. | 33 # Test-only properties. |
| 22 # TODO: Replace these with proper mod_test_data | 34 # TODO: Replace these with proper mod_test_data |
| 23 self.dummy_regression_confidence = bisect_config.get( | 35 self.dummy_regression_confidence = bisect_config.get( |
| 24 'dummy_regression_confidence', None) | 36 'dummy_regression_confidence', None) |
| 25 self.dummy_builds = bisect_config.get('dummy_builds', False) | 37 self.dummy_builds = bisect_config.get('dummy_builds', False) |
| 26 | 38 |
| 27 # Loading configuration items | 39 # Loading configuration items |
| 28 self.test_type = bisect_config.get('test_type', 'perf') | 40 self.test_type = bisect_config.get('test_type', 'perf') |
| 29 self.improvement_direction = int(bisect_config.get( | 41 self.improvement_direction = int(bisect_config.get( |
| 30 'improvement_direction', 0)) or None | 42 'improvement_direction', 0)) or None |
| 31 | 43 |
| (...skipping 22 matching lines...) Expand all Loading... | |
| 54 @property | 66 @property |
| 55 def api(self): | 67 def api(self): |
| 56 return self._api | 68 return self._api |
| 57 | 69 |
| 58 @staticmethod | 70 @staticmethod |
| 59 def _commit_pos_range(a, b): | 71 def _commit_pos_range(a, b): |
| 60 """Given 2 commit positions, returns a list with the ones between.""" | 72 """Given 2 commit positions, returns a list with the ones between.""" |
| 61 a, b = sorted(map(int, [a, b])) | 73 a, b = sorted(map(int, [a, b])) |
| 62 return xrange(a + 1, b) | 74 return xrange(a + 1, b) |
| 63 | 75 |
| 76 def make_deps_sha_file(self, deps_sha): | |
| 77 """Make a diff patch that creates DEPS.sha. | |
| 78 | |
| 79 Args: | |
| 80 deps_sha (str): Hash of the diff that patches DEPS. | |
| 81 | |
| 82 Returns: A string containing a git diff. | |
|
qyearsley
2015/02/22 20:37:54
Usually the description of the return value goes o
RobertoCN
2015/02/24 20:01:13
Done.
| |
| 83 """ | |
| 84 return '\n' + DEPS_SHA_PATCH % {'deps_sha': deps_sha} | |
|
qyearsley
2015/02/22 20:37:54
Maybe the newline should be part of DEPS_PATCH_SHA
RobertoCN
2015/02/24 20:01:13
Done.
| |
| 85 | |
| 86 def make_deps_patch(self, base_revision, base_file_contents, deps_var, | |
| 87 new_commit_hash): | |
| 88 """Make a diff patch that updates a specifc dependency revision. | |
|
qyearsley
2015/02/22 20:37:54
specifc -> specific
RobertoCN
2015/02/24 20:01:13
Done.
| |
| 89 | |
| 90 Args: | |
| 91 base_revision (RevisionState): The revision whose DEPS is to be patched. | |
|
qyearsley
2015/02/22 20:37:53
Possible alternative phrasing:
The revision for wh
RobertoCN
2015/02/24 20:01:13
Done.
| |
| 92 base_file_contents (str): The content of the original DEPS file. | |
| 93 deps_var (str): The variable name identifying the dependency to modify. | |
| 94 new_commit_hash (str): The revision to put in place of the old one. | |
| 95 | |
| 96 Returns: A pair containing the git diff patch that updates DEPS, and the | |
| 97 full text of such modified DEPS, both as strings. | |
|
qyearsley
2015/02/22 20:37:53
full text of the modified DEPS file, ...
RobertoCN
2015/02/24 20:01:13
Done.
| |
| 98 """ | |
| 99 original_contents = str(base_file_contents) | |
| 100 patched_contents = str(original_contents) | |
| 101 | |
| 102 # Modify DEPS | |
| 103 rxp = re.compile( | |
| 104 r'(?<=["\']%s["\']: ["\'])([a-fA-F0-9]{40})(?=["\'])' % deps_var, | |
| 105 re.MULTILINE) | |
|
qyearsley
2015/02/22 20:37:54
This variable name could be changed -- maybe somet
RobertoCN
2015/02/24 20:01:13
Done.
| |
| 106 if not re.search(rxp, original_contents): | |
| 107 raise ValueError('DEPS file does not contain entry for ' + deps_var) | |
| 108 re.sub(rxp, new_commit_hash, patched_contents) | |
| 109 | |
| 110 # Intern modified DEPS | |
| 111 cmd = 'hash-object -t blob -w --stdin'.split(' ') | |
|
qyearsley
2015/02/22 20:37:53
1. split() with no args would have the same effect
RobertoCN
2015/02/24 20:01:13
Leaving the .split(' ') as it is explicit.
Separa
| |
| 112 stdin = self.api.m.raw_io.input(patched_contents) | |
| 113 stdout = self.api.m.raw_io.output() | |
| 114 step_result = self.api.m.git(*cmd, cwd=self.api.m.path['checkout'], | |
| 115 stdin=stdin, stdout=stdout) | |
| 116 interned_deps_hash = step_result.stdout.splitlines()[0] | |
| 117 | |
| 118 # Gen diff patch | |
| 119 cmd = 'diff %s:DEPS %s --src-prefix=IAMSRC: --dst-prefix=IAMDST:' | |
| 120 cmd %= (base_revision, interned_deps_hash) | |
| 121 cmd = cmd.split(' ') | |
| 122 stdout = self.api.m.raw_io.output() | |
| 123 step_result = self.api.m.git(*cmd, cwd=self.api.m.path['checkout'], | |
| 124 stdout=stdout) | |
| 125 patch_text = step_result.stdout | |
| 126 src_string = 'IAMSRC:%s:DEPS' % base_revision | |
| 127 dst_string = 'IAMDST:' + interned_deps_hash | |
| 128 patch_text = patch_text.replace(src_string, 'a/DEPS') | |
| 129 patch_text = patch_text.replace(dst_string, 'b/DEPS') | |
| 130 | |
| 131 return patch_text, patched_contents | |
|
qyearsley
2015/02/22 20:37:54
I think that parts of this function could potentia
RobertoCN
2015/02/24 20:01:13
Done.
| |
| 132 | |
| 133 def _get_rev_range_for_depot(self, depot, min_rev, max_rev): | |
| 134 results = [] | |
| 135 depot_path = self.api.m.path['checkout'].join(depot['src']) | |
| 136 step_name = 'Expanding revision range for ' + depot['deps_var'] | |
| 137 step_result = self.api.m.git('log', '--format==%H', min_rev, max_rev, | |
| 138 stdout=self.api.m.raw_io.output(), | |
| 139 cwd=depot_path, name=step_name) | |
| 140 # We skip the first revision in the list as it is max_rev | |
| 141 new_revisions = step_result.stdout.split_lines()[1:] | |
| 142 for revision in new_revisions: | |
| 143 results.append(self.revision_class(None, self, | |
| 144 base_revision=min_rev, | |
| 145 deps_revision=revision, | |
| 146 depot=depot)) | |
| 147 results.reverse() | |
| 148 return results | |
| 149 | |
| 64 def _expand_revision_range(self, revision_to_expand=None): | 150 def _expand_revision_range(self, revision_to_expand=None): |
| 65 """This method populates the revisions attribute. | 151 """This method populates the revisions attribute. |
|
qyearsley
2015/02/22 20:37:53
You could drop the words "This method" here.
RobertoCN
2015/02/24 20:01:13
Done.
| |
| 66 | 152 |
| 67 After running method self.revisions should contain all the revisions | 153 After running method self.revisions should contain all the revisions |
|
qyearsley
2015/02/22 20:37:53
-> After running this method, self.revisions shoul
RobertoCN
2015/02/24 20:01:13
Done.
| |
| 68 between the good and bad revisions. If given a `revision_to_expand`, it'll | 154 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. | 155 insert the revisions from the external repos in the appropriate place. |
| 70 | 156 |
| 71 Args: | 157 Args: |
| 72 revision_to_expand: A revision where there is a deps change. | 158 revision_to_expand: A revision where there is a deps change. |
|
qyearsley
2015/02/22 20:37:54
Would it make sense to possibly have two separate
RobertoCN
2015/02/24 20:01:13
Done.
| |
| 159 | |
| 160 Returns: | |
| 161 A boolean indicating whether any revisions were inserted. | |
| 73 """ | 162 """ |
| 74 if revision_to_expand is not None: | 163 if revision_to_expand is not None: |
| 75 # TODO: Implement this path (insert revisions when deps change) | 164 try: |
| 76 raise NotImplementedError() | 165 min_revision = revision_to_expand.previous_revision |
| 166 max_revision = revision_to_expand | |
| 167 min_revision.read_deps() | |
| 168 max_revision.read_deps() | |
|
qyearsley
2015/02/22 20:37:54
What does read_deps() do here? It sets the deps at
RobertoCN
2015/02/24 20:01:13
Yes, added in-line comment.
| |
| 169 for depot in depot_config.DEPOT_DEPS_NAME: | |
| 170 deps_var = depot['deps_var'] | |
| 171 if (deps_var in min_revision.deps and deps_var in max_revision.deps): | |
|
qyearsley
2015/02/22 20:37:53
Parentheses unnecessary.
RobertoCN
2015/02/24 20:01:13
Done.
| |
| 172 dep_revision_min = min_revision.deps[deps_var] | |
| 173 dep_revision_max = max_revision.deps[deps_var] | |
| 174 if (dep_revision_min and dep_revision_max and | |
| 175 dep_revision_min != dep_revision_max): | |
| 176 rev_list = self._get_rev_range_for_depot(depot, dep_revision_min, | |
| 177 dep_revision_max) | |
| 178 new_revisions = self.revisions[:max_revision.list_index] | |
| 179 new_revisions += rev_list | |
| 180 new_revisions += self.revisions[max_revision.list_index:] | |
| 181 self.revisions = new_revisions | |
| 182 self._update_revision_list_indexes() | |
| 183 return True | |
| 184 except RuntimeError: | |
| 185 warning_text = ("Could not expand dependency revisions for " + | |
|
qyearsley
2015/02/22 20:37:53
Single quotes.
RobertoCN
2015/02/24 20:01:13
Done.
| |
| 186 revision_to_expand.revision_string) | |
| 187 if warning_text not in self.bisector.warnings: | |
| 188 self.bisector.warnings.append(warning_text) | |
| 189 return False | |
| 190 | |
| 77 rev_list = self._commit_pos_range( | 191 rev_list = self._commit_pos_range( |
| 78 self.good_rev.commit_pos, self.bad_rev.commit_pos) | 192 self.good_rev.commit_pos, self.bad_rev.commit_pos) |
| 79 intermediate_revs = [self.revision_class(str(x), self) for x in rev_list] | 193 intermediate_revs = [self.revision_class(str(x), self) for x in rev_list] |
| 80 self.revisions = [self.good_rev] + intermediate_revs + [self.bad_rev] | 194 self.revisions = [self.good_rev] + intermediate_revs + [self.bad_rev] |
| 195 self._update_revision_list_indexes() | |
| 196 return True | |
| 197 | |
| 198 def _update_revision_list_indexes(self): | |
| 199 """Sets list_index, next and previous properties for each revision.""" | |
| 81 for i, rev in enumerate(self.revisions): | 200 for i, rev in enumerate(self.revisions): |
| 82 rev.list_index = i | 201 rev.list_index = i |
| 83 for i in xrange(len(self.revisions)): | 202 for i in xrange(len(self.revisions)): |
| 84 if i: | 203 if i: |
| 85 self.revisions[i].previous_revision = self.revisions[i - 1] | 204 self.revisions[i].previous_revision = self.revisions[i - 1] |
| 86 if i < len(self.revisions) - 1: | 205 if i < len(self.revisions) - 1: |
| 87 self.revisions[i].next_revision = self.revisions[i + 1] | 206 self.revisions[i].next_revision = self.revisions[i + 1] |
| 88 | 207 |
| 89 def check_improvement_direction(self): | 208 def check_improvement_direction(self): |
| 90 """Verifies that the change from 'good' to 'bad' is in the right direction. | 209 """Verifies that the change from 'good' to 'bad' is in the right direction. |
| 91 | 210 |
| 92 The change between the test results obtained for the given 'good' and 'bad' | 211 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` | 212 '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 | 213 `improvement_direction` attribute is positive if a larger number is |
| 95 smaller number is considered better. | 214 considered better, and negative if a smaller number is considered better. |
| 96 """ | 215 """ |
| 97 direction = self.improvement_direction | 216 direction = self.improvement_direction |
| 98 if direction is None: | 217 if direction is None: |
| 99 return True | 218 return True |
| 100 good = self.good_rev.mean_value | 219 good = self.good_rev.mean_value |
| 101 bad = self.bad_rev.mean_value | 220 bad = self.bad_rev.mean_value |
| 102 if ((bad > good and direction > 0) or | 221 if ((bad > good and direction > 0) or |
| 103 (bad < good and direction < 0)): | 222 (bad < good and direction < 0)): |
| 104 self._set_failed_direction_results() | 223 self._set_failed_direction_results() |
| 105 return False | 224 return False |
| (...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 177 def check_bisect_finished(self, revision): | 296 def check_bisect_finished(self, revision): |
| 178 """Checks if this revision completes the bisection process. | 297 """Checks if this revision completes the bisection process. |
| 179 | 298 |
| 180 In this case 'finished' refers to finding one revision considered 'good' | 299 In this case 'finished' refers to finding one revision considered 'good' |
| 181 immediately preceding a revision considered 'bad' where the 'bad' revision | 300 immediately preceding a revision considered 'bad' where the 'bad' revision |
| 182 does not contain a deps change. | 301 does not contain a deps change. |
| 183 """ | 302 """ |
| 184 if (revision.bad and revision.previous_revision and | 303 if (revision.bad and revision.previous_revision and |
| 185 revision.previous_revision.good): | 304 revision.previous_revision.good): |
| 186 if revision.deps_change(): | 305 if revision.deps_change(): |
| 187 self._expand_revision_range(revision) | 306 more_revisions = self._expand_revision_range(revision) |
| 188 return False | 307 return not more_revisions |
| 189 self.culprit = revision | 308 self.culprit = revision |
| 190 return True | 309 return True |
| 191 if (revision.good and revision.next_revision and | 310 if (revision.good and revision.next_revision and |
| 192 revision.next_revision.bad): | 311 revision.next_revision.bad): |
| 193 if revision.next_revision.deps_change(): | 312 if revision.next_revision.deps_change(): |
| 194 self._expand_revision_range(revision.next_revision) | 313 more_revisions = self._expand_revision_range(revision.next_revision) |
| 195 return False | 314 return not more_revisions |
| 196 self.culprit = revision.next_revision | 315 self.culprit = revision.next_revision |
| 197 return True | 316 return True |
| 198 return False | 317 return False |
| 199 | 318 |
| 200 def wait_for_all(self, revision_list): | 319 def wait_for_all(self, revision_list): |
| 201 """Waits for all revisions in list to finish.""" | 320 """Waits for all revisions in list to finish.""" |
| 202 while any([r.in_progress for r in revision_list]): | 321 while any([r.in_progress for r in revision_list]): |
| 203 self.wait_for_any(revision_list) | 322 self.wait_for_any(revision_list) |
| 204 for revision in revision_list: | 323 for revision in revision_list: |
| 205 revision.update_status() | 324 revision.update_status() |
| (...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 269 return 'linux_perf_tester' | 388 return 'linux_perf_tester' |
| 270 | 389 |
| 271 def get_builder_bot_for_this_platform(self): | 390 def get_builder_bot_for_this_platform(self): |
| 272 # TODO: Actually look at the current platform. | 391 # TODO: Actually look at the current platform. |
| 273 return 'linux_perf_bisect_builder' | 392 return 'linux_perf_bisect_builder' |
| 274 | 393 |
| 275 def get_platform_gs_prefix(self): | 394 def get_platform_gs_prefix(self): |
| 276 # TODO: Actually check the current platform | 395 # TODO: Actually check the current platform |
| 277 return 'gs://chrome-perf/Linux Builder/full-build-linux_' | 396 return 'gs://chrome-perf/Linux Builder/full-build-linux_' |
| 278 | 397 |
| OLD | NEW |