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

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: WIP: Early feedback request Created 5 years, 10 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 = """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
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
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
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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698