Linux & Bash · Part II · Module 33 · Debugging Failed Jobs and Broken Flows
Classifying Failures Across a Regression
Thirty-six failures are not thirty-six problems, and the useful question is how many problems they are. This chapter groups them by signature, measures a message-only grouping merging four distinct bugs into two groups of ten, and finds the narrow window between normalising so little that nothing groups and so much that different failures look identical.
Advanced25 min readLinuxPart IIDebuggingRegressionClassification
Part II · Module 33 · Chapter 33.7 · Classifying Failures Across a Regression
Chapter 33.6 worked on one run. This chapter is the opposite scale, and it starts from a number nobody can act on:
50 runs: 14 PASS 11 FAIL 0 TIMEOUT 5 CRASH
5 NO_VERDICT 10 INCONSISTENT 5 VACUOUSThirty-six runs are not a pass. The question that decides the next week is not how many failed; it is how many problems that is, because the answer determines whether anybody can finish by Friday.
1. Thirty-Six Runs, Eight Problems
#!/bin/bash
# classify REGRDIR -- group a regression's non-passing runs by SIGNATURE, so
# one fix can be matched to every run it explains.
#
# A signature is (verdict class, normalised first diagnostic). The class is in
# it because two different bugs can print the identical message: a design
# property violation and a testbench that prints PASSED anyway both emit
# "ERROR t=35 gnt=10 expected 01".
set -u
R=${1:-}
[ -n "$R" ] || { echo "classify: need a regression directory" >&2; exit 2; }
res=$R/results.tsv
[ -r "$res" ] || { echo "classify: no results.tsv in '$R'" >&2; exit 2; }
# Normalise by POSITION, not by alphabet. [0-9a-f] matches ordinary letters and
# turns "dout expected" into "Nout Nxpected" -- measured in 29.4.
# And anchor each rule: an unanchored s/t=[0-9]*/t=T/ also rewrites gnt=10,
# merging three genuinely different grant values into one signature (measured).
norm() {
sed -e 's/\([^a-zA-Z]\)t=[0-9][0-9]*/\1t=T/g' \
-e 's/cycle [0-9][0-9]*/cycle N/g' \
-e 's/(draw=[0-9][0-9]*)/(draw=N)/g' \
-e 's/:[0-9][0-9]*:/:L:/g' \
-e 's/[[:space:]][[:space:]]*/ /g' \
-e 's/^ //' -e 's/ $//'
}
tmp=$(mktemp) || exit 2
trap 'rm -f "$tmp"' EXIT INT TERM
awk -F'\t' 'NR>1 && $4!="PASS" {print $1"\t"$2"\t"$3"\t"$4}' "$res" | while IFS="$(printf '\t')" read -r spec t s cls; do
log=$R/runs/${t}_s${s}/run.log
d=$(/usr/bin/grep -m1 -E 'ERROR|FATAL' "$log" 2>/dev/null | norm)
[ -n "$d" ] || d="(no diagnostic line)"
printf '%s\t%s\t%s\n' "$cls" "$d" "$spec"
done > "$tmp"
total=$(grep -c . "$tmp")
groups=$(cut -f1,2 "$tmp" | sort -u | grep -c .)
printf 'classify: %d non-passing run(s) in %d signature group(s)\n\n' "$total" "$groups"
cut -f1,2 "$tmp" | sort | uniq -c | sort -k1,1rn -k2,2 | while read -r n rest; do
cls=${rest%% *}; msg=${rest#* }
printf ' %3d %-13s %s\n' "$n" "$cls" "$msg"
# name a representative and the full span, because a count alone is not actionable
members=$(awk -F'\t' -v c="$cls" -v m="$msg" '$1==c && $2==m {printf "%s ", $3}' "$tmp")
printf ' %s\n' "$(printf '%s' "$members" | cut -c1-70)"
doneclassify: 36 non-passing run(s) in 8 signature group(s)
5 CRASH FATAL: tb/tb_arb.v:L: deliberate fatal
crash:1 crash:2 crash:3 crash:4 crash:5
5 FAIL TB ERROR t=T gnt=10 expected 01
prio0:1 prio0:2 prio0:3 prio0:4 prio0:5
5 FAIL TB ERROR t=T late check failed at cycle N
latefail:1 latefail:2 latefail:3 latefail:4 latefail:5
5 INCONSISTENT FATAL: tb/tb_arb.v:L: failed while closing out
latecrash:1 latecrash:2 latecrash:3 latecrash:4 latecrash:5
5 INCONSISTENT TB ERROR t=T gnt=10 expected 01
inconsistent:1 inconsistent:2 inconsistent:3 inconsistent:4 inconsiste
5 NO_VERDICT (no diagnostic line)
silent:1 silent:2 silent:3 silent:4 silent:5
5 VACUOUS (no diagnostic line)
vacuous:1 vacuous:2 vacuous:3 vacuous:4 vacuous:5
1 FAIL TB ERROR t=T seed-dependent check failed (draw=N)
flaky:2Thirty-six down to eight, and the eight are actionable in a way the thirty-six are not: seven of them are "fix one thing, clear five runs", and one of them is a single seed-dependent failure that needs Chapter 31.3's treatment.
The member list matters as much as the count. A group that says 5 tells you how much a fix is worth; a group that names prio0:1 … prio0:5 tells you what to re-run to prove it, which is Chapter 32.6's selection list arriving ready-made.
2. The Message Alone Is Not a Signature
Group on the diagnostic and nothing else — which is what every first version of this does:
10 (no diagnostic line)
10 TB ERROR t=T gnt=10 expected 01
5 FATAL: tb/tb_arb.v:L: deliberate fatal
5 FATAL: tb/tb_arb.v:L: failed while closing out
5 TB ERROR t=T late check failed at cycle N
1 TB ERROR t=T seed-dependent check failed (draw=N)Six groups instead of eight, and the two biggest are each two different bugs.
3. Normalising: the Window Is Narrow in Both Directions
Raw messages do not group at all, because every one of them carries a time:
three runs of the SAME bug at different times -> 3 group(s) unnormalised
with the time normalised -> 1 group(s)So some normalisation is mandatory. And my first rule normalised too much:
sed -e 's/t=[0-9][0-9]*/t=T/g' three genuinely different failures:
TB ERROR t=35 gnt=10 expected 01
TB ERROR t=35 gnt=11 expected 01
TB ERROR t=35 gnt=00 expected 01
normalised with s/t=[0-9]*/t=T/g :
TB ERROR t=T gnt=T expected 01
-> 1 distinct signature(s) from 3 distinct failuresgnt=10 became gnt=T, because t= occurs inside gnt=. Three different wrong grant values — three potentially different bugs — collapsed into one signature. Anchoring the rule so it cannot start mid-word:
sed -e 's/\([^a-zA-Z]\)t=[0-9][0-9]*/\1t=T/g' TB ERROR t=T gnt=00 expected 01
TB ERROR t=T gnt=10 expected 01
TB ERROR t=T gnt=11 expected 01
-> 3 distinct signature(s)And the real regression still groups to eight, now with the grant value preserved:
5 FAIL TB ERROR t=T gnt=10 expected 01| what it does | the cost | |
|---|---|---|
| too little | every timestamp makes its own group | 36 groups for 8 problems — no grouping at all |
| too much | different values look identical | 3 bugs in 1 group; a fix credited with runs it did not touch |
The rule that keeps you inside the window is the one Chapter 29.4 arrived at from the counting side: normalise by position, not by alphabet. Rewrite what follows a known marker — t=, cycle , draw=, a :line: — and never rewrite a character class. That chapter measured s/[0-9a-f]*/N/g turning dout=a4 expected a5 into Nout=N NxpNtN N, which still grouped, which is what makes it dangerous.
4. Choosing the Diagnostic Line
classify takes the first ERROR or FATAL line:
d=$(/usr/bin/grep -m1 -E 'ERROR|FATAL' "$log" 2>/dev/null | norm)Three decisions in that line.
First, not last. Chapter 29.2 measured why: a failure cascades, so the last error is usually a consequence and the first is usually the cause. A classifier keyed on the last line groups by symptom.
-m1 rather than head -1, so grep stops reading. On a regression of two thousand runs with hundred-megabyte logs that is the difference between a report and a lunch break.
And a fallback that is not empty. A run with no diagnostic at all gets the literal string (no diagnostic line) rather than an empty signature:
[ -n "$d" ] || d="(no diagnostic line)"An empty signature would group with every other empty one and sort oddly, and — worse — an empty field in a tab-separated line shifts every later column, which is Chapter 32.4's measured hazard. The (no diagnostic line) group is also informative in its own right: ten runs reached it here, and splitting them by class is what revealed two separate testbench defects.
5. The Workflow It Enables
The point of classification is the order it imposes on a morning:
# 1. how many problems, not how many failures
$ classify regr/latest
# 2. the classes that are not design questions, first
$ classify regr/latest | grep -E 'CRASH|TIMEOUT|NOSTATUS'
# 3. one log per group, not one per run
$ repro regr/latest/runs/prio0_s1 # 33.2
$ triage regr/latest/runs/prio0_s1 # 33.1
# 4. prove a fix against the group's own member list
$ rerun regr/latest FAIL # 32.6Step 2 is the one most often skipped and it is where Chapter 32.5's environment-versus-design split pays off. Of the eight groups here, two are CRASH and INCONSISTENT-from-a-late-death — tool and environment questions that will waste a verification engineer's day if they are handed one.
Step 3 is where this module's earlier chapters attach: one representative per group, triaged to a layer before it is debugged. Eight representatives, eight triages, and Chapter 33.1 measured that two of its three layer answers need no log reading at all.
And step 4 closes the loop with a number: if a fix was supposed to clear a group of five and clears four, the group was two problems, and you have learned that from a rerun rather than from a week.
Practice
- Run a regression with enough failures that the count is not actionable, and write down how many problems you think it is.
- Group them by the first diagnostic line and count the groups.
- Group them by class and message together and count again.
- Find a group that split and explain what distinguishes the two causes.
- Say what happens when somebody fixes one of the two and re-runs.
- Group the raw messages with no normalisation and count.
- Normalise the timestamps and count again.
- Write an unanchored
s/t=[0-9]*/t=T/and apply it to three different values of a second field. - Anchor the rule and re-apply.
- State the two failure directions of normalisation and what each costs.
- Apply
s/[0-9a-f]*/N/gto a message with hex values and read the result. - Print a representative with every group and explain why the pattern alone is not enough.
- Add the group's span across tests and find a group that spans several.
- Add the span across seeds and separate the deterministic from the seed-dependent.
- Compare tonight's signatures against last night's and find the new group.
- Take the first diagnostic rather than the last, then swap them and see how the grouping changes.
- Use
-m1and explain what it saves on a large log. - Give a run with no diagnostic a literal fallback, then make it empty and watch the columns shift.
- Find a single-run group that matters more than a five-run one.
- Add fifteen seeds and watch a group size quadruple with nothing fixed.
- Work the four-step morning on your own regression.
Check Your Understanding
- What is the question that matters about thirty-six failures?
- How many groups did thirty-six non-passing runs reduce to?
- Why does the member list matter as much as the count?
- How many groups does grouping on the message alone give?
- Which two groups merge distinct causes, and what are the four causes?
- Why would fixing the RTL leave the
inconsistentdefect in place? - What is the specific cost of the message-only grouping?
- What does the class carry that the message does not?
- What happens with no normalisation at all?
- What did the unanchored
t=rule do, and why? - What does the anchored version give?
- State the cost of normalising too little, and of too much.
- What is the rule that keeps you in the window?
- What did
s/[0-9a-f]*/N/gdo in Chapter 29.4, and why was it dangerous? - Why must a group report a representative?
- What happens if a reader greps the regression for a normalised pattern?
- Name three further things worth printing per group.
- Why take the first diagnostic line and not the last?
- Why
-m1rather thanhead -1? - Why must the no-diagnostic fallback be a literal string?
- Why is a signature group a hypothesis rather than a diagnosis?
- Why are group sizes not severities?
- What happens to group sizes when the matrix changes?
- What should be compared between nights?
- What exactly does this chapter produce, stated narrowly?
- Give the four steps of the morning, and which is most often skipped.
6. Remember
- The useful question is how many problems, not how many failures. Thirty-six non-passing runs were eight groups.
- Print the member list, not just the count — it is the rerun selection, ready-made.
- A signature is the class plus the message. Message alone gave six groups, and two of them were four different bugs.
prio0andinconsistentprint the identical line and need opposite fixes; fixing the RTL would hide the testbench defect rather than remove it.- No normalisation means no grouping; too much means different failures look identical. Three grant values collapsed to one signature because
t=matched insidegnt=. - Normalise by position, not by alphabet, and anchor every rule so it cannot start mid-word.
- Always report a representative. A normalised pattern cannot be grepped for in the logs it came from.
- Take the first diagnostic, with
-m1, and give a no-diagnostic run a literal fallback string. - A group is a hypothesis. Two independent defects can share a message; group sizes are not severities; and a one-run group can outrank a five-run one.
- Group sizes move with the matrix. Compare signatures between nights, never sizes.
- The morning is: classify, non-design classes first, one representative per group, then prove the fix against the member list.
7. What Is Next
Every technique so far has assumed you can run the thing again where you are sitting. Chapter 33.8 handles the case where that assumption fails — a failure that happens only on a compute node, under a scheduler, in an environment you were never in — which is where this module's reproduce-and-compare discipline has to work at a distance.
Where this fits
Part of the Linux & Bash curriculum.
