Skip to content
DenseChip
Learn
PracticeCode labInterview

DenseChip

Navigate

Menu

LearnPracticeCode labInterview

Tools

All toolsSV editorCode repoResume
DenseChip

Practice, courses, and chip-design tools in one focused workspace.

LearnPracticeCode labInterviewToolsLearning paths

© 2026 DenseChip

AboutContactPrivacyTerms
Skip to lesson

Course contents

  • How to Use This Course
  • Reading Paths
  • Concept Map
  • Meet DCRouter
  • Glossary
  • Part 0: What Verification Is8
  • Part 1: What Can Go Wrong11
  • Part 2: Specifications as Checkable Rules8
  • Part 3: Observability and Controllability7
  • Part 4: Stimulus11
  • Part 5: Checking11
  • Part 6: Coverage10
  • Part 7: Formal and Simulation10
  • Part 8: Verification Levels and Reuse8
  • Part 9: Planning and Risk9
  • Part 10: Emulation and Prototyping8
  • Part 11: Post-Silicon Validation8
  • Part 12: Bug Economics8
  • Part 13: Debug7
    • The Debug Loop
    • Reproducing Failures
    • Following the Wait
    • Bisection and Delta Debugging
    • Triage at Scale
    • Is It the Testbench?
    • Drills: Debug
  • Part 14: Sign-Off7
  • Part 15: Verification in Context8
  • Part 16: Case Studies5
  • Part 17: Capstones5
  • Part 18: Interview Preparation6
Loading lesson

‹Part 13: Debug
  1. Topics
  2. First Principles of Verification Strategy
  3. Part 13: Debug
  4. Bisection and Delta Debugging

Part 13 · Debug · Senior (S)

Bisection and Delta Debugging

Two searches answer most "what caused this" questions mechanically. Bisection over the change history finds the commit that broke a test in about log2(n) runs. Delta debugging over the failing test finds the few stimulus steps that matter. A runnable ddmin shrinks a 200-step failure to its 3 essential steps in 59 runs.

