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?
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.
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:
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.
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).
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.
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.
Order by harm, not by count. Silent wrong data and hangs first; twenty runs of one bug are still one bug.
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.
$ 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.
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.
# 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})")$ 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.
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:
#!/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$ ../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 registersNineteen 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".
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 | Tue |
S3 X on stream | 20/20 seeds; 122e88a removes a clamp from | Power-domain owner | Probe the domain's outputs while it is off. Predicts: | 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.
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.
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.
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.
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.