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 """An interface for holding state and result of revisions in a bisect job. | 5 """An interface for holding state and result of revisions in a bisect job. |
6 | 6 |
7 When implementing support for tests other than perf, one should extend this | 7 When implementing support for tests other than perf, one should extend this |
8 class so that the bisect module and recipe can use it. | 8 class so that the bisect module and recipe can use it. |
9 | 9 |
10 See perf_revision_state for an example. | 10 See perf_revision_state for an example. |
11 """ | 11 """ |
12 | 12 |
13 import collections | |
13 import hashlib | 14 import hashlib |
14 import json | 15 import json |
15 import math | 16 import math |
16 import os | 17 import os |
17 import tempfile | 18 import tempfile |
18 import re | 19 import re |
19 import uuid | 20 import uuid |
20 | 21 |
21 from . import depot_config | 22 from auto_bisect import depot_config |
23 from auto_bisect import bisect_exceptions | |
22 | 24 |
23 # These relate to how to increase the number of repetitions during re-test | 25 # These relate to how to increase the number of repetitions during re-test |
24 MINIMUM_SAMPLE_SIZE = 5 | 26 MINIMUM_SAMPLE_SIZE = 5 |
25 INCREASE_FACTOR = 1.5 | 27 INCREASE_FACTOR = 1.5 |
26 | 28 |
29 NOT_SIGNIFICANTLY_DIFFERENT, SIGNIFICANTLY_DIFFERENT, NEED_MORE_DATA = range(3) | |
dtu
2016/09/13 23:57:27
I think Nat or Tony? said that whenever you're goi
RobertoCN
2016/09/21 22:22:48
Done.
| |
30 | |
27 class RevisionState(object): | 31 class RevisionState(object): |
28 """Abstracts the state of a single revision on a bisect job.""" | 32 """Abstracts the state of a single revision on a bisect job.""" |
29 | 33 |
30 # Possible values for the status attribute of RevisionState: | |
31 ( | |
32 NEW, # A revision_state object that has just been initialized. | |
33 BUILDING, # Requested a build for this revision, waiting for it. | |
34 TESTING, # A test job for this revision was triggered, waiting for it. | |
35 TESTED, # The test job completed with non-failing results. | |
36 FAILED, # Either the build or the test jobs failed or timed out. | |
37 ABORTED, # The build or test job was aborted. (For use in multi-secting). | |
38 SKIPPED, # A revision that was not built or tested for a special reason, | |
39 # such as those ranges that we know are broken, or when nudging | |
40 # revisions. | |
41 NEED_MORE_DATA, # Current number of test values is too small to establish | |
42 # a statistically significant difference between this | |
43 # revision and the revisions known to be good and bad. | |
44 ) = xrange(8) | |
45 | |
46 def __init__(self, bisector, commit_hash, depot_name=None, | 34 def __init__(self, bisector, commit_hash, depot_name=None, |
47 base_revision=None): | 35 base_revision=None): |
48 """Creates a new instance to track the state of a revision. | 36 """Creates a new instance to track the state of a revision. |
49 | 37 |
50 Args: | 38 Args: |
51 bisector (Bisector): The object performing the bisection. | 39 bisector (Bisector): The object performing the bisection. |
52 commit_hash (str): The hash identifying the revision to represent. | 40 commit_hash (str): The hash identifying the revision to represent. |
53 depot_name (str): The name of the depot as specified in DEPS. Must be a | 41 depot_name (str): The name of the depot as specified in DEPS. Must be a |
54 key in depot_config.DEPOT_DEPS_NAME . | 42 key in depot_config.DEPOT_DEPS_NAME . |
55 base_revision (RevisionState): The revision state to patch with the deps | 43 base_revision (RevisionState): The revision state to patch with the deps |
56 change. | 44 change. |
57 """ | 45 """ |
58 super(RevisionState, self).__init__() | 46 super(RevisionState, self).__init__() |
59 self.bisector = bisector | 47 self.bisector = bisector |
60 self._good = None | 48 self._good = None |
49 self.failed = False | |
61 self.deps = None | 50 self.deps = None |
62 self.test_results_url = None | 51 self.test_results_url = None |
63 self.build_archived = False | 52 self.build_archived = False |
64 self.status = RevisionState.NEW | |
65 self.next_revision = None | 53 self.next_revision = None |
66 self.previous_revision = None | 54 self.previous_revision = None |
67 self.job_name = None | 55 self.job_name = None |
68 self.patch_file = None | 56 self.patch_file = None |
69 self.deps_revision = None | 57 self.deps_revision = None |
70 self.depot_name = depot_name or self.bisector.base_depot | 58 self.depot_name = depot_name or self.bisector.base_depot |
71 self.depot = depot_config.DEPOT_DEPS_NAME[self.depot_name] | 59 self.depot = depot_config.DEPOT_DEPS_NAME[self.depot_name] |
72 self.commit_hash = str(commit_hash) | 60 self.commit_hash = str(commit_hash) |
73 self._rev_str = None | 61 self._rev_str = None |
74 self.base_revision = base_revision | 62 self.base_revision = base_revision |
75 self.revision_overrides = {} | 63 self.revision_overrides = {} |
76 self.build_id = None | 64 self.build_id = None |
77 if self.base_revision: | 65 if self.base_revision: |
78 assert self.base_revision.deps_file_contents | |
79 self.needs_patch = True | 66 self.needs_patch = True |
80 self.revision_overrides[self.depot['src']] = self.commit_hash | 67 self.revision_overrides[self.depot['src']] = self.commit_hash |
81 self.deps_patch, self.deps_file_contents = self.bisector.make_deps_patch( | 68 self.deps_sha = hashlib.sha1(self.revision_string()).hexdigest() |
82 self.base_revision, self.base_revision.deps_file_contents, | |
83 self.depot, self.commit_hash) | |
84 self.deps_sha = hashlib.sha1(self.deps_patch).hexdigest() | |
85 self.deps_sha_patch = self.bisector.make_deps_sha_file(self.deps_sha) | |
86 self.deps = dict(base_revision.deps) | 69 self.deps = dict(base_revision.deps) |
87 self.deps[self.depot_name] = self.commit_hash | 70 self.deps[self.depot_name] = self.commit_hash |
88 else: | 71 else: |
89 self.needs_patch = False | 72 self.needs_patch = False |
90 self.build_url = self.bisector.get_platform_gs_prefix() + self._gs_suffix() | 73 self.build_url = self.bisector.get_platform_gs_prefix() + self._gs_suffix() |
91 self.values = [] | 74 self.valueset_paths = [] |
92 self.mean_value = None | 75 self.chartjson_paths = [] |
93 self.overall_return_code = None | 76 self.debug_values = [] |
94 self.std_dev = None | 77 self.return_codes = [] |
95 self._test_config = None | 78 self._test_config = None |
96 | 79 |
97 if self.bisector.test_type == 'perf': | 80 if self.bisector.test_type == 'perf': |
98 self.repeat_count = MINIMUM_SAMPLE_SIZE | 81 self.repeat_count = MINIMUM_SAMPLE_SIZE |
99 else: | 82 else: |
100 self.repeat_count = self.bisector.bisect_config.get( | 83 self.repeat_count = self.bisector.bisect_config.get( |
101 'repeat_count', MINIMUM_SAMPLE_SIZE) | 84 'repeat_count', MINIMUM_SAMPLE_SIZE) |
102 | 85 |
103 @property | 86 @property |
104 def tested(self): | |
105 return self.status in (RevisionState.TESTED,) | |
106 | |
107 @property | |
108 def in_progress(self): | |
109 return self.status in (RevisionState.BUILDING, RevisionState.TESTING, | |
110 RevisionState.NEED_MORE_DATA) | |
111 | |
112 @property | |
113 def failed(self): | |
114 return self.status == RevisionState.FAILED | |
115 | |
116 @property | |
117 def aborted(self): | |
118 return self.status == RevisionState.ABORTED | |
119 | |
120 @property | |
121 def good(self): | 87 def good(self): |
122 return self._good == True | 88 return self._good == True |
123 | 89 |
124 @property | 90 @property |
125 def bad(self): | 91 def bad(self): |
126 return self._good == False | 92 return self._good == False |
127 | 93 |
128 @good.setter | 94 @good.setter |
129 def good(self, value): | 95 def good(self, value): |
130 self._good = value | 96 self._good = value |
131 | 97 |
132 @bad.setter | 98 @bad.setter |
133 def bad(self, value): | 99 def bad(self, value): |
134 self._good = not value | 100 self._good = not value |
135 | 101 |
102 @property | |
103 def test_run_count(self): | |
104 return max( | |
105 len(self.valueset_paths), | |
106 len(self.chartjson_paths), | |
107 len(self.return_codes)) | |
108 | |
109 @property | |
110 def mean(self): | |
111 if self.debug_values: | |
112 return float(sum(self.debug_values))/len(self.debug_values) | |
113 | |
114 @property | |
115 def std_dev(self): | |
116 if self.debug_values: | |
117 mn = self.mean | |
118 return math.sqrt(sum(pow(x - mn, 2) for x in self.debug_values)) | |
119 | |
136 def start_job(self): | 120 def start_job(self): |
137 """Starts a build, or a test job if the build is available.""" | 121 api = self.bisector.api |
138 if self.status == RevisionState.NEW and not self._is_build_archived(): | 122 try: |
139 self._request_build() | 123 if not self._is_build_archived(): |
140 self.status = RevisionState.BUILDING | 124 self._request_build() |
141 return | 125 with api.m.step.nest('Waiting for build'): |
126 while not self._is_build_archived(): | |
127 api.m.python.inline( | |
128 'sleeping', | |
129 """ | |
130 import sys | |
131 import time | |
132 time.sleep(20*60) | |
133 sys.exit(0) | |
134 """) | |
135 if self._is_build_failed(): | |
136 self.failed = True | |
137 return | |
142 | 138 |
143 if self._is_build_archived() and self.status in ( | |
144 RevisionState.NEW, RevisionState.BUILDING, | |
145 RevisionState.NEED_MORE_DATA): | |
146 self._do_test() | 139 self._do_test() |
147 self.status = RevisionState.TESTING | 140 # TODO(robertocn): Add a test to remove this CL |
141 while not self._check_revision_good(): # pragma: no cover | |
142 min(self, self.bisector.lkgr, self.bisector.fkbr, | |
143 key=lambda(x): x.test_run_count)._do_test() | |
144 | |
145 except bisect_exceptions.UntestableRevisionException: | |
146 self.failed = True | |
148 | 147 |
149 def deps_change(self): | 148 def deps_change(self): |
150 """Uses `git show` to see if a given commit contains a DEPS change.""" | 149 """Uses `git show` to see if a given commit contains a DEPS change.""" |
151 # Avoid checking DEPS changes for dependency repo revisions. | 150 # Avoid checking DEPS changes for dependency repo revisions. |
152 # crbug.com/580681 | 151 # crbug.com/580681 |
153 if self.needs_patch: # pragma: no cover | 152 if self.needs_patch: # pragma: no cover |
154 return False | 153 return False |
155 api = self.bisector.api | 154 api = self.bisector.api |
156 working_dir = api.working_dir | 155 working_dir = api.working_dir |
157 cwd = working_dir.join( | 156 cwd = working_dir.join( |
158 depot_config.DEPOT_DEPS_NAME[self.depot_name]['src']) | 157 depot_config.DEPOT_DEPS_NAME[self.depot_name]['src']) |
159 name = 'Checking DEPS for ' + self.commit_hash | 158 name = 'Checking DEPS for ' + self.commit_hash |
160 step_result = api.m.git( | 159 step_result = api.m.git( |
161 'show', '--name-only', '--pretty=format:', | 160 'show', '--name-only', '--pretty=format:', |
162 self.commit_hash, cwd=cwd, stdout=api.m.raw_io.output(), name=name) | 161 self.commit_hash, cwd=cwd, stdout=api.m.raw_io.output(), name=name, |
163 if self.bisector.dummy_builds and not self.commit_hash.startswith('dcdc'): | 162 step_test_data=lambda: api._test_data['deps_change'][self.commit_hash] |
164 return False | 163 ) |
165 if 'DEPS' in step_result.stdout.splitlines(): # pragma: no cover | 164 if 'DEPS' in step_result.stdout.splitlines(): # pragma: no cover |
166 return True | 165 return True |
167 return False # pragma: no cover | 166 return False # pragma: no cover |
168 | 167 |
169 def _gen_deps_local_scope(self): | 168 def _gen_deps_local_scope(self): |
170 """Defines the Var and From functions in a dict for calling exec. | 169 """Defines the Var and From functions in a dict for calling exec. |
171 | 170 |
172 This is needed for executing the DEPS file. | 171 This is needed for executing the DEPS file. |
173 """ | 172 """ |
174 deps_data = { | 173 deps_data = { |
175 'Var': lambda _: deps_data['vars'][_], | 174 'Var': lambda _: deps_data['vars'][_], |
176 'From': lambda *args: None, | 175 'From': lambda *args: None, |
177 } | 176 } |
178 return deps_data | 177 return deps_data |
179 | 178 |
180 def _read_content(self, url, file_name, branch): # pragma: no cover | 179 def _read_content(self, url, file_name, branch): # pragma: no cover |
181 """Uses gitiles recipe module to download and read file contents.""" | 180 """Uses gitiles recipe module to download and read file contents.""" |
182 try: | 181 try: |
183 return self.bisector.api.m.gitiles.download_file( | 182 return self.bisector.api.m.gitiles.download_file( |
184 repository_url=url, file_path=file_name, branch=branch) | 183 repository_url=url, file_path=file_name, branch=branch) |
185 except TypeError: | 184 except TypeError: |
186 print 'Could not read content for %s/%s/%s' % (url, file_name, branch) | 185 print 'Could not read content for %s/%s/%s' % (url, file_name, branch) |
187 return None | 186 return None |
188 | 187 |
189 def read_deps(self, recipe_tester_name): | 188 def read_deps(self, recipe_tester_name): |
190 """Sets the dependencies for this revision from the contents of DEPS.""" | 189 """Sets the dependencies for this revision from the contents of DEPS.""" |
191 api = self.bisector.api | 190 api = self.bisector.api |
192 if self.deps: | |
193 return | |
194 if self.bisector.internal_bisect: # pragma: no cover | 191 if self.bisector.internal_bisect: # pragma: no cover |
195 self.deps_file_contents = self._read_content( | 192 deps_file_contents = self._read_content( |
196 depot_config.DEPOT_DEPS_NAME[self.depot_name]['url'], | 193 depot_config.DEPOT_DEPS_NAME[self.depot_name]['url'], |
197 depot_config.DEPOT_DEPS_NAME[self.depot_name]['deps_file'], | 194 depot_config.DEPOT_DEPS_NAME[self.depot_name]['deps_file'], |
198 self.commit_hash) | 195 self.commit_hash) |
199 # On April 5th, 2016 .DEPS.git was changed to DEPS on android-chrome repo, | 196 # On April 5th, 2016 .DEPS.git was changed to DEPS on android-chrome repo, |
200 # we are doing this in order to support both deps files. | 197 # we are doing this in order to support both deps files. |
201 if not self.deps_file_contents: | 198 if not deps_file_contents: |
202 self.deps_file_contents = self._read_content( | 199 deps_file_contents = self._read_content( |
203 depot_config.DEPOT_DEPS_NAME[self.depot_name]['url'], | 200 depot_config.DEPOT_DEPS_NAME[self.depot_name]['url'], |
204 depot_config.DEPS_FILENAME, | 201 depot_config.DEPS_FILENAME, |
205 self.commit_hash) | 202 self.commit_hash) |
206 else: | 203 else: |
207 step_result = api.m.python( | 204 step_result = api.m.python( |
208 'fetch file %s:%s' % (self.commit_hash, depot_config.DEPS_FILENAME), | 205 'fetch file %s:%s' % (self.commit_hash, depot_config.DEPS_FILENAME), |
209 api.resource('fetch_file.py'), | 206 api.resource('fetch_file.py'), |
210 [depot_config.DEPS_FILENAME, '--commit', self.commit_hash], | 207 [depot_config.DEPS_FILENAME, '--commit', self.commit_hash], |
211 stdout=api.m.raw_io.output()) | 208 stdout=api.m.raw_io.output(), |
212 self.deps_file_contents = step_result.stdout | 209 step_test_data=lambda: api._test_data['deps'][self.commit_hash] |
210 ) | |
211 deps_file_contents = step_result.stdout | |
213 try: | 212 try: |
214 deps_data = self._gen_deps_local_scope() | 213 deps_data = self._gen_deps_local_scope() |
215 exec(self.deps_file_contents or 'deps = {}', {}, deps_data) | 214 exec (deps_file_contents or 'deps = {}') in {}, deps_data |
216 deps_data = deps_data['deps'] | 215 deps_data = deps_data['deps'] |
217 except ImportError: # pragma: no cover | 216 except ImportError: # pragma: no cover |
218 # TODO(robertocn): Implement manual parsing of DEPS when exec fails. | 217 # TODO(robertocn): Implement manual parsing of DEPS when exec fails. |
219 raise NotImplementedError('Path not implemented to manually parse DEPS') | 218 raise NotImplementedError('Path not implemented to manually parse DEPS') |
220 | 219 |
221 revision_regex = re.compile('.git@(?P<revision>[a-fA-F0-9]+)') | 220 revision_regex = re.compile('.git@(?P<revision>[a-fA-F0-9]+)') |
222 results = {} | 221 results = {} |
223 for depot_name, depot_data in depot_config.DEPOT_DEPS_NAME.iteritems(): | 222 for depot_name, depot_data in depot_config.DEPOT_DEPS_NAME.iteritems(): |
224 if (depot_data.get('platform') and | 223 if (depot_data.get('platform') and |
225 depot_data.get('platform') not in recipe_tester_name.lower()): | 224 depot_data.get('platform') not in recipe_tester_name.lower()): |
(...skipping 10 matching lines...) Expand all Loading... | |
236 else: # pragma: no cover | 235 else: # pragma: no cover |
237 warning_text = ('Could not parse revision for %s while bisecting ' | 236 warning_text = ('Could not parse revision for %s while bisecting ' |
238 '%s' % (depot_name, self.depot)) | 237 '%s' % (depot_name, self.depot)) |
239 if warning_text not in self.bisector.warnings: | 238 if warning_text not in self.bisector.warnings: |
240 self.bisector.warnings.append(warning_text) | 239 self.bisector.warnings.append(warning_text) |
241 else: | 240 else: |
242 results[depot_name] = None | 241 results[depot_name] = None |
243 self.deps = results | 242 self.deps = results |
244 return | 243 return |
245 | 244 |
246 def update_status(self): | |
247 """Checks on the pending jobs and updates status accordingly. | |
248 | |
249 This method will check for the build to complete and then trigger the test, | |
250 or will wait for the test as appropriate. | |
251 | |
252 To wait for the test we try to get the buildbot job url from GS, and if | |
253 available, we query the status of such job. | |
254 """ | |
255 if self.status == RevisionState.BUILDING: | |
256 if self._is_build_archived(): | |
257 self.start_job() | |
258 elif self._is_build_failed(): # pragma: no cover | |
259 self.status = RevisionState.FAILED | |
260 elif (self.status in (RevisionState.TESTING, RevisionState.NEED_MORE_DATA) | |
261 and self._results_available()): | |
262 # If we have already decided whether the revision is good or bad we | |
263 # shouldn't check again | |
264 check_revision_goodness = not(self.good or self.bad) | |
265 self._read_test_results( | |
266 check_revision_goodness=check_revision_goodness) | |
267 # We assume _read_test_results may have changed the status to a broken | |
268 # state such as FAILED or ABORTED. | |
269 if self.status in (RevisionState.TESTING, RevisionState.NEED_MORE_DATA): | |
270 self.status = RevisionState.TESTED | |
271 | |
272 def _is_build_archived(self): | 245 def _is_build_archived(self): |
273 """Checks if the revision is already built and archived.""" | 246 """Checks if the revision is already built and archived.""" |
274 if not self.build_archived: | 247 if not self.build_archived: |
275 api = self.bisector.api | 248 api = self.bisector.api |
276 self.build_archived = api.gsutil_file_exists(self.build_url) | 249 self.build_archived = api.gsutil_file_exists( |
277 | 250 self.build_url, |
278 if self.bisector.dummy_builds: | 251 step_test_data=lambda: api._test_data.get( |
279 self.build_archived = self.in_progress | 252 'gsutil_exists', {}).get(self.commit_hash).pop() |
253 if api._test_data.get('gsutil_exists', {}).get(self.commit_hash) | |
254 else collections.namedtuple('retcode_attr', ['retcode'])(0)) | |
dtu
2016/09/13 23:57:27
Maybe this is:
api._test_data.get('gsutil_exists'
RobertoCN
2016/09/21 22:22:48
That wouldn't work because after popping the last
| |
280 | 255 |
281 return self.build_archived | 256 return self.build_archived |
282 | 257 |
283 def _is_build_failed(self): | 258 def _is_build_failed(self): |
284 api = self.bisector.api | 259 api = self.bisector.api |
285 result = api.m.buildbucket.get_build( | 260 result = api.m.buildbucket.get_build( |
286 self.build_id, | 261 self.build_id, |
287 api.m.service_account.get_json_path(api.SERVICE_ACCOUNT), | 262 api.m.service_account.get_json_path(api.SERVICE_ACCOUNT), |
288 step_test_data=lambda: api.m.json.test_api.output_stream( | 263 step_test_data=lambda: api.test_api.buildbot_job_status_mock( |
289 {'build': {'result': 'SUCCESS', 'status': 'COMPLETED'}} | 264 api._test_data.get('build_status', {}).get(self.commit_hash, []))) |
290 )) | |
291 return (result.stdout['build']['status'] == 'COMPLETED' and | 265 return (result.stdout['build']['status'] == 'COMPLETED' and |
292 result.stdout['build'].get('result') != 'SUCCESS') | 266 result.stdout['build'].get('result') != 'SUCCESS') |
293 | 267 |
294 def _results_available(self): | |
295 """Checks if the results for the test job have been uploaded.""" | |
296 api = self.bisector.api | |
297 result = api.gsutil_file_exists(self.test_results_url) | |
298 if self.bisector.dummy_builds: | |
299 return self.in_progress | |
300 return result # pragma: no cover | |
301 | |
302 def _gs_suffix(self): | 268 def _gs_suffix(self): |
303 """Provides the expected right half of the build filename. | 269 """Provides the expected right half of the build filename. |
304 | 270 |
305 This takes into account whether the build has a deps patch. | 271 This takes into account whether the build has a deps patch. |
306 """ | 272 """ |
307 top_revision = self | 273 top_revision = self |
308 while top_revision.base_revision: | 274 while top_revision.base_revision: |
309 top_revision = top_revision.base_revision | 275 top_revision = top_revision.base_revision |
310 name_parts = [top_revision.commit_hash] | 276 name_parts = [top_revision.commit_hash] |
311 if self.needs_patch: | 277 if self.needs_patch: |
312 name_parts.append(self.deps_sha) | 278 name_parts.append(self.deps_sha) |
313 return '%s.zip' % '_'.join(name_parts) | 279 return '%s.zip' % '_'.join(name_parts) |
314 | 280 |
315 def _read_test_results(self, check_revision_goodness=True): | 281 def _read_test_results(self, results): |
316 """Gets the test results from GS and checks if the rev is good or bad.""" | 282 # Results will be a dictionary containing path to chartjsons, paths to |
317 test_results = self._get_test_results() | 283 # valueset, list of return codes. |
318 # Results will contain the keys 'results' and 'output' where output is the | 284 self.return_codes.extend(results.get('retcodes', [])) |
319 # stdout of the command, and 'results' is itself a dict with the key | 285 if results.get('errors'): # pragma: no cover |
320 # 'values' unless the test failed, in which case 'results' will contain | 286 self.failed = True |
321 # the 'error' key explaining the type of error. | 287 if 'MISSING_METRIC' in results.get('errors'): |
322 results = test_results['results'] | |
323 if results.get('errors'): | |
324 self.status = RevisionState.FAILED | |
325 if 'MISSING_METRIC' in results.get('errors'): # pragma: no cover | |
326 self.bisector.surface_result('MISSING_METRIC') | 288 self.bisector.surface_result('MISSING_METRIC') |
327 return | 289 raise bisect_exceptions.UntestableRevisionException(results['errors']) |
328 self.values += results['values'] | 290 elif self.bisector.is_return_code_mode(): |
329 api = self.bisector.api | 291 assert len(results['retcodes']) |
330 if test_results.get('retcodes') and test_results['retcodes'][-1] != 0 and ( | |
331 api.m.chromium.c.TARGET_PLATFORM == 'android'): #pragma: no cover | |
332 api.m.chromium_android.device_status() | |
333 current_connected_devices = api.m.chromium_android.devices | |
334 current_device = api.m.bisect_tester.device_to_test | |
335 if current_device not in current_connected_devices: | |
336 # We need to manually raise step failure here because we are catching | |
337 # them further down the line to enable return_code bisects and bisecting | |
338 # on benchmarks that are a little flaky. | |
339 raise api.m.step.StepFailure('Test device disconnected.') | |
340 if self.bisector.is_return_code_mode(): | |
341 retcodes = test_results['retcodes'] | |
342 self.overall_return_code = 0 if all(v == 0 for v in retcodes) else 1 | |
343 # Keeping mean_value for compatibility with dashboard. | |
344 # TODO(robertocn): refactor mean_value, specially when uploading results | |
345 # to dashboard. | |
346 self.mean_value = self.overall_return_code | |
347 elif self.values: | |
348 api = self.bisector.api | |
349 self.mean_value = api.m.math_utils.mean(self.values) | |
350 self.std_dev = api.m.math_utils.standard_deviation(self.values) | |
351 # Values were not found, but the test did not otherwise fail. | |
352 else: | 292 else: |
353 self.status = RevisionState.FAILED | 293 self.valueset_paths.extend(results.get('valueset_paths')) |
354 self.bisector.surface_result('MISSING_METRIC') | 294 self.chartjson_paths.extend(results.get('chartjson_paths')) |
355 return | 295 if results.get('retcodes') and 0 not in results['retcodes']: |
356 # If we have already decided on the goodness of this revision, we shouldn't | 296 raise bisect_exceptions.UntestableRevisionException( |
357 # recheck it. | 297 'got non-zero return code on all runs for ' + self.commit_hash) |
358 if self.good or self.bad: | |
359 check_revision_goodness = False | |
360 # We cannot test the goodness of the initial rev range. | |
361 if (self.bisector.good_rev != self and self.bisector.bad_rev != self and | |
362 check_revision_goodness): | |
363 if self._check_revision_good(): | |
364 self.good = True | |
365 else: | |
366 self.bad = True | |
367 | 298 |
368 def _request_build(self): | 299 def _request_build(self): |
369 """Posts a request to buildbot to build this revision and archive it.""" | 300 """Posts a request to buildbot to build this revision and archive it.""" |
370 api = self.bisector.api | 301 api = self.bisector.api |
371 bot_name = self.bisector.get_builder_bot_for_this_platform() | 302 bot_name = self.bisector.get_builder_bot_for_this_platform() |
372 | 303 |
373 # To allow multiple nested levels, we go to the topmost revision. | 304 # To allow multiple nested levels, we go to the topmost revision. |
374 top_revision = self | 305 top_revision = self |
375 while top_revision.base_revision: | 306 while top_revision.base_revision: |
376 top_revision = top_revision.base_revision | 307 top_revision = top_revision.base_revision |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
424 self._test_config = result | 355 self._test_config = result |
425 return result | 356 return result |
426 | 357 |
427 def _do_test(self): | 358 def _do_test(self): |
428 """Triggers tests for a revision, either locally or via try job. | 359 """Triggers tests for a revision, either locally or via try job. |
429 | 360 |
430 If local testing is enabled (i.e. director/tester merged) then | 361 If local testing is enabled (i.e. director/tester merged) then |
431 the test will be run on the same machine. Otherwise, this posts | 362 the test will be run on the same machine. Otherwise, this posts |
432 a request to buildbot to download and perf-test this build. | 363 a request to buildbot to download and perf-test this build. |
433 """ | 364 """ |
434 if self.bisector.bisect_config.get('dummy_job_names'): | 365 if self.test_run_count: # pragma: no cover |
435 self.job_name = self.commit_hash + '-test' | 366 self.repeat_count = max(MINIMUM_SAMPLE_SIZE, math.ceil( |
436 else: # pragma: no cover | 367 self.test_run_count * 1.5)) - self.test_run_count |
437 self.job_name = uuid.uuid4().hex | 368 |
438 api = self.bisector.api | 369 api = self.bisector.api |
439 # Stores revision map for different repos eg, android-chrome, src, v8 etc. | 370 # Stores revision map for different repos eg, android-chrome, src, v8 etc. |
440 revision_ladder = {} | 371 revision_ladder = {} |
441 top_revision = self | 372 top_revision = self |
442 revision_ladder[top_revision.depot_name] = top_revision.commit_hash | 373 revision_ladder[top_revision.depot_name] = top_revision.commit_hash |
443 while top_revision.base_revision: # pragma: no cover | 374 while top_revision.base_revision: # pragma: no cover |
444 revision_ladder[top_revision.depot_name] = top_revision.commit_hash | 375 revision_ladder[top_revision.depot_name] = top_revision.commit_hash |
445 top_revision = top_revision.base_revision | 376 top_revision = top_revision.base_revision |
446 perf_test_properties = { | 377 perf_test_properties = { |
447 'builder_name': self.bisector.get_perf_tester_name(), | |
448 'properties': { | 378 'properties': { |
449 'revision': top_revision.commit_hash, | 379 'revision': top_revision.commit_hash, |
450 'parent_got_revision': top_revision.commit_hash, | 380 'parent_got_revision': top_revision.commit_hash, |
451 'parent_build_archive_url': self.build_url, | 381 'parent_build_archive_url': self.build_url, |
452 'bisect_config': self._get_bisect_config_for_tester(), | 382 'bisect_config': self._get_bisect_config_for_tester(), |
453 'job_name': self.job_name, | |
454 'revision_ladder': revision_ladder, | 383 'revision_ladder': revision_ladder, |
455 }, | 384 }, |
456 } | 385 } |
457 self.test_results_url = (self.bisector.api.GS_RESULTS_URL + | 386 skip_download = self.bisector.last_tested_revision == self |
458 self.job_name + '.results') | 387 self.bisector.last_tested_revision = self |
459 if (api.m.bisect_tester.local_test_enabled() or | 388 overrides = perf_test_properties['properties'] |
460 self.bisector.internal_bisect): # pragma: no cover | |
461 skip_download = self.bisector.last_tested_revision == self | |
462 self.bisector.last_tested_revision = self | |
463 overrides = perf_test_properties['properties'] | |
464 api.run_local_test_run(overrides, skip_download=skip_download) | |
465 else: | |
466 step_name = 'Triggering test job for ' + self.commit_hash | |
467 api.m.trigger(perf_test_properties, name=step_name) | |
468 | 389 |
469 def retest(self): # pragma: no cover | 390 def run_test_step_test_data(): |
dtu
2016/09/13 23:57:27
Hmm, this feels out of place. Is there a way to pu
RobertoCN
2016/09/21 22:22:48
That makes sense. I'll give it a try in a separate
| |
470 # We need at least 5 samples for applying Mann-Whitney U test | 391 """Returns a single step data object when called. |
471 # with P < 0.01, two-tailed . | |
472 target_sample_size = max(5, math.ceil(len(self.values) * 1.5)) | |
473 self.status = RevisionState.NEED_MORE_DATA | |
474 self.repeat_count = target_sample_size - len(self.values) | |
475 self.start_job() | |
476 self.bisector.wait_for(self) | |
477 | 392 |
478 def _get_test_results(self): | 393 These are expected to be populated by the test_api. |
479 """Tries to get the results of a test job from cloud storage.""" | 394 """ |
480 api = self.bisector.api | 395 if api._test_data['run_results'].get(self.commit_hash): |
481 try: | 396 return api._test_data['run_results'][self.commit_hash].pop(0) |
482 stdout = api.m.raw_io.output() | 397 return api._test_data['run_results']['default'] |
483 name = 'Get test results for build ' + self.commit_hash | |
484 step_result = api.m.gsutil.cat(self.test_results_url, stdout=stdout, | |
485 name=name) | |
486 if not step_result.stdout: | |
487 raise api.m.step.StepFailure('Test for build %s failed' % | |
488 self.revision_string()) | |
489 except api.m.step.StepFailure as sf: # pragma: no cover | |
490 self.bisector.surface_result('TEST_FAILURE') | |
491 return {'results': {'errors': str(sf)}} | |
492 else: | |
493 return json.loads(step_result.stdout) | |
494 | 398 |
495 def _check_revision_good(self): | 399 self._read_test_results(api.run_local_test_run( |
400 overrides, skip_download=skip_download, | |
401 step_test_data=run_test_step_test_data | |
402 )) | |
403 | |
404 def _check_revision_good(self): # pragma: no cover | |
496 """Determines if a revision is good or bad. | 405 """Determines if a revision is good or bad. |
497 | 406 |
498 Iteratively increment the sample size of the revision being tested, the last | 407 Returns: |
499 known good revision, and the first known bad revision until a relationship | 408 True if the revision is either good or bad, False if it cannot be |
500 of significant difference can be established betweeb the results of the | 409 determined from the available data. |
501 revision being tested and one of the other two. | 410 """ |
411 # Do not reclassify revisions. Important for reference range. | |
412 if self.good or self.bad: | |
413 return True | |
502 | 414 |
503 If the results do not converge towards finding a significant difference in | |
504 either direction, this is expected to timeout eventually. This scenario | |
505 should be rather rare, since it is expected that the fkbr and lkgr are | |
506 significantly different as a precondition. | |
507 | |
508 Returns: | |
509 True if the results of testing this revision are significantly different | |
510 from those of testing the earliest known bad revision. | |
511 False if they are instead significantly different form those of testing | |
512 the latest knwon good revision. | |
513 """ | |
514 lkgr = self.bisector.lkgr | 415 lkgr = self.bisector.lkgr |
515 fkbr = self.bisector.fkbr | 416 fkbr = self.bisector.fkbr |
417 if self.bisector.is_return_code_mode(): | |
418 if self.overall_return_code == lkgr.overall_return_code: | |
419 self.good = True | |
420 else: | |
421 self.bad = True | |
422 return True | |
423 diff_from_good = self.bisector.compare_revisions(self, lkgr) | |
424 diff_from_bad = self.bisector.compare_revisions(self, fkbr) | |
425 if (diff_from_good == NOT_SIGNIFICANTLY_DIFFERENT and | |
426 diff_from_bad == NOT_SIGNIFICANTLY_DIFFERENT): | |
427 # We have reached the max number of samples and have not established | |
428 # difference, give up. | |
429 raise bisect_exceptions.InconclusiveBisectException() | |
430 if (diff_from_good == SIGNIFICANTLY_DIFFERENT and | |
431 diff_from_bad == SIGNIFICANTLY_DIFFERENT): | |
432 # Multiple regressions. | |
433 # For now, proceed bisecting the biggest difference of the means. | |
434 dist_from_good = abs(self.mean - lkgr.mean) | |
435 dist_from_bad = abs(self.mean - fkbr.mean) | |
436 if dist_from_good > dist_from_bad: | |
437 # TODO(robertocn): Add way to handle the secondary regression | |
438 #self.bisector.handle_secondary_regression(self, fkbr) | |
439 self.bad = True | |
440 return True | |
441 else: | |
442 #self.bisector.handle_secondary_regression(lkgr, self) | |
443 self.good = True | |
444 return True | |
445 if diff_from_good == SIGNIFICANTLY_DIFFERENT: | |
446 self.bad = True | |
447 return True | |
448 elif diff_from_bad == SIGNIFICANTLY_DIFFERENT: | |
449 self.good = True | |
450 return True | |
451 # NEED_MORE_DATA | |
452 return False | |
516 | 453 |
517 if self.bisector.is_return_code_mode(): | |
518 return self.overall_return_code == lkgr.overall_return_code | |
519 | |
520 while True: | |
521 diff_from_good = self.bisector.significantly_different( | |
522 lkgr.values[:len(fkbr.values)], self.values) | |
523 diff_from_bad = self.bisector.significantly_different( | |
524 fkbr.values[:len(lkgr.values)], self.values) | |
525 | |
526 if diff_from_good and diff_from_bad: | |
527 # Multiple regressions. | |
528 # For now, proceed bisecting the biggest difference of the means. | |
529 dist_from_good = abs(self.mean_value - lkgr.mean_value) | |
530 dist_from_bad = abs(self.mean_value - fkbr.mean_value) | |
531 if dist_from_good > dist_from_bad: | |
532 # TODO(robertocn): Add way to handle the secondary regression | |
533 #self.bisector.handle_secondary_regression(self, fkbr) | |
534 return False | |
535 else: | |
536 #self.bisector.handle_secondary_regression(lkgr, self) | |
537 return True | |
538 | |
539 if diff_from_good or diff_from_bad: # pragma: no cover | |
540 return diff_from_bad | |
541 | |
542 self._next_retest() # pragma: no cover | |
543 | 454 |
544 def revision_string(self): | 455 def revision_string(self): |
545 if self._rev_str: | 456 if self._rev_str: |
546 return self._rev_str | 457 return self._rev_str |
547 result = '' | 458 result = '' |
548 if self.base_revision: # pragma: no cover | 459 if self.base_revision: # pragma: no cover |
549 result += self.base_revision.revision_string() + ',' | 460 result += self.base_revision.revision_string() + ',' |
550 commit = self.commit_hash[:10] | 461 commit = self.commit_hash[:10] |
551 if self.depot_name == 'chromium': | 462 if self.depot_name == 'chromium': |
552 try: | 463 try: |
553 commit = str(self.bisector.api.m.commit_position | 464 commit = str(self.bisector.api.m.commit_position |
554 .chromium_commit_position_from_hash(self.commit_hash)) | 465 .chromium_commit_position_from_hash(self.commit_hash)) |
555 except self.bisector.api.m.step.StepFailure: | 466 except self.bisector.api.m.step.StepFailure: |
556 pass # Failure to resolve a commit position is no reason to break. | 467 pass # Failure to resolve a commit position is no reason to break. |
557 result += '%s@%s' % (self.depot_name, commit) | 468 result += '%s@%s' % (self.depot_name, commit) |
558 self._rev_str = result | 469 self._rev_str = result |
559 return self._rev_str | 470 return self._rev_str |
560 | 471 |
561 def _next_retest(self): # pragma: no cover | 472 def __repr__(self): # pragma: no cover |
562 """Chooses one of current, lkgr, fkbr to retest. | 473 if not self.test_run_count: |
474 return ('RevisionState(rev=%s), values=[]' % self.revision_string()) | |
475 if self.bisector.is_return_code_mode(): | |
476 return ('RevisionState(rev=%s, mean=%r, overall_return_code=%r, ' | |
477 'std_dev=%r)') % (self.revision_string(), self.mean, | |
478 self.overall_return_code, self.std_dev) | |
479 return ('RevisionState(rev=%s, mean_value=%r, std_dev=%r)' % ( | |
480 self.revision_string(), self.mean, self.std_dev)) | |
563 | 481 |
564 Look for the smallest sample and retest that. If the last tested revision | 482 @property |
565 is tied for the smallest sample, use that to take advantage of the fact | 483 def overall_return_code(self): |
566 that it is already downloaded and unzipped. | 484 if self.bisector.is_return_code_mode(): |
567 """ | 485 if self.return_codes: |
568 next_revision_to_test = min(self.bisector.lkgr, self, self.bisector.fkbr, | 486 if max(self.return_codes): |
569 key=lambda x: len(x.values)) | 487 return 1 |
570 if (len(self.bisector.last_tested_revision.values) == | 488 return 0 |
571 next_revision_to_test.values): | 489 raise ValueError('overall_return_code needs non-empty sample' |
572 self.bisector.last_tested_revision.retest() | 490 ) # pragma: no cover |
573 else: | 491 raise ValueError('overall_return_code only applies to return_code bisects' |
574 next_revision_to_test.retest() | 492 ) # pragma: no cover |
575 | |
576 def __repr__(self): | |
577 if self.overall_return_code is not None: | |
578 return ('RevisionState(rev=%s, values=%r, overall_return_code=%r, ' | |
579 'std_dev=%r)') % (self.revision_string(), self.values, | |
580 self.overall_return_code, self.std_dev) | |
581 return ('RevisionState(rev=%s, values=%r, mean_value=%r, std_dev=%r)' % ( | |
582 self.revision_string(), self.values, self.mean_value, self.std_dev)) | |
OLD | NEW |