Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(308)

Side by Side Diff: scripts/slave/recipe_modules/auto_bisect/bisector.py

Issue 940123005: Adding ability to bisect recipe to bisect into dependency repos. (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/build.git@hax
Patch Set: Addressing feedback. Created 5 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
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
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
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
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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698