Linux & Bash · Part II · Module 27 · Invoking Simulators and EDA Tools from the Shell
Controlling Waveform Dumping
Dumping is the one simulation option that changes a run's cost without changing its result, and the cost is large enough to reshape a regression. This chapter measures it four ways on the same run and finds that the standard advice — limit the hierarchy depth — is the weaker of the two available levers by two orders of magnitude.
Intermediate20 min readLinuxPart IIWaveformsPerformanceDisk
Part II · Module 27 · Chapter 27.6 · Controlling Waveform Dumping
A waveform database is the only artifact a simulation produces that is bigger than its input, and the only option whose cost you pay on every run whether or not anybody ever opens the result. Chapter 27.5 got one test launched correctly; this chapter decides what it writes while it runs.
1. Dumping Is a Testbench Decision the Shell Can Make
The calls that produce a waveform live in the source:
$dumpfile("dump.vcd"); // where
$dumpvars(0, tb_arb); // what: level 0 means "every level below here"Hard-coded, that means every run dumps. The pattern that makes it a shell decision is to guard them with a plusarg and read the parameters the same way:
if ($test$plusargs("DUMP")) begin
if (!$value$plusargs("DUMP_FILE=%s", dumpname)) dumpname = "dump.vcd";
if (!$value$plusargs("DUMP_DEPTH=%d", depth)) depth = 0;
$dumpfile(dumpname);
$dumpvars(depth, tb_arb);
if ($value$plusargs("DUMP_FROM=%d", dump_from)) begin
$dumpoff; // armed but off; turned on at DUMP_FROM
$display("TB dumping %0s depth=%0d window from %0d", dumpname, depth, dump_from);
end else begin
$display("TB dumping %0s depth=%0d (whole run)", dumpname, depth);
end
endWith that in place, dumping is off unless asked for — and off means nothing is written at all:
$ vvp build.vvp +TESTNAME=smoke +DUMP
VCD info: dumpfile dump.vcd opened for output.
TB dumping dump.vcd depth=0 (whole run)
TB TEST PASSED test=smoke checks=3 errors=0
$ ls -l dump.vcd
-rw-r--r-- 1 … 1249 … dump.vcd
$ rm -f dump.vcd
$ vvp build.vvp +TESTNAME=smoke
TB test=smoke seed=1 cycles=40 verbosity=LOW
TB TEST PASSED test=smoke checks=3 errors=0
$ ls dump.vcd
ls: dump.vcd: No such file or directoryNo file, not an empty one. That distinction matters downstream: a validator can treat "the dump is missing" as "dumping was off" rather than as "the run failed", and an empty file would be ambiguous between the two.
2. What It Costs, Measured Four Ways
Two levers on the same cost, and they are not the same size
One workload — the random test, 200,000 cycles, seed 3 — run four times with the same stimulus and four dumping configurations. The design is an arbiter with four ports plus its testbench, so this is a small design; the proportions are discussed below.
| configuration | runtime | VCD size |
|---|---|---|
| no dumping | 567 ms | 0 |
| depth 0 (all levels), whole run | 969 ms | 21,394,015 |
| depth 1 (top level only), whole run | 837 ms | 19,463,864 |
| depth 0, 200-cycle window | 600 ms | 23,080 |
Read the second row first, because it is the baseline cost of the default behaviour: 1.71× the runtime and 21 MB, for a design with four signals in it. Multiply by a regression of two thousand runs and the disk figure alone decides whether the regression can run.
Now the two ways of reducing it.
Limiting the depth saved 9% of the file. 19.46 MB against 21.39 MB. It also took 14% off the runtime overhead. This is the advice everybody gives — dump only the level you need — and on this design it is close to useless.
Limiting the time window saved 99.89% of the file. 23 KB against 21.39 MB: 927 times smaller. And the runtime overhead fell from 402 ms to 33 ms, which is 8% of what whole-run dumping cost.
3. Windowing in Practice
A window is only useful if you know where to put it, and there are three ways to know.
You already have the failure time. This is the common case and the best one: a run failed, the log says at what time, and the rerun dumps a window around it.
# first run: no dumping at all, so it is cheap
$ ( cd "$rundir" && exec vvp sim.vvp +TESTNAME=smoke +STRICT ) > "$rundir/run.log" 2>&1
$ cat "$rundir/run.log"
TB test=smoke seed=1 cycles=40 verbosity=LOW
TB ERROR t=45 gnt=10 expected 01
TB TEST FAILED test=smoke checks=3 errors=1
# it failed. The log says when, so the rerun knows where to put the window.
$ t=$(sed -n 's/^TB ERROR t=\([0-9]*\).*/\1/p' "$rundir/run.log" | head -1)
$ echo "first error at $t"
first error at 45Two details in that transcript. The time is extracted from the testbench's first ERROR line — head -1, because the originating failure is the one worth looking at and the rest are usually consequences, which is Chapter 24.6's rule and Module 29's subject. And the failing run exited 0: the only reason the flow knows to rerun at all is the log.
The testbench arms the window itself around whatever it is about to do, which is the right answer for a known-interesting phase:
// in the testbench: dump only across the phase under suspicion
if (dump_from >= 0 && i == dump_from) begin $dumpon; end
if (dump_from >= 0 && i == dump_from + 200) begin $dumpoff; endThe testbench dumps on its own failure, which needs the dump armed from the start and is therefore the expensive option — but it catches a failure you cannot predict. Note the asymmetry that makes this awkward: a waveform is interesting in the cycles before a failure, and by the time the testbench knows about the failure those cycles have passed. The usual compromise is a rolling window, which not every tool supports, or the two-run approach above.
The two-run approach is the one to default to, and it is worth naming why: the first run is cheap because it does not dump, and the second run is cheap because it dumps a window. A flow that dumps everything on every run pays the full cost on the 99% of runs that pass, in order to avoid a rerun on the 1% that fail.
4. The Switch Belongs in the Wrapper
Three plusargs, one wrapper flag:
#!/bin/bash
# the dumping part of a run wrapper
set -u
rargs=(+TESTNAME="$test" +SEED="$seed" +STRICT)
case "${dump:-off}" in
(off) : ;; # the default
(window) rargs+=(+DUMP +DUMP_FILE=dump.vcd +DUMP_DEPTH=0 +DUMP_FROM="${dump_from:?window needs a start cycle}") ;;
(full) rargs+=(+DUMP +DUMP_FILE=dump.vcd +DUMP_DEPTH="${dump_depth:-0}") ;;
(*) echo "unknown dump mode '$dump' (off|window|full)" >&2; exit 2 ;;
esacFour things that are deliberate there.
off is the default, so the expensive behaviour has to be asked for. A flow whose default is full will have its disk filled by somebody who did not know there was a switch.
window requires its start cycle via ${dump_from:?…}, which fails at the point of use rather than silently dumping the whole run. A window with no start is full wearing a cheaper name, and that is exactly the kind of quiet substitution that makes a disk-usage mystery.
An unknown mode is an error, not a fall-through to off — the lesson from Chapter 27.4, where a silently-defaulted value ran the wrong test.
The file name is relative. dump.vcd, not $rundir/dump.vcd, because the run already happens inside $rundir — Chapter 27.3's arrangement. An absolute path works too. A path whose directory does not exist does something worse than failing:
$ vvp build.vvp +TESTNAME=smoke +DUMP +DUMP_FILE=/tmp/nodir/d.vcd
TB test=smoke seed=1 cycles=40 verbosity=LOW
VCD Error: tb/tb_arb.v:42: Unable to open /tmp/nodir/d.vcd for output.
$ echo $?
1The simulation aborted at the $dumpfile call and never reached a verdict — no TEST PASSED, no TEST FAILED, nothing. Compare that with the failing run in the next section, which prints TEST FAILED and exits 0. So the two statuses are the wrong way round from what you would guess: a dumping configuration error exits 1 and a design failure exits 0.
That is the NO-VERDICT state from Chapter 27.3 arriving for a reason that has nothing to do with the design, and it is why the wrapper should create the directory before the tool needs it:
# the run directory exists before the tool is asked to write into it
mkdir -p "$rundir" || exit 25. Knowing What It Cost After the Fact
Two numbers belong in the run's record: the dump's size, and the runtime. Neither is interesting on one run and both are interesting across a hundred.
# after the run, beside the log
{ printf 'dump_bytes %s\n' "$( [ -f "$rundir/dump.vcd" ] && wc -c < "$rundir/dump.vcd" | tr -d ' ' || echo 0 )"
printf 'dump_mode %s\n' "${dump:-off}"
} >> "$rundir/config.txt"And then the question a flow can actually answer: which runs are paying for dumping that nobody reads?
# every run directory, its dump size, largest first
$ for d in runs/*/; do
> s=$( [ -f "$d/dump.vcd" ] && wc -c < "$d/dump.vcd" | tr -d ' ' || echo 0 )
> printf '%12s %s\n' "$s" "$(basename "$d")"
> done | sort -rn | head -5The sort -rn is deliberate, and the reason is more delicate than it first looks. sort without -n compares as text:
$ cat sizes.txt
9000000 big
100 small
21394015 huge
23080 window
$ sort -rn sizes.txt | head -2 # correct
21394015 huge
9000000 big
$ sort -r sizes.txt | head -2 # text order
9000000 big
23080 windowText order puts 9000000 above 21394015, because 9 sorts after 2. That is Chapter 13.3's trap.
That is the sort of small, previously-established rule that quietly decides whether a disk-usage report is true — and the sort that is worth re-measuring rather than remembering.
Module 28 takes this further and makes it a cleanup policy; here the point is narrower. If the size is not recorded, the cost of dumping is invisible until a filesystem fills up, and at that moment the information you need — which runs are large, and whether anybody opened them — no longer exists.
Practice
- Guard a
$dumpfileand$dumpvarspair with$test$plusargsand confirm that without the plusarg no file is created, rather than an empty one. - Run a long randomised test with no dumping and record the runtime.
- Run it again with whole-run dumping at depth 0, and record both runtime and file size. Compute the ratio.
- Run it with depth 1 and compute what fraction of the file the depth limit removed.
- Run it with a 200-cycle window and compute the same fraction.
- Compare the two fractions and decide which lever matters on your design.
- Explain the difference in terms of signals and value changes, and predict what would change on a large DUT under a small testbench.
- Build the two-run flow: a cheap non-dumping run, then extract the failure time from the log and rerun with a window there.
- Write the wrapper's dump-mode
casewithoffas the default, and prove thatwindowwithout a start cycle fails rather than dumping everything. - Give the dump an absolute path into a directory that does not exist and observe when the failure happens relative to elaboration.
- Record the dump size per run and produce a largest-first listing. Then remove the
-nfrom the sort and see which run it claims is largest.
Check Your Understanding
- Why does guarding dumping with a plusarg produce no file rather than an empty one, and why is that useful?
- On the measured workload, what did whole-run dumping cost in runtime and disk?
- How much of the file did limiting the depth remove?
- How much did a 200-cycle window remove?
- Express a dump's size as a product of two factors, and say which lever acts on which.
- Why did depth limiting barely help on this design, and when would it help more?
- Why is the two-run approach cheaper than always dumping, given that it sometimes runs twice?
- Why must a window's start cycle be required rather than defaulted?
- Why is the dump file name given relative to the run directory?
- A largest-first listing uses
printf '%12s'and omits-n. Why does it still give the right answer, and when does it stop?
6. Remember
- Guard dumping with a plusarg so it is off by default, and note that off means no file, not an empty one.
- Whole-run dumping cost 1.71× the runtime and 21 MB on a four-signal design.
- Depth limiting removed 9% of the file. A time window removed 99.89% — 927× smaller.
- Size is roughly signals × value changes: depth acts on the first factor, windowing on the second, and the second is usually the larger lever.
- Measure your own ratio. Three runs and a
wc -c; do not inherit a proportion measured on somebody else's hierarchy. - Default to two runs: a cheap run with no dumping, then a windowed rerun at the failure time from the log.
offis the default, an unknown mode is an error, and a window without a start cycle must fail rather than quietly become a full dump.- Record the dump size with the run, and sort numerically when you report it — a fixed-width
printfmakes text order look correct until one value outgrows the field.
7. What Is Next
The database exists and is as small as it can be. Chapter 27.7 opens it — and confronts the question the previous chapters have been circling: given twenty run directories, which database belongs to the failure, and is it even complete?
Where this fits
Part of the Linux & Bash curriculum.
