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

  • Part 0 · What Debugging Is6
  • Part 1 · Reproduce8
  • Part 2 · Observe9
  • Part 3 · Localise10
  • Part 4 · Hypothesise and Test6
  • Part 5 · Bug Classes16
  • Part 6 · Debug by Stage9
  • Part 7 · Hangs and Deadlocks7
  • Part 8 · Intermittent Failures7
  • Part 9 · Fix, Prove, Prevent7
  • Part 10 · Case Studies12
  • Part 11 · Debug as a Team5
    • Triage Meetings That Converge
    • Ownership and Hand-Offs
    • Bug Tracking and Severity
    • Debug Metrics That Help, and Ones That Hurt
    • Designing for Debuggability
  • Part 12 · Interview Prep5
Loading lesson

‹Part 11 · Debug as a Team
  1. Topics
  2. The Art of Hardware Debug
  3. Part 11 · Debug as a Team
  4. Triage Meetings That Converge

Part 11 · Debug as a Team · Senior (S)

Triage Meetings That Converge

How to run a triage meeting that ends with an owner and a next experiment for every failure signature: automate the preparation, cluster by signature, find each first bad commit, and keep the debugging out of the room. Shown on a real dbglab regression with 66 failures and five bugs.

On this page

    Why this matters

    A triage meeting that walks failures one by one and argues about causes ends with "everyone take a look", and a week later the same red lines are discussed again. A meeting that starts from signatures and ends with one owner and one experiment per signature closes bugs instead of describing them.

    After this lesson you can: prepare a triage from a red regression with scripts, group failures by signature, attach a repro and a first bad commit to each, and chair a meeting that leaves no signature without an owner and a next experiment.

    Note

    Level: Senior (S) · Debug-loop step: Report (for a whole regression), handing each bug to Reproduce

    Before this: A Bug Report Others Can Act On, Keeping Several Hypotheses Alive, Bisection in History

    Sixty-six red lines and thirty minutes

    Monday's nightly of dbglab ran every test over twenty seeds and 66 of 140 runs failed. The triage meeting is at ten: the CSR owner, the DMA owner, the engineer who owns the power domain, two verification engineers and you, chairing. Everyone has seen the red. Release candidate builds stop until the regression is understood.

    Thirty minutes, six people, 66 failures. What has to be true when people stand up?

    What people usually do, and why it fails

    The common meeting opens the regression page and starts at the top. The first failure is a hang, so someone shares a waveform, and the DMA owner and a verification engineer spend twelve minutes reasoning about the arbiter. The next three failures are the same hang on other seeds. Then come twenty interrupt failures, which someone says "are probably the same thing". The meeting ends with half the list unread and the action "everyone look at their blocks".

    Three things went wrong, and none is about skill. The unit was wrong: runs, not bugs, so the same hang was discussed four times and two different bugs were merged in passing. The preparation happened in the room: grouping, finding the repro and checking history are mechanical jobs, done slowly by six people watching one screen. And the output was wrong: a meeting that ends with opinions about causes, rather than owned experiments, has to happen again tomorrow.

    The method: prepare with scripts, decide in the room

    A triage meeting is the Report step done for a whole regression. Its output is not a diagnosis; it is a set of bug reports, each with an owner who will take it to Reproduce and beyond. Run it to these rules:

    1. Automate the preparation and post it before the meeting. For every failure: the signature, the shortest repro, the first bad commit and the files that commit touched. All of it is scripts, minutes of machine time.

    2. One row per signature. A signature is the failure message with the run-specific numbers taken out. Keep apart anything that is a different mechanism (an unknown value against a wrong real value, a hang against a mismatch).

    3. Two or three minutes a row. Read the facts, agree the owner, agree the next experiment. If two people start reasoning about the cause, write their names down as a follow-up and move on.

    4. Each row leaves with four things: one named person (not a team), one experiment, the prediction that experiment tests, and when the result is due.

    5. Order by harm, not by count. Silent wrong data and hangs first; twenty runs of one bug are still one bug.

    6. The next meeting starts from results. Each row comes back confirmed, refuted, merged with another row or split in two.

    The test of convergence is simple: at the end, the number of signatures without an owner and an experiment is zero, and from one meeting to the next the number of open signatures falls.

    The picture

    Three of the four stages are scripts that run before anyone sits down; only the last needs the people.

    Do it: prepare Monday's triage

    The regression

    Terminal
    $ rm -f build/*.log; tb/regress.sh icarus $(seq 1 20)
    smoke                    seed 1      PASS
    ...
    irq_w1c                  seed 1      FAIL
    ...
    random                   seed 20     FAIL
    66 failing run(s)

    One number, 66, and it is the wrong one to discuss. The logs are in build/, one per run, each with its FAIL line.

    From runs to signatures

    The script below takes the run-specific parts out of every FAIL line (seed, cycle, addresses, data, word and run numbers) and groups what is left. It keeps an unknown value as X, because an X and a wrong real value come from different mechanisms. For each group it names the run that fails earliest, the cheapest repro.

    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
    # not-run: reads dbglab's build logs after tb/regress.sh (output below is from a real run)
    """triage.py: group a regression's FAIL lines into signatures, one row per likely bug.
    usage: python3 triage.py build/*.log      (run from the dbglab folder after tb/regress.sh)
    A signature is the FAIL line with the run-specific parts taken out: seed, cycle, addresses,
    data values, word and run numbers. An unknown value (xxxxxxxx) is kept, because X and a wrong
    real value are different mechanisms. Each row names the shortest failing run as the repro."""
    import os
    import re
    import sys
    
    def signature(line):
        body = line.split(": ", 1)[1]                      # drop "FAIL test=.. seed=.. cycle=.."
        test = re.search(r"test=(\S+)", line).group(1)
        body = re.sub(r"\b[0-9a-f]{8}\b", lambda m: "X" if m.group(0) == "xxxxxxxx" else "<hex>", body)
        body = body.replace("xxxxxxxx", "X")
        body = re.sub(r"(word|run) #?\d+", r"\1 <n>", body)
        body = re.sub(r"\b\d+\b", "<n>", body)
        return f"{test}: {body}"
    
    groups = {}
    for path in sys.argv[1:]:
        sim = os.path.basename(path).split("-")[0]         # build/<sim>-<test>-<seed>.log
        with open(path) as f:
            for line in f:
                if not line.startswith("FAIL"):
                    continue
                seed = int(re.search(r"seed=(\d+)", line).group(1))
                cycle = int(re.search(r"cycle=(\d+)", line).group(1))
                groups.setdefault(signature(line.strip()), []).append((cycle, seed, sim))
    
    print(f"{sum(len(v) for v in groups.values())} failing runs, {len(groups)} signatures")
    for i, (sig, runs) in enumerate(sorted(groups.items(), key=lambda kv: -len(kv[1])), 1):
        cycle, seed, sim = min(runs)
        seeds = ",".join(str(r[1]) for r in sorted(runs, key=lambda r: r[1]))
        test = sig.split(":")[0]
        print(f"\nS{i}  {len(runs)} run(s)  seeds {seeds}")
        print(f"    {sig}")
        print(f"    repro: tb/run.sh {test} {seed} {sim}   (fails at cycle {cycle})")
    Terminal
    $ python3 -I ../tools/triage.py build/icarus-*-*.log
    66 failing runs, 5 signatures
    
    S1  20 run(s)  seeds 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20
        dma_burst_backpressure: WATCHDOG: no bus or stream progress for <n> cycles; cpu_valid=<n> cpu_ready=<n> cpu_addr=<hex>
        repro: tb/run.sh dma_burst_backpressure 1 icarus   (fails at cycle 2165)
    
    S2  20 run(s)  seeds 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20
        irq_w1c: IRQ: irq=<n> while CTRL=<n> enables no interrupt
        repro: tb/run.sh irq_w1c 1 icarus   (fails at cycle 84)
    
    S3  20 run(s)  seeds 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20
        periph_power: STREAM: pstream_valid is unknown
        repro: tb/run.sh periph_power 1 icarus   (fails at cycle 431)
    
    S4  3 run(s)  seeds 10,13,20
        random: SCOREBOARD: RAM word <n> (address <hex>) read <hex>, expected <hex> (last written by DMA run <n>)
        repro: tb/run.sh random 13 icarus   (fails at cycle 3701)
    
    S5  3 run(s)  seeds 16,17,19
        random: SCOREBOARD: RAM word <n> (address <hex>) read X, expected <hex> (last written by DMA run <n>)
        repro: tb/run.sh random 19 icarus   (fails at cycle 415)

    Read the shape before the details. Three signatures fail on every seed: deterministic, cheap to reproduce, and probably caused by a fixed sequence in the test rather than by random stimulus. Two fail on a few seeds of random only, and they differ in one word: S4 reads a wrong real value, S5 reads X. The repro for each is the earliest failure, so S5 is debugged on seed 19 at cycle 415, not on seed 17 at cycle 7949.

    A first bad commit for every row

    Each repro passes at the v0.1 release and fails at HEAD, so history can localise all five before the meeting. A loop around git bisect run does it unattended:

    terminal
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #!/bin/sh
    # bisect-all.sh: for each "test seed" pair given, find the first bad commit between v0.1 and HEAD.
    # usage: bisect-all.sh "test seed" ...   (run from the dbglab folder; the tree must be clean)
    for pair in "$@"; do
      set -- $pair
      git bisect start HEAD v0.1 > /dev/null
      first=$(git bisect run tb/run.sh "$1" "$2" 2>&1 | sed -n 's/^\([0-9a-f]\{7\}\)[0-9a-f]* is the first bad commit/\1/p')
      runs=$(git bisect log | grep -c '^git bisect \(good\|bad\)')
      git bisect reset > /dev/null 2>&1
      printf '%-26s seed %-3s first bad %s  (%s runs)  %s\n' "$1" "$2" "$first" "$runs" "$(git log -1 --format=%s "$first")"
    done
    Terminal
    $ ../tools/bisect-all.sh "dma_burst_backpressure 1" "irq_w1c 1" "periph_power 1" "random 13" "random 19"
    dma_burst_backpressure     seed 1   first bad c4a71b5  (4 runs)  dma: keep the bus for a whole chunk
    irq_w1c                    seed 1   first bad 026f8bf  (4 runs)  csr: collect register reset values in dbglab_pkg
    periph_power               seed 1   first bad 122e88a  (4 runs)  periph_pd: isolate only the outputs that leave the peripheral
    random                     seed 13  first bad 03ebdc1  (3 runs)  dma: one width for the beat counters
    random                     seed 19  first bad b357bd8  (4 runs)  csr: no reset on the DMA address and data registers

    Nineteen simulations, 2.7 seconds. Five signatures, five different commits. That is strong evidence for five different bugs, and it answers the question every triage gets stuck on ("are these the same thing?") before anyone has an opinion. git show --stat adds the files: S1 touched bus_fabric.sv, dbglab_top.sv and dma_engine.sv; S2 csr_block.sv and dbglab_pkg.sv; S3 one line of periph_pd.sv; S4 dma_engine.sv; S5 csr_block.sv. The files name the owners. One more cheap fact goes on S5's row: tb/run.sh random 19 verilator passes, and with XRAND=1, which starts unreset flops at random values, it fails at the same cycle. That is the fingerprint of a register with no reset.

    A bisect points at the commit that changed the verdict; it is a strong hint, not a root cause (see bisection in history for the ways it lies). The minutes say "first bad commit", never "cause".

    The meeting: twelve minutes, five rows

    With the table posted an hour earlier, the meeting only decides. The minutes below are illustrative (dbglab has no team), but every fact in them comes from the runs above, and each prediction is one that a lesson of this course tests for real.

    Row

    Facts

    Owner

    Next experiment and its prediction

    Due

    S1 hang

    20/20 seeds; c4a71b5 "keep the bus for a whole chunk"

    DMA owner

    Snapshot every agent at cycle 2165 and build the wait-for table. Predicts: the DMA holds the grant while waiting for FIFO space.

    Tue

    S5 X read

    3/20 seeds; b357bd8 drops resets; passes in Verilator, fails with XRAND=1

    Verification engineer 1, CSR owner reviews

    Check write data for X where the DMA writes RAM. Predicts: the first X write comes from a zero-fill with FILL never written.

    Tue

    S4 wrong value

    3/20 seeds; 03ebdc1 narrows the beat counter

    Verification engineer 2, DMA owner reviews

    Print LEN and left_q of the failing DMA run. Predicts: a run longer than 255 words, short by exactly 256.

    Tue

    S3 X on stream

    20/20 seeds; 122e88a removes a clamp from periph_pd.sv

    Power-domain owner

    Probe the domain's outputs while it is off. Predicts: rinc goes X while iso_en = 1.

    Tue

    S2 irq

    20/20 seeds; 026f8bf "no functional change intended"

    CSR owner

    Read CTRL straight after reset. Predicts: 01, not the documented 00.

    Wed

    Notice the order: the hang and the two silent data corruptions first, the interrupt last, although S2 has twenty failures and S4 three. Notice the balance too: S1 and S4 both land on the DMA, so S4 goes to a verification engineer with the DMA owner as reviewer, instead of queuing behind the hang. And nobody explained a cause. Each prediction can be wrong, which is the point: Tuesday's meeting reads five results, not five opinions.

    What the evidence says

    The preparation did the work people usually do in the room: 66 failures became five signatures, five repros and five first-bad commits in about fourteen seconds of machine time. What was left for the people was what only people can do: choose an owner, balance the load, and agree a falsifiable next step. That is why the meeting can converge in minutes.

    Traps

    Common pitfall

    Grouping too coarsely. Group by test name and you get four groups, not five: random puts S4 and S5 together, two bugs from two commits with two owners. Group the Verilator run by check name and you get 40 WATCHDOG: the back-pressure deadlock and the power-domain bug, which Verilator's supply model turns into a hang (FAIL test=periph_power seed=1 cycle=2421: WATCHDOG: ...), merged into one row. Each merge hides a bug behind a fix for the other.

    Common pitfall

    Reading one simulator's table as the truth. The same regression in Verilator gives 64 failures and also five signatures, but not the same five: S5's X reads pass (two-state flops start at 0), and seed 16 now fails on the stream (STREAM: word <n> = <hex>, expected <hex>), because without the X the wrap bug is the first thing to go wrong in that run. A signature that vanishes in one simulator is a clue to record, not a bug to close.

    Common pitfall

    Grouping too finely. Group by the raw FAIL line and every seed is its own row: 66 rows, and the meeting is back to reading runs. Strip the numbers that change from run to run, then check the groups against history: one signature with two first-bad commits is two bugs.

    Why this matters

    Senior lens. The chair's job is to protect the meeting's output, not to know the answers. Keep a visible list of follow-ups for the two people who started debugging, read each row's four fields back before moving on, and end on time. A short meeting that converges every day beats a long one that is skipped.

    Publish the prep script and run it on every nightly, so the table exists before anyone asks. When owners take a row, they start from a repro and a first-bad commit, which is often most of the localisation. The hand-off that follows has its own rules: ownership and hand-offs.

    Interview

    Asked: "You own triage for a nightly regression that has 66 failures this morning. How do you run it?"

    Weak answer: I would go through the failures with the team, look at the logs and waveforms of the first few, and assign each block owner the failures in their area.

    Strong answer: Before the meeting, scripts group the failures by signature, stripping seeds, cycles and data but keeping things like X against a wrong value apart. For each signature I want the earliest failing run as the repro and, if an older build passes, the first bad commit from an automatic bisect, plus the files it touched. Then the meeting goes row by row, worst harm first: one named owner, one experiment with a prediction, a due time. No debugging in the room. Next day each row comes back confirmed, refuted, merged or split.

    Follow-ups: (1) Two signatures bisect to the same commit. What do you do? (2) A signature fails in one simulator and passes in the other. How does that change the row?

    Check yourself A colleague says S4 and S5 "are obviously the same DMA bug, it is the same scoreboard message". What do you show them, and what goes in the minutes?

    Two facts from the prep: they bisect to different commits (03ebdc1 narrows the DMA counter, b357bd8 removes CSR resets), and S5 reads an unknown value that disappears in a two-state simulator while S4 reads a real wrong value in both. The minutes keep two rows with two owners. If the experiments later show one cause, merging two rows takes a minute; separating a merged row usually takes a second bug report after a fix that did not work.

    Check yourself Tuesday: S1's owner reports the wait-for table shows the DMA holding the grant, as predicted, but has not fixed it. Is S1 done for triage purposes? What is its next row?

    Not closed, but converged: the prediction was confirmed, so the row moves from "next experiment" to "fix and prove", with the same owner and a new due time, and the minutes record the confirmed hypothesis. The next experiment is the fix's own prediction: with the grant released while waiting, dma_burst_backpressure passes on all seeds and no other signature changes. The fix-and-prove step has its own part: Part 9.

    Remember

    • Prepare before the meeting, with scripts: signatures, one repro each, the first bad commit and the files it touched. The meeting is for decisions.

    • One row per signature, not per failing run. 66 red lines here are five bugs; too coarse or too fine a grouping both mislead.

    • Every row leaves with one named owner, one experiment, its prediction and a due time. A signature without them is the meeting's failure.

    Rule of thumb: nobody debugs in the triage meeting; the meeting assigns the next experiment and moves on.

    How this was checked

    dbglab at HEAD 49c5f04 (rebuilt with dbglab/build-history.sh), 2026-10-18. tb/regress.sh icarus $(seq 1 20) (Icarus Verilog 13.0): 140 runs, 66 failing run(s), exit 66, 10.6 s. python3 -I triage.py build/icarus-*-*.log as shown: 5 signatures. The same with tb/regress.sh verilator $(seq 1 20) (Verilator 5.048): 64 failing runs, 5 signatures, two of them different from Icarus's.

    bisect-all.sh as shown (git 2.54.0) on the five repros: c4a71b5, 026f8bf, 122e88a, 03ebdc1, b357bd8, in 4, 4, 4, 3 and 4 runs, 2.7 s in all. git show --stat on each as quoted. tb/run.sh random 19 verilator passes; with XRAND=1 it fails at cycle 415 with a non-X wrong value. tb/run.sh periph_power 1 verilator fails with the WATCHDOG line quoted.

    Grouping traps: grep -h '^FAIL' build/icarus-*.log | awk '{print $2}' | sort | uniq -c gives 4 groups; the same on the check name of the Verilator logs gives 40 WATCHDOG.

    Limits: the meeting, its owners and its due times are illustrative; dbglab has no team. The predictions in the minutes are the hypotheses the course's other lessons test for real; nothing here proves them. Bisect was run only because each repro fails at HEAD and passes at v0.1.

    Keep going

    • Practice: Debugging rare seed failures in regression

    • Practice: Report catcher triage: noise down, real failures up

    • Interview: run a mock interview on this topic

    Related topics

    • Part 11 · Debug as a Team

    • A bug report others can act on

    • Keeping several hypotheses alive

    • Bisection in history

    • Ownership and hand-offs

    Previous

    98 / 107

    Next: Ownership and Hand-Offs

    Up next

    Up nextOwnership and Hand-Offs

    Sign in to save progress

    Practice this lessonReport an error

    See also

    • Bug Tracking and Severity
    • Debug Metrics That Help, and Ones That Hurt
    • Designing for Debuggability