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

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

Issue 2247373002: Refactor stages 1, 2 and test_api overhaul. (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/build.git@master
Patch Set: Got full coverage for new and changed code. Created 4 years, 3 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 """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 . import depot_config
23 from . import 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
27 class RevisionState(object): 29 class RevisionState(object):
28 """Abstracts the state of a single revision on a bisect job.""" 30 """Abstracts the state of a single revision on a bisect job."""
29 31
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, 32 def __init__(self, bisector, commit_hash, depot_name=None,
47 base_revision=None): 33 base_revision=None):
48 """Creates a new instance to track the state of a revision. 34 """Creates a new instance to track the state of a revision.
49 35
50 Args: 36 Args:
51 bisector (Bisector): The object performing the bisection. 37 bisector (Bisector): The object performing the bisection.
52 commit_hash (str): The hash identifying the revision to represent. 38 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 39 depot_name (str): The name of the depot as specified in DEPS. Must be a
54 key in depot_config.DEPOT_DEPS_NAME . 40 key in depot_config.DEPOT_DEPS_NAME .
55 base_revision (RevisionState): The revision state to patch with the deps 41 base_revision (RevisionState): The revision state to patch with the deps
56 change. 42 change.
57 """ 43 """
58 super(RevisionState, self).__init__() 44 super(RevisionState, self).__init__()
59 self.bisector = bisector 45 self.bisector = bisector
60 self._good = None 46 self._good = None
47 self.failed = False
61 self.deps = None 48 self.deps = None
62 self.test_results_url = None 49 self.test_results_url = None
63 self.build_archived = False 50 self.build_archived = False
64 self.status = RevisionState.NEW
65 self.next_revision = None 51 self.next_revision = None
66 self.previous_revision = None 52 self.previous_revision = None
67 self.job_name = None 53 self.job_name = None
68 self.patch_file = None 54 self.patch_file = None
69 self.deps_revision = None 55 self.deps_revision = None
70 self.depot_name = depot_name or self.bisector.base_depot 56 self.depot_name = depot_name or self.bisector.base_depot
71 self.depot = depot_config.DEPOT_DEPS_NAME[self.depot_name] 57 self.depot = depot_config.DEPOT_DEPS_NAME[self.depot_name]
72 self.commit_hash = str(commit_hash) 58 self.commit_hash = str(commit_hash)
73 self._rev_str = None 59 self._rev_str = None
74 self.base_revision = base_revision 60 self.base_revision = base_revision
75 self.revision_overrides = {} 61 self.revision_overrides = {}
76 self.build_id = None 62 self.build_id = None
77 if self.base_revision: 63 if self.base_revision:
78 assert self.base_revision.deps_file_contents 64 assert self.base_revision.deps_file_contents
79 self.needs_patch = True 65 self.needs_patch = True
80 self.revision_overrides[self.depot['src']] = self.commit_hash 66 self.revision_overrides[self.depot['src']] = self.commit_hash
81 self.deps_patch, self.deps_file_contents = self.bisector.make_deps_patch( 67 self.deps_patch, self.deps_file_contents = self.bisector.make_deps_patch(
82 self.base_revision, self.base_revision.deps_file_contents, 68 self.base_revision, self.base_revision.deps_file_contents,
83 self.depot, self.commit_hash) 69 self.depot, self.commit_hash)
84 self.deps_sha = hashlib.sha1(self.deps_patch).hexdigest() 70 self.deps_sha = hashlib.sha1(self.deps_patch).hexdigest()
85 self.deps_sha_patch = self.bisector.make_deps_sha_file(self.deps_sha) 71 self.deps_sha_patch = self.bisector.make_deps_sha_file(self.deps_sha)
86 self.deps = dict(base_revision.deps) 72 self.deps = dict(base_revision.deps)
87 self.deps[self.depot_name] = self.commit_hash 73 self.deps[self.depot_name] = self.commit_hash
88 else: 74 else:
89 self.needs_patch = False 75 self.needs_patch = False
90 self.build_url = self.bisector.get_platform_gs_prefix() + self._gs_suffix() 76 self.build_url = self.bisector.get_platform_gs_prefix() + self._gs_suffix()
91 self.values = [] 77 self.valueset_paths = []
92 self.mean_value = None 78 self.chartjson_paths = []
93 self.overall_return_code = None 79 self.debug_values = []
80 self.return_codes = []
81 self.mean = None
94 self.std_dev = None 82 self.std_dev = None
95 self._test_config = None 83 self._test_config = None
96 84
97 if self.bisector.test_type == 'perf': 85 if self.bisector.test_type == 'perf':
98 self.repeat_count = MINIMUM_SAMPLE_SIZE 86 self.repeat_count = MINIMUM_SAMPLE_SIZE
99 else: 87 else:
100 self.repeat_count = self.bisector.bisect_config.get( 88 self.repeat_count = self.bisector.bisect_config.get(
101 'repeat_count', MINIMUM_SAMPLE_SIZE) 89 'repeat_count', MINIMUM_SAMPLE_SIZE)
102 90
103 @property 91 @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): 92 def good(self):
122 return self._good == True 93 return self._good == True
123 94
124 @property 95 @property
125 def bad(self): 96 def bad(self):
126 return self._good == False 97 return self._good == False
127 98
128 @good.setter 99 @good.setter
129 def good(self, value): 100 def good(self, value):
130 self._good = value 101 self._good = value
131 102
132 @bad.setter 103 @bad.setter
133 def bad(self, value): 104 def bad(self, value):
134 self._good = not value 105 self._good = not value
135 106
107 @property
108 def test_run_count(self):
109 return max(
110 len(self.valueset_paths),
111 len(self.chartjson_paths),
112 len(self.return_codes))
113
136 def start_job(self): 114 def start_job(self):
137 """Starts a build, or a test job if the build is available.""" 115 api = self.bisector.api
138 if self.status == RevisionState.NEW and not self._is_build_archived(): 116 try:
139 self._request_build() 117 if not self._is_build_archived():
140 self.status = RevisionState.BUILDING 118 self._request_build()
141 return 119 with api.m.step.nest('Waiting for build'):
120 while not self._is_build_archived():
121 api.m.python.inline(
122 'sleeping',
123 """
124 import sys
125 import time
126 time.sleep(20*60)
127 sys.exit(0)
128 """)
129 if self._is_build_failed():
130 self.failed = True
131 return
142 132
143 if self._is_build_archived() and self.status in (
144 RevisionState.NEW, RevisionState.BUILDING,
145 RevisionState.NEED_MORE_DATA):
146 self._do_test() 133 self._do_test()
147 self.status = RevisionState.TESTING 134 # TODO(robertocn): Add a test to remove this CL
135 while not self._check_revision_good(): # pragma: no cover
136 min(self, self.bisector.lkgr, self.bisector.fkbr,
137 key=lambda(x): x.test_run_count)._do_test()
138
139 except exceptions.UntestableRevisionException:
140 self.failed = True
148 141
149 def deps_change(self): 142 def deps_change(self):
150 """Uses `git show` to see if a given commit contains a DEPS change.""" 143 """Uses `git show` to see if a given commit contains a DEPS change."""
151 # Avoid checking DEPS changes for dependency repo revisions. 144 # Avoid checking DEPS changes for dependency repo revisions.
152 # crbug.com/580681 145 # crbug.com/580681
153 if self.needs_patch: # pragma: no cover 146 if self.needs_patch: # pragma: no cover
154 return False 147 return False
155 api = self.bisector.api 148 api = self.bisector.api
156 working_dir = api.working_dir 149 working_dir = api.working_dir
157 cwd = working_dir.join( 150 cwd = working_dir.join(
158 depot_config.DEPOT_DEPS_NAME[self.depot_name]['src']) 151 depot_config.DEPOT_DEPS_NAME[self.depot_name]['src'])
159 name = 'Checking DEPS for ' + self.commit_hash 152 name = 'Checking DEPS for ' + self.commit_hash
160 step_result = api.m.git( 153 step_result = api.m.git(
161 'show', '--name-only', '--pretty=format:', 154 'show', '--name-only', '--pretty=format:',
162 self.commit_hash, cwd=cwd, stdout=api.m.raw_io.output(), name=name) 155 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'): 156 step_test_data=lambda: api._test_data['deps_change'][self.commit_hash]
164 return False 157 )
165 if 'DEPS' in step_result.stdout.splitlines(): # pragma: no cover 158 if 'DEPS' in step_result.stdout.splitlines(): # pragma: no cover
166 return True 159 return True
167 return False # pragma: no cover 160 return False # pragma: no cover
168 161
169 def _gen_deps_local_scope(self): 162 def _gen_deps_local_scope(self):
170 """Defines the Var and From functions in a dict for calling exec. 163 """Defines the Var and From functions in a dict for calling exec.
171 164
172 This is needed for executing the DEPS file. 165 This is needed for executing the DEPS file.
173 """ 166 """
174 deps_data = { 167 deps_data = {
175 'Var': lambda _: deps_data['vars'][_], 168 'Var': lambda _: deps_data['vars'][_],
176 'From': lambda *args: None, 169 'From': lambda *args: None,
177 } 170 }
178 return deps_data 171 return deps_data
179 172
180 def _read_content(self, url, file_name, branch): # pragma: no cover 173 def _read_content(self, url, file_name, branch): # pragma: no cover
181 """Uses gitiles recipe module to download and read file contents.""" 174 """Uses gitiles recipe module to download and read file contents."""
182 try: 175 try:
183 return self.bisector.api.m.gitiles.download_file( 176 return self.bisector.api.m.gitiles.download_file(
184 repository_url=url, file_path=file_name, branch=branch) 177 repository_url=url, file_path=file_name, branch=branch)
185 except TypeError: 178 except TypeError:
186 print 'Could not read content for %s/%s/%s' % (url, file_name, branch) 179 print 'Could not read content for %s/%s/%s' % (url, file_name, branch)
187 return None 180 return None
188 181
189 def read_deps(self, recipe_tester_name): 182 def read_deps(self, recipe_tester_name):
190 """Sets the dependencies for this revision from the contents of DEPS.""" 183 """Sets the dependencies for this revision from the contents of DEPS."""
191 api = self.bisector.api 184 api = self.bisector.api
192 if self.deps:
193 return
194 if self.bisector.internal_bisect: # pragma: no cover 185 if self.bisector.internal_bisect: # pragma: no cover
195 self.deps_file_contents = self._read_content( 186 self.deps_file_contents = self._read_content(
196 depot_config.DEPOT_DEPS_NAME[self.depot_name]['url'], 187 depot_config.DEPOT_DEPS_NAME[self.depot_name]['url'],
197 depot_config.DEPOT_DEPS_NAME[self.depot_name]['deps_file'], 188 depot_config.DEPOT_DEPS_NAME[self.depot_name]['deps_file'],
198 self.commit_hash) 189 self.commit_hash)
199 # On April 5th, 2016 .DEPS.git was changed to DEPS on android-chrome repo, 190 # 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. 191 # we are doing this in order to support both deps files.
201 if not self.deps_file_contents: 192 if not self.deps_file_contents:
202 self.deps_file_contents = self._read_content( 193 self.deps_file_contents = self._read_content(
203 depot_config.DEPOT_DEPS_NAME[self.depot_name]['url'], 194 depot_config.DEPOT_DEPS_NAME[self.depot_name]['url'],
204 depot_config.DEPS_FILENAME, 195 depot_config.DEPS_FILENAME,
205 self.commit_hash) 196 self.commit_hash)
206 else: 197 else:
207 step_result = api.m.python( 198 step_result = api.m.python(
208 'fetch file %s:%s' % (self.commit_hash, depot_config.DEPS_FILENAME), 199 'fetch file %s:%s' % (self.commit_hash, depot_config.DEPS_FILENAME),
209 api.resource('fetch_file.py'), 200 api.resource('fetch_file.py'),
210 [depot_config.DEPS_FILENAME, '--commit', self.commit_hash], 201 [depot_config.DEPS_FILENAME, '--commit', self.commit_hash],
211 stdout=api.m.raw_io.output()) 202 stdout=api.m.raw_io.output(),
203 step_test_data=lambda: api._test_data['deps'][self.commit_hash]
204 )
212 self.deps_file_contents = step_result.stdout 205 self.deps_file_contents = step_result.stdout
213 try: 206 try:
214 deps_data = self._gen_deps_local_scope() 207 deps_data = self._gen_deps_local_scope()
215 exec(self.deps_file_contents or 'deps = {}', {}, deps_data) 208 exec (self.deps_file_contents or 'deps = {}') in {}, deps_data
216 deps_data = deps_data['deps'] 209 deps_data = deps_data['deps']
217 except ImportError: # pragma: no cover 210 except ImportError: # pragma: no cover
218 # TODO(robertocn): Implement manual parsing of DEPS when exec fails. 211 # TODO(robertocn): Implement manual parsing of DEPS when exec fails.
219 raise NotImplementedError('Path not implemented to manually parse DEPS') 212 raise NotImplementedError('Path not implemented to manually parse DEPS')
220 213
221 revision_regex = re.compile('.git@(?P<revision>[a-fA-F0-9]+)') 214 revision_regex = re.compile('.git@(?P<revision>[a-fA-F0-9]+)')
222 results = {} 215 results = {}
223 for depot_name, depot_data in depot_config.DEPOT_DEPS_NAME.iteritems(): 216 for depot_name, depot_data in depot_config.DEPOT_DEPS_NAME.iteritems():
224 if (depot_data.get('platform') and 217 if (depot_data.get('platform') and
225 depot_data.get('platform') not in recipe_tester_name.lower()): 218 depot_data.get('platform') not in recipe_tester_name.lower()):
(...skipping 10 matching lines...) Expand all
236 else: # pragma: no cover 229 else: # pragma: no cover
237 warning_text = ('Could not parse revision for %s while bisecting ' 230 warning_text = ('Could not parse revision for %s while bisecting '
238 '%s' % (depot_name, self.depot)) 231 '%s' % (depot_name, self.depot))
239 if warning_text not in self.bisector.warnings: 232 if warning_text not in self.bisector.warnings:
240 self.bisector.warnings.append(warning_text) 233 self.bisector.warnings.append(warning_text)
241 else: 234 else:
242 results[depot_name] = None 235 results[depot_name] = None
243 self.deps = results 236 self.deps = results
244 return 237 return
245 238
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): 239 def _is_build_archived(self):
273 """Checks if the revision is already built and archived.""" 240 """Checks if the revision is already built and archived."""
274 if not self.build_archived: 241 if not self.build_archived:
275 api = self.bisector.api 242 api = self.bisector.api
276 self.build_archived = api.gsutil_file_exists(self.build_url) 243 self.build_archived = api.gsutil_file_exists(
277 244 self.build_url,
278 if self.bisector.dummy_builds: 245 step_test_data=lambda: api._test_data.get(
279 self.build_archived = self.in_progress 246 'gsutil_exists', {}).get(self.commit_hash).pop()
247 if api._test_data.get('gsutil_exists', {}).get(self.commit_hash)
248 else collections.namedtuple('retcode_attr', ['retcode'])(0))
280 249
281 return self.build_archived 250 return self.build_archived
282 251
283 def _is_build_failed(self): 252 def _is_build_failed(self):
284 api = self.bisector.api 253 api = self.bisector.api
285 result = api.m.buildbucket.get_build( 254 result = api.m.buildbucket.get_build(
286 self.build_id, 255 self.build_id,
287 api.m.service_account.get_json_path(api.SERVICE_ACCOUNT), 256 api.m.service_account.get_json_path(api.SERVICE_ACCOUNT),
288 step_test_data=lambda: api.m.json.test_api.output_stream( 257 step_test_data=lambda: api.test_api.buildbot_job_status_mock(
289 {'build': {'result': 'SUCCESS', 'status': 'COMPLETED'}} 258 api._test_data.get('build_status', {}).get(self.commit_hash, [])))
290 ))
291 return (result.stdout['build']['status'] == 'COMPLETED' and 259 return (result.stdout['build']['status'] == 'COMPLETED' and
292 result.stdout['build'].get('result') != 'SUCCESS') 260 result.stdout['build'].get('result') != 'SUCCESS')
293 261
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): 262 def _gs_suffix(self):
303 """Provides the expected right half of the build filename. 263 """Provides the expected right half of the build filename.
304 264
305 This takes into account whether the build has a deps patch. 265 This takes into account whether the build has a deps patch.
306 """ 266 """
307 top_revision = self 267 top_revision = self
308 while top_revision.base_revision: 268 while top_revision.base_revision:
309 top_revision = top_revision.base_revision 269 top_revision = top_revision.base_revision
310 name_parts = [top_revision.commit_hash] 270 name_parts = [top_revision.commit_hash]
311 if self.needs_patch: 271 if self.needs_patch:
312 name_parts.append(self.deps_sha) 272 name_parts.append(self.deps_sha)
313 return '%s.zip' % '_'.join(name_parts) 273 return '%s.zip' % '_'.join(name_parts)
314 274
315 def _read_test_results(self, check_revision_goodness=True): 275 def _read_test_results(self, results):
316 """Gets the test results from GS and checks if the rev is good or bad.""" 276 # Results will be a dictionary containing path to chartjsons, paths to
317 test_results = self._get_test_results() 277 # valueset, list of return codes.
318 # Results will contain the keys 'results' and 'output' where output is the 278 self.return_codes.extend(results.get('retcodes', []))
319 # stdout of the command, and 'results' is itself a dict with the key 279 if results.get('errors'): # pragma: no cover
320 # 'values' unless the test failed, in which case 'results' will contain 280 self.failed = True
321 # the 'error' key explaining the type of error. 281 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') 282 self.bisector.surface_result('MISSING_METRIC')
327 return 283 raise exceptions.UntestableRevisionException(results['errors'])
328 self.values += results['values'] 284 elif self.bisector.is_return_code_mode():
329 api = self.bisector.api 285 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: 286 else:
353 self.status = RevisionState.FAILED 287 self.valueset_paths.extend(results.get('valueset_paths'))
354 self.bisector.surface_result('MISSING_METRIC') 288 self.chartjson_paths.extend(results.get('chartjson_paths'))
355 return 289 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 290 raise exceptions.UntestableRevisionException(
357 # recheck it. 291 '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 292
368 def _request_build(self): 293 def _request_build(self):
369 """Posts a request to buildbot to build this revision and archive it.""" 294 """Posts a request to buildbot to build this revision and archive it."""
370 api = self.bisector.api 295 api = self.bisector.api
371 bot_name = self.bisector.get_builder_bot_for_this_platform() 296 bot_name = self.bisector.get_builder_bot_for_this_platform()
372 297
373 # To allow multiple nested levels, we go to the topmost revision. 298 # To allow multiple nested levels, we go to the topmost revision.
374 top_revision = self 299 top_revision = self
375 while top_revision.base_revision: 300 while top_revision.base_revision:
376 top_revision = top_revision.base_revision 301 top_revision = top_revision.base_revision
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after
424 self._test_config = result 349 self._test_config = result
425 return result 350 return result
426 351
427 def _do_test(self): 352 def _do_test(self):
428 """Triggers tests for a revision, either locally or via try job. 353 """Triggers tests for a revision, either locally or via try job.
429 354
430 If local testing is enabled (i.e. director/tester merged) then 355 If local testing is enabled (i.e. director/tester merged) then
431 the test will be run on the same machine. Otherwise, this posts 356 the test will be run on the same machine. Otherwise, this posts
432 a request to buildbot to download and perf-test this build. 357 a request to buildbot to download and perf-test this build.
433 """ 358 """
434 if self.bisector.bisect_config.get('dummy_job_names'): 359 if self.test_run_count: # pragma: no cover
435 self.job_name = self.commit_hash + '-test' 360 self.repeat_count = max(MINIMUM_SAMPLE_SIZE, math.ceil(
436 else: # pragma: no cover 361 self.test_run_count * 1.5)) - self.test_run_count
437 self.job_name = uuid.uuid4().hex 362
438 api = self.bisector.api 363 api = self.bisector.api
439 # Stores revision map for different repos eg, android-chrome, src, v8 etc. 364 # Stores revision map for different repos eg, android-chrome, src, v8 etc.
440 revision_ladder = {} 365 revision_ladder = {}
441 top_revision = self 366 top_revision = self
442 revision_ladder[top_revision.depot_name] = top_revision.commit_hash 367 revision_ladder[top_revision.depot_name] = top_revision.commit_hash
443 while top_revision.base_revision: # pragma: no cover 368 while top_revision.base_revision: # pragma: no cover
444 revision_ladder[top_revision.depot_name] = top_revision.commit_hash 369 revision_ladder[top_revision.depot_name] = top_revision.commit_hash
445 top_revision = top_revision.base_revision 370 top_revision = top_revision.base_revision
446 perf_test_properties = { 371 perf_test_properties = {
447 'builder_name': self.bisector.get_perf_tester_name(),
448 'properties': { 372 'properties': {
449 'revision': top_revision.commit_hash, 373 'revision': top_revision.commit_hash,
450 'parent_got_revision': top_revision.commit_hash, 374 'parent_got_revision': top_revision.commit_hash,
451 'parent_build_archive_url': self.build_url, 375 'parent_build_archive_url': self.build_url,
452 'bisect_config': self._get_bisect_config_for_tester(), 376 'bisect_config': self._get_bisect_config_for_tester(),
453 'job_name': self.job_name,
454 'revision_ladder': revision_ladder, 377 'revision_ladder': revision_ladder,
455 }, 378 },
456 } 379 }
457 self.test_results_url = (self.bisector.api.GS_RESULTS_URL + 380 skip_download = self.bisector.last_tested_revision == self
458 self.job_name + '.results') 381 self.bisector.last_tested_revision = self
459 if (api.m.bisect_tester.local_test_enabled() or 382 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 383
469 def retest(self): # pragma: no cover 384 def run_test_step_test_data():
470 # We need at least 5 samples for applying Mann-Whitney U test 385 """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 386
478 def _get_test_results(self): 387 These are expected to be populated by the test_api.
479 """Tries to get the results of a test job from cloud storage.""" 388 """
480 api = self.bisector.api 389 if api._test_data['run_results'].get(self.commit_hash):
481 try: 390 return api._test_data['run_results'][self.commit_hash].pop(0)
482 stdout = api.m.raw_io.output() 391 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 392
495 def _check_revision_good(self): 393 self._read_test_results(api.run_local_test_run(
394 overrides, skip_download=skip_download,
395 step_test_data=run_test_step_test_data
396 ))
397
398 def _check_revision_good(self): # pragma: no cover
496 """Determines if a revision is good or bad. 399 """Determines if a revision is good or bad.
497 400
498 Iteratively increment the sample size of the revision being tested, the last 401 Returns:
499 known good revision, and the first known bad revision until a relationship 402 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 403 determined from the available data.
501 revision being tested and one of the other two. 404 """
405 # Do not reclassify revisions. Important for reference range.
406 if self.good or self.bad:
407 return True
502 408
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 409 lkgr = self.bisector.lkgr
515 fkbr = self.bisector.fkbr 410 fkbr = self.bisector.fkbr
411 if self.bisector.is_return_code_mode():
412 if self.overall_return_code == lkgr.overall_return_code:
413 self.good = True
414 else:
415 self.bad = True
416 return True
417 diff_from_good = self.bisector.compare_revisions(self, lkgr)
418 diff_from_bad = self.bisector.compare_revisions(self, fkbr)
419 if diff_from_good == False and diff_from_bad == False:
420 # We have reached the max number of samples and have not established
421 # difference, give up.
422 raise exceptions.InconclusiveBisectException()
423 if diff_from_good and diff_from_bad:
424 # Multiple regressions.
425 # For now, proceed bisecting the biggest difference of the means.
426 dist_from_good = abs(self.mean - lkgr.mean)
427 dist_from_bad = abs(self.mean - fkbr.mean)
428 if dist_from_good > dist_from_bad:
429 # TODO(robertocn): Add way to handle the secondary regression
430 #self.bisector.handle_secondary_regression(self, fkbr)
431 self.bad = True
432 return True
433 else:
434 #self.bisector.handle_secondary_regression(lkgr, self)
435 self.good = True
436 return True
437 if diff_from_good:
438 self.bad = True
439 return True
440 elif diff_from_bad:
441 self.good = True
442 return True
443 return False
516 444
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 445
544 def revision_string(self): 446 def revision_string(self):
545 if self._rev_str: 447 if self._rev_str:
546 return self._rev_str 448 return self._rev_str
547 result = '' 449 result = ''
548 if self.base_revision: # pragma: no cover 450 if self.base_revision: # pragma: no cover
549 result += self.base_revision.revision_string() + ',' 451 result += self.base_revision.revision_string() + ','
550 commit = self.commit_hash[:10] 452 commit = self.commit_hash[:10]
551 if self.depot_name == 'chromium': 453 if self.depot_name == 'chromium':
552 try: 454 try:
553 commit = str(self.bisector.api.m.commit_position 455 commit = str(self.bisector.api.m.commit_position
554 .chromium_commit_position_from_hash(self.commit_hash)) 456 .chromium_commit_position_from_hash(self.commit_hash))
555 except self.bisector.api.m.step.StepFailure: 457 except self.bisector.api.m.step.StepFailure:
556 pass # Failure to resolve a commit position is no reason to break. 458 pass # Failure to resolve a commit position is no reason to break.
557 result += '%s@%s' % (self.depot_name, commit) 459 result += '%s@%s' % (self.depot_name, commit)
558 self._rev_str = result 460 self._rev_str = result
559 return self._rev_str 461 return self._rev_str
560 462
561 def _next_retest(self): # pragma: no cover 463 def __repr__(self): # pragma: no cover
562 """Chooses one of current, lkgr, fkbr to retest. 464 if not self.test_run_count:
465 return ('RevisionState(rev=%s), values=[]' % self.revision_string())
466 if self.bisector.is_return_code_mode():
467 return ('RevisionState(rev=%s, mean=%r, overall_return_code=%r, '
468 'std_dev=%r)') % (self.revision_string(), self.mean,
469 self.overall_return_code, self.std_dev)
470 return ('RevisionState(rev=%s, mean_value=%r, std_dev=%r)' % (
471 self.revision_string(), self.mean, self.std_dev))
563 472
564 Look for the smallest sample and retest that. If the last tested revision 473 @property
565 is tied for the smallest sample, use that to take advantage of the fact 474 def overall_return_code(self):
566 that it is already downloaded and unzipped. 475 if self.bisector.is_return_code_mode():
567 """ 476 if self.return_codes:
568 next_revision_to_test = min(self.bisector.lkgr, self, self.bisector.fkbr, 477 if max(self.return_codes):
569 key=lambda x: len(x.values)) 478 return 1
570 if (len(self.bisector.last_tested_revision.values) == 479 return 0
571 next_revision_to_test.values): 480 raise ValueError('overall_return_code needs non-empty sample'
572 self.bisector.last_tested_revision.retest() 481 ) # pragma: no cover
573 else: 482 raise ValueError('overall_return_code only applies to return_code bisects'
574 next_revision_to_test.retest() 483 ) # 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))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698