awk Part III · Module 9 · Simulation and UVM Log Analysis
Understanding Simulation Logs
Part III applies the whole of Parts I and II to real semiconductor data, and it starts where every log-parsing job should start: reading the file's shape before writing a single pattern. A simulation log is not a uniform stream of records. It has a tool banner nobody wants, a body of UVM message records that is the actual data, and a report summary at the end that is both a trap for a careless pattern and — used deliberately — the one piece of ground truth that lets a parser check its own arithmetic.
Intermediate16 min readawkPart IIIUVMLog AnalysisVerification
Part III · Module 9 · Chapter 9.1 · Understanding Simulation Logs
1. Why This Matters
Parts I and II built the tools. Part III points them at the files an engineer actually has, and the first of those is a simulation log.
The temptation with a log is to write the pattern immediately — /UVM_ERROR/ and count. Chapter 9.3 shows what that returns and why it is wrong, and the reason is in this chapter: the log is not all one kind of line. Until you know which regions exist, a pattern that looks obviously correct will match things you did not mean.
So this chapter has no awk one-liner as its centrepiece. It has a file, read carefully.
2. A Log, Whole
Here is the fixture the rest of the module uses:
Chronologic VCS simulator copyright 1991-2024
Compiler version X-2024.03; Runtime version X-2024.03; Oct 12 14:30 2026
UVM_INFO @ 0: reporter [RNTST] Running test alu_smoke_test...
UVM_INFO ../tb/env/alu_env.sv(87) @ 0: uvm_test_top.env [BUILD] env built with 2 agents
UVM_INFO ../tb/seq/alu_seq.sv(42) @ 1000: uvm_test_top.env.agt.seqr@@seq [SEQ] starting 50 items
UVM_WARNING ../tb/env/alu_sb.sv(210) @ 15500: uvm_test_top.env.sb [UNUSED_CLK] clock gate never toggled
UVM_WARNING ../tb/env/alu_sb.sv(210) @ 16000: uvm_test_top.env.sb [UNUSED_CLK] clock gate never toggled
UVM_ERROR ../tb/env/alu_sb.sv(145) @ 20500: uvm_test_top.env.sb [MISMATCH] exp=32'hdead act=32'hbeef
UVM_ERROR ../tb/env/alu_sb.sv(145) @ 21000: uvm_test_top.env.sb [MISMATCH] exp=32'hcafe act=32'h0000
UVM_INFO ../tb/seq/alu_seq.sv(99) @ 30000: uvm_test_top.env.agt.seqr@@seq [SEQ] done
UVM_FATAL ../tb/env/alu_env.sv(300) @ 30500: uvm_test_top.env [DEADLOCK] no response for 500ns
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO : 4
UVM_WARNING : 2
UVM_ERROR : 2
UVM_FATAL : 1
$finish called from file "../tb/top.sv", line 55.wc -l < sim.log 18Eighteen lines. Nine of them are message records. The other nine are not, and that ratio is typical — a short real log is mostly not data.
3. The Three Regions
Finding the boundary is one pattern:
awk '/^--- UVM Report Summary ---/ { printf "summary starts at line %d of %d\n", NR, 18; exit }' sim.logsummary starts at line 12 of 18So lines 1–2 are the preamble, 3–11 the records, 12–18 the summary. Counting the regions directly:
awk '/^--- UVM Report Summary ---/ { insum = 1 } insum { tail++; next } /^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { rec++; next } { head++ } END { printf "preamble=%d records=%d summary+tail=%d (total %d)\n", head, rec, tail, NR }' sim.logpreamble=2 records=9 summary+tail=7 (total 18)The three numbers sum to 18, which is Chapter 8.3 section 6's arithmetic check — every line accounted for by exactly one category.
4. The Record Format
A message record, decomposed:
UVM_ERROR ../tb/env/alu_sb.sv(145) @ 20500: uvm_test_top.env.sb [MISMATCH] exp=32'hdead act=32'hbeef
└───┬───┘ └──────────┬──────────┘ └─┬─┘ └────────┬───────┘ └───┬────┘ └──────────┬──────────┘
severity file(line) time hierarchy id messageFive pieces of structured information plus free text. Each is worth something different:
- severity —
UVM_INFO,UVM_WARNING,UVM_ERROR,UVM_FATAL. The triage axis. - file(line) — where the check lives, not where the bug is. Chapter 9.4 groups on it.
- time — simulation time, which orders events and is not wall-clock.
- hierarchy — which component reported.
uvm_test_top.env.sbis the scoreboard. - id — the string the testbench author chose, in brackets. The most useful grouping key, because it is deliberate.
- message — free text, often containing the actual values.
The id deserves emphasis. A verification engineer wrote [MISMATCH] on purpose, so it names a kind of failure in the project's own vocabulary. Grouping on it is grouping the way the testbench's author intended, which is why Chapter 9.4 prefers it over anything parsed out of the message text.
5. The Field Count Varies
The format is regular and the field count is not:
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { printf "NF=%-3d %s\n", NF, substr($0, 1, 60) }' sim.logNF=8 UVM_INFO @ 0: reporter [RNTST] Running test alu_smoke_test..
NF=11 UVM_INFO ../tb/env/alu_env.sv(87) @ 0: uvm_test_top.env [BUI
NF=9 UVM_INFO ../tb/seq/alu_seq.sv(42) @ 1000: uvm_test_top.env.a
NF=10 UVM_WARNING ../tb/env/alu_sb.sv(210) @ 15500: uvm_test_top.e
NF=10 UVM_WARNING ../tb/env/alu_sb.sv(210) @ 16000: uvm_test_top.e
NF=8 UVM_ERROR ../tb/env/alu_sb.sv(145) @ 20500: uvm_test_top.env
NF=8 UVM_ERROR ../tb/env/alu_sb.sv(145) @ 21000: uvm_test_top.env
NF=7 UVM_INFO ../tb/seq/alu_seq.sv(99) @ 30000: uvm_test_top.env.
NF=10 UVM_FATAL ../tb/env/alu_env.sv(300) @ 30500: uvm_test_top.enSeven to eleven fields for records of the same format, because the message is free text and contributes a variable number of whitespace-separated words.
That rules out one whole approach immediately. Anything counted from the right — $NF, $(NF-1) — lands on a different part of the message on every record. Chapter 3.2 made $NF the idiom for "the last field"; here the last field is a word of prose.
And there is a worse case:
awk 'NR == 3 || NR == 4 { printf "line %d: $2=[%s] $3=[%s]\n", NR, $2, $3 }' sim.logline 3: $2=[@] $3=[0:]
line 4: $2=[../tb/env/alu_env.sv(87)] $3=[@]Line 3 has no file. A message issued by the reporter component before any file context exists omits the FILE(LINE) piece entirely, so every field after the severity shifts by one.
So positional extraction is wrong in two independent ways: the right end moves with the message length, and the left end moves depending on whether a file is present. Chapter 9.2 is about extracting by pattern instead.
6. The Summary Is Ground Truth
The last region is the part most parsers ignore and the most useful thing in the file.
awk '/^--- UVM Report Summary ---/ { insum = 1; next } insum && /^UVM_[A-Z]+ *:/ { gsub(/ /, ""); split($0, p, ":"); printf "%-12s log claims %s\n", p[1], p[2] }' sim.logUVM_INFO log claims 4
UVM_WARNING log claims 2
UVM_ERROR log claims 2
UVM_FATAL log claims 1UVM counts its own messages and prints the totals. So the log contains an independent statement of what a parser should find — and comparing the two is the strongest self-check available:
awk '/^--- UVM Report Summary ---/ { insum = 1; next } insum && /^UVM_[A-Z]+ *:/ { gsub(/ /, ""); split($0, p, ":"); claimed[p[1]] = p[2] } !insum && /^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { counted[$1]++ } END { for (s in claimed) printf "%-12s counted=%d log says=%d %s\n", s, counted[s] + 0, claimed[s], (counted[s] + 0 == claimed[s] + 0 ? "agree" : "DISAGREE") }' sim.log | sortUVM_ERROR counted=2 log says=2 agree
UVM_FATAL counted=1 log says=1 agree
UVM_INFO counted=4 log says=4 agree
UVM_WARNING counted=2 log says=2 agreeFour for four. The parser's count and the log's own count agree on every severity.
7. What to Check in Your Own Logs
The method, as a short list to run against a real log before writing anything:
awk 'NR <= 3 { printf "%d: %s\n", NR, substr($0, 1, 70) } NR == 4 { exit }' sim.log1: Chronologic VCS simulator copyright 1991-2024
2: Compiler version X-2024.03; Runtime version X-2024.03; Oct 12 14:30 2
3: UVM_INFO @ 0: reporter [RNTST] Running test alu_smoke_test...Where do the records start? Read the first few lines. The preamble length is tool-specific and sometimes hundreds of lines.
awk '/^UVM_/ { n[$1]++ } END { for (s in n) printf "%-14s %d line(s) begin with this\n", s, n[s] | "sort" }' sim.logUVM_ERROR 3 line(s) begin with this
UVM_FATAL 2 line(s) begin with this
UVM_INFO 5 line(s) begin with this
UVM_WARNING 3 line(s) begin with thisDoes a bare severity pattern over-count? It does here — 3 errors where there are 2, which is Chapter 9.3's subject and visible in one command.
awk '/^--- / || /^\*\* / || /^\$finish/ { printf "%d: %s\n", NR, $0 }' sim.log12: --- UVM Report Summary ---
13: ** Report counts by severity
18: $finish called from file "../tb/top.sv", line 55.What structural markers exist? These are the lines a parser uses to find region boundaries, and their exact text is tool-specific.
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { if (NF > mx) mx = NF; if (mn == 0 || NF < mn) mn = NF } END { printf "record field count ranges %d to %d\n", mn, mx }' sim.logrecord field count ranges 7 to 11How much does NF vary? A wide range confirms that positional extraction from the right is unsafe.
8. Common Mistakes
Writing the pattern before reading the file. The log has regions; a pattern does not know that.
Assuming the whole file is records. Nine of eighteen lines here.
Counting with a bare /UVM_ERROR/. The report summary matches it — Chapter 9.3.
Anchoring and assuming that fixes it. The summary line also starts with the severity.
Using $NF or $(NF-1). The message is free text, so the field count varies — section 5.
Assuming every record has a file. A reporter message does not, and the fields shift — section 5.
Ignoring the report summary. It is an independent count of what you are trying to parse — section 6.
Requiring the report summary. A log that died has none, and those matter most.
Treating these fixtures as a real tool's format. The record shape is UVM's; everything around it varies — section 1.
9. Exercises
Exercise 1 — Find the regions
Report how many lines are preamble, records and summary.
awk '/^--- UVM Report Summary ---/ { insum = 1 } insum { tail++; next } /^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { rec++; next } { head++ } END { printf "preamble=%d records=%d summary=%d total=%d sum=%d\n", head, rec, tail, NR, head + rec + tail }' sim.logpreamble=2 records=9 summary=7 total=18 sum=18The categories sum to the line count, so nothing was missed.
Exercise 2 — Decompose a record
Print the severity and the raw second field for every record, showing which records lack a file.
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { printf "%-12s $2=[%s]%s\n", $1, $2, ($2 == "@" ? " <- no file" : "") }' sim.logUVM_INFO $2=[@] <- no file
UVM_INFO $2=[../tb/env/alu_env.sv(87)]
UVM_INFO $2=[../tb/seq/alu_seq.sv(42)]
UVM_WARNING $2=[../tb/env/alu_sb.sv(210)]
UVM_WARNING $2=[../tb/env/alu_sb.sv(210)]
UVM_ERROR $2=[../tb/env/alu_sb.sv(145)]
UVM_ERROR $2=[../tb/env/alu_sb.sv(145)]
UVM_INFO $2=[../tb/seq/alu_seq.sv(99)]
UVM_FATAL $2=[../tb/env/alu_env.sv(300)]One record in nine has no file — enough to break any positional script.
Exercise 3 — Measure the field-count spread
awk '/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { n[NF]++ } END { for (k in n) printf "NF=%s on %d record(s)\n", k, n[k] | "sort -t= -k2 -n" }' sim.logNF=7 on 1 record(s)
NF=8 on 3 record(s)
NF=9 on 1 record(s)
NF=10 on 3 record(s)
NF=11 on 1 record(s)Five distinct field counts across nine records — and the two most common, 8 and 10, account for only six of them. There is no modal width to rely on.
Exercise 4 — Read the log's own summary
awk '/^--- UVM Report Summary ---/ { insum = 1; next } insum && /^UVM_[A-Z]+ *:/ { gsub(/ /, ""); split($0, p, ":"); printf "%-12s %s\n", p[1], p[2] }' sim.logUVM_INFO 4
UVM_WARNING 2
UVM_ERROR 2
UVM_FATAL 1gsub(/ /, "") strips all spaces so split on : gives clean halves — the summary's column alignment uses a variable number of spaces.
Exercise 5 — Engineering application
Write a log profiler: given a log you have not seen, report its regions, its record format spread, and whether a bare severity pattern would over-count.
awk '
/^--- UVM Report Summary ---/ { insum = 1 }
insum { tail++ }
insum && /^UVM_[A-Z]+ *:/ { gsub(/ /, ""); split($0, p, ":"); claimed[p[1]] = p[2] }
insum { next }
/^UVM_(INFO|WARNING|ERROR|FATAL) (\.|\/|@)/ { rec++; counted[$1]++; if (NF > mx) mx = NF; if (!mn || NF < mn) mn = NF; next }
/^UVM_/ { strays++; next }
{ head++ }
END {
printf "lines=%d preamble=%d records=%d summary=%d stray UVM_ lines=%d\n", NR, head + 0, rec + 0, tail + 0, strays + 0
printf "record NF ranges %d..%d\n", mn + 0, mx + 0
for (s in claimed) {
bare = counted[s] + strays_by[s]
printf " %-12s parsed=%d log claims=%s %s\n", s, counted[s] + 0, claimed[s], (counted[s] + 0 == claimed[s] + 0 ? "agree" : "DISAGREE")
}
}' sim.log | sort UVM_ERROR parsed=2 log claims=2 agree
UVM_FATAL parsed=1 log claims=1 agree
UVM_INFO parsed=4 log claims=4 agree
UVM_WARNING parsed=2 log claims=2 agree
lines=18 preamble=2 records=9 summary=7 stray UVM_ lines=0
record NF ranges 7..11Everything a reader needs before writing a real pattern: the region sizes, the field-count spread, and a four-way agreement with the log's own counts.
Note stray UVM_ lines=0: the rule /^UVM_/ { strays++ } catches any line beginning with a severity that the record pattern rejected outside the summary. Zero here means the positive pattern did not miss a record format — and a non-zero value would be the signal to widen it.
The engineering caveat this whole module carries: these counts are of message records, not of defects. UVM_ERROR : 2 means the report server printed two error messages, which Chapter 9.3 shows can be one bug or two.
Exercise 6 — Challenge: make the cross-check fail
A self-check that has never failed is a comment. Break the record pattern and confirm the cross-check notices.
awk '/^--- UVM Report Summary ---/ { insum = 1; next } insum && /^UVM_[A-Z]+ *:/ { gsub(/ /, ""); split($0, p, ":"); claimed[p[1]] = p[2] } !insum && /^UVM_(INFO|WARNING|ERROR|FATAL) \.\./ { counted[$1]++ } END { for (s in claimed) printf "%-12s counted=%d log says=%d %s\n", s, counted[s] + 0, claimed[s], (counted[s] + 0 == claimed[s] + 0 ? "agree" : "DISAGREE") }' sim.log | sortUVM_ERROR counted=2 log says=2 agree
UVM_FATAL counted=1 log says=1 agree
UVM_INFO counted=3 log says=4 DISAGREE
UVM_WARNING counted=2 log says=2 agreeNarrowing the pattern to require a relative path (\.\.) dropped the reporter record, and the cross-check caught it: UVM_INFO counted=3, log says 4.
That is the check doing exactly its job — it found a record format the pattern did not cover, which is the failure mode no amount of reading the script would reveal. Three of the four severities still agree, so the disagreement even localises the problem to UVM_INFO.
The lesson carries to any log format: a parser should agree with something the file asserts about itself. Where no such assertion exists, Chapter 9.5 builds the substitute — comparing the set of logs against the set of tests that were supposed to run.
10. Summary
- Read the log's shape before writing a pattern. It has regions, and a pattern does not know that.
- Three regions: tool preamble, UVM message records, report summary and tail. Nine of eighteen lines here are records.
- The record format is
SEVERITY FILE(LINE) @ TIME: HIERARCHY [ID] message, which is a UVM library convention rather than a tool behaviour. - The
idin brackets is the best grouping key, because a verification engineer chose it deliberately. NFvaries — 7 to 11 here — because the message is free text. Never count fields from the right.- A
reporterrecord has no file, so the fields shift. Never extract by position. - The report summary is the log's own count of the records you are parsing — an independent second measurement, free.
- Cross-check against it when present, and do not require it: a log that died has none, and those matter most.
- A count of records is not a count of defects.
- Everything except the record shape is tool-specific. Profile your own logs with section 7's commands.
You can now read a log before parsing it. Chapter 9.2 — Extracting UVM Messages does the parsing: pulling severity, file, line, id and message out of a record with one rule instead of four near-identical greps, and doing it by pattern so the reporter record and the free-text message cannot shift the result.
Where this fits
Part of the awk curriculum.
