Through all of Part 4, Bug Zoo BUG_5 escaped every testbench. Its round-robin pointer never advances, so the lowest-numbered waiting input always wins. Under DCRouter's traffic every packet still arrived, intact and in order, and every counter matched. The bug does not make anything wrong. It makes some packets wait much longer than the specification allows, and nothing in the testbench was measuring waiting.
How do you check a design's performance, not just its function?
The obvious check is throughput: count packets per input and compare. Below saturation, a design that delivers everything eventually has the same throughput however unfairly it schedules, because every packet is served in the end. Unfairness shows only in when packets are served: in the tail of the latency distribution. Averages hide it too: a few packets waiting a very long time barely move a mean.
Property | Specified as | Checked by |
|---|---|---|
Latency | A bound, often on a percentile or the maximum | Timestamps per transaction, compared with the bound |
Throughput | Minimum rate under a stated load | Counting over a window at saturation |
Fairness | Bounded waiting: at most N others served first | Head-of-line wait, or a count of skips per requester |
Starvation freedom | Every request eventually served, within a bound | Maximum wait against the bound |
The specification must give numbers. "Round-robin" implies one: with 4 inputs, a waiting packet lets at most 3 others go first.
One output serves 4 inputs at 96% offered load; each packet occupies it for 4 cycles. The checker measures each packet's head-of-line wait, from reaching the front of its queue to being granted, and asserts the round-robin bound of 16 cycles (3 other packets of 4 cycles, plus the one already being sent).
"""Checking performance: a head-of-line wait checker on a 4-input round-robin output.
One output serves 4 inputs; each packet occupies it for 4 cycles. Each input receives a new
packet with probability 0.06 per cycle (total offered load 0.96 of the output). Two arbiters:
round-robin the pointer moves past each winner (DCRouter's rule R6)
stuck pointer the pointer never moves, so the lowest-numbered waiting input always wins
(Bug Zoo BUG_5)
For every packet the checker measures its head-of-line wait: cycles from reaching the front of
its input queue to being granted. Round-robin guarantees at most 3 other packets go first, so
the wait is at most 3 x 4 cycles plus the 4-cycle packet already being sent: 16 cycles. The
checker asserts that bound and prints the median, 99th percentile and maximum per input.
"""
import random
SERVICE, RATE, CYCLES, BOUND = 4, 0.06, 20000, 16
def simulate(stuck, seed=2):
rng = random.Random(seed)
queues = [[] for _ in range(4)] # arrival cycles
head_since = [None] * 4 # cycle the current head reached the front
waits = [[] for _ in range(4)]
ptr, busy = 0, 0
for t in range(CYCLES):
for i in range(4):
if rng.random() < RATE:
queues[i].append(t)
if len(queues[i]) == 1:
head_since[i] = t
if busy > 0:
busy -= 1
if busy == 0:
order = [(ptr + k) % 4 for k in range(4)]
win = next((i for i in order if queues[i]), None)
if win is not None:
waits[win].append(t - head_since[win])
queues[win].pop(0)
head_since[win] = t if queues[win] else None
busy = SERVICE
if not stuck:
ptr = (win + 1) % 4
return waits
def pct(xs, q):
xs = sorted(xs)
return xs[min(len(xs) - 1, int(q * len(xs)))]
results = {}
for name, stuck in [("round-robin", False), ("stuck pointer", True)]:
w = simulate(stuck)
results[name] = w
print(f"{name}: head-of-line wait in cycles (median / 99th percentile / max), packets served")
for i in range(4):
print(f" input {i}: {pct(w[i], 0.5):4d} / {pct(w[i], 0.99):4d} / {max(w[i]):4d} {len(w[i]):5d}")
viol = sum(x > BOUND for xs in w for x in xs)
print(f" packets over the {BOUND}-cycle bound: {viol}")
rr, st = results["round-robin"], results["stuck pointer"]
assert max(max(x) for x in rr) <= BOUND
assert max(st[3]) > 10 * BOUND
print(f"PASS: round-robin never exceeds {BOUND} cycles; with the stuck pointer input 3 waits up to {max(st[3])} cycles")It prints:
round-robin: head-of-line wait in cycles (median / 99th percentile / max), packets served
input 0: 12 / 16 / 16 1201
input 1: 12 / 16 / 16 1263
input 2: 9 / 16 / 16 1165
input 3: 10 / 16 / 16 1159
packets over the 16-cycle bound: 0
stuck pointer: head-of-line wait in cycles (median / 99th percentile / max), packets served
input 0: 2 / 4 / 4 1201
input 1: 3 / 17 / 27 1263
input 2: 4 / 43 / 68 1165
input 3: 4 / 128 / 208 1159
packets over the 16-cycle bound: 338
PASS: round-robin never exceeds 16 cycles; with the stuck pointer input 3 waits up to 208 cyclesHow to read it: both arbiters serve exactly the same packets per input (1201, 1263, 1165, 1159), so a throughput check sees nothing. The medians do not reveal the bug either; the stuck pointer's medians are actually lower. The tail does: round-robin never exceeds 16 cycles, while with the stuck pointer input 3's 99th percentile is 128 cycles and its worst wait 208, and 338 packets break the bound. A checker on the maximum against a specified bound catches it in one run.
Get numbers into the specification: bounds on latency, minimum throughput, a fairness guarantee.
Check tails, not averages: maximum or high percentiles against the bound.
Create the load the bound is about: saturation and hot spots (lesson 4.6); a quiet test cannot violate a fairness bound.
Measure at the pins where possible, and where the decision is hidden, judge only where timing is certain (lesson 5.5).
Keep performance checks separate from functional ones, so a slow design is reported as slow, not as wrong.
Practice | Purpose |
|---|---|
Latency and bandwidth monitors in the testbench | Every transaction timestamped; statistics per test |
Performance regressions with bounds | A change that slows the design fails a test |
Architecture models as performance references | Expected numbers to compare against (lesson 8.7) |
A team (illustrative) checked average latency against a target and met it comfortably. In the system, one low-priority client occasionally waited thousands of cycles and timed out. The average was dominated by thousands of fast transactions; the starvation lived in the tail. A maximum-wait check under a hot-spot load would have failed in block simulation.
The load is below saturation, so every packet is served eventually; unfairness changes when packets are served, not whether.
Once a packet is at the head, at most 3 other inputs' packets (4 cycles each) go first, plus up to 4 cycles for the packet already being sent: 16.
Performance bugs pass functional checks; they must be specified as numbers and checked.
Check tails (maximum, high percentiles) under the load the bound is about.
A stuck round-robin pointer served the same packets per input; only the wait tail revealed it.