On this page

    Note

    Level: Senior (S) · Answers: How do we find the cause fast?

    Before this: Following the Wait

    Two questions come up with almost every regression failure. Which change broke it? The test passed last week, and 64 commits have landed since. And which part of the test matters? The failing random test is 200 stimulus steps long, most of them irrelevant. Both questions can be answered without understanding anything, by searching, and searching is something a computer does well.

    How do you find which change broke it, and which part of the test matters?

    The obvious answer, and why it breaks

    The obvious approach reads the 64 commits and guesses, or stares at all 200 steps in the waveform. Both depend on luck and on expertise in code someone else wrote. A good-and-bad pair (a passing commit and a failing one, or a full test and an empty one) is all a mechanical search needs.

    The idea: two searches

    Search

    Over

    Method

    Runs

    Bisection

    The change history between a good and a bad version

    Test the middle commit; keep the half that contains the change from pass to fail

    About log2(n): 6 runs for 64 commits

    Delta debugging (ddmin)

    The failing input (stimulus steps, transactions, instructions)

    Try removing chunks; keep any smaller input that still fails; refine the chunks (fact M8)

    Grows slowly with input size; tens of runs here

    Build it: ddmin on a failing test

    A 200-step failing test whose failure needs three particular steps, as in a sleep-and-wake bug: a packet, a long idle gap, then traffic on input 3.

    example.py
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    """Delta debugging: shrinking a failing test to the few steps that matter.
    
    A failing random test is a list of 200 stimulus steps (illustrative). The failure needs three
    particular steps to be present, in their original order: a packet to output 2 (step 37), a
    long idle gap (step 118) and a packet on input 3 (step 161), as in a sleep-and-wake bug. The
    ddmin algorithm (fact M8) repeatedly removes chunks of the test, keeping any smaller test that
    still fails. The model counts test runs and prints the size of the failing test as it shrinks.
    """
    NEEDED = {37, 118, 161}
    runs = 0
    def fails(steps):
        global runs
        runs += 1
        return NEEDED <= set(steps)
    
    def ddmin(steps):
        n, trace = 2, [(runs, len(steps))]
        while len(steps) >= 2:
            size = len(steps) // n
            chunks = [steps[i:i + size] for i in range(0, len(steps), size)]
            reduced = False
            for i in range(len(chunks)):
                complement = [s for j, c in enumerate(chunks) if j != i for s in c]
                if fails(complement):
                    steps, n, reduced = complement, max(n - 1, 2), True
                    trace.append((runs, len(steps)))
                    break
            if not reduced:
                if n >= len(steps):
                    break
                n = min(2 * n, len(steps))
        return steps, trace
    
    test = list(range(200))
    assert fails(test)
    minimal, trace = ddmin(test)
    print("test runs  failing test size")
    for r, size in trace:
        print(f"{r:9d}  {size:17d}")
    print(f"minimal failing test: steps {minimal} after {runs} runs")
    assert set(minimal) == NEEDED and runs < 200
    print(f"PASS: {runs} runs shrink a 200-step failure to the 3 steps that cause it")

    It prints:

    Diagram
    test runs  failing test size
            1                200
            5                150
            9                125
           12                100
           16                 75
           20                 63
           22                 51
           25                 39
           28                 26
           32                 20
           34                 14
           38                 11
           41                  8
           45                  6
           50                  5
           53                  4
           56                  3
    minimal failing test: steps [37, 118, 161] after 59 runs
    PASS: 59 runs shrink a 200-step failure to the 3 steps that cause it
    The model's numbers: the failing test shrinks from 200 steps to 100 after 12 runs, to 20 after 32 and to the 3 essential steps after 56 (three more runs confirm none of them can be removed).

    How to read it: early runs remove large chunks quickly (half the test is gone after 12 runs); later runs remove single steps more slowly. After 59 runs only steps 37, 118 and 161 remain, and removing any one of them makes the failure disappear. A three-step reproduction is a debug problem a person can see whole.

    Using both well

    • Bisection needs reproducibility: the same test, seed and environment at every commit (lesson 13.2).

    • Skip commits that do not build, and beware of failures that are intermittent: a flaky test misleads bisection.

    • ddmin needs a test that can run a subset of its stimulus: transaction lists, instruction sequences, packet lists.

    • Keep the failure's signature as the oracle, not just "fails": a different failure in a reduced test is not the same bug.

    • Automate both in the regression system: the bisected commit and the reduced test arrive with the bug report.

    How real teams automate it

    Practice

    Purpose

    Automatic bisection of new regression failures

    The breaking commit named without human search

    Test reduction for random and software-driven tests

    Minimal reproductions attached to bug reports

    Signatures as the reduction oracle (lesson 13.5)

    Reduction keeps the same bug

    What breaks

    A team (illustrative) ran automatic test reduction with "the test fails" as its only check. The reduced tests often failed for a different reason than the original: a missing configuration step that caused an unrelated error. Engineers debugged the wrong bug from tidy-looking reproductions. Using the failure's signature as the condition kept reductions honest.

    Why this matters

    Senior lens. Make bisection and test reduction automatic and routine: a failure should arrive with the commit that introduced it and a minimal reproduction. Both depend on reproducible runs and on a precise oracle for "the same failure".

    Interview

    Asked: "How do you find which change broke it?"

    Weak answer: Read through the recent commits.

    Strong answer: Bisection: with a known good and a known bad version, test the middle commit and keep the half that contains the transition, which takes about log2(n) runs, six for 64 commits. It needs reproducible runs and care with commits that don't build or flaky tests. For which part of the test matters, delta debugging (ddmin) removes chunks of the stimulus while the failure persists; in a model it shrank a 200-step failure to its 3 essential steps in 59 runs. I use the failure's signature as the condition, so the reduced test keeps the same bug.

    Follow-ups: (1) What can mislead bisection? (2) Why use the signature as the oracle?

    Check yourself How many runs does bisection need for 64 commits, and why?

    About 6: each run halves the range, and 2^6 = 64.

    Check yourself Why is "the test fails" a poor oracle for test reduction?

    A reduced test can fail for a different reason, such as a missing setup step; the signature ensures it is still the same failure.

    Key takeaways

    • Bisection finds the breaking change in about log2(n) runs.

    • Delta debugging shrinks a failing test to the steps that matter: 200 to 3 in 59 runs here.

    • Both need reproducible runs and the failure's signature as the oracle; automate them.

    Keep going

    Next: Lesson 13.5, Triage at Scale. Bucketing thousands of failures by signature.

    Related topics

    • Part 13 overview: Debug

    Previous

    126 / 160

    Next: Triage at Scale

    Up next

    Up nextTriage at Scale

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • The Debug Loop
    • Reproducing Failures
    • Is It the Testbench?