awk Part III · Module 10 · Timing and STA Report Analysis
Extracting Startpoints and Endpoints
Chapter 10.1 established that a timing path's identity and its answer are sixteen to twenty-three lines apart, which leaves awk one mechanism for joining them: a variable that survives between records. That makes this the module's spine chapter — every later chapter is built on the pattern here. It also makes Chapter 5.1's between-records persistence dangerous for the first time in this track, because carried state outlives the thing it describes. Delete a single header line from a report and the obvious parser reports two different slack values for the same path, confidently and without a word of complaint.
Advanced17 min readawkPart IIISTAStateMulti-Line Records
Part III · Module 10 · Chapter 10.2 · Extracting Startpoints and Endpoints
1. Why This Matters
Everything a timing report is useful for needs a path and a number together. The worst path, the violations per clock, the signoff table — all of them join a header line to a slack line twenty lines later.
awk has exactly one mechanism for that, and it is the one Chapter 5.1 introduced as a convenience: a variable keeps its value between records. Here it is load-bearing, and for the first time in this track its persistence is a liability as well as a feature — because a variable that outlives the path it describes will be read as belonging to the next one.
2. The Pattern
****************************************
Report : timing
Design : alu_core
****************************************
Startpoint: u_regfile/dout_reg[3]
(rising edge-triggered flip-flop clocked by clk_core)
Endpoint: u_alu/sum_reg[7]
(rising edge-triggered flip-flop clocked by clk_core)
Path Group: clk_core
Path Type: max
Point Incr Path
----------------------------------------------------------
clock clk_core (rise edge) 0.000 0.000
u_regfile/dout_reg[3]/CK (DFFX1) 0.000 0.000 r
u_regfile/dout_reg[3]/Q (DFFX1) 0.112 0.112 f
u_alu/U17/Z (AND2X2) 0.085 0.197 f
u_alu/sum_reg[7]/D (DFFX1) 0.041 0.238 f
data arrival time 0.238
----------------------------------------------------------
clock clk_core (rise edge) 0.250 0.250
library setup time -0.030 0.220
data required time 0.220
----------------------------------------------------------
data required time 0.220
data arrival time -0.238
----------------------------------------------------------
slack (VIOLATED) -0.018
Startpoint: u_ctrl/state_reg[1]
(rising edge-triggered flip-flop clocked by clk_core)
Endpoint: u_alu/flag_reg[0]
(rising edge-triggered flip-flop clocked by clk_core)
Path Group: clk_core
Path Type: max
Point Incr Path
----------------------------------------------------------
clock clk_core (rise edge) 0.000 0.000
u_ctrl/state_reg[1]/Q (DFFX1) 0.098 0.098 r
u_alu/flag_reg[0]/D (DFFX1) 0.055 0.153 r
data arrival time 0.153
----------------------------------------------------------
data required time 0.220
----------------------------------------------------------
slack (MET) 0.067
Startpoint: u_io/pad_reg[0]
(rising edge-triggered flip-flop clocked by clk_io)
Endpoint: u_io/out_reg[0]
(rising edge-triggered flip-flop clocked by clk_io)
Path Group: clk_io
Path Type: min
Point Incr Path
----------------------------------------------------------
clock clk_io (rise edge) 0.000 0.000
u_io/pad_reg[0]/Q (DFFX1) 0.045 0.045 r
u_io/out_reg[0]/D (DFFX1) 0.012 0.057 r
data arrival time 0.057
----------------------------------------------------------
data required time 0.080
----------------------------------------------------------
slack (VIOLATED) -0.023Four rules: three that remember and one that reports.
awk '/^ *Startpoint:/ { s = $2 } /^ *Endpoint:/ { e = $2 } /^ *Path Group:/ { g = $3 } /^ *slack/ { printf "%-24s -> %-20s %-10s %8s %s\n", s, e, g, $NF, $2 }' timing.rptu_regfile/dout_reg[3] -> u_alu/sum_reg[7] clk_core -0.018 (VIOLATED)
u_ctrl/state_reg[1] -> u_alu/flag_reg[0] clk_core 0.067 (MET)
u_io/pad_reg[0] -> u_io/out_reg[0] clk_io -0.023 (VIOLATED)Three paths, each with its own identity and its own number. That is the whole technique and it is worth being precise about why it works:
- The three header rules assign and nothing else. They do not print, so they produce no output of their own.
- The slack rule prints and reads the variables, which still hold the most recent header values.
- Chapter 2.3's source order is irrelevant here: the header lines come earlier in the file than the slack line, so by the time the slack rule fires, the assignments have already happened.
$3 for Path Group rather than $2, because the marker is two words — Path, Group:, then the value.
3. Why This Is Not the Same as Module 9
Module 9's logs were parsed with one rule per record type and no state at all. Every UVM line carried its own severity, time, component and id, so a rule could extract everything it needed from $0.
The difference is worth stating because it changes what can go wrong:
| Module 9 (UVM log) | Module 10 (timing report) | |
|---|---|---|
| a record is | one line | 16–23 lines |
| a rule needs | only $0 | variables set by earlier lines |
| a missing line costs | that one record | the wrong value on a later record |
| detection | cross-check against the log's own summary | count the markers — section 7 |
That third row is the chapter's subject. In Module 9, a line that failed to parse was simply absent from the output. Here, a line that is absent lets a stale value be reported as current, which is a wrong answer rather than a missing one.
4. Adding the Path Type
The fourth header line, which Chapter 10.4 needs:
awk '/^ *Startpoint:/ { s = $2 } /^ *Endpoint:/ { e = $2 } /^ *Path Group:/ { g = $3 } /^ *Path Type:/ { ty = $3 } /^ *slack/ { printf "%-24s %-10s type=%-5s %8s %s\n", s, g, ty, $NF, $2 }' timing.rptu_regfile/dout_reg[3] clk_core type=max -0.018 (VIOLATED)
u_ctrl/state_reg[1] clk_core type=max 0.067 (MET)
u_io/pad_reg[0] clk_io type=min -0.023 (VIOLATED)Five facts per path from five separate lines. max is a setup check and min is a hold check — Chapter 10.4's subject, and already visible here as the reason the third path's -0.023 is not comparable to the first path's -0.018.
5. Failure Mode One: A Truncated Report
A report cut off mid-path — a full disk, a killed tool, a head in a wrapper script:
head -51 timing.rpt > truncated.rpt
tail -3 truncated.rpt Startpoint: u_io/pad_reg[0]
(rising edge-triggered flip-flop clocked by clk_io)
Endpoint: u_io/out_reg[0]The third path has a header and no slack. The parser:
awk '/^ *Startpoint:/ { s = $2 } /^ *Endpoint:/ { e = $2 } /^ *slack/ { printf "%-24s -> %-20s %8s\n", s, e, $NF }' truncated.rptu_regfile/dout_reg[3] -> u_alu/sum_reg[7] -0.018
u_ctrl/state_reg[1] -> u_alu/flag_reg[0] 0.067Two rows. The third path is gone, and there is nothing in the output to suggest it existed.
That is the gentler of the two failures — a missing row rather than a wrong one — and it is still serious, because the path that vanishes is the last one, and a report sorted worst-first puts the most important path first while a report sorted by group can put anything there.
6. Failure Mode Two: Stale State Mispairs
The dangerous one. Delete a single header line — the second path's Startpoint:
awk 'NR != 31' timing.rpt > nostart.rpt
sed -n '29,33p' nostart.rpt slack (VIOLATED) -0.018
(rising edge-triggered flip-flop clocked by clk_core)
Endpoint: u_alu/flag_reg[0]
(rising edge-triggered flip-flop clocked by clk_core)Path two now has its continuation line, its endpoint, its group and its slack — and no startpoint. Run the parser:
awk '/^ *Startpoint:/ { s = $2 } /^ *slack/ { printf "slack %8s paired with startpoint %s\n", $NF, s }' nostart.rptslack -0.018 paired with startpoint u_regfile/dout_reg[3]
slack 0.067 paired with startpoint u_regfile/dout_reg[3]
slack -0.023 paired with startpoint u_io/pad_reg[0]Two different slack values claim the same startpoint. u_regfile/dout_reg[3] is reported with -0.018 and with 0.067, and the second is a value belonging to a path the parser never identified.
7. Clear the State When You Consume It
Set the variables back to empty in the rule that reads them:
awk '/^ *Startpoint:/ { s = $2 } /^ *slack/ { printf "slack %8s startpoint %s%s\n", $NF, (s == "" ? "<MISSING>" : s), (s == "" ? " <- UNPAIRED" : ""); s = "" }' nostart.rptslack -0.018 startpoint u_regfile/dout_reg[3]
slack 0.067 startpoint <MISSING> <- UNPAIRED
slack -0.023 startpoint u_io/pad_reg[0]The second row now says <MISSING> and flags itself. The stale value cannot be reported, because it was cleared the moment it was used.
And the same command on the intact report is unaffected:
awk '/^ *Startpoint:/ { s = $2 } /^ *slack/ { printf "slack %8s startpoint %s%s\n", $NF, (s == "" ? "<MISSING>" : s), (s == "" ? " <- UNPAIRED" : ""); s = "" }' timing.rptslack -0.018 startpoint u_regfile/dout_reg[3]
slack 0.067 startpoint u_ctrl/state_reg[1]
slack -0.023 startpoint u_io/pad_reg[0]Three correct pairings. The guard costs nothing when the input is good.
The full pattern, with every carried variable cleared:
awk '/^ *Startpoint:/ { s = $2 } /^ *Endpoint:/ { e = $2 } /^ *Path Group:/ { g = $3 } /^ *Path Type:/ { ty = $3 } /^ *slack/ { miss = (s == "") + (e == "") + (g == "") + (ty == ""); printf "%-24s %-20s %-9s %-4s %8s%s\n", (s == "" ? "<MISSING>" : s), (e == "" ? "<MISSING>" : e), (g == "" ? "<MISSING>" : g), (ty == "" ? "<?>" : ty), $NF, (miss ? " <- " miss " field(s) missing" : ""); s = ""; e = ""; g = ""; ty = "" }' nostart.rptu_regfile/dout_reg[3] u_alu/sum_reg[7] clk_core max -0.018
<MISSING> u_alu/flag_reg[0] clk_core max 0.067 <- 1 field(s) missing
u_io/pad_reg[0] u_io/out_reg[0] clk_io min -0.023One row flagged, with a count of how many of its four header fields were absent — which distinguishes a single changed marker from a wholesale format change.
8. Check the Counts First
Clearing the state detects a mispairing per row. Counting the markers detects a problem before any row is produced, which Chapter 10.1 section 8 introduced:
for f in timing.rpt truncated.rpt nostart.rpt; do printf "%-16s " "$f"; awk '/^ *Startpoint:/ { s++ } /^ *slack/ { k++ } END { printf "startpoints=%d slack=%d %s\n", s + 0, k + 0, (s == k ? "paired" : "MISMATCH") }' "$f"; donetiming.rpt startpoints=3 slack=3 paired
truncated.rpt startpoints=3 slack=2 MISMATCH
nostart.rpt startpoints=2 slack=3 MISMATCHThree reports, three outcomes — and the direction of the mismatch names the fault:
- startpoints > slack — a truncation; paths at the end have no answer.
- startpoints < slack — a missing header line; some slack has no path.
- equal — pairing is safe, and the per-row guard from section 7 is a second line of defence rather than the only one.
Both checks are worth having. The count check refuses the whole report, which is right for a signoff script; the per-row guard reports which rows are affected, which is right when you need the usable rows out of a partly-broken report.
9. Common Mistakes
Printing from the header rules. They should assign only; the slack rule reports.
Using $2 for Path Group. The marker is two words — use $3.
Not clearing the carried state. A missing header line makes a stale value look current — section 6.
Assuming a duplicate startpoint is a parsing bug. One flip-flop genuinely drives many endpoints, which is why the mispairing is plausible.
Not counting the markers. The mismatch and its direction are one rule — section 8.
Treating a truncated report as merely incomplete. The lost path is the last one, which a worst-first sort makes the most important.
Relying on the per-row guard alone. It cannot detect a truncation, because the dropped path produces no row to flag.
Carrying state without an END flush when the last record needs reporting — the opposite problem, and Chapter 7.4 section 6's prev pattern.
10. Exercises
Exercise 1 — Pair the headers with the slack
awk '/^ *Startpoint:/ { s = $2 } /^ *Endpoint:/ { e = $2 } /^ *slack/ { printf "%-24s -> %-20s %8s\n", s, e, $NF; s = ""; e = "" }' timing.rptu_regfile/dout_reg[3] -> u_alu/sum_reg[7] -0.018
u_ctrl/state_reg[1] -> u_alu/flag_reg[0] 0.067
u_io/pad_reg[0] -> u_io/out_reg[0] -0.023Exercise 2 — Add the group and type
awk '/^ *Startpoint:/ { s = $2 } /^ *Path Group:/ { g = $3 } /^ *Path Type:/ { ty = $3 } /^ *slack/ { printf "%-24s %-10s %-4s %8s\n", s, g, ty, $NF; s = ""; g = ""; ty = "" }' timing.rptu_regfile/dout_reg[3] clk_core max -0.018
u_ctrl/state_reg[1] clk_core max 0.067
u_io/pad_reg[0] clk_io min -0.023Exercise 3 — Debug a vanished path
A parser reports two paths from a report that has three startpoints.
head -51 timing.rpt > truncated.rpt
awk '/^ *Startpoint:/ { s = $2 } /^ *slack/ { printf "%-24s %8s\n", s, $NF; s = "" } END { printf "(%d row(s))\n", NR - NR + n }' truncated.rpt
awk '/^ *Startpoint:/ { s++ } /^ *slack/ { k++ } END { printf "startpoints=%d slack=%d\n", s + 0, k + 0 }' truncated.rptu_regfile/dout_reg[3] -0.018
u_ctrl/state_reg[1] 0.067
(0 row(s))
startpoints=3 slack=2Three startpoints, two slack lines — the report was cut off and the third path has no answer, so no row was produced for it.
(The (0 row(s)) line is the END rule's own arithmetic: NR - NR + n is just n, which was never incremented. It is left in deliberately as a reminder that a counter has to be incremented somewhere — Chapter 5.1 section 6's point about printing n + 0.)
Exercise 4 — Debug a mispairing
A timing summary lists one startpoint twice with two different slacks.
awk 'NR != 31' timing.rpt > nostart.rpt
awk '/^ *Startpoint:/ { s = $2 } /^ *slack/ { printf "slack %8s paired with %s\n", $NF, s }' nostart.rptslack -0.018 paired with u_regfile/dout_reg[3]
slack 0.067 paired with u_regfile/dout_reg[3]
slack -0.023 paired with u_io/pad_reg[0]Path two's Startpoint line is absent, so s still held path one's value when the second slack arrived.
awk '/^ *Startpoint:/ { s = $2 } /^ *slack/ { printf "slack %8s paired with %s%s\n", $NF, (s == "" ? "<MISSING>" : s), (s == "" ? " <- UNPAIRED" : ""); s = "" }' nostart.rptslack -0.018 paired with u_regfile/dout_reg[3]
slack 0.067 paired with <MISSING> <- UNPAIRED
slack -0.023 paired with u_io/pad_reg[0]One cleared variable turns a wrong answer into a reported gap.
Exercise 5 — Engineering application
Write a path extractor with both defences: the count check as a precondition and the cleared state as a per-row guard, emitting a delimited stream for the rest of the module.
# paths.awk — extract one record per timing path.
#
# Usage: awk -f paths.awk timing.rpt
# Output: startpoint TAB endpoint TAB group TAB type TAB slack TAB verdict
# Exit: 0 every path paired cleanly
# 1 at least one path had a missing header field
# 2 the marker counts disagree -- pairing is unsafe
BEGIN { OFS = "\t" }
/^ *Startpoint:/ { s = $2; nstart++ }
/^ *Endpoint:/ { e = $2 }
/^ *Path Group:/ { g = $3 }
/^ *Path Type:/ { ty = $3 }
/^ *slack/ {
nslack++
miss = (s == "") + (e == "") + (g == "") + (ty == "")
if (miss) { bad++; printf "WARNING: path at line %d is missing %d header field(s)\n", NR, miss > "/dev/stderr" }
print (s == "" ? "-" : s), (e == "" ? "-" : e), (g == "" ? "-" : g), (ty == "" ? "-" : ty), $NF, $2
s = ""; e = ""; g = ""; ty = ""
}
END {
if (!nslack) { print "ERROR: no slack lines -- is this a timing report?" > "/dev/stderr"; exit 2 }
if (nstart != nslack) {
printf "ERROR: %d startpoint(s) but %d slack line(s) -- %s\n", nstart, nslack,
(nstart > nslack ? "report looks truncated" : "a header line is missing") > "/dev/stderr"
exit 2
}
printf "# %d path(s), %d with a missing header field\n", nslack, bad + 0
if (bad) exit 1
}awk -f paths.awk timing.rpt
echo "status: $?"u_regfile/dout_reg[3] u_alu/sum_reg[7] clk_core max -0.018 (VIOLATED)
u_ctrl/state_reg[1] u_alu/flag_reg[0] clk_core max 0.067 (MET)
u_io/pad_reg[0] u_io/out_reg[0] clk_io min -0.023 (VIOLATED)
# 3 path(s), 0 with a missing header field
status: 0Six tab-separated columns per path — the form Chapter 7.6 endorses, and the input every later chapter in this module uses.
The two failure cases are refused with a diagnosis that names the likely cause:
awk -f paths.awk truncated.rpt 2>&1 >/dev/null
echo "status: $?"ERROR: 3 startpoint(s) but 2 slack line(s) -- report looks truncated
status: 2awk -f paths.awk nostart.rpt 2>&1 >/dev/null
echo "status: $?"WARNING: path at line 46 is missing 1 header field(s)
ERROR: 2 startpoint(s) but 3 slack line(s) -- a header line is missing
status: 2The second case produces both diagnostics — the per-row warning naming the line, and the count check naming the fault. That is the point of having two defences: the warning tells you which path, the count tells you what kind of damage, and the exit status of 2 stops a signoff script from using any of it.
The engineering caveat: 3 path(s) is three paths the report contains, not three paths in the design. A report_timing prints one path per group by default, so a report with three paths may be summarising thousands — Chapter 10.1 section 10's point, and the reason a signoff flow passes -max_paths and then checks that the count came back as expected.
Exercise 6 — Challenge: show that the un-cleared version cannot be fixed by inspection
Section 7's fix is one assignment. Show that no amount of reading the output of the un-cleared version reveals the problem.
awk '/^ *Startpoint:/ { s = $2 } /^ *Endpoint:/ { e = $2 } /^ *slack/ { printf "%-24s -> %-20s %8s\n", s, e, $NF }' nostart.rptu_regfile/dout_reg[3] -> u_alu/sum_reg[7] -0.018
u_regfile/dout_reg[3] -> u_alu/flag_reg[0] 0.067
u_io/pad_reg[0] -> u_io/out_reg[0] -0.023Read that as a timing engineer. Three paths, three slacks, all plausible. Row two says u_regfile/dout_reg[3] reaches u_alu/flag_reg[0] with 0.067 slack — and a register genuinely can drive two endpoints, so this is an entirely ordinary-looking timing report.
Now the same report through the cleared version:
awk '/^ *Startpoint:/ { s = $2 } /^ *Endpoint:/ { e = $2 } /^ *slack/ { printf "%-24s -> %-20s %8s%s\n", (s == "" ? "<MISSING>" : s), (e == "" ? "<MISSING>" : e), $NF, (s == "" || e == "" ? " <- UNPAIRED" : ""); s = ""; e = "" }' nostart.rptu_regfile/dout_reg[3] -> u_alu/sum_reg[7] -0.018
<MISSING> -> u_alu/flag_reg[0] 0.067 <- UNPAIRED
u_io/pad_reg[0] -> u_io/out_reg[0] -0.023The same input, and now row two announces itself.
The two outputs are equally well-formed and only one is honest. That is the argument for clearing on consumption as a habit rather than as a response to a bug: the bug it prevents is not detectable downstream, so the only place it can be caught is where the state is read.
This is the same reasoning as Chapter 8.7 section 6 — a defect whose output looks correct can only be found by a check placed where the defect occurs — and Chapter 7.5 section 8, where a guard had to assert on something the failure could not corrupt.
11. Summary
- A timing path needs state because its identity and its answer are separate lines.
- Three rules that assign, one rule that reports — the header rules print nothing.
$3forPath GroupandPath Type; the markers are two words.- Module 9 needed no state, so a missing line cost one record. Here it costs a wrong value on a later record.
- A truncated report silently drops its last path — a missing row, with nothing to indicate it.
- A missing header line reports two different slacks for one startpoint, and the duplicate looks legitimate because one register really does drive many endpoints.
- Clear every carried variable in the rule that consumes it. One assignment turns a wrong answer into a reported gap.
- The rule generalises: state that should belong to one record must not survive into the next — the same mistake as an unreset per-key seed or an unreset primed separator.
- Count the markers as a precondition, and the direction of a mismatch names the fault: more startpoints means truncation, fewer means a missing header.
- Keep both defences. The count check refuses the report; the per-row guard identifies the usable rows.
Paths are now records. Chapter 10.3 — Slack Extraction takes the number itself: pulling the slack and the MET or VIOLATED word beside it, and getting the sign convention right so a passing path is never reported as a failure.
Where this fits
Part of the awk curriculum.
