Metric analyzers and the ledger
Measure the latency of every read and the throughput over a window, decide their bounds in check_phase, report the numbers that make a passing test believable, and keep a ledger that catches a scoreboard losing predictions.
- Lesson
- U8.4
- Time
- 30 min
- Runs on
- Verilator with UVM
- Check
- 5 questions
The memory passes its functional regression and fails in the lab: under load, a read from bank 1 waits behind a burst of bank 0 reads until the controller times out. No test measured how long a read took. Every value was right, so every test was green.
Tag 1 is ready at 65 ns and loses to bank 0 twice. Its latency is 60 ns instead of 40.
Requirements about time rather than values
A specification promises more than values. It promises that a read completes within so many cycles, that a link sustains so many bytes per second, that an arbiter never starves a port. The scoreboard of U8.2 checks none of that: it would happily match a response that arrived a week late. A design can be functionally perfect and still unusable, and the bench has to be able to say so.
The Cookbook calls the component for this a metric analyzer. It is an ordinary analysis component, fed by the same monitors through the same analysis ports, and it knows nothing about the right data. It knows when things happened. The example builds a latency_meter with two analysis imps, one for requests and one for responses, beside the scoreboard.
Record, compute, decide, tell
The meter spreads its work over four phases, and each phase has a reason.
| Phase | What the meter does | Why there |
|---|---|---|
run_phase (via write) | Records when each read was taken and when it was answered | The data exists only while the bus is running |
extract_phase | Computes each latency, the maximum, the mean, how many exceed the bound, and the throughput over the busy window | A function, after the last response, before any verdict |
check_phase | Raises UVM_ERROR if a read exceeded the bound, if a read was taken but never answered, or if the throughput fell below its floor | By now every component's numbers exist; a parent can combine them |
report_phase | Prints the count, maximum, mean and bound | Runs whether or not the check failed, so the log always carries the numbers |
The post-run phases run bottom-up, children before parents, one phase at a time across the whole bench.
The split is more than tidiness. Every component finishes extract_phase before anyone starts check_phase. So an environment can read its analyzers' numbers in its own check_phase and decide something none of them can alone, such as a total bandwidth across three ports. And the report phase prints the same numbers after a pass as after a fail, which is what makes a pass believable: "4 reads, max 60 ns" says more than "0 errors".
function void extract_phase(uvm_phase phase); // compute
foreach (taken[i]) begin
time lat = answered[i] - taken[i];
if (lat > max_latency) max_latency = lat;
if (lat > bound) late++;
end
endfunction
function void check_phase(uvm_phase phase); // decide
if (late != 0)
`uvm_error("LATENCY", $sformatf("%0d of %0d reads took longer than %0d ns", late, taken.size(), bound))
endfunction
function void report_phase(uvm_phase phase); // tell
$display("latency: %0d reads, max %0d ns, ...", taken.size(), max_latency);
endfunctionPairing a response with its read
A latency is the time between a read being taken and its response being seen, so the meter must pair the two. Both time stamps come from the monitor, which stamps each transaction at the sampling edge, so the meter never reads the clock itself. The pairing problem is the one U8.2 solved: responses arrive out of order and tags repeat. So the meter keeps time issued[bit [3:0]][$], a queue of issue times per tag, and pops the front when a response with that tag arrives.
The tempting shortcut, "time since the most recent request", measures nothing once two reads overlap. In the example the response to tag 1 arrives at 95 ns; the most recent read was taken at 65 ns, which gives 30 ns, while the true latency is 60 ns. The spot question has that version.
The example's numbers: tags 2, 3 and 4 take 20 ns each, and tag 1 takes 60 ns because bank 0 was ready at the same edge twice and goes first. The maximum sits exactly on the 60 ns bound, so the check passes. One more bank 0 read behind tag 1 pushes it to 70 ns, and the quiz asks for the resulting report line.
A read that is taken and never answered leaves no sample, and a meter that only averages its samples would never notice. So the meter keeps outstanding(), the reads still in its queues, and check_phase reports any that remain. The test waits for both the scoreboard and the meter to drain. Then it keeps observing for one latency bound, 60 ns, before it drops its objection: the U8.1 quiet-window rule with this test's bound. In the example the counts balance at 95 ns and the run phase ends at 155 ns. A meter with work left cannot be cut off by a scoreboard that has none.
Throughput over a declared window
Each read has its own latency bar; throughput is one window over all of them: 4 reads from 35 to 95 ns, 0.67 reads per 10 ns cycle against a floor of 0.50.
Throughput is a count divided by a time, and the time is the whole decision. The meter uses the busy window, from the first read taken to the last response seen. Idle time outside it is left out, because the test, not the design, chose it.
Say which window you measured, in the report line, so the number can be reproduced. A window that starts at the first request includes the ramp while the pipeline fills, which understates a sustained rate; a long test measures a middle stretch instead. A window that ends at the last request rather than the last response leaves reads out of the count while their time is in the window. Whatever the choice, the floor is a specification value and the check lives in check_phase like the latency bound.
The ledger
U7.3 introduced the idea: every input creates an obligation and every checked result discharges one.
check_phase reads.- The books balance155 ns (illustrative)
After the run phase ends,
check_phasereads all three counters: 4 reads = 4 checked + 0 waiting.More context
The example prints "ledger: 4 reads = 4 checked + 0 waiting; mismatches 0, orphans 0, checks on".
Follow the calls, values and waits
- data reads = 4: reads → reads == checked + waiting
- data checked = 4: checked → reads == checked + waiting
- data waiting = 0: waiting() → reads == checked + waiting
- Something was checked155 ns (illustrative)
With checks on,
checkedmust not be 0. Here it is 4, so the run proved something.More context
A run that compares nothing also balances: 0 reads = 0 checked + 0 waiting. This second test catches it.
Follow the calls, values and waits
- data checked = 4: checked → checked != 0 with checks on
- A lost prediction
The quiz scoreboard keeps one slot per tag and drops a response that finds nothing. It ends with 5 reads, 4 checked and 0 waiting: the rule fails, and only the ledger reports it.
More context
Its own compares report zero errors, zero mismatches and zero orphans. A read still in flight would be waiting; this one was dropped on the floor.
Follow the calls, values and waits
- data reads = 5: reads → reads == checked + waiting
- data checked = 4: checked → reads == checked + waiting
- data waiting = 0: waiting() → reads == checked + waiting
- A run that proved nothing
If the scoreboard never compared anything while checks were on,
checkedis 0 and the second test reports it.More context
For example, an analysis port left unconnected: the scoreboard sees no traffic, every count stays at 0 and the ledger still balances.
Follow the calls, values and waits
- data checked = 0: checked → checked != 0 with checks on
Read the diagram as text
- reads: write_req, one per READ
- checked: write_rsp, one per compared response
- waiting(): predictions still in pending
- reads == checked + waiting: else: the scoreboard lost a prediction
- checked != 0 with checks on: else: a run that proved nothing
- reads → reads == checked + waiting
- checked → reads == checked + waiting
- waiting() → reads == checked + waiting
- checked → checked != 0 with checks on
Both tests check the scoreboard's own bookkeeping, not whether the monitor saw every transfer.
What does the ledger catch that the orphan and missing-read reports do not? A scoreboard that is wrong quietly. The quiz program keeps one prediction per tag and ignores a response that finds nothing, two small shortcuts a tired engineer makes. Two reads with tag 3 are issued back to back. The second prediction overwrites the first, the first response matches it, the second response finds nothing and is dropped. Zero errors, zero mismatches, zero orphans, and only four of five reads were checked. The ledger is the only line that notices.
Counts only what it saw
function void check_phase(uvm_phase phase);
foreach (pending[t]) if (pending[t].size() != 0)
`uvm_error("SB", "...never answered")
endfunction
Silent when a prediction was lost before it could be waited for.
Balances the books
function void check_phase(uvm_phase phase);
foreach (pending[t]) if (pending[t].size() != 0)
`uvm_error("SB", "...never answered")
if (reads != checked + waiting())
`uvm_error("SB", $sformatf("ledger: %0d reads, but %0d checked + %0d waiting",
reads, checked, waiting()))
if (checks_enable && checked == 0)
`uvm_error("SB", "no response was checked, so this run proves nothing")
endfunction
A lost prediction and an empty run both fail, whatever the design did.
The last line guards against the oldest false pass of all: zero errors with zero checks. A sequence that sends only writes, a monitor whose virtual interface points at the wrong instance, an analysis port left unconnected. Each produces a clean run that checked nothing. "No response was checked" turns that into a failure with a cause.
Knobs that switch checking off
Some tests must run with the scoreboard quiet. One sends illegal requests to exercise error handling; another resets the design half way and expects responses to vanish. The Cookbook's monitor has checks_enable and coverage_enable for this, and the scoreboard takes the same knob through uvm_config_db.
When checks_enable is 0, write_req and write_rsp return before counting anything, so the ledger stays balanced at zero and the zero-check guard, which tests the knob first, stays silent. A knob that only skips the compare while the counts keep running would make the ledger lie, which is why both sides of the bookkeeping must honour it.
Treat the knob as a build-time choice, set once from the test. Flipping it during the run would leave predictions in the array with no response coming, or let responses arrive for reads that were never predicted. The next read with a reused tag would then pair with stale state. A reset in the middle of traffic needs its own rule for what happens to outstanding work, which U10.2 covers. And a scoreboard with checking off is only one consumer: the latency meter in the example keeps measuring, so the test still has to wait for it.
The ledger is a consistency check among the scoreboard's own counters. It proves that nothing was lost between write_req and check_phase; it cannot prove that the monitor saw every transfer, or that the masked bits it did not compare mattered. Those need the monitor's own counts of both streams and functional coverage of what was compared.
Print the counts in report_phase whether checks were on or off. In the log of a reset test, a line saying that no read was checked and checks were disabled is a fact. The same line in a data test is a bug. Only the printed numbers tell the two apart, and only if they are printed every time.
Try it
The bank 1 read (tag 1) is taken at 35 ns and ready at 65 ns, but bank 0 reads are ready at 65 and 75 ns and go first, so it is seen at 95 ns: 60 ns, on the bound. The three bank 0 reads take 20 ns each. Four reads completed between the first read at 35 ns and the last response at 95 ns: 0.67 reads per cycle, above the floor of 0.50. The meter prints its numbers in report_phase, after check_phase has passed them, and the test prints the balanced ledger with the checking mode.
Set bound = 40ns in the meter and check_phase reports one late read. Add a fourth bank 0 read to the script and tag 1 takes 70 ns.
65 ns rsp tag=2 data=11 matches READ tag=2 bank=0 addr=1 taken at 45 ns, waiting: 1 3 4 75 ns rsp tag=3 data=11 matches READ tag=3 bank=0 addr=1 taken at 55 ns, waiting: 1 4 85 ns rsp tag=4 data=11 matches READ tag=4 bank=0 addr=1 taken at 65 ns, waiting: 1 95 ns rsp tag=1 data=22 matches READ tag=1 bank=1 addr=2 taken at 35 ns, waiting: none 155 ns test counts balanced, quiet window over, run phase ends latency: 4 reads, max 60 ns, mean 30.0 ns, bound 60 ns, 0 late throughput: 4 reads in 60 ns (35 to 95 ns) = 0.67 reads per cycle, floor 0.50 ledger: 4 reads = 4 checked + 0 waiting; mismatches 0, orphans 0, checks on UVM_ERROR : 0Check yourself
Remember
- A metric analyzer is an analysis component that measures time or throughput; it knows nothing about the right data.
- Record during the run, compute in
extract_phase, decide withuvm_errorincheck_phase, print inreport_phase; the phases run bottom-up, so parents can combine children's numbers. - Throughput is a count over a declared window; say which window, and leave the test's idle time out of it.
- Pair each response with its read by tag, with a queue per tag; "time since the last request" measures nothing once reads overlap.
- The ledger
reads == checked + waitingcatches a scoreboard that loses predictions silently,checked == 0catches a run that proved nothing, and the counts printed inreport_phasemake a pass believable.
Practice
- A latency meter with two bounds Implement · Medium