Skip to content
VLSI Mentor

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
 50 runs: 14 PASS  11 FAIL  0 TIMEOUT  5 CRASH
          5 NO_VERDICT  10 INCONSISTENT  5 VACUOUS

Thirty-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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/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)"
done
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
classify: 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:2

Thirty-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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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.

Thirty-six non-passing runs. Grouping by the diagnostic message alone yields six groups, and two of them hold ten runs each while containing two causes each. Grouping by the verdict class together with the message yields eight groups, one problem each.36 not a passa number nobody can act onMessage only6 groupsTwo groups of tenfour distinct bugsAdd the classstatus, verdict, counts8 groupsone problem each8 logs to readnot 3612
Figure 1 — thirty-six non-passing runs, grouped two ways. Keying on the diagnostic alone gives six groups, two of which are ten runs each and four distinct bugs. Adding the verdict class gives eight groups that each correspond to one problem.

3. Normalising: the Window Is Narrow in Both Directions

Raw messages do not group at all, because every one of them carries a time:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sed -e 's/t=[0-9][0-9]*/t=T/g'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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 failures

gnt=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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sed -e 's/\([^a-zA-Z]\)t=[0-9][0-9]*/\1t=T/g'
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    5  FAIL          TB ERROR t=T gnt=10 expected 01
what it doesthe cost
too littleevery timestamp makes its own group36 groups for 8 problems — no grouping at all
too muchdifferent values look identical3 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
[ -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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# 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.6

Step 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

  1. Run a regression with enough failures that the count is not actionable, and write down how many problems you think it is.
  2. Group them by the first diagnostic line and count the groups.
  3. Group them by class and message together and count again.
  4. Find a group that split and explain what distinguishes the two causes.
  5. Say what happens when somebody fixes one of the two and re-runs.
  6. Group the raw messages with no normalisation and count.
  7. Normalise the timestamps and count again.
  8. Write an unanchored s/t=[0-9]*/t=T/ and apply it to three different values of a second field.
  9. Anchor the rule and re-apply.
  10. State the two failure directions of normalisation and what each costs.
  11. Apply s/[0-9a-f]*/N/g to a message with hex values and read the result.
  12. Print a representative with every group and explain why the pattern alone is not enough.
  13. Add the group's span across tests and find a group that spans several.
  14. Add the span across seeds and separate the deterministic from the seed-dependent.
  15. Compare tonight's signatures against last night's and find the new group.
  16. Take the first diagnostic rather than the last, then swap them and see how the grouping changes.
  17. Use -m1 and explain what it saves on a large log.
  18. Give a run with no diagnostic a literal fallback, then make it empty and watch the columns shift.
  19. Find a single-run group that matters more than a five-run one.
  20. Add fifteen seeds and watch a group size quadruple with nothing fixed.
  21. Work the four-step morning on your own regression.

Check Your Understanding

  1. What is the question that matters about thirty-six failures?
  2. How many groups did thirty-six non-passing runs reduce to?
  3. Why does the member list matter as much as the count?
  4. How many groups does grouping on the message alone give?
  5. Which two groups merge distinct causes, and what are the four causes?
  6. Why would fixing the RTL leave the inconsistent defect in place?
  7. What is the specific cost of the message-only grouping?
  8. What does the class carry that the message does not?
  9. What happens with no normalisation at all?
  10. What did the unanchored t= rule do, and why?
  11. What does the anchored version give?
  12. State the cost of normalising too little, and of too much.
  13. What is the rule that keeps you in the window?
  14. What did s/[0-9a-f]*/N/g do in Chapter 29.4, and why was it dangerous?
  15. Why must a group report a representative?
  16. What happens if a reader greps the regression for a normalised pattern?
  17. Name three further things worth printing per group.
  18. Why take the first diagnostic line and not the last?
  19. Why -m1 rather than head -1?
  20. Why must the no-diagnostic fallback be a literal string?
  21. Why is a signature group a hypothesis rather than a diagnosis?
  22. Why are group sizes not severities?
  23. What happens to group sizes when the matrix changes?
  24. What should be compared between nights?
  25. What exactly does this chapter produce, stated narrowly?
  26. 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.
  • prio0 and inconsistent print 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 inside gnt=.
  • 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.