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

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