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

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

Powered by Google App Engine
This is Rietveld 408576698