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

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: Further expansion of example.py for auto_bisect. 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)
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
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
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
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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698