Chromium Code Reviews| OLD | NEW |
|---|---|
| 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 Loading... | |
| 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) |
| 125 | |
| 126 # Retrieve revisions in the regression range. The returned revisions are in | |
| 127 # order from older one to newer one. | |
|
lijeffrey
2016/04/11 23:52:58
nit: in order from oldest to newest.
stgao
2016/04/12 01:29:56
Done.
| |
| 120 revisions_to_check = api.findit.revisions_between(good_revision, bad_revision) | 128 revisions_to_check = api.findit.revisions_between(good_revision, bad_revision) |
| 121 | 129 |
| 130 # If suspected commits are provided, divide the big regression range into | |
| 131 # a list of smaller sub-ranges. A sub-range starts at the revision right | |
| 132 # before a suspected revision. | |
| 133 # | |
| 134 # Example (previous build cycle passed at r0): | |
| 135 # Entire regression range: [r1, r2, r3, r4, ..., r10] | |
| 136 # Suspected commits: [r5, r8] | |
| 137 # Then the sub-ranges are: | |
| 138 # sub-range1: r7 and [r8, r9, r10] | |
| 139 # sub-range2: r4 and [r5, r6, r7] | |
|
lijeffrey
2016/04/11 23:52:58
nit: shouldn't this be r4 and [r5, r6]?
stgao
2016/04/12 01:29:56
Good catch.
| |
| 140 # sub-range3: None and [r1, r2, r3] | |
| 141 # | |
| 142 # Sub-ranges with new revisions are checked first, because a compile failure | |
|
lijeffrey
2016/04/11 23:52:58
per discussion, maybe include a comment how if the
stgao
2016/04/12 01:29:56
Done. Good idea.
| |
| 143 # might be due to conflicting commits in which case the later one should be | |
| 144 # the beginning of the compile breakage. | |
| 145 # | |
| 146 # In a sub-range, we check the revision right before the suspected one (e.g. | |
| 147 # r7 and r4 above) first: | |
| 148 # * if that revision fails, the remaining revisions in the same sub-range | |
| 149 # need no checking, because there are only two cases: | |
| 150 # case 1: the culprit is that revision itself | |
| 151 # case 2: the culprit is in a sub-range with older revisions | |
| 152 # * if that revision passes, we keep checking remaining revisions in the same | |
| 153 # sub-range: | |
| 154 # case 3: if any revision fails, the culprit is found since it is the | |
| 155 # first failure after a row of pass. | |
| 156 # case 4: if no revision fails, the culprit is either case 1 above or | |
| 157 # there is no culprit and the compile failure is a flaky one. | |
| 158 # | |
| 159 # In the example above: | |
| 160 # * if r8 is the actual culprit, r7 should pass. We continue checking r8 which | |
| 161 # should fail and we find r8. | |
| 162 # * if r5 is the actual culprit, r7 should fail. We skip [r8, r9, r10], | |
| 163 # instead check r4 and r5 in the sub-range2 and find r5. | |
| 164 # * if r7 is the actual culprit, r7 should fail. We skip [r8, r9, r10], | |
| 165 # instead check revisions in the sub-range2 which should all pass, and we | |
| 166 # find r7. | |
| 167 # * if r1 is the actual culprit, we check r7 and r4 before checking the | |
|
lijeffrey
2016/04/11 23:52:58
Question: So in this case, this is the worst, beca
stgao
2016/04/12 01:29:56
If the failure is a CC or CXX compile failure, it
| |
| 168 # sub-range3 in which r0 is skipped because it is known as good revision in | |
| 169 # previous build cycle. | |
| 170 suspected_revision_index = [ | |
|
lijeffrey
2016/04/11 23:52:58
nit: remove 1 space before =
stgao
2016/04/12 01:29:56
Done.
| |
| 171 revisions_to_check.index(r) | |
| 172 for r in set(suspected_revisions) if r in revisions_to_check] | |
| 173 if suspected_revision_index: | |
| 174 sub_ranges = [] | |
| 175 remaining_revisions = revisions_to_check[:] | |
| 176 for index in sorted(suspected_revision_index, reverse=True): | |
| 177 if index > 0: | |
| 178 sub_ranges.append(remaining_revisions[index - 1:]) | |
| 179 remaining_revisions = remaining_revisions[:index - 1] | |
| 180 # None is a placeholder for the known good revision. | |
| 181 sub_ranges.append([None] + remaining_revisions) | |
| 182 else: | |
| 183 sub_ranges = [[None] + revisions_to_check] | |
| 184 | |
| 122 compile_results = {} | 185 compile_results = {} |
| 123 try_job_metadata = { | 186 try_job_metadata = { |
| 124 'regression_range_size': len(revisions_to_check) | 187 'regression_range_size': len(revisions_to_check), |
| 188 'sub_ranges': sub_ranges[:], | |
| 125 } | 189 } |
| 126 report = { | 190 report = { |
| 127 'result': compile_results, | 191 'result': compile_results, |
| 128 'metadata': try_job_metadata, | 192 'metadata': try_job_metadata, |
| 129 } | 193 } |
| 130 | 194 |
| 195 suspected_revision = None | |
| 196 revision_being_checked = None | |
| 197 found = False | |
| 131 try: | 198 try: |
| 132 for current_revision in revisions_to_check: | 199 while not found and sub_ranges: |
| 133 last_revision = None | 200 first_revision = sub_ranges[0][0] |
| 134 compile_result = _run_compile_at_revision( | 201 following_revisions = sub_ranges[0][1:] |
| 135 api, target_mastername, target_buildername, | 202 sub_ranges.pop(0) |
| 136 current_revision, compile_targets, use_analyze) | |
| 137 | 203 |
| 138 compile_results[current_revision] = compile_result | 204 if first_revision is not None: # No compile for the known good revision. |
| 139 last_revision = current_revision | 205 revision_being_checked = first_revision |
| 140 if compile_result == CompileResult.FAILED: | 206 compile_result = _run_compile_at_revision( |
| 141 break # Found the culprit, no need to check later revisions. | 207 api, target_mastername, target_buildername, |
| 208 first_revision, compile_targets, use_analyze) | |
| 209 compile_results[first_revision] = compile_result | |
| 210 if compile_result == CompileResult.FAILED: | |
| 211 # The first revision of this range already failed, thus either it is | |
| 212 # the culprit or the culprit is in a range with older revisions. | |
| 213 suspected_revision = first_revision | |
| 214 continue | |
| 215 | |
| 216 # If first revision passed, the culprit is either in the current range or | |
| 217 # is the first revision of previous range as identified above. | |
|
lijeffrey
2016/04/11 23:52:58
is this comment always true?
for example in our c
stgao
2016/04/12 01:29:56
Sorry for the confusion. I updated the wording and
| |
| 218 for revision in following_revisions: | |
| 219 revision_being_checked = revision | |
| 220 compile_result = _run_compile_at_revision( | |
| 221 api, target_mastername, target_buildername, | |
| 222 revision, compile_targets, use_analyze) | |
| 223 compile_results[revision] = compile_result | |
| 224 if compile_result == CompileResult.FAILED: | |
| 225 # First failure after a serial of pass. | |
|
lijeffrey
2016/04/11 23:52:58
nit: a series of passes
stgao
2016/04/12 01:29:56
Done.
| |
| 226 suspected_revision = revision | |
| 227 found = True | |
| 228 break | |
| 229 | |
| 230 if not found and suspected_revision is not None: | |
| 231 # If all revisions in the current range passed, and the first revision | |
| 232 # of previous range failed, the culprit is found too. | |
| 233 found = True | |
| 234 except api.step.InfraFailure: | |
| 235 compile_results[revision_being_checked] = CompileResult.INFRA_FAILED | |
| 236 raise | |
| 142 finally: | 237 finally: |
| 143 # Report the result. | 238 # Report the result. |
| 144 step_result = api.python.succeeding_step( | 239 step_result = api.python.succeeding_step( |
| 145 'report', [json.dumps(report, indent=2)], as_log='report') | 240 'report', [json.dumps(report, indent=2)], as_log='report') |
| 146 | 241 |
| 147 if (last_revision and | 242 if found: |
| 148 compile_results.get(last_revision) == CompileResult.FAILED): | 243 step_result.presentation.step_text = ( |
| 149 step_result.presentation.step_text = '<br/>Culprit: %s' % last_revision | 244 '<br/>Culprit: <a href="https://crrev.com/%s">%s</a>' % ( |
| 245 suspected_revision, suspected_revision)) | |
| 150 | 246 |
| 151 # Set the report as a build property too, so that it will be reported back | 247 # 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. | 248 # to Buildbucket and Findit will pull from there instead of buildbot master. |
| 153 step_result.presentation.properties['report'] = report | 249 step_result.presentation.properties['report'] = report |
| 154 | 250 |
| 155 return report | 251 return report |
| 156 | 252 |
| 157 | 253 |
| 158 def GenTests(api): | 254 def GenTests(api): |
| 159 def props(compile_targets=None, use_analyze=False): | 255 def props(compile_targets=None, use_analyze=False, |
| 256 good_revision=None, bad_revision=None, | |
| 257 suspected_revisions=None): | |
| 160 properties = { | 258 properties = { |
| 161 'mastername': 'tryserver.chromium.linux', | 259 'mastername': 'tryserver.chromium.linux', |
| 162 'buildername': 'linux_variable', | 260 'buildername': 'linux_variable', |
| 163 'slavename': 'build1-a1', | 261 'slavename': 'build1-a1', |
| 164 'buildnumber': '1', | 262 'buildnumber': '1', |
| 165 'target_mastername': 'chromium.linux', | 263 'target_mastername': 'chromium.linux', |
| 166 'target_buildername': 'Linux Builder', | 264 'target_buildername': 'Linux Builder', |
| 167 'good_revision': 'r0', | 265 'good_revision': good_revision or 'r0', |
| 168 'bad_revision': 'r1', | 266 'bad_revision': bad_revision or 'r1', |
| 169 'use_analyze': use_analyze, | 267 'use_analyze': use_analyze, |
| 170 } | 268 } |
| 171 if compile_targets: | 269 if compile_targets: |
| 172 properties['compile_targets'] = compile_targets | 270 properties['compile_targets'] = compile_targets |
| 271 if suspected_revisions: | |
| 272 properties['suspected_revisions'] = suspected_revisions | |
| 173 return api.properties(**properties) + api.platform.name('linux') | 273 return api.properties(**properties) + api.platform.name('linux') |
| 174 | 274 |
| 175 yield ( | 275 yield ( |
| 176 api.test('compile_specified_targets') + | 276 api.test('compile_specified_targets') + |
| 177 props(compile_targets=['target_name']) + | 277 props(compile_targets=['target_name']) + |
| 178 api.override_step_data('test r1.check_targets', | 278 api.override_step_data('test r1.check_targets', |
| 179 api.json.output({ | 279 api.json.output({ |
| 180 'found': ['target_name'], | 280 'found': ['target_name'], |
| 181 'not_found': [], | 281 'not_found': [], |
| 182 })) | 282 })) |
| (...skipping 127 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 310 })) + | 410 })) + |
| 311 api.override_step_data( | 411 api.override_step_data( |
| 312 'test r1.analyze', | 412 'test r1.analyze', |
| 313 api.json.output({ | 413 api.json.output({ |
| 314 'status': 'Found dependency', | 414 'status': 'Found dependency', |
| 315 'compile_targets': ['a', 'a_run'], | 415 'compile_targets': ['a', 'a_run'], |
| 316 'test_targets': ['a', 'a_run'], | 416 'test_targets': ['a', 'a_run'], |
| 317 }) | 417 }) |
| 318 ) | 418 ) |
| 319 ) | 419 ) |
| 420 | |
| 421 # Entire regression range: (r1, r6] | |
| 422 # Suspected_revisions: [r4] | |
| 423 # Expected smaller ranges: [r3, [r4, r5, r6]], [None, [r2]] | |
| 424 # Actual culprit: r4 | |
| 425 # Should only run compile on r3, and then r4. | |
| 426 yield ( | |
| 427 api.test('find_culprit_in_middle_of_a_sub_range') + | |
| 428 props(compile_targets=['target_name'], | |
| 429 good_revision='r1', | |
| 430 bad_revision='r6', | |
| 431 suspected_revisions=['r4']) + | |
| 432 api.override_step_data( | |
| 433 'git commits in range', | |
| 434 api.raw_io.stream_output( | |
| 435 '\n'.join('r%d' % i for i in reversed(range(2, 7))))) + | |
| 436 api.override_step_data('test r3.check_targets', | |
| 437 api.json.output({ | |
| 438 'found': ['target_name'], | |
| 439 'not_found': [], | |
| 440 })) + | |
| 441 api.override_step_data('test r4.check_targets', | |
| 442 api.json.output({ | |
| 443 'found': ['target_name'], | |
| 444 'not_found': [], | |
| 445 })) + | |
| 446 api.override_step_data('test r4.compile', retcode=1) | |
| 447 ) | |
| 448 | |
| 449 # Entire regression range: (r1, r6] | |
| 450 # Suspected_revisions: [r4] | |
| 451 # Expected smaller ranges: [r3, [r4, r5, r6]], [None, [r2]] | |
| 452 # Actual culprit: r3 | |
| 453 # Should only run compile on r3, and then r2. | |
| 454 yield ( | |
| 455 api.test('find_culprit_at_first_revision_of_a_sub_range') + | |
| 456 props(compile_targets=['target_name'], | |
| 457 good_revision='r1', | |
| 458 bad_revision='r6', | |
| 459 suspected_revisions=['r4']) + | |
| 460 api.override_step_data( | |
| 461 'git commits in range', | |
| 462 api.raw_io.stream_output( | |
| 463 '\n'.join('r%d' % i for i in reversed(range(2, 7))))) + | |
| 464 api.override_step_data('test r3.check_targets', | |
| 465 api.json.output({ | |
| 466 'found': ['target_name'], | |
| 467 'not_found': [], | |
| 468 })) + | |
| 469 api.override_step_data('test r3.compile', retcode=1) + | |
| 470 api.override_step_data('test r2.check_targets', | |
| 471 api.json.output({ | |
| 472 'found': ['target_name'], | |
| 473 'not_found': [], | |
| 474 })) | |
| 475 ) | |
| 476 | |
| 477 # Entire regression range: (r1, r10] | |
| 478 # Suspected_revisions: [r4, r8] | |
| 479 # Expected smaller ranges: | |
| 480 # [r7, [r8, r9, r10]], [r3, [r4, r5, r6]], [None, [r2]] | |
| 481 # Actual culprit: r4 | |
| 482 # Should only run compile on r7(failed), then r3(pass) and r4(failed). | |
| 483 yield ( | |
| 484 api.test('find_culprit_in_second_sub_range') + | |
| 485 props(compile_targets=['target_name'], | |
| 486 good_revision='r1', | |
| 487 bad_revision='r6', | |
| 488 suspected_revisions=['r4', 'r8']) + | |
| 489 api.override_step_data( | |
| 490 'git commits in range', | |
| 491 api.raw_io.stream_output( | |
| 492 '\n'.join('r%d' % i for i in reversed(range(2, 11))))) + | |
| 493 api.override_step_data('test r7.check_targets', | |
| 494 api.json.output({ | |
| 495 'found': ['target_name'], | |
| 496 'not_found': [], | |
| 497 })) + | |
| 498 api.override_step_data('test r7.compile', retcode=1) + | |
| 499 api.override_step_data('test r3.check_targets', | |
| 500 api.json.output({ | |
| 501 'found': ['target_name'], | |
| 502 'not_found': [], | |
| 503 })) + | |
| 504 api.override_step_data('test r4.check_targets', | |
| 505 api.json.output({ | |
| 506 'found': ['target_name'], | |
| 507 'not_found': [], | |
| 508 })) + | |
| 509 api.override_step_data('test r4.compile', retcode=1) | |
| 510 ) | |
| 511 | |
| 512 # Entire regression range: (r1, r5] | |
| 513 # Suspected_revisions: [r2] | |
| 514 # Expected smaller ranges: | |
| 515 # [None, r2, r3, r4, r5] | |
| 516 # Actual culprit: r2 | |
| 517 # Should only run compile on r2(failed). | |
| 518 yield ( | |
| 519 api.test('find_culprit_as_first_revision_of_entire_range') + | |
| 520 props(compile_targets=['target_name'], | |
| 521 good_revision='r1', | |
| 522 bad_revision='r5', | |
| 523 suspected_revisions=['r2']) + | |
| 524 api.override_step_data( | |
| 525 'git commits in range', | |
| 526 api.raw_io.stream_output( | |
| 527 '\n'.join('r%d' % i for i in reversed(range(2, 6))))) + | |
| 528 api.override_step_data('test r2.check_targets', | |
| 529 api.json.output({ | |
| 530 'found': ['target_name'], | |
| 531 'not_found': [], | |
| 532 })) + | |
| 533 api.override_step_data('test r2.compile', retcode=1) | |
| 534 ) | |
| OLD | NEW |