UCIe · Module 27
Performance Review Checklist
The pre-tapeout performance audit — deriving the ceiling before any measurement, the counter pipeline that attributes every lost cycle to a named cause, why offered traffic and retry bytes silently double a throughput report, torn snapshots that produce impossible ratios, and the credit window that caps a correct design at 40 % of its target.
27.3 asked whether the design can be proven correct. This gate asks whether it can meet its workload — and whether you can explain where every lost cycle went.
1. The One-Sentence Model
Performance closure means every lost service opportunity has an owner: if useful throughput sits below the derived ceiling, the missing cycles must be attributable to a named cause, and "overhead" is not a cause.
The gate's structure follows from that. First derive a ceiling from rate, width, direction and payload fraction (§6) — before any measurement, so the measurement has something to be compared against. Then account for the gap with counters that separate offered from accepted, physical from useful, and stall from idle (§8). A number without an attribution is a rumour.
And the failure this gate exists to catch is a design that is correct, well-verified, and structurally incapable of hitting its target (§17). Not a bug — a ceiling. No amount of arbitration tuning, buffer resizing or RTL optimisation moves it, because the limit is a resource that was sized before anyone wrote code.
2. What This Gate Owns
| Gate | Asks | Not this chapter |
|---|---|---|
| 27.1 — Architecture | what are we building? | the budget's derivation — commissioned there |
| 27.2 — RTL | did we implement it safely? | counter code correctness |
| 27.3 — Verification | can we prove it correct? | functional closure |
| 27.4 — Performance (this chapter) | can it meet the workload, with margin, explainably? | — |
| 27.5 — Integration | can the pieces work together? | package seams |
| 27.6 — Debug | can we diagnose it in silicon? | first-failure capture, trace |
Three boundaries.
27.1 §12 commissioned the budget; this gate audits whether it was met. That division matters: a target invented at this gate is not a target, it is a rationalisation. If no derived budget exists from the architecture gate, this review cannot pass — there is nothing to measure against, and §6 explains why deriving it now is not a repair.
27.2 reviews whether the counter RTL is correct; this gate reviews whether the counters mean what the report claims. A saturating counter implemented perfectly (27.2's concern) can still be counting the wrong event (§10) — and only this gate asks what event it counts.
And this gate does not own silicon observability, which is 27.6's. But every counter this chapter requires must exist in hardware to be readable after tapeout, so §22 hands the list over explicitly rather than assuming someone else asked.
3. Gate Decisions Applied to Performance
27.3 §3 defined PASS / CONDITIONAL PASS / FAIL. Here is what each means when the subject is a number.
| Decision | In this gate |
|---|---|
| PASS | target met on the defined workload, with documented margin, and the gap to ceiling fully attributed |
| CONDITIONAL PASS | target met but margin is thin and the sensitivity analysis (§21) bounds the risk, with a named owner and a closure precondition |
| FAIL | target not demonstrated · gap to ceiling unattributed · measured on a workload that is not the target workload · counters not trusted (§10–§13) · no margin analysis |
Three readings.
"Gap unattributed" is a FAIL on its own, even when the target is met. A design hitting its number with 40 % of the ceiling unexplained has 40 % of its behaviour outside anyone's model — and the first workload change moves it in a direction nobody can predict. Meeting a target by accident is not closure.
"Counters not trusted" is the row that turns §10 and §11 into gate-failing findings. If the throughput report is built on a counter that increments on valid rather than on transfer, the number is not low or high — it is meaningless, and every conclusion drawn from it is void.
And there is no CONDITIONAL PASS for "we'll measure it in silicon". That is 27.3 §3's mechanical test failing: 27.5 and the product schedule proceed on the assumption that performance closes, and discovering §17's ceiling after tapeout invalidates all of it. Undemonstrated is FAIL.
4. Statement Versus Evidence
| Review statement (worthless) | Evidence (accepted) |
|---|---|
| "bandwidth meets spec" | offered / accepted / transmitted / useful-delivered / retired counters captured under the target workload; ceiling derived; bottleneck boundary identified; margin quantified |
| "the link is 98 % utilised" | utilisation split into unique useful bytes, retry bytes and control bytes (§11) |
| "latency is 200 ns" | the distribution: min, median, tail, and the queueing-versus-service split (§14) |
| "we have enough buffering" | occupancy high-water correlated with arrival rate, service rate and stall cause (§18) |
| "performance is fine under retry" | the retry-rate assumption, its source, and measured useful throughput at that rate (§20) |
| "we hit the target" | on which workload, in which window, with what margin (§7, §21) |
| "we'll tune it later" | nothing — §17 cannot be tuned |
Three readings.
Every accepted answer names a workload and a window. A throughput number without both is unfalsifiable — peak over a 100-cycle window is a different claim from sustained over a millisecond, and teams present the first while meaning the second.
Row 2 is the most common way a performance report misleads while being arithmetically correct. 98 % utilisation is a true statement about the wire. If 30 % of those bytes are retransmissions, useful throughput is 68 % and the link is more congested than the number suggests, not less (§11).
And row 7 is the reason this gate exists before tapeout rather than after. Some performance limits are not tunable — §17's is a fixed resource multiplied by a fixed latency. The review either finds it now or the product ships with it.
5. Sourcing and Scope
6. Area A — Derive the Ceiling First
Before any measurement is presented, the review must see a ceiling derived from first principles. Otherwise a measured number has nothing to be compared against, and any value can be made to sound acceptable.
| # | Must be defined | Evidence that counts | FAIL if |
|---|---|---|---|
| A1 | signalling rate and width | the numbers, with their source | assumed |
| A2 | direction — per direction or aggregate | an explicit statement | ambiguous (26.4 §16) |
| A3 | payload fraction — useful bytes per transferred byte | the framing/overhead derivation | raw rate used as useful |
| A4 | protocol and framing overhead | itemised, with its source | folded into a fudge factor |
| A5 | the workload the ceiling applies to | the traffic profile (§7) | "generic traffic" |
| A6 | the observation window | a duration, stated | peak presented as sustained |
| A7 | the derivation is reproducible on paper | the arithmetic, shown | a tool output nobody can re-derive |
Worked derivation — illustrative numbers throughout (§5).
Given. A link direction carrying 64-byte payloads, each framed with 8 bytes of header and control. The per-direction transfer capability is 400 GB/s of wire bytes. Target workload: read-dominated, so this direction carries responses.
Step 1 — payload fraction. 64 / (64 + 8) = 0.889.
Step 2 — useful ceiling. 400 GB/s × 0.889 = 355.6 GB/s of useful payload.
Step 3 — express it in operations. 355.6 GB/s ÷ 64 B = 5.56 G operations/s.
Step 4 — state the window. Sustained over ≥ 1 ms, not peak over a burst.
Three readings.
A2 is where budgets are wrong by exactly 2×, and the error is invisible because both readings of an aggregate figure are plausible. The review must see the word "per direction" in writing (26.4 §16) — and a read-dominated workload loads the two directions completely differently from a write-dominated one.
A3's payload fraction is the difference between a wire number and an application number. In the worked example it is 11 % of the ceiling, immediately — and a team that compares measured useful throughput against the raw rate will conclude they are 11 % short before considering anything else.
And A7 exists because a ceiling nobody can re-derive cannot be challenged. If the derivation is a spreadsheet cell, the review cannot audit it. Four steps on a whiteboard can be checked by anyone in the room, which is the whole point of doing it first.
7. Area B — Define the Workload
| # | Must be true | Evidence that counts | FAIL if |
|---|---|---|---|
| B1 | the target workload is written down | the traffic profile: sizes, mix, burstiness, direction | "typical traffic" |
| B2 | the read/write mix is stated | the ratio, and which phase it applies to | unstated |
| B3 | burstiness is characterised | arrival distribution, not just mean rate | mean rate only |
| B4 | measurements use that workload | the test configuration | measured on a different profile |
| B5 | phase behaviour is represented | per-phase profiles if the workload has phases | one averaged profile |
| B6 | worst-case as well as nominal | both measured | nominal only |
Three readings.
B4 is the most common invalidating finding in a performance review, and it is easy to miss because the number looks good. A design measured on uniform, fully-pipelined, single-size traffic will exceed a target that its real workload misses — and the presentation shows a passing number either way. The review question is "is this the workload from B1?"
B3 matters because burstiness is invisible in a mean. Two workloads with identical average rates stress buffering, arbitration and the outstanding window completely differently — and the bursty one is the one that finds §18's occupancy problems.
And B5 is where averaged profiles hide the real limit (26.4 §16). A workload with a read-heavy phase and a write-heavy phase loads opposite directions in each. A single averaged profile is a workload that never runs, and it is comfortable in both directions while the real phases are not.
8. Area C — The Accounting Pipeline
The instrument that makes §1's attribution possible. Seven service-rate counters read in order, plus five loss-cause counters.
| Stage | Counts | The first stage whose rate falls below target is the bottleneck |
|---|---|---|
| offered | work the source wanted to issue | below target → the source is the limit |
| accepted | work admitted (valid && ready) | offered ≫ accepted → downstream is refusing |
| transmitted | physical units put on the wire | includes retries |
| retried | retransmitted units | §11 — must be separable |
| delivered | units received | — |
| useful delivered | unique payload bytes, once | this is the number in the report |
| retired | semantically completed | delivered ≫ retired → the consumer is the limit |
And the loss-cause counters, which are what turn a gap into an attribution.
| Counter | Attributes lost cycles to |
|---|---|
| idle with work available | arbitration or scheduling loss |
| no-credit cycles | flow control (25.6) |
| downstream-stall cycles | receiver backpressure |
| recovery cycles | fault handling (§20) |
| stalled on no free ID | the outstanding window (§16–§17) |
Three readings.
The gap between offered and accepted and the gap between delivered and retired bracket the design. If offered is already below target, no amount of internal optimisation helps — the workload or the source is the limit, and that is a B-area finding rather than a design one.
"Idle with work available" is the counter most often missing and the one that names arbitration loss. Cycles where the resource was free and work was waiting are pure scheduling loss. Without it, those cycles land in an unattributed remainder and get labelled "overhead" — which §1 rejects.
And "stalled on no free ID" is the only counter that identifies §17's failure, because that failure makes nothing look full: link idle, receiver idle, credits available, source ready and blocked. Every utilisation counter reads low and none of them explains why.
9. Illustrative RTL — the Performance Counter Bank
// ILLUSTRATIVE. A synthesisable counter bank implementing §8. The design
// decisions being reviewed are: ACCEPTED events only, useful separated from
// physical, loss causes counted separately, saturating arithmetic, and ONE
// next-state expression per register (27.2 §8).
localparam int CW = 48;
logic [CW-1:0] offered_q, accepted_q, transmitted_q, retried_q;
logic [CW-1:0] delivered_q, useful_bytes_q, retry_bytes_q, retired_q;
logic [CW-1:0] idle_with_work_q, no_credit_q, dnstream_stall_q;
logic [CW-1:0] recovery_cyc_q, no_free_id_q;
logic [15:0] occupancy_q, occ_high_water_q;
logic ctr_saturated_q; // sticky: any counter pinned
function automatic logic [CW-1:0] sat_add(logic [CW-1:0] a, logic [CW-1:0] b);
sat_add = ((a + b) < a) ? {CW{1'b1}} : (a + b); // saturate, never wrap
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n || ctr_clear) begin
offered_q <= '0; accepted_q <= '0; transmitted_q <= '0; retried_q <= '0;
delivered_q <= '0; useful_bytes_q <= '0; retry_bytes_q <= '0; retired_q <= '0;
idle_with_work_q <= '0; no_credit_q <= '0; dnstream_stall_q <= '0;
recovery_cyc_q <= '0; no_free_id_q <= '0;
occupancy_q <= '0; occ_high_water_q <= '0; ctr_saturated_q <= 1'b0;
end else begin
// ---- service-rate stages. NOTE: 'offered' is the ONLY counter that may
// increment on a non-accepted event, and it exists precisely so the
// offered-vs-accepted gap is measurable (§8).
if (src_valid) offered_q <= sat_add(offered_q, 1);
if (src_valid && src_ready) accepted_q <= sat_add(accepted_q, 1);
if (tx_fire) transmitted_q <= sat_add(transmitted_q, 1);
// A retransmission is transmitted work and NOT useful work (§11).
if (tx_fire && tx_is_retry) begin
retried_q <= sat_add(retried_q, 1);
retry_bytes_q <= sat_add(retry_bytes_q, CW'(tx_bytes));
end
if (rx_fire) delivered_q <= sat_add(delivered_q, 1);
// USEFUL bytes counted at RETIREMENT of a unique semantic operation, so a
// replayed delivery cannot inflate it (27.3 §22's mutation target).
if (retire_fire) begin
retired_q <= sat_add(retired_q, 1);
useful_bytes_q <= sat_add(useful_bytes_q, CW'(retire_bytes));
end
// ---- loss causes. These are CYCLE counters and must be mutually
// exclusive by construction, or the attribution double-counts.
if (!tx_fire) begin
if (in_recovery) recovery_cyc_q <= sat_add(recovery_cyc_q, 1);
else if (!credit_avail) no_credit_q <= sat_add(no_credit_q, 1);
else if (!dnstream_ready) dnstream_stall_q <= sat_add(dnstream_stall_q, 1);
else if (!id_avail && work_pending) no_free_id_q <= sat_add(no_free_id_q, 1);
else if (work_pending) idle_with_work_q <= sat_add(idle_with_work_q, 1);
// else: genuinely nothing to do — not a loss, and deliberately uncounted.
end
// ---- occupancy: ONE signed next-state expression (27.2 §8).
occupancy_q <= occupancy_q + 16'(accept_fire) - 16'(retire_fire);
if (occupancy_q > occ_high_water_q) occ_high_water_q <= occupancy_q;
if ((transmitted_q == {CW{1'b1}}) || (useful_bytes_q == {CW{1'b1}}))
ctr_saturated_q <= 1'b1;
end
endArchitecture. Thirteen counters in two groups — service rates that localise the bottleneck by first divergence, and loss causes that attribute the remaining gap. Together they answer §1.
State. All 48-bit and saturating. occ_high_water_q survives the moment because the pathological occupancy may be transient and an average will hide it.
Event. accepted uses valid && ready; offered deliberately does not — the gap between them is the measurement, and §10 is what happens when someone confuses which is which. useful_bytes increments at retirement, not at delivery, so a replay cannot inflate it.
Contract. The loss-cause counters must be mutually exclusive, which is why they are a priority chain rather than parallel ifs. If a cycle can increment two of them, the attribution sums to more than the gap and the analysis is unusable. The priority order encodes a claim — recovery dominates credit, which dominates backpressure — and that order is a review item, because it decides what a cycle is blamed on.
Failure. The realistic mistakes are all in the event definitions: counting src_valid as throughput (§10), counting retries as useful (§11), and counting idle cycles with no work as loss — the last inflates the apparent loss and sends the analysis chasing cycles the workload never wanted.
DV/debug. The identity that validates the whole bank: transmitted = useful_ops + retried and total_cycles = tx_fire_cycles + Σ(loss causes) + genuine_idle. If those do not close, the counters are wrong and every conclusion drawn from them is void — which is §3's "counters not trusted" FAIL.
10. Wrong RTL — Offered Counted as Throughput
// WRONG. ILLUSTRATIVE. The throughput counter increments on valid. This is the
// same defect as 27.2 §7 with a different consequence: there, a metric was
// inflated; here, the PERFORMANCE SIGNOFF is inflated.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) throughput_ops_q <= '0;
else if (src_valid) throughput_ops_q <= throughput_ops_q + 48'd1; // BUG
endArchitecture. One counter, feeding the reported throughput number.
State. throughput_ops_q.
Event. src_valid — an offering. Under backpressure the source holds valid with the same object, and each held cycle counts again.
Contract. The review consumes this as operations completed. It is cycles of offering.
Failure — the timeline. ready low 50 % of the time, in a simple alternating pattern.
| Cycle | src_valid | src_ready | Real transfers | Counter | Reported rate |
|---|---|---|---|---|---|
| 0 | 1 | 0 | 0 | 1 | — |
| 1 | 1 | 1 | 1 | 2 | — |
| 2 | 1 | 0 | 1 | 3 | — |
| 3 | 1 | 1 | 2 | 4 | — |
| … | — | — | — | — | — |
| 1000 | — | — | 500 transfers | 1000 | 2× actual |
The signoff reports exactly twice the achieved throughput, and the error scales with backpressure — so the measurement is most wrong under the load conditions the target was written for.
Root cause. valid means "I have something"; valid && ready means "it moved". One token, and the performance gate is decided on the wrong number.
And the reason it passes review is that the counter is otherwise excellent: 48-bit, saturating, correctly reset, single next-state expression. 27.2 would pass it on code quality. Only this gate asks what event does it count?
Corrected.
// CORRECT. Count the accepted event, and keep 'offered' as a SEPARATE counter
// so the gap between them stays visible (§8) instead of being lost.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
offered_ops_q <= '0; accepted_ops_q <= '0;
end else begin
if (src_valid) offered_ops_q <= sat_add(offered_ops_q, 1);
if (src_valid && src_ready) accepted_ops_q <= sat_add(accepted_ops_q, 1);
end
end
// MANDATORY. English: the accepted count never exceeds the offered count.
// A cheap structural check that catches an accepted counter wired to the
// wrong event, and it holds for every workload.
a_accepted_le_offered: assert property (
@(posedge clk) disable iff (!rst_n)
accepted_ops_q <= offered_ops_q
);DV/debug. The review question is one line: "show me the increment condition for every counter in the report." And the corroboration is arithmetic — if accepted / offered is 1.00 under a workload known to apply backpressure, one of the two counters is wrong.
11. Wrong RTL — Retry Bytes Counted as Useful
// WRONG. ILLUSTRATIVE. Useful throughput is derived from bytes placed on the
// wire. Correct when nothing is ever retransmitted — which is exactly the
// condition under which nobody notices.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) useful_bytes_q <= '0;
else if (tx_fire) useful_bytes_q <= useful_bytes_q + CW'(tx_bytes); // BUG
endArchitecture. One byte counter at the transmit boundary.
State. useful_bytes_q, which is actually physical bytes.
Event. Every transmission, including retransmissions.
Contract. The report presents this as application-visible throughput.
Failure — the arithmetic. A link running at 98 % wire utilisation with a 30 % retransmission rate.
| Quantity | Value | What it means |
|---|---|---|
| wire utilisation | 98 % | the link is nearly saturated |
| of which retransmitted | 30 % | the same payloads, again |
| reported "useful" throughput | 98 % | wrong |
| actual unique useful throughput | ≈ 68 % | what the application sees |
| the report's conclusion | "link is at capacity; add bandwidth" | wrong action |
| the correct conclusion | "fix the retransmission cause" | — |
And the failure inverts the diagnostic signal, which is the truly dangerous part. As link quality degrades, retransmissions rise, and this counter goes up — so worsening conditions make the performance report look better. A metric that improves as the system degrades is worse than no metric.
Root cause. Physical bytes and unique useful bytes are different quantities, and one counter was asked to be both (26.3 §16).
Corrected. §9's bank separates them: retry_bytes_q at transmit when tx_is_retry, and useful_bytes_q at retirement of a unique semantic operation — so a replay cannot contribute to useful throughput by construction.
// MANDATORY. English: physical transmitted units equal unique useful units
// plus retried units. This identity is what makes the utilisation split
// trustworthy; if it does not close, the report is void (§3).
a_transmit_accounting_closes: assert property (
@(posedge clk) disable iff (!rst_n)
transmitted_q == (retired_q + retried_q)
);DV/debug. The review artefact is the three-way split of wire bytes: unique useful, retry, control. A presentation showing one utilisation number has not done this analysis, and "the link is 98 % utilised" should trigger the question immediately (§4 row 2).
12. Area D — Snapshot Atomicity
Counters are read by software. If they are read one at a time while traffic runs, the set describes a state that never existed.
| # | Must be true | Evidence that counts | FAIL if |
|---|---|---|---|
| D1 | counters are captured atomically | the snapshot mechanism | sequential reads of live counters |
| D2 | the snapshot carries an epoch or timestamp | the epoch register | samples cannot be correlated |
| D3 | a valid bit indicates capture completed | the valid flag | torn reads unnoticed |
| D4 | saturation is reported, not silent | the sticky saturation bit | a pinned counter lies |
| D5 | the identity checks of §9 hold on the snapshot | the closure arithmetic | the snapshot is inconsistent |
Two readings.
D1's failure produces impossible ratios rather than wrong ones, which is a mercy if anyone checks. Read accepted before a rollover and delivered after, and the arithmetic yields more delivered than accepted — visibly absurd. The dangerous case is a small skew that yields a ratio which is merely wrong and entirely plausible.
And D4 matters because a saturated counter is indistinguishable from a busy one. A 48-bit counter at a few GHz will not saturate in a test; a 32-bit byte counter can saturate in seconds, and the resulting flat line reads as a throughput plateau. A sticky saturation bit costs one flop and prevents a wrong diagnosis.
13. Illustrative RTL — the Snapshot Bank
// ILLUSTRATIVE (§9). Live counters are copied into a shadow bank in ONE cycle
// on request, so software reads a consistent set. The epoch lets two snapshots
// be differenced correctly, which is how a RATE is obtained from counters.
localparam int N_CTR = 13;
logic [CW-1:0] live_ctr [N_CTR]; // driven by §9's bank
logic [CW-1:0] snap_ctr [N_CTR];
logic [31:0] snap_epoch_q, snap_cycle_q;
logic snap_valid_q;
logic [31:0] cycle_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
snap_valid_q <= 1'b0; snap_epoch_q <= '0; snap_cycle_q <= '0; cycle_q <= '0;
for (int i = 0; i < N_CTR; i++) snap_ctr[i] <= '0;
end else begin
cycle_q <= cycle_q + 32'd1;
// ONE cycle, ALL counters. This is what makes the set consistent — a
// multi-cycle copy reintroduces exactly the tear it exists to prevent.
if (snap_req) begin
for (int i = 0; i < N_CTR; i++) snap_ctr[i] <= live_ctr[i];
snap_cycle_q <= cycle_q; // the window's end, for rate arithmetic
snap_epoch_q <= snap_epoch_q + 32'd1;
snap_valid_q <= 1'b1;
end
// Software clears valid AFTER reading the whole bank, so a snapshot taken
// mid-read is detectable: epoch changed => discard and retry.
else if (snap_ack) begin
snap_valid_q <= 1'b0;
end
end
end
// MANDATORY. English: the snapshot epoch never changes while a snapshot is
// being consumed without software observing it. Software reads epoch, then the
// bank, then epoch again; unequal epochs mean the read was torn (D2).
a_snap_stable_while_valid: assert property (
@(posedge clk) disable iff (!rst_n)
(snap_valid_q && !snap_req) |=> $stable(snap_epoch_q)
);Architecture. A shadow bank copied in one cycle, plus an epoch and a cycle stamp. The cycle stamp is what converts two snapshots into a rate — without it, software has counts and no window.
State. N_CTR shadow registers, an epoch, a cycle stamp, a valid bit.
Event. A single-cycle capture on snap_req. A loop that copies one counter per cycle would reintroduce the tear — the review item is whether the copy is genuinely simultaneous.
Contract. Software's protocol is read epoch → read bank → read epoch, discarding if the epochs differ. That protocol must be documented with the register map, or software will read the bank and trust it.
Failure. Without the cycle stamp, two snapshots taken at unknown times give counts without a rate — and someone will divide by an assumed window and report a number that is off by however much the assumption was wrong.
DV/debug. Run §9's closure identities on the snapshot, not on the live counters (D5). A snapshot that fails transmitted == retired + retried proves the capture is not atomic, regardless of what the RTL looks like.
14. Area E — Latency Is a Distribution
| # | Must be true | Evidence that counts | FAIL if |
|---|---|---|---|
| E1 | the distribution is reported, not just the mean | min, median, tail buckets | mean only |
| E2 | queueing and service latency are separable | the two components measured | one aggregate number |
| E3 | loaded latency is measured, not just unloaded | measurement at target offered rate | unloaded only |
| E4 | recovery outliers are visible | tail bins, or a separate recovery-latency counter | outliers averaged away |
| E5 | latency classes are separated where the architecture defines them | per-class measurement | mixed classes |
| E6 | the measurement point is defined | the two events bounding the interval | ambiguous endpoints |
Three readings.
E1 is the item that decides whether a latency claim means anything. A mean hides the tail completely, and the tail is what a dependent workload experiences (26.3 §10's dependency-depth argument). A system with a 200 ns mean and a 20 µs tail behaves like a 20 µs system for anything that cannot proceed until the operation completes.
E2 separates two problems with opposite fixes. Service latency is the pipeline's inherent cost — reduced by design changes. Queueing latency is congestion — reduced by rate control, more resources, or a better arbiter. An aggregate number cannot tell you which you have, so it cannot tell you what to do.
And E6 is the item that makes two measurements comparable. "Latency" measured from request issue to response retirement is a different number from wire-to-wire, often by a large factor. Two teams reporting "latency" without defining endpoints are reporting different quantities, and the review will compare them anyway.
15. Illustrative RTL — Latency Buckets
// ILLUSTRATIVE (§9). A full histogram is expensive; coarse logarithmic buckets
// capture the DISTRIBUTION SHAPE at a fraction of the cost. The engineering
// content here is the TRADE-OFF, not the code.
localparam int N_BUCKET = 8; // <32, <64, <128 ... >=2048 cycles
logic [CW-1:0] lat_bucket_q [N_BUCKET];
logic [31:0] lat_max_q, lat_min_q;
logic [CW-1:0] lat_sum_q; // with 'retired' gives the mean
logic [31:0] start_time_q [N_ID]; // per outstanding operation
function automatic int unsigned bucket_of(logic [31:0] cyc);
// Log2 bucketing: cheap (a priority encoder), and the shape is what matters.
if (cyc < 32) bucket_of = 0;
else if (cyc < 64) bucket_of = 1;
else if (cyc < 128) bucket_of = 2;
else if (cyc < 256) bucket_of = 3;
else if (cyc < 512) bucket_of = 4;
else if (cyc < 1024) bucket_of = 5;
else if (cyc < 2048) bucket_of = 6;
else bucket_of = 7; // the TAIL bucket — E1, E4
endfunction
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int b = 0; b < N_BUCKET; b++) lat_bucket_q[b] <= '0;
lat_max_q <= '0; lat_min_q <= 32'hFFFF_FFFF; lat_sum_q <= '0;
end else begin
if (issue_fire) start_time_q[issue_id] <= cycle_q;
if (retire_fire) begin
automatic logic [31:0] lat = cycle_q - start_time_q[retire_id];
lat_bucket_q[bucket_of(lat)] <= sat_add(lat_bucket_q[bucket_of(lat)], 1);
lat_sum_q <= sat_add(lat_sum_q, CW'(lat));
if (lat > lat_max_q) lat_max_q <= lat; // the worst case, kept
if (lat < lat_min_q) lat_min_q <= lat; // the unloaded floor
end
end
endArchitecture. Eight logarithmic buckets, a running sum, and min/max. Eight counters plus a per-ID timestamp array give the distribution's shape, the mean, and both extremes — enough to answer E1 without a full histogram.
State. The per-ID start_time_q array is the real cost: N_ID × 32 bits. That is the instrumentation trade-off, and the review should see it costed rather than assumed free.
Event. Timestamp at issue, measure at retirement — the semantic completion, not the transport one.
Contract. cycle_q - start_time_q must handle wrap. At 32 bits and a few GHz that is seconds, which is fine for a test and not fine for a long soak. The review item is whether the wrap behaviour was considered or merely survived.
Failure. Two realistic mistakes. A too-coarse top bucket hides the tail's shape — everything over 2048 cycles is one number, so a 3000-cycle tail and a 300 000-cycle tail look identical. And lat_max_q alone, without buckets, cannot distinguish one pathological outlier from a systematically slow class.
DV/debug. lat_min_q is the unloaded floor and lat_bucket_q under load is the queueing story (E2). The difference between them is the queueing component, obtained without a second measurement setup — which is why min is worth its one register.
16. Area F — The Outstanding Window
A path with sufficient bandwidth at every stage still idles if too little is in flight.
required_outstanding ≈ target_rate × round_trip_latency
Units are where this goes wrong. Express the rate in operations per cycle and latency in cycles, and the product is a dimensionless count of operations.
| # | Must be true | Evidence that counts | FAIL if |
|---|---|---|---|
| F1 | the required window is derived | the arithmetic, shown | not derived |
| F2 | the latency it assumes is stated and owned | a number with a source and a name | assumed |
| F3 | the provided window ≥ required, with margin | the comparison | provided < required |
| F4 | it was re-derived against silicon latency, not model latency | the re-derivation | model-only (27.1 §17) |
| F5 | stalled on no free ID is measured | the counter's value under load | the failure is invisible |
| F6 | the window is reached in test | the coverage bin at max (27.3 §17 H2) | boundary never exercised |
And F5 is the item that makes this findable at all, because §17's failure makes nothing look full.
17. Flagship — a Correct Design That Cannot Reach Its Target
The scenario that defines this gate. Illustrative numbers (§5).
The claim. Target throughput is 1 object per cycle. The RTL is correct, fully verified, and has passed 27.2 and 27.3. Measured throughput is 0.4 objects per cycle. The team's proposal: tune the arbiter.
The derivation the review does, on the whiteboard, in two minutes.
Step 1 — what limits concurrency. The interface has 8 credits. One credit is consumed per object issued and returned when that object's resource is released.
Step 2 — how long a credit is unavailable. Round-trip reuse latency is 20 cycles: issue → traverse → process → release → credit return → available again.
Step 3 — the ceiling. With 8 credits each unavailable for 20 cycles, the sustained issue rate is
8 credits ÷ 20 cycles = 0.4 objects/cycle.
Step 4 — compare. Target 1.0. Ceiling 0.4. The design is at 100 % of its ceiling and 40 % of its target.
| Proposed fix | Effect on the ceiling |
|---|---|
| tune the arbiter | none — arbitration is not the constraint |
| deepen internal FIFOs | none — nothing is backing up |
| raise the clock 10 % | none in objects/cycle; the ratio is unchanged |
| optimise the datapath | none — the datapath is idle 60 % of the time |
| more credits (8 → 20) | ceiling → 1.0 ✔ |
| shorter round trip (20 → 8) | ceiling → 1.0 ✔ |
Four readings, and this is why the gate exists before tapeout.
Nothing is broken. No bug, no failing assertion, no coverage hole. The design achieves exactly its ceiling — it is a perfect implementation of an insufficient resource allocation.
The signature is that nothing looks full (§8, F5). Link idle 60 % of cycles, receiver idle, internal queues shallow, source ready and blocked. Every utilisation counter reads low, which is why teams reach for the arbiter — it is the only mechanism they can see. stalled on no free credit is the single counter that names it.
Only two things move the ceiling, and both are architecture changes: more credits (buffering at the far end, an interface-visible parameter) or shorter round-trip latency (a pipeline or protocol change). Both are 27.1 decisions, and after tapeout the first is a fixed array and the second is a fixed pipeline.
And the review question that catches it is 27.1 §9 D3, asked months earlier: "what outstanding depth does the target rate require, and what latency does that assume?" Two minutes of arithmetic at the architecture gate; a respin after it. The gate decision here is FAIL — §3's "target not demonstrated", with no CONDITIONAL PASS available because no mitigation exists within the current silicon.
18. Area G — Interpreting Occupancy
High occupancy is ambiguous. It means healthy pipelining or a bottleneck, and the two need opposite responses.
| Observation | Plus | Means |
|---|---|---|
| high occupancy | high service rate | healthy — the pipeline is full and working |
| high occupancy | low service rate, downstream stall high | downstream bottleneck |
| high occupancy | low service rate, no-credit high | flow control is the limit |
| low occupancy | low service rate, no-free-ID high | §17 — the window |
| low occupancy | low service rate, offered low | the source; a B-area finding |
| high water mark high, average low | — | bursty arrivals — B3 |
Three readings.
Occupancy alone is not evidence — it needs arrival rate, service rate and stall cause beside it to be interpretable. That is why §9's bank pairs occupancy_q with the loss-cause counters rather than reporting it standalone.
Row 6 is the burstiness signal, and it is the reason high-water marks are kept. A high-water mark far above the average means the buffer is sized for bursts, not for the mean — and a workload characterised only by its mean rate (B3) will under-size it.
And rows 2 and 3 look identical in a queue-depth graph. The distinguishing evidence is which loss counter is high, which is the entire argument for counting stall causes separately rather than counting "stalled".
19. Area H — Arbitration, Fairness and Head-of-Line Blocking
| # | Must be true | Evidence that counts | FAIL if |
|---|---|---|---|
| H1 | traffic mixes are tested, not single classes | the multi-class measurement | single-class only |
| H2 | per-class latency and grant rate are measured | per-class counters | aggregate only |
| H3 | a bulk class cannot starve a latency-sensitive one | the per-class tail under mixed load | untested |
| H4 | fairness ageing resets on service, not on grant | the RTL (21.5 §29) | starvation hidden |
| H5 | the fairness bound is derived, not tuned | its derivation | raised until it stops firing |
| H6 | head-of-line blocking is measured where queues are shared | the blocked-behind counter | unmeasured |
Three readings.
H1 is the systematic gap. A design measured one traffic class at a time can meet every individual target and fail catastrophically on the mix — which is the only configuration the product ever runs.
H4 is a 27.2-flavoured finding surfacing here because its consequence is a performance one. A grant into a blocked path is not service (21.5 §29). Ageing that resets on grant makes a starving requester look healthy — and it hides starvation more effectively the busier the system gets, which is when starvation matters.
And H5 is the discipline item. A fairness bound raised each time the assertion fires is a bound that cannot detect the bug it exists for (21.6 §29). The bound comes from the architecture's fairness contract, or it is not a bound.
20. Area I — Performance Under Fault and Recovery
| # | Must be true | Evidence that counts | FAIL if |
|---|---|---|---|
| I1 | the assumed retry/error rate is stated with its source | the assumption and its basis | assumed zero implicitly |
| I2 | useful throughput is measured at that rate, not only at zero | the measurement | steady-state only |
| I3 | recovery duration is measured | the recovery-cycle counter (§8) | estimated |
| I4 | traffic resumption behaviour is measured | throughput ramp after recovery | assumed instant |
| I5 | the retry-rate sensitivity is characterised | throughput vs retry rate, several points | one point |
| I6 | a degraded participant's effect is bounded | 26.3 §14's adaptation | one straggler halves the system |
Three readings.
I1's implicit-zero assumption is extremely common and rarely written down. A design that meets its target at a 0 % retry rate and has never been measured at 1 % has an unknown margin — and §11's counter defect means the report may not even show the degradation.
I5 matters because the relationship is usually non-linear. Retries consume bandwidth and hold outstanding resources, so they attack both terms of §16's window arithmetic simultaneously. Throughput can fall much faster than the retry rate rises, and one measured point cannot reveal that.
And I6 hands off cleanly to 26.3 §18: in a multi-participant system, an even work distribution turns one degraded participant into a system-wide collapse. The review item is whether any adaptation exists, not whether degradation is prevented.
21. Area J — Margin and Sensitivity
Passing exactly at the requirement is a FAIL in everything but arithmetic.
| # | Must be true | Evidence that counts | FAIL if |
|---|---|---|---|
| J1 | margin is quantified | measured vs target, as a number | "we meet it" |
| J2 | margin's assumptions are stated | which conditions the margin holds under | unstated |
| J3 | sensitivity is characterised | the table below, measured | no sensitivity analysis |
| J4 | worst-case corner is included | the corner measurement | nominal only |
| J5 | the analysis is reproducible | the configurations and seeds | one-off runs |
The sensitivity table a review should demand — and each row must be measured, not estimated.
| Perturbation | Question |
|---|---|
| clock −5 % | does the target still hold? |
| round-trip latency +20 % | §16 — does the window still suffice? |
| workload burstier | does buffering still absorb it? |
| retry rate +1 % | §20 I5 |
| one participant degraded | 26.3 §18 |
| a different read/write mix | 26.4 §16 — the other direction |
Three readings.
Row 2 is the sensitivity that most often converts a PASS into a FAIL, because latency is the term that arrives last and worst. Silicon latency exceeding the model's is the normal case (27.1 §17), and it multiplies directly into the required window.
J3 is a FAIL criterion on its own. A design with no sensitivity analysis has a number and no understanding of what preserves it — so the review cannot distinguish comfortable headroom from a result that collapses under a 5 % perturbation.
And this gate deliberately does not prescribe a margin percentage. The right margin depends on how well the workload is known, how much latency uncertainty remains, and how much of the gap to ceiling is attributed. A design with a fully attributed gap and a characterised workload can pass on less margin than one with 40 % unexplained.
22. Area K — Silicon Observability of Performance
Handed to 27.6, but commissioned here — because a counter that does not exist in hardware cannot be read after tapeout.
| # | Must be true | FAIL if |
|---|---|---|
| K1 | every counter in §8 exists in hardware, not only in the testbench | simulation-only instrumentation |
| K2 | the snapshot mechanism (§13) is in hardware | software reads live counters |
| K3 | counters are clearable without disturbing function | cannot re-arm on a live system |
| K4 | saturation is reported | a pinned counter reads as a plateau |
| K5 | latency buckets (§15) are present or explicitly waived with a reason | tail invisible in silicon |
| K6 | the register map documents the read protocol (§13) | software trusts a torn read |
And K1 is the item most often discovered too late. A performance analysis built entirely on testbench probes produces a beautiful pre-silicon story and nothing to measure against in the lab — at which point the first performance question about real silicon has no instrument to answer it.
23. The Performance Gate Checklist
Thirty-six items. The reasoning for each is in the referenced section.
Ceiling and workload
| # | Question | FAIL if |
|---|---|---|
| 1 | Is a ceiling derived before any measurement is shown? | derived after, to match the result |
| 2 | Is the derivation reproducible on paper? | a spreadsheet nobody can audit |
| 3 | Per direction or aggregate — stated? | ambiguous (§6 A2) |
| 4 | Is the payload fraction accounted separately from raw rate? | raw rate compared to useful |
| 5 | Is the target workload written down? | "typical traffic" |
| 6 | Were measurements taken on that workload? | measured on a different profile |
| 7 | Is burstiness characterised, not just the mean rate? | mean only |
| 8 | Is the observation window stated? | peak presented as sustained |
| 9 | Are per-phase profiles used where the workload has phases? | one averaged profile |
Counters and trust
| # | Question | FAIL if |
|---|---|---|
| 10 | Show the increment condition for every counter in the report | any counts valid without ready (§10) |
| 11 | Are offered and accepted counted separately? | one counter for both |
| 12 | Are retry bytes separable from useful bytes? | §11 — utilisation inflated |
| 13 | Is useful throughput counted at retirement? | counted at transmit |
| 14 | Does transmitted == retired + retried close? | identity fails → report void |
| 15 | Are loss-cause counters mutually exclusive? | attribution double-counts |
| 16 | Is idle-with-work counted? | arbitration loss unattributable |
| 17 | Is stalled-on-no-free-ID/credit counted? | §17 invisible |
| 18 | Are counters saturating, with saturation reported? | a pinned counter reads as a plateau |
Attribution
| # | Question | FAIL if |
|---|---|---|
| 19 | Is the entire gap from ceiling to measured attributed? | any residual labelled "overhead" |
| 20 | Which stage's rate falls first? | not identified |
| 21 | Do the loss-cause cycles sum to the gap? | they do not close |
| 22 | Is the bottleneck boundary named, not just the subsystem? | vague |
Snapshots and latency
| # | Question | FAIL if |
|---|---|---|
| 23 | Are counters captured atomically? | sequential live reads (§12) |
| 24 | Does the snapshot carry an epoch and cycle stamp? | rates cannot be computed |
| 25 | Do §9's identities hold on the snapshot? | capture is not atomic |
| 26 | Is latency reported as a distribution? | mean only |
| 27 | Are queueing and service latency separable? | one aggregate number |
| 28 | Is latency measured under load? | unloaded only |
| 29 | Are the measurement endpoints defined? | ambiguous interval |
Window, arbitration, faults, margin
| # | Question | FAIL if |
|---|---|---|
| 30 | Is the required outstanding window derived and compared to provided? | not derived (§16) |
| 31 | Was it re-derived against silicon latency? | model latency only |
| 32 | Are traffic mixes measured, with per-class latency? | single-class only |
| 33 | Does fairness ageing reset on service, not grant? | starvation hidden |
| 34 | Is the retry-rate assumption stated, and throughput measured at it? | implicit zero |
| 35 | Is margin quantified with its assumptions? | "we meet it" |
| 36 | Is there a sensitivity analysis across §21's rows? | none → FAIL |
And run items 10, 14, 19, 30 and 36 first. They take under an hour and they decide most gates — a report built on untrusted counters (10, 14), with an unattributed gap (19), no window derivation (30) or no sensitivity (36) cannot pass regardless of the number it shows.
24. How the Performance Review Itself Fails
| Failure mode | Looks like | Why it is fatal |
|---|---|---|
| reviewing the number, not the counters | a passing throughput figure | §10/§11 — the number may be meaningless |
| deriving the ceiling after the measurement | everything is consistent | the ceiling was fitted to the result |
| accepting "overhead" as an attribution | a tidy pie chart with a large slice | §1 — the gap has no owner |
| measuring on a convenient workload | a comfortable margin | the product runs a different one |
| passing at exactly the requirement | target met | §21 — no margin, no sensitivity |
| deferring to silicon | schedule preserved | §3 — §17 is not tunable after tapeout |
Two readings.
Row 2 is subtle and common. A ceiling derived after the measurement will be constructed, unconsciously, to make the measurement look reasonable. Deriving it first is not a formality — it is what makes the comparison honest, which is why §6 is the first area rather than a supporting one.
And row 6 is the failure that produces §17 in silicon. "We'll characterise it on real hardware" is reasonable-sounding and, for a resource-limited ceiling, catastrophic: the window is a taped-out array and the round trip is a taped-out pipeline. The measurement will be accurate and the finding unactionable.
25. Red Flags
| Heard in review | Usually means | Ask |
|---|---|---|
| "the link is 98 % utilised" | retry bytes counted as useful | "what fraction is retransmission?" |
| "we hit the target" | workload or window undefined | "on which workload, over what window?" |
| "the rest is overhead" | the gap has no owner | "which counter accounts for those cycles?" |
| "we'll tune the arbiter" | §17 — possibly not the constraint | "what is the credit-window ceiling?" |
| "latency is about 200 ns" | mean presented as the story | "what is the tail?" |
| "buffers are big enough" | occupancy uninterpreted | "high-water, with which stall cause?" |
| "it's fine at steady state" | retry rate assumed zero | "measured at what retry rate?" |
| "we measured it in the testbench" | K1 — no silicon instrument | "which register reads that in the lab?" |
| "the counters are in the report" | increment conditions unaudited | "show me every increment condition" |
| "we're 2 % over target" | no margin, no sensitivity | "what happens at latency +20 %?" |
And the highest-yield question in the room is the third one. "Which counter accounts for those cycles?" applied to every slice of the gap. A slice with no counter behind it is not an attribution, and §1 says the gate cannot pass on it.
26. Common Misconceptions
"High wire utilisation means high throughput." §11: at 30 % retransmission, 98 % utilisation is 68 % useful — and the metric rises as the link degrades.
"The throughput counter is simple, it can't be wrong." §10: incrementing on valid rather than valid && ready reports 2× under 50 % backpressure, and it is most wrong under the load the target was written for.
"We meet the target, so performance is closed." §3: an unattributed gap is a FAIL even with the target met — 40 % of the behaviour is outside anyone's model.
"Overhead explains the rest." §1: overhead is a label, not a cause. Every lost cycle needs a counter behind it.
"Mean latency is the headline number." §14: a 200 ns mean with a 20 µs tail behaves like a 20 µs system for any dependent workload.
"Low throughput with everything idle means the arbiter." §17: it means a concurrency window. Arbitration tuning, deeper FIFOs and datapath optimisation move the ceiling by zero.
"More credits is always a tuning knob." §17: it is an interface-visible architecture parameter, fixed at tapeout.
"A faster clock fixes throughput." §17: in objects per cycle, a clock change leaves the credit-window ratio unchanged.
"We'll measure it in silicon." §24: accurate, and unactionable for a resource-limited ceiling.
"Passing at the requirement is passing." §21: without margin and sensitivity, you do not know whether a 5 % perturbation removes it.
27. Understanding Check
28. Summary
Seven things.
Derive the ceiling first (§6). Rate, width, direction, payload fraction, workload, window — four steps on a whiteboard, before any measurement, or the comparison is fitted to the result.
Every lost cycle needs an owner (§1, §8). Seven service-rate stages plus five loss causes, mutually exclusive, summing to the gap. "Overhead" is a label, not a cause.
Audit what the counters count (§10–§11). valid instead of valid && ready reports 2× under 50 % backpressure; retry bytes counted as useful make 98 % utilisation mean 68 % — and make the metric rise as the link degrades.
Snapshots must be atomic (§12–§13), with an epoch and a cycle stamp, and §9's identities must close on the snapshot — otherwise the capture is not atomic whatever the RTL looks like.
Latency is a distribution (§14–§15), separable into queueing and service, measured under load, with defined endpoints. A mean hides the tail, and the tail is what dependent work experiences.
Some ceilings are not tunable (§17). 8 credits ÷ 20-cycle round trip = 0.4 objects/cycle, against a target of 1.0, with nothing full and no bug anywhere. Arbitration, FIFOs, datapath and clock move it by zero.
And margin without sensitivity is not margin (§21). Six perturbations, measured — especially latency +20 %, which multiplies straight into the outstanding window.