awk Part III · Module 10 · Timing and STA Report Analysis
Worst Timing Paths
The worst slack in a report is the number everyone quotes, and on its own it is close to useless: Chapter 10.4 showed setup and hold need opposite fixes, so a single minimum taken across both compares quantities that are not comparable. This chapter builds the ranking properly — per check, per clock, with the path named — and then shows the comparison that goes wrong most often in practice. A slack threshold written in quotes is a string comparison, and on the six-path report it flags three paths that pass while missing the only one that fails.
Advanced18 min readawkPart IIISTAMinimumSorting
Part III · Module 10 · Chapter 10.5 · Worst Timing Paths
1. Why This Matters
"What's the worst slack?" is the first question asked about any timing run, and a single number is the wrong shape of answer.
Chapter 10.4 established why: setup and hold are different checks with opposite remedies. A minimum taken across both tells you the magnitude of the largest failure and not which of two unrelated problems it belongs to — and as this chapter's section 4 shows, which check owns the worst path changes from report to report, so you cannot assume.
The second half of the chapter is about the comparison itself. Ranking means comparing, and comparing a field against a threshold is the single place in awk where a plausible-looking expression silently does something else.
2. The Running Minimum
The idiom, on the three-path report from Chapter 10.1:
awk '/^ *slack/ { if (w == "" || $3 + 0 < w + 0) w = $3 } END { printf "worst slack: %s\n", w }' timing.rptworst slack: -0.023Two conditions, and both are load-bearing:
w == ""seeds the minimum on the first record. Section 3 is what happens without it.$3 + 0 < w + 0forces a numeric comparison. Here it is belt-and-braces — section 6 explains exactly when it is required and when it is not — but it costs nothing and it documents the intent.
Keeping the path name alongside the value needs Chapter 10.2's carried state:
awk '/^ *Startpoint:/ { s = $2 } /^ *slack/ { if (w == "" || $3 + 0 < w + 0) { w = $3; ws = s } s = "" } END { printf "worst slack %s on %s\n", w, ws }' timing.rptworst slack -0.023 on u_io/pad_reg[0]Note that ws is updated inside the comparison, so the name and the number can never disagree. Updating them in separate rules is how a report ends up quoting one path's slack against another path's name.
3. An All-Passing Report Reports Zero
Drop the w == "" guard and give it a report with nothing wrong:
slack (MET) 0.067
slack (MET) 0.031awk '/^ *slack/ { if ($3 + 0 < w + 0) w = $3 } END { printf "worst slack: [%s] (as a number %.3f)\n", w, w + 0 }' allpass.rptworst slack: [] (as a number 0.000)Empty, and zero if anything formats it as a number. An uninitialised variable is the empty string, which converts to 0 — Chapter 5.1 section 5 — and no positive slack is below zero, so the comparison never fires and w is never assigned.
The guard gives the right answer:
awk '/^ *slack/ { if (w == "" || $3 + 0 < w + 0) w = $3 } END { printf "worst slack: [%s]\n", w }' allpass.rptworst slack: [0.031]4. The Worst Path Overall Belongs to One Check
Which check owns the worst path in timing.rpt:
awk '/^ *Startpoint:/ { s = $2 } /^ *Path Type:/ { ty = $3 } /^ *slack/ { if (w == "" || $3 + 0 < w + 0) { w = $3; ws = s; wt = ty } s = ""; ty = "" } END { printf "worst overall: %s on %s (a %s check)\n", w, ws, (wt == "max" ? "SETUP" : "HOLD") }' timing.rptworst overall: -0.023 on u_io/pad_reg[0] (a HOLD check)awk '/^ *Startpoint:/ { s = $2 } /^ *Path Type:/ { ty = $3 } /^ *slack/ { c = (ty == "max" ? "setup" : "hold"); if (w[c] == "" || $3 + 0 < w[c] + 0) { w[c] = $3; p[c] = s } s = ""; ty = "" } END { printf "worst setup: %s on %s\nworst hold: %s on %s\n", w["setup"], p["setup"], w["hold"], p["hold"] }' timing.rptworst setup: -0.018 on u_regfile/dout_reg[3]
worst hold: -0.023 on u_io/pad_reg[0]-0.023 is the worst number in the report and it is a hold check. A summary quoting "worst slack −0.023" to a team working on setup closure is quoting a figure from a different problem — and the worst setup path, the one that work would act on, is −0.018 and does not appear.
Now the six-path report from Chapter 10.4:
awk '/^ *Startpoint:/ { s = $2 } /^ *Path Type:/ { ty = $3 } /^ *slack/ { if (w == "" || $3 + 0 < w + 0) { w = $3; ws = s; wt = ty } s = ""; ty = "" } END { printf "worst overall: %s on %s (a %s check)\n", w, ws, (wt == "max" ? "SETUP" : "HOLD") }' sh.rpt
awk '/^ *Startpoint:/ { s = $2 } /^ *Path Type:/ { ty = $3 } /^ *slack/ { c = (ty == "max" ? "setup" : "hold"); if (w[c] == "" || $3 + 0 < w[c] + 0) { w[c] = $3; p[c] = s } s = ""; ty = "" } END { printf "worst setup: %s on %s\nworst hold: %s on %s\n", w["setup"], p["setup"], w["hold"], p["hold"] }' sh.rptworst overall: -0.112 on u_dma/len_reg[4] (a SETUP check)
worst setup: -0.112 on u_dma/len_reg[4]
worst hold: -0.041 on u_alu/acc_reg[0]The two reports disagree about which check owns the worst path. On timing.rpt it is hold; on sh.rpt it is setup. There is nothing to generalise and no sensible default — the only way to know is to split, which is why a per-check minimum is the correct primitive and the combined one is a derived curiosity.
5. Per Check, Per Clock
A design has more than one clock, and a path's slack is only comparable against paths checked against the same clock at the same corner. The signoff shape is therefore one row per (check, clock):
awk '/^ *Path Group:/ { g = $3 } /^ *Path Type:/ { ty = $3 } /^ *Startpoint:/ { s = $2 } /^ *slack/ { c = (ty == "max" ? "setup" : "hold"); k = c "/" g; n[k]++; if ($2 == "(VIOLATED)") v[k]++; if (w[k] == "" || $3 + 0 < w[k] + 0) { w[k] = $3; p[k] = s } s=""; ty=""; g="" } END { split("setup/clk_core setup/clk_io hold/clk_core hold/clk_io", o, " "); printf "%-18s %6s %6s %9s %s\n", "check/clock", "paths", "viol", "worst", "worst path"; for (i=1;i<=4;i++) { k=o[i]; printf "%-18s %6d %6d %9s %s\n", k, n[k]+0, v[k]+0, (w[k]==""?"-":w[k]), (p[k]==""?"-":p[k]) } }' sh.rptcheck/clock paths viol worst worst path
setup/clk_core 3 2 -0.112 u_dma/len_reg[4]
setup/clk_io 1 0 0.031 u_io/cfg_reg[2]
hold/clk_core 1 1 -0.041 u_alu/acc_reg[0]
hold/clk_io 1 1 -0.023 u_io/pad_reg[0]Four rows, and each one is a question with an owner. setup/clk_core has two violations out of three paths and the worst at −0.112 — the core frequency or the core logic. hold/clk_io has one path and it violates — an I/O race. setup/clk_io is clean.
Three carried-over disciplines in that command, all from earlier chapters:
- The compound key
c "/" g— Chapter 6.3 section 4, and Chapter 9.6's finding that the grouping key decides how many problems you appear to have. split(...)for a fixed row order — Chapter 10.4 section 8, becausefor (k in w)has no defined order.- Every carried variable cleared — Chapter 10.2 section 7 and Chapter 10.4 section 6.
Printing every combination from the split list rather than only the ones that occurred also means a (check, clock) pair with no paths at all shows as a row of zeros instead of silently not existing. On this report all four pairs occur; on a real one, a missing hold/clk_io row would be the news.
6. A Quoted Threshold Is a String Comparison
Flag every path worse than −0.050 ns. The obvious expression, with the threshold in quotes:
awk '/^ *slack/ { if ($3 < "-0.050") { n++; printf " flagged: %s\n", $3 } } END { printf "quoted threshold -0.050 flagged %d path(s)\n", n+0 }' sh.rpt flagged: -0.018
flagged: -0.041
flagged: -0.023
quoted threshold -0.050 flagged 3 path(s)Three paths. Every one of them has a slack above −0.050, and the one path in the report that is genuinely worse — -0.112 — is not listed.
awk '/^ *slack/ { if ($3 + 0 < -0.050) { n++; printf " flagged: %s\n", $3 } } END { printf "numeric threshold -0.050 flagged %d path(s)\n", n+0 }' sh.rpt flagged: -0.112
numeric threshold -0.050 flagged 1 path(s)One path, and it is the right one.
The quoted comparison is lexical, so it asks whether the text -0.018 sorts before the text -0.050. Both start -0.0; at the next character 1 sorts before 5, so it does. Every slack between −0.001 and −0.049 is "less than" −0.050 as text, and every slack from −0.100 downwards is not.
Which raises the question of when + 0 is actually needed, because a running minimum in section 2 compared two fields with no apparent trouble. The three cases:
awk 'BEGIN { print "two constants: \"-0.018\" < \"-0.112\" ->", ("-0.018" < "-0.112" ? "TRUE (lexical)" : "false") }'
awk '/^ *slack/ { n++; if (n == 1) a = $3; if (n == 5) b = $3 } END { printf "two numeric fields: %s < %s -> %s\n", a, b, (a < b ? "TRUE" : "false (numeric)") }' sh.rpt
awk '/^ *slack/ { n++; if (n == 1) a = $3 } END { printf "field vs constant: %s < \"-0.112\" -> %s\n", a, (a < "-0.112" ? "TRUE (lexical)" : "false") }' sh.rpttwo constants: "-0.018" < "-0.112" -> TRUE (lexical)
two numeric fields: -0.018 < -0.112 -> false (numeric)
field vs constant: -0.018 < "-0.112" -> TRUE (lexical)The rule, which is POSIX awk's comparison rule from Chapter 5.2 section 7 in its sharpest form:
- Two values that both came from input and both look numeric compare numerically. This is why the running minimum in section 2 is correct without
+ 0: both sides are fields. - A string constant on either side forces a lexical comparison, whatever the other side is.
So the dangerous construct is specifically a literal threshold in quotes, and it is dangerous regardless of how carefully the rest of the expression is written.
Two safe forms:
awk -v t=-0.050 '/^ *slack/ { if ($3 + 0 < t + 0) n++ } END { printf "-v t=-0.050 with +0 -> %d path(s)\n", n+0 }' sh.rpt
awk -v t=-0.050 '/^ *slack/ { if ($3 < t) n++ } END { printf "-v t=-0.050 without +0 -> %d path(s)\n", n+0 }' sh.rpt-v t=-0.050 with +0 -> 1 path(s)
-v t=-0.050 without +0 -> 1 path(s)Both give 1. A value assigned with -v is treated the same way as a field — numeric-looking, so compared numerically — which is Chapter 8.2's point about -v and why a threshold belongs on the command line rather than baked into the program. Write + 0 anyway. It is free, it makes the intent explicit, and it is the one habit that makes the quoted form impossible to write by accident.
7. Ranking More Than One Path
For a top-N, emit a delimited stream and sort it — Chapter 7.6's shape:
awk '/^ *Startpoint:/ { s = $2 } /^ *Path Type:/ { ty = $3 } /^ *slack/ { if ($2 == "(VIOLATED)") printf "%s\t%s\t%s\n", $3, (ty == "max" ? "setup" : "hold"), s; s=""; ty="" }' sh.rpt | sort -t"$(printf '\t')" -k1,1n -k2,2 -k3,3 | head -3-0.112 setup u_dma/len_reg[4]
-0.041 hold u_alu/acc_reg[0]
-0.023 hold u_io/pad_reg[0]The three worst violations, worst first, each labelled with its check so the ranking is readable even though it mixes them.
The sort keys are fully specified on purpose. sort -k1,1n alone orders by slack and says nothing about paths with equal slack — and equal slacks are common, because a report quantises to three decimals and because many paths share a critical sub-path:
printf -- '-0.018\tsetup\tu_b/r\n-0.018\tsetup\tu_a/r\n-0.041\thold\tu_c/r\n' > ties.tsv
sort -t"$(printf '\t')" -k1,1n ties.tsv
sort -t"$(printf '\t')" -k1,1n -k3,3 ties.tsv-0.041 hold u_c/r
-0.018 setup u_a/r
-0.018 setup u_b/r
-0.041 hold u_c/r
-0.018 setup u_a/r
-0.018 setup u_b/rBoth orders agree here. That is not a guarantee of anything — sort defines no order among records whose specified keys are equal, so the agreement is a property of this input and this implementation, not of the command. Naming -k3,3 as a tiebreaker makes the order a property of the data, which is what a report diffed against yesterday's run needs. Module 9 lost ten fences to exactly this before the keys were specified.
8. Common Mistakes
Quoting a numeric threshold. $3 < "-0.050" is lexical and returns a disjoint set — section 6.
An unseeded minimum. Reports 0.000 on an all-passing report, which reads as zero margin — section 3.
One worst slack across both checks. Which check owns it varies by report — section 4.
Updating the value and the name in separate rules. They drift apart; update both inside the comparison.
Comparing slacks across clocks. Only paths checked against the same clock are comparable.
sort -k1,1n with no tiebreaker. No defined order among equal slacks — section 7.
Taking the top N from a default report. It holds one path per group, so N groups, not N paths.
Reporting a worst slack with no path count. A single extremum hides how much was summarised — and Chapter 10.4 section 7 showed a misfiled value leaves no trace in an extremum at all.
9. Exercises
Exercise 1 — Worst slack with its path
awk '/^ *Startpoint:/ { s = $2 } /^ *slack/ { if (w == "" || $3 + 0 < w + 0) { w = $3; ws = s } s = "" } END { printf "worst slack %s on %s\n", w, ws }' sh.rptworst slack -0.112 on u_dma/len_reg[4]Exercise 2 — Worst per check
awk '/^ *Startpoint:/ { s = $2 } /^ *Path Type:/ { ty = $3 } /^ *slack/ { c = (ty == "max" ? "setup" : "hold"); if (w[c] == "" || $3 + 0 < w[c] + 0) { w[c] = $3; p[c] = s } s = ""; ty = "" } END { printf "setup %8s %s\nhold %8s %s\n", w["setup"], p["setup"], w["hold"], p["hold"] }' sh.rptsetup -0.112 u_dma/len_reg[4]
hold -0.041 u_alu/acc_reg[0]Exercise 3 — Debug a worst slack of zero
A nightly summary reports a worst slack of 0.000 on a report where everything passes.
awk '/^ *slack/ { if ($3 + 0 < w + 0) w = $3 } END { printf "worst slack: [%s] -> %.3f\n", w, w + 0 }' allpass.rptworst slack: [] -> 0.000w is never assigned. It starts as the empty string, which is 0 numerically, and no positive slack is below 0.
awk '/^ *slack/ { if (w == "" || $3 + 0 < w + 0) w = $3 } END { printf "worst slack: [%s]\n", w }' allpass.rptworst slack: [0.031]The direction of the error is worth noting: on a report with a violation the unseeded version is correct, because a negative value does fall below zero. The bug only shows on healthy runs.
Exercise 4 — Debug a threshold that flags the wrong paths
A script is asked for every path worse than −0.050 and returns three, none of which look bad.
awk '/^ *slack/ { if ($3 < "-0.050") printf "flagged %s\n", $3 }' sh.rptflagged -0.018
flagged -0.041
flagged -0.023Three paths, all better than −0.050, and -0.112 is missing. The threshold is a string constant, so the comparison is lexical: -0.018 sorts before -0.050 because 1 precedes 5.
awk -v t=-0.050 '/^ *slack/ { if ($3 + 0 < t + 0) printf "flagged %s\n", $3 }' sh.rptflagged -0.112Exercise 5 — Engineering application
The worst-path table a timing review opens with: per check and clock, with the path named, the violation count beside every extremum, and the report's own path count so the reader knows what was summarised.
# worst.awk — worst path per (check, clock) from a timing report.
#
# Usage: awk -f worst.awk sh.rpt
# awk -v t=-0.050 -f worst.awk sh.rpt # also list paths past a threshold
# Exit: 0 nothing violating 1 violations present
/^ *Startpoint:/ { s = $2 }
/^ *Path Group:/ { g = $3 }
/^ *Path Type:/ { ty = $3 }
/^ *slack/ {
npath++
chk = (ty == "max" ? "setup" : (ty == "min" ? "hold" : "unknown"))
key = chk "/" g
if (!(key in n)) order[++nk] = key
n[key]++
if ($2 == "(VIOLATED)") { v[key]++; nviol++ }
if (!(key in w) || $3 + 0 < w[key] + 0) { w[key] = $3; p[key] = s }
if (t != "" && $3 + 0 < t + 0) past = past sprintf(" %8s %-6s %s\n", $3, chk, s)
s = ""; g = ""; ty = ""
}
END {
printf "%-20s %6s %6s %9s %s\n", "check/clock", "paths", "viol", "worst", "worst path"
for (i = 1; i <= nk; i++) {
k = order[i]
printf "%-20s %6d %6d %9.3f %s\n", k, n[k], v[k] + 0, w[k], p[k]
}
printf "# %d path(s) in this report, %d violating\n", npath + 0, nviol + 0
if (past != "") printf "paths past %s:\n%s", t, past
if (nviol) exit 1
}awk -f worst.awk sh.rpt
echo "status: $?"check/clock paths viol worst worst path
setup/clk_core 3 2 -0.112 u_dma/len_reg[4]
hold/clk_core 1 1 -0.041 u_alu/acc_reg[0]
hold/clk_io 1 1 -0.023 u_io/pad_reg[0]
setup/clk_io 1 0 0.031 u_io/cfg_reg[2]
# 6 path(s) in this report, 4 violating
status: 1The row order is first-seen rather than alphabetical, which is what order[++nk] = key on the !(key in n) guard gives — Chapter 6.3 section 6's per-key seeding used to record an insertion order, and the third answer to Chapter 10.4 section 8's unordered-iteration problem. First-seen order means the rows come out in the order the tool reported the groups, which is usually worst-first and is at least reproducible.
With a threshold:
awk -v t=-0.050 -f worst.awk sh.rpt | tail -3# 6 path(s) in this report, 4 violating
paths past -0.050:
-0.112 setup u_dma/len_reg[4]And on the three-path report:
awk -f worst.awk timing.rpt
echo "status: $?"check/clock paths viol worst worst path
setup/clk_core 2 1 -0.018 u_regfile/dout_reg[3]
hold/clk_io 1 1 -0.023 u_io/pad_reg[0]
# 3 path(s) in this report, 2 violating
status: 1Two rows rather than four, because this report has no setup/clk_io or hold/clk_core paths at all — and the first-seen construction cannot invent a row for a combination the report never mentioned. That is the trade against Chapter 10.4's split list: a fixed list shows an expected-but-absent category as zeros, and first-seen order shows only what was there. Use the fixed list when you know which categories must exist, and first-seen when you do not.
Exercise 6 — Challenge: show that the worst-slack figure cannot be checked against itself
A reviewer wants to validate a worst-slack script by confirming the number appears somewhere in the report. Show that the check passes for a figure attributed to the wrong path.
awk '/^ *Startpoint:/ { s = $2 } /^ *slack/ { if (w == "" || $3 + 0 < w + 0) w = $3; ws = s; s = "" } END { printf "worst slack %s on %s\n", w, ws }' sh.rpt
grep -c -- "-0.112" sh.rptworst slack -0.112 on u_io/cfg_reg[2]
1The slack is right and the path is wrong. ws = s sits outside the comparison — note the semicolon placement — so it is reassigned on every slack line and ends up holding the last path in the report rather than the worst one.
And the proposed check passes: -0.112 does appear in the file, once, exactly as the script reported it. The check confirms the number was read correctly and says nothing whatsoever about which path it was read from — which is the whole of the defect. On a full report with its delay tables the situation is worse rather than better, because a slack value can coincide with an incremental or cumulative delay elsewhere in the table, so a grep count of more than one proves even less.
awk '/^ *Startpoint:/ { s = $2 } /^ *slack/ { if (w == "" || $3 + 0 < w + 0) { w = $3; ws = s } s = "" } END { printf "worst slack %s on %s\n", w, ws }' sh.rptworst slack -0.112 on u_dma/len_reg[4]Moving ws = s inside the braces makes the pairing structural: the name can only be written when the number is.
The general lesson is that a value and its label have to be bound by construction, not by convention. A check that validates them separately — the number exists in the report, the path name exists in the report — passes for every possible mispairing of the two, which is Chapter 10.2 section 6's finding reached from the opposite direction.
10. Summary
- Seed a running minimum with
w == ""or!(k in w). Unseeded, it reports0.000— zero margin — on reports where everything passes. - Update the value and its path name inside the same comparison. Separated, they drift to the last record.
- Which check owns the worst path varies by report — hold on one fixture, setup on the other. Split before quoting a worst slack.
- The signoff shape is one row per (check, clock), each with a path count, a violation count and a named worst path.
- A quoted threshold is lexical.
$3 < "-0.050"flagged three paths that pass and missed the only one that fails. - Two numeric-looking values from input compare numerically; a string constant on either side forces a lexical comparison. Write
+ 0, and pass thresholds with-v. - Specify every sort key. No order is defined among equal slacks, and equal slacks are common at three decimals.
- A default report holds one path per group, so the top N of a report is not the top N of the design. Print the path count.
- Bind a number to its label by construction. Checking that each exists in the report passes for every mispairing.
The ranking is sound. Chapter 10.6 — Grouping Paths by Clock takes the other axis: aggregating per clock domain, where the path group is not always the clock you think it is, and a report with two clocks can hide a third.
Where this fits
Part of the awk curriculum.
