Perl Part II · Module 18 · Log Parsing and Text Processing
What a Tool Log Actually Looks Like
Parsing a tool log is the core skill of Part II, and almost everything built in the next eleven modules reads one. So it is worth spending a chapter on what is actually in the file, because a log is not a flat stream of messages — it has sections, each written by a different phase of the tool, and a pattern that is correct in one section is wrong in another. This chapter measures the specific consequence of ignoring that, and it is the measurement the whole module is built on: the single most common log-parsing command in this industry gives the wrong answer on a normal log, and it gives it quietly.
Foundation12 min readPerlPart IILog Parsing
Part II · Module 18 · Chapter 18.1 · What a Tool Log Actually Looks Like
1. Five Sections, Not One Stream
Figure 18.1.1 — The five sections of a tool log
A log of the kind this module parses, in the shape a real one has:
Chronologic VCS simulator copyright 1991-2023
Compiler version V-2023.12; Runtime version V-2023.12
TimeScale is 1 ps / 1 ps
Elaboration: 412 modules, 1844 instances
UVM_INFO @ 0: reporter [RNTST] Running test alu_smoke_test...
UVM_ERROR tb/env/scoreboard.sv(151) @ 2400: [SB_MISMATCH] expected 0x0f got 0x0e
UVM_WARNING tb/env/driver.sv(88) @ 2401: [DRV] backpressure for 3 cycles
--- UVM Report Summary ---
UVM_INFO : 3
UVM_WARNING : 1
UVM_ERROR : 1
UVM_FATAL : 0
$finish at simulation time 9600 psLook at lines 6 and 10. One is a message reporting a mismatch; the other is a count saying how many such messages there were. Both begin with UVM_ERROR.
2. The Log Counts Itself
use strict;
use warnings;
use File::Temp qw(tempdir);
use File::Spec;
# ---- the SAMPLE log above, written to a file
my $dir = tempdir(CLEANUP => 1);
my $log = File::Spec->catfile($dir, "sim.log");
open my $w, ">", $log or die "cannot write: $!\n";
print $w <<'LOG';
Chronologic VCS simulator copyright 1991-2023
Compiler version V-2023.12; Runtime version V-2023.12
TimeScale is 1 ps / 1 ps
Elaboration: 412 modules, 1844 instances
UVM_INFO @ 0: reporter [RNTST] Running test alu_smoke_test...
UVM_ERROR tb/env/scoreboard.sv(151) @ 2400: [SB_MISMATCH] expected 0x0f got 0x0e
UVM_WARNING tb/env/driver.sv(88) @ 2401: [DRV] backpressure for 3 cycles
--- UVM Report Summary ---
UVM_INFO : 3
UVM_WARNING : 1
UVM_ERROR : 1
UVM_FATAL : 0
$finish at simulation time 9600 ps
LOG
close $w;
# ---- the pattern everyone writes first
my $RUN = qr/^UVM_(?:INFO|WARNING|ERROR|FATAL)\b/;
open my $fh, "<", $log or die "cannot read: $!\n";
my (@matched, $ln);
while (my $l = <$fh>) {
$ln++;
chomp $l;
push @matched, [$ln, $l] if $l =~ $RUN;
}
close $fh;
printf "lines matching %s : %d\n", 'qr/^UVM_(INFO|WARNING|ERROR|FATAL)\b/', scalar @matched;
printf " %2d %s\n", @$_ for @matched;
# a summary COUNT line is "UVM_WORD : <number>" and nothing else
my $COUNT = qr/^UVM_\w+\s*:\s*\d+\s*\z/;
my @genuine = grep { $_->[1] !~ $COUNT } @matched;
my @counts = grep { $_->[1] =~ $COUNT } @matched;
printf "\n genuine run messages : %d\n", scalar @genuine;
printf " summary count lines misclassified as messages: %d\n", scalar @counts;
my $naive = grep { $_->[1] =~ /UVM_ERROR/ } @matched;
my $truth = grep { $_->[1] =~ /UVM_ERROR/ } @genuine;
printf "\n a naive UVM_ERROR count reports %d; the truth is %d\n", $naive, $truth;
printf " so the error count is overstated by %d, every time\n", $naive - $truth;lines matching qr/^UVM_(INFO|WARNING|ERROR|FATAL)\b/ : 7
5 UVM_INFO @ 0: reporter [RNTST] Running test alu_smoke_test...
6 UVM_ERROR tb/env/scoreboard.sv(151) @ 2400: [SB_MISMATCH] expected 0x0f got 0x0e
7 UVM_WARNING tb/env/driver.sv(88) @ 2401: [DRV] backpressure for 3 cycles
9 UVM_INFO : 3
10 UVM_WARNING : 1
11 UVM_ERROR : 1
12 UVM_FATAL : 0
genuine run messages : 3
summary count lines misclassified as messages: 4
a naive UVM_ERROR count reports 2; the truth is 1
so the error count is overstated by 1, every timeA naive error count reports 2 where the truth is 1.
That is not a contrived failure. grep -c UVM_ERROR sim.log is the most widely used log check in this industry, and on any log that ends with a UVM report summary it returns one more than the real number — because the summary's own UVM_ERROR : 1 line contains the string.
The consequences are both directions of wrong:
A clean run reports a failure. Zero errors, but the summary still prints UVM_ERROR : 0, which contains UVM_ERROR. So grep -c returns 1 on a passing test, and a regression keyed on "count greater than zero" fails every test it runs.
A failing run reports the wrong count. Three real errors become four, so a threshold of "fewer than four is acceptable" passes a run with three.
And the error is invisible, because the number is plausible. Chapter 14.1's third kind of failure, from the most ordinary command in the flow.
3. What Each Section Is For
The banner. Tool name, version, build date, copyright. A parser usually skips it — except to record the version, which Chapter 18.8 shows is the thing you need when the same parser has to handle two tool releases.
Elaboration. Module and instance counts, timescale, library resolution, and the compile warnings. Mostly skipped, with one exception: an elaboration error here means the run never started, which is a completely different outcome from a run that started and failed. Chapter 18.9 separates them.
The run. The messages. This is what a parser is for, and it is usually the largest section by a wide margin — Chapter 18.10 is about the case where it is larger than memory.
The summary. The tool's own counts. Useful as a cross-check on your parse rather than as the parse itself: if your count of run messages disagrees with the summary's count, one of you is wrong and you want to know. Treating the summary as the answer is tempting and fragile, because not every tool writes one and a truncated log has none.
The epilogue. $finish, simulation time, CPU time, memory. The presence of $finish is the cheapest available signal that the run reached its end rather than being killed — which Chapter 18.9 uses, and which matters because a killed run leaves a log that looks complete until you check.
4. Finding the Boundaries
use strict;
use warnings;
use File::Temp qw(tempdir);
use File::Spec;
my $dir = tempdir(CLEANUP => 1);
my $log = File::Spec->catfile($dir, "sim.log");
open my $w, ">", $log or die "cannot write: $!\n";
print $w <<'LOG';
Chronologic VCS simulator copyright 1991-2023
Contains Synopsys proprietary information.
Compiler version V-2023.12; Runtime version V-2023.12
Top Level Modules:
tb_top
TimeScale is 1 ps / 1 ps
Elaboration: 412 modules, 1844 instances
UVM_INFO @ 0: reporter [RNTST] Running test alu_smoke_test...
UVM_INFO tb/env/scoreboard.sv(142) @ 1200: [SB] compare ok pin=u_alu/A
UVM_ERROR tb/env/scoreboard.sv(151) @ 2400: [SB_MISMATCH] expected 0x0f got 0x0e
UVM_WARNING tb/env/driver.sv(88) @ 2401: [DRV] backpressure for 3 cycles
--- UVM Report Summary ---
** Report counts by severity
UVM_INFO : 3
UVM_WARNING : 1
UVM_ERROR : 1
UVM_FATAL : 0
$finish called from file "tb/tb_top.sv" line 91.
$finish at simulation time 9600 ps
CPU Time: 1.420 seconds
LOG
close $w;
# Boundaries are recognised by MARKERS, not by line numbers -- a log's
# section lengths change with every run, its markers do not.
my @boundary = (
[ "banner", qr/\A(?:Chronologic|Contains|Compiler version)/ ],
[ "elaboration", qr/\A(?:Top Level Modules|\s+\w+\s*\z|TimeScale|Elaboration)/ ],
[ "summary", qr/\A(?:---\s*UVM Report Summary|\*\* Report counts)/ ],
[ "epilogue", qr/\A(?:\$finish|CPU Time:|Time:)/ ],
);
my $RUN_MSG = qr/\AUVM_(?:INFO|WARNING|ERROR|FATAL)\s+\S|\AUVM_\w+\s+@/;
open my $fh, "<", $log or die "cannot read: $!\n";
my ($state, %lines, %first, $ln) = ("banner");
while (my $l = <$fh>) {
$ln++;
chomp $l;
next unless $l =~ /\S/;
# a marker moves the state; otherwise the state persists
for my $b (@boundary) {
if ($l =~ $b->[1]) { $state = $b->[0]; last }
}
$state = "run" if $l =~ $RUN_MSG && $state ne "summary" && $state ne "epilogue";
$lines{$state}++;
$first{$state} //= $ln;
}
close $fh;
printf "%-13s %6s %s\n", "SECTION", "lines", "first line";
for my $name (qw(banner elaboration run summary epilogue)) {
printf "%-13s %6d %s\n", $name, $lines{$name} // 0, $first{$name} // "-";
}
printf "\ntotal non-blank lines: %d\n", eval { my $s = 0; $s += $_ for values %lines; $s };SECTION lines first line
banner 3 1
elaboration 4 4
run 4 8
summary 6 12
epilogue 3 18
total non-blank lines: 20Three principles are doing the work there, and all three carry into the rest of the module.
Boundaries are markers, not line numbers. --- UVM Report Summary --- appears at a different line in every run. A parser that skips "the first seven lines" breaks the first time elaboration emits an extra warning.
The state persists between markers. A line that matches nothing belongs to the section it is in. That is a one-variable state machine, and Chapter 18.7 is what happens when one variable is not enough.
The run pattern is guarded by the state. $state ne "summary" is what stops the summary's count lines being classified as run messages — the same fix the measurement in section 2 was asking for, in its simplest form.
5. What To Remember
- A log is five sections, not one stream: banner, elaboration, run, summary, epilogue.
- The summary restates the run as counts, using the same leading token as the messages.
- Measured: a naive
UVM_ERRORcount reported 2 where the truth was 1. grep -c UVM_ERRORreturns 1 on a passing test, because the summary printsUVM_ERROR : 0.- So a regression keyed on "count greater than zero" fails every test it runs — and the number always looks plausible.
- The same substring match also catches
NO_UVM_ERROR, a quoted mention, a test name, andUVM_ERROR_LIMIT(18.4). - Record the tool version from the banner — 18.8 needs it.
- An elaboration error means the run never started, which is a different outcome from a run that failed (18.9).
- The run section is usually the bulk, and sometimes larger than memory (18.10).
- Use the summary as a cross-check on your parse, never as the parse — not every tool writes one, and a truncated log has none.
$finishis the cheapest signal that the run reached its end rather than being killed (18.9).- Recognise boundaries by markers, not line numbers — section lengths change every run.
- Let the state persist between markers, so an unmatched line belongs to the section it is in.
- Guard the run pattern with the state, which is what stops the summary being counted as messages.
- Before writing a parser,
grep -n -m1the markers and readheadandtail. The middle is what the parser is for.
Chapter 18.2 assembles the loop that every parser in the rest of the curriculum uses. It is seven steps long, it is rated Critical, and the reason is that six of the seven are easy to leave out and each omission produces a wrong answer rather than an error.
Where this fits
Part of the Perl curriculum.
