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

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: Missing docstring 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', 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
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
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
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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698