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

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: Addressing all early 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 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
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
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))
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698