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

Side by Side Diff: scripts/slave/recipes/findit/chromium/compile.py

Issue 1869223002: [Findit] Use results from heuristic analysis to do faster culprit finding. (Closed) Base URL: https://chromium.googlesource.com/chromium/tools/build.git@setup_local_test
Patch Set: Fix nit. Created 4 years, 8 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
« no previous file with comments | « no previous file | scripts/slave/recipes/findit/chromium/compile.expected/compile_affected_targets_only.json » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 import json 5 import json
6 6
7 from recipe_engine.config import List 7 from recipe_engine.config import List
8 from recipe_engine.config import Single 8 from recipe_engine.config import Single
9 from recipe_engine.recipe_api import Property 9 from recipe_engine.recipe_api import Property
10 10
11 11
12 DEPS = [ 12 DEPS = [
13 'chromium', 13 'chromium',
14 'chromium_tests', 14 'chromium_tests',
15 'findit', 15 'findit',
16 'depot_tools/gclient', 16 'depot_tools/gclient',
17 'recipe_engine/json', 17 'recipe_engine/json',
18 'recipe_engine/path', 18 'recipe_engine/path',
19 'recipe_engine/platform', 19 'recipe_engine/platform',
20 'recipe_engine/properties', 20 'recipe_engine/properties',
21 'recipe_engine/python', 21 'recipe_engine/python',
22 'recipe_engine/raw_io',
22 'recipe_engine/step', 23 'recipe_engine/step',
23 ] 24 ]
24 25
25 26
26 PROPERTIES = { 27 PROPERTIES = {
27 'target_mastername': Property( 28 'target_mastername': Property(
28 kind=str, help='The target master to match compile config to.'), 29 kind=str, help='The target master to match compile config to.'),
29 'target_buildername': Property( 30 'target_buildername': Property(
30 kind=str, help='The target builder to match compile config to.'), 31 kind=str, help='The target builder to match compile config to.'),
31 'good_revision': Property( 32 'good_revision': Property(
32 kind=str, help='The last known good chromium revision.'), 33 kind=str, help='The last known good chromium revision.'),
33 'bad_revision': Property( 34 'bad_revision': Property(
34 kind=str, help='The first known bad chromium revision.'), 35 kind=str, help='The first known bad chromium revision.'),
35 'compile_targets': Property( 36 'compile_targets': Property(
36 kind=List(basestring), default=None, 37 kind=List(basestring), default=None,
37 help='The failed compile targets, eg: browser_tests, ' 38 help='The failed compile targets, eg: browser_tests, '
38 'obj/path/to/source.o, gen/path/to/generated.cc, etc.'), 39 'obj/path/to/source.o, gen/path/to/generated.cc, etc.'),
39 'use_analyze': Property( 40 'use_analyze': Property(
40 kind=Single(bool, empty_val=False, required=False), default=True, 41 kind=Single(bool, empty_val=False, required=False), default=True,
41 help='Use analyze to filter out affected targets.'), 42 help='Use analyze to filter out affected targets.'),
43 'suspected_revisions': Property(
44 kind=List(basestring), default=[],
45 help='A list of suspected revisions from heuristic analysis.'),
42 } 46 }
43 47
44 48
45 class CompileResult(object): 49 class CompileResult(object):
46 SKIPPED = 'skipped' # No compile is needed. 50 SKIPPED = 'skipped' # No compile is needed.
47 PASSED = 'passed' # Compile passed. 51 PASSED = 'passed' # Compile passed.
48 FAILED = 'failed' # Compile failed. 52 FAILED = 'failed' # Compile failed.
53 INFRA_FAILED = 'infra_failed' # Infra failed.
49 54
50 55
51 def _run_compile_at_revision(api, target_mastername, target_buildername, 56 def _run_compile_at_revision(api, target_mastername, target_buildername,
52 revision, compile_targets, use_analyze): 57 revision, compile_targets, use_analyze):
53 with api.step.nest('test %s' % str(revision)): 58 with api.step.nest('test %s' % str(revision)):
54 # Checkout code at the given revision to recompile. 59 # Checkout code at the given revision to recompile.
55 bot_config = api.chromium_tests.create_bot_config_object( 60 bot_config = api.chromium_tests.create_bot_config_object(
56 target_mastername, target_buildername) 61 target_mastername, target_buildername)
57 bot_update_step, bot_db = api.chromium_tests.prepare_checkout( 62 bot_update_step, bot_db = api.chromium_tests.prepare_checkout(
58 bot_config, root_solution_revision=revision) 63 bot_config, root_solution_revision=revision)
(...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after
100 override_bot_type='builder_tester') 105 override_bot_type='builder_tester')
101 return CompileResult.PASSED 106 return CompileResult.PASSED
102 except api.step.InfraFailure: 107 except api.step.InfraFailure:
103 raise 108 raise
104 except api.step.StepFailure: 109 except api.step.StepFailure:
105 return CompileResult.FAILED 110 return CompileResult.FAILED
106 111
107 112
108 def RunSteps(api, target_mastername, target_buildername, 113 def RunSteps(api, target_mastername, target_buildername,
109 good_revision, bad_revision, 114 good_revision, bad_revision,
110 compile_targets, use_analyze): 115 compile_targets, use_analyze, suspected_revisions):
111 bot_config = api.chromium_tests.create_bot_config_object( 116 bot_config = api.chromium_tests.create_bot_config_object(
112 target_mastername, target_buildername) 117 target_mastername, target_buildername)
113 api.chromium_tests.configure_build( 118 api.chromium_tests.configure_build(
114 bot_config, override_bot_type='builder_tester') 119 bot_config, override_bot_type='builder_tester')
115 120
116 # Sync to bad revision, and retrieve revisions in the regression range. 121 # Sync to bad revision, and retrieve revisions in the regression range.
117 api.chromium_tests.prepare_checkout( 122 api.chromium_tests.prepare_checkout(
118 bot_config, 123 bot_config,
119 root_solution_revision=bad_revision) 124 root_solution_revision=bad_revision)
120 revisions_to_check = api.findit.revisions_between(good_revision, bad_revision) 125
126 # Retrieve revisions in the regression range. The returned revisions are in
127 # order from oldest to newest.
128 all_revisions = api.findit.revisions_between(good_revision, bad_revision)
129
130 # If suspected revisions are provided, divide the entire regression range into
131 # a list of smaller sub-ranges. Because only a failure immediately following a
132 # pass could identify the culprit, we rerun compile at the revision right
133 # before a suspected revision and then at the suspected revision itself. So a
134 # sub-range starts at the revision right before a suspected revision.
135 #
136 # Normally, heuristic analysis provides only 1 suspected revision and there
137 # will be 2 sub-ranges. Example (previous build cycle passed at r0):
138 # Entire regression range: [r1, r2, r3, r4, ..., r10]
139 # Suspected revisions: [r5]
140 # Then the sub-ranges are:
141 # sub-range1: r4 and [r5, r6, ..., r10]
142 # sub-range2: None and [r1, r2, r3]
143 # In this example, compile is run at r4 first, and there will be a few cases:
144 # 1) if r4 passes, the culprit is in [r5, r6, ..., r10]. Compile should be
145 # rerun in order from r5 to r10.
146 # 1.1) if a failure occurs at rN (5<=N<=10), rN is the actual culprit
147 # because it is the first failure after a series of pass.
148 # 1.2) if no failure occurs, the compile failure is a flaky one. This
149 # sometimes happens and the compile log shows no error while the
150 # step ran into an exception.
151 # 2) if r4 fails, the culprit is either r4 itself or one of [r1, r2, r3].
152 # Compile should be rerun in order from r1 to r3. No compile is run at
153 # r0, because it is the last known good revision.
154 # 2.1) if a failure occurs at rN (1<=N<=3), rN is the actual culprit.
155 # 2.2) if no failure occurs, r4 is the actual culprit instead.
156 #
157 # Occasionally, heuristic analysis provides 2+ suspected revisions (e.g. there
158 # are conflicting commits). In this case, there will be 3+ sub-ranges.
159 # For the above example, if the suspected revisions are [r5, r8], there will
160 # be three sub-ranges:
161 # sub-range1: r7 and [r8, r9, r10]
162 # sub-range2: r4 and [r5, r6]
163 # sub-range3: None and [r1, r2, r3]
164 # Sub-ranges with newer revisions are tested first (sub-range1 -> sub-range2
165 # -> sub-range3), because it is more likely that a newer revision is the
166 # beginning of the compile breakage.
167 suspected_revision_index = [
168 all_revisions.index(r)
169 for r in set(suspected_revisions) if r in all_revisions]
170 if suspected_revision_index:
171 sub_ranges = []
172 remaining_revisions = all_revisions[:]
173 for index in sorted(suspected_revision_index, reverse=True):
174 if index > 0:
175 sub_ranges.append(remaining_revisions[index - 1:])
176 remaining_revisions = remaining_revisions[:index - 1]
177 # None is a placeholder for the last known good revision.
178 sub_ranges.append([None] + remaining_revisions)
179 else:
180 # Treat the entire regression range as a single sub-range.
181 sub_ranges = [[None] + all_revisions]
121 182
122 compile_results = {} 183 compile_results = {}
123 try_job_metadata = { 184 try_job_metadata = {
124 'regression_range_size': len(revisions_to_check) 185 'regression_range_size': len(all_revisions),
186 'sub_ranges': sub_ranges[:],
125 } 187 }
126 report = { 188 report = {
127 'result': compile_results, 189 'result': compile_results,
128 'metadata': try_job_metadata, 190 'metadata': try_job_metadata,
129 } 191 }
130 192
193 suspected_revision = None
194 revision_being_checked = None
195 found = False
131 try: 196 try:
132 for current_revision in revisions_to_check: 197 while not found and sub_ranges:
133 last_revision = None 198 # Sub-ranges with newer revisions are tested first.
134 compile_result = _run_compile_at_revision( 199 first_revision = sub_ranges[0][0]
135 api, target_mastername, target_buildername, 200 following_revisions = sub_ranges[0][1:]
136 current_revision, compile_targets, use_analyze) 201 sub_ranges.pop(0)
137 202
138 compile_results[current_revision] = compile_result 203 if first_revision is not None: # No compile for last known good revision.
139 last_revision = current_revision 204 revision_being_checked = first_revision
140 if compile_result == CompileResult.FAILED: 205 compile_result = _run_compile_at_revision(
141 break # Found the culprit, no need to check later revisions. 206 api, target_mastername, target_buildername,
207 first_revision, compile_targets, use_analyze)
208 compile_results[first_revision] = compile_result
209 if compile_result == CompileResult.FAILED:
210 # The first revision of this sub-range already failed, thus either it
211 # is the culprit or the culprit is in a sub-range with older
212 # revisions.
213 suspected_revision = first_revision
214 continue
215
216 # If the first revision passed, the culprit is either in the current range
217 # or is the first revision of previous range with newer revisions as
218 # identified above.
219 for revision in following_revisions:
220 revision_being_checked = revision
221 compile_result = _run_compile_at_revision(
222 api, target_mastername, target_buildername,
223 revision, compile_targets, use_analyze)
224 compile_results[revision] = compile_result
225 if compile_result == CompileResult.FAILED:
226 # First failure after a series of pass.
227 suspected_revision = revision
228 found = True
229 break
230
231 if not found and suspected_revision is not None:
232 # If all revisions in the current range passed, and the first revision
233 # of previous range failed, the culprit is found too.
234 found = True
235 except api.step.InfraFailure:
236 compile_results[revision_being_checked] = CompileResult.INFRA_FAILED
237 raise
142 finally: 238 finally:
143 # Report the result. 239 # Report the result.
144 step_result = api.python.succeeding_step( 240 step_result = api.python.succeeding_step(
145 'report', [json.dumps(report, indent=2)], as_log='report') 241 'report', [json.dumps(report, indent=2)], as_log='report')
146 242
147 if (last_revision and 243 if found:
148 compile_results.get(last_revision) == CompileResult.FAILED): 244 step_result.presentation.step_text = (
149 step_result.presentation.step_text = '<br/>Culprit: %s' % last_revision 245 '<br/>Culprit: <a href="https://crrev.com/%s">%s</a>' % (
246 suspected_revision, suspected_revision))
150 247
151 # Set the report as a build property too, so that it will be reported back 248 # Set the report as a build property too, so that it will be reported back
152 # to Buildbucket and Findit will pull from there instead of buildbot master. 249 # to Buildbucket and Findit will pull from there instead of buildbot master.
153 step_result.presentation.properties['report'] = report 250 step_result.presentation.properties['report'] = report
154 251
155 return report 252 return report
156 253
157 254
158 def GenTests(api): 255 def GenTests(api):
159 def props(compile_targets=None, use_analyze=False): 256 def props(compile_targets=None, use_analyze=False,
257 good_revision=None, bad_revision=None,
258 suspected_revisions=None):
160 properties = { 259 properties = {
161 'mastername': 'tryserver.chromium.linux', 260 'mastername': 'tryserver.chromium.linux',
162 'buildername': 'linux_variable', 261 'buildername': 'linux_variable',
163 'slavename': 'build1-a1', 262 'slavename': 'build1-a1',
164 'buildnumber': '1', 263 'buildnumber': '1',
165 'target_mastername': 'chromium.linux', 264 'target_mastername': 'chromium.linux',
166 'target_buildername': 'Linux Builder', 265 'target_buildername': 'Linux Builder',
167 'good_revision': 'r0', 266 'good_revision': good_revision or 'r0',
168 'bad_revision': 'r1', 267 'bad_revision': bad_revision or 'r1',
169 'use_analyze': use_analyze, 268 'use_analyze': use_analyze,
170 } 269 }
171 if compile_targets: 270 if compile_targets:
172 properties['compile_targets'] = compile_targets 271 properties['compile_targets'] = compile_targets
272 if suspected_revisions:
273 properties['suspected_revisions'] = suspected_revisions
173 return api.properties(**properties) + api.platform.name('linux') 274 return api.properties(**properties) + api.platform.name('linux')
174 275
175 yield ( 276 yield (
176 api.test('compile_specified_targets') + 277 api.test('compile_specified_targets') +
177 props(compile_targets=['target_name']) + 278 props(compile_targets=['target_name']) +
178 api.override_step_data('test r1.check_targets', 279 api.override_step_data('test r1.check_targets',
179 api.json.output({ 280 api.json.output({
180 'found': ['target_name'], 281 'found': ['target_name'],
181 'not_found': [], 282 'not_found': [],
182 })) 283 }))
(...skipping 127 matching lines...) Expand 10 before | Expand all | Expand 10 after
310 })) + 411 })) +
311 api.override_step_data( 412 api.override_step_data(
312 'test r1.analyze', 413 'test r1.analyze',
313 api.json.output({ 414 api.json.output({
314 'status': 'Found dependency', 415 'status': 'Found dependency',
315 'compile_targets': ['a', 'a_run'], 416 'compile_targets': ['a', 'a_run'],
316 'test_targets': ['a', 'a_run'], 417 'test_targets': ['a', 'a_run'],
317 }) 418 })
318 ) 419 )
319 ) 420 )
421
422 # Entire regression range: (r1, r6]
423 # Suspected_revisions: [r4]
424 # Expected smaller ranges: [r3, [r4, r5, r6]], [None, [r2]]
425 # Actual culprit: r4
426 # Should only run compile on r3, and then r4.
427 yield (
428 api.test('find_culprit_in_middle_of_a_sub_range') +
429 props(compile_targets=['target_name'],
430 good_revision='r1',
431 bad_revision='r6',
432 suspected_revisions=['r4']) +
433 api.override_step_data(
434 'git commits in range',
435 api.raw_io.stream_output(
436 '\n'.join('r%d' % i for i in reversed(range(2, 7))))) +
437 api.override_step_data('test r3.check_targets',
438 api.json.output({
439 'found': ['target_name'],
440 'not_found': [],
441 })) +
442 api.override_step_data('test r4.check_targets',
443 api.json.output({
444 'found': ['target_name'],
445 'not_found': [],
446 })) +
447 api.override_step_data('test r4.compile', retcode=1)
448 )
449
450 # Entire regression range: (r1, r6]
451 # Suspected_revisions: [r4]
452 # Expected smaller ranges: [r3, [r4, r5, r6]], [None, [r2]]
453 # Actual culprit: r3
454 # Should only run compile on r3, and then r2.
455 yield (
456 api.test('find_culprit_at_first_revision_of_a_sub_range') +
457 props(compile_targets=['target_name'],
458 good_revision='r1',
459 bad_revision='r6',
460 suspected_revisions=['r4']) +
461 api.override_step_data(
462 'git commits in range',
463 api.raw_io.stream_output(
464 '\n'.join('r%d' % i for i in reversed(range(2, 7))))) +
465 api.override_step_data('test r3.check_targets',
466 api.json.output({
467 'found': ['target_name'],
468 'not_found': [],
469 })) +
470 api.override_step_data('test r3.compile', retcode=1) +
471 api.override_step_data('test r2.check_targets',
472 api.json.output({
473 'found': ['target_name'],
474 'not_found': [],
475 }))
476 )
477
478 # Entire regression range: (r1, r10]
479 # Suspected_revisions: [r4, r8]
480 # Expected smaller ranges:
481 # [r7, [r8, r9, r10]], [r3, [r4, r5, r6]], [None, [r2]]
482 # Actual culprit: r4
483 # Should only run compile on r7(failed), then r3(pass) and r4(failed).
484 yield (
485 api.test('find_culprit_in_second_sub_range') +
486 props(compile_targets=['target_name'],
487 good_revision='r1',
488 bad_revision='r6',
489 suspected_revisions=['r4', 'r8']) +
490 api.override_step_data(
491 'git commits in range',
492 api.raw_io.stream_output(
493 '\n'.join('r%d' % i for i in reversed(range(2, 11))))) +
494 api.override_step_data('test r7.check_targets',
495 api.json.output({
496 'found': ['target_name'],
497 'not_found': [],
498 })) +
499 api.override_step_data('test r7.compile', retcode=1) +
500 api.override_step_data('test r3.check_targets',
501 api.json.output({
502 'found': ['target_name'],
503 'not_found': [],
504 })) +
505 api.override_step_data('test r4.check_targets',
506 api.json.output({
507 'found': ['target_name'],
508 'not_found': [],
509 })) +
510 api.override_step_data('test r4.compile', retcode=1)
511 )
512
513 # Entire regression range: (r1, r5]
514 # Suspected_revisions: [r2]
515 # Expected smaller ranges:
516 # [None, r2, r3, r4, r5]
517 # Actual culprit: r2
518 # Should only run compile on r2(failed).
519 yield (
520 api.test('find_culprit_as_first_revision_of_entire_range') +
521 props(compile_targets=['target_name'],
522 good_revision='r1',
523 bad_revision='r5',
524 suspected_revisions=['r2']) +
525 api.override_step_data(
526 'git commits in range',
527 api.raw_io.stream_output(
528 '\n'.join('r%d' % i for i in reversed(range(2, 6))))) +
529 api.override_step_data('test r2.check_targets',
530 api.json.output({
531 'found': ['target_name'],
532 'not_found': [],
533 })) +
534 api.override_step_data('test r2.compile', retcode=1)
535 )
OLDNEW
« no previous file with comments | « no previous file | scripts/slave/recipes/findit/chromium/compile.expected/compile_affected_targets_only.json » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698