Every timing path has a slack: required time minus arrival time, negative when the path misses. But a timing report does not hand you one line per path. It hands you one line per endpoint: each endpoint keeps only its worst path, the smallest slack among all paths that end there. Two totals summarise those endpoint lines. Worst negative slack, WNS, is the smallest endpoint slack in the design: how bad the single worst endpoint is. Total negative slack, TNS, is the sum of the negative endpoint slacks only: how much failure exists across the design. Positive endpoints contribute nothing to TNS, and when nothing violates, WNS is the smallest positive slack here (many tools clamp that case to WNS 0; the convention in this lesson keeps the headroom visible).
Metric (standard timing-report semantics) | Definition | Answers |
|---|---|---|
WNS | Minimum endpoint slack over the design | How bad is the single worst endpoint. |
TNS | Sum of the negative endpoint slacks | How much failure exists across the design. |
The per-endpoint part is the correction that matters. Summing every failing path overcounts: one endpoint with six failing paths is one failing endpoint, not six. The program further down proves the gap with numbers: the same sample report gives -52 ps counted per endpoint, and -187 ps counted per path.
The script below maps a two-output design: a five-stage AND-XOR chain behind y_deep next to a single AND gate behind y_side, the same shape as the critical-path lesson in miniature. Both runs use a very loose target so the reports show each cone's natural arrival: 5 cells at area 19 and a worst arrival of 432 ps under the typical 0.05 pF load. The assertions prove the generic gates are gone. The figure draws the recorded netlist both endpoints share, so the sample report below has a shape you can point at.
read_verilog <<EOV
module twoep(input [5:0] a, input b, output y_deep, output y_side);
wire d0 = a[0] & a[1];
wire d1 = d0 & a[2];
wire d2 = d1 & a[3];
wire d3 = d2 ^ b;
assign y_deep = d3 ^ a[4];
assign y_side = a[4] & a[5];
endmodule
EOV
synth -top twoep -flatten
design -save twoep
tee -o ep_typ.txt abc -liberty sized.lib -constr typical.constr -D 100000
tee -o ep_typ_stat.txt stat -liberty sized.lib
write_json ep_net.json
select -assert-none t:$and t:$or t:$xor t:$xnor t:$mux t:$not
select -assert-none t:$_AND_ t:$_OR_ t:$_XOR_ t:$_XNOR_ t:$_MUX_ t:$_NOT_ t:$_NAND_ t:$_NOR_
check
design -load twoep
tee -o ep_heavy.txt abc -liberty sized.lib -constr heavy.constr -D 100000
tee -o ep_heavy_stat.txt stat -liberty sized.lib
write_json ep_heavy_net.json
select -assert-none t:$and t:$or t:$xor t:$xnor t:$mux t:$not
select -assert-none t:$_AND_ t:$_OR_ t:$_XOR_ t:$_XNOR_ t:$_MUX_ t:$_NOT_ t:$_NAND_ t:$_NOR_
check
log PASS: two endpoints map once per loadNo open tool here prints per-endpoint slack: OpenSTA is not installed and ABC reports only the worst arrival, 432 ps above. So the report below is hand-written, labelled sample, with slacks in the shape of this netlist at a sample required time of 380 ps: six failing paths into y_deep, two comfortable paths into y_side. The 380 ps is chosen, not recorded: minus the recorded 432 ps arrival it gives the sample's -52 ps worst slack. Read it as the report a timer would print for these two endpoints, then watch the program recompute both totals from it.
SAMPLE endpoint report (ps), required time 380 ps
endpoint y_deep path slacks: -52, -47, -39, -28, -15, -6 (worst -52)
endpoint y_side path slacks: +180, +195 (worst +180)
WNS = minimum over endpoints = min(-52, +180) = -52
TNS = worst per endpoint, negatives only = -52 + 0 = -52
all-paths sum = -52 + -47 + -39 + -28 + -15 + -6 = -187 (wrong: counts paths, not endpoints)WNS jumps to +5: the worst endpoint is fixed. TNS moves to 0 for the same reason. Fix the endpoint, not the path.
The program below holds the sample report and computes all three figures: WNS, per-endpoint TNS, and the all-paths sum a per-path reading would give. Its checks pin each one and pin that the wrong total differs from the right one; if any figure drifts, the run goes red. The AFTER case replays the prediction: with y_deep's worst path at +5, WNS reads +5, the smallest positive slack in this lesson's convention, and TNS reads 0 because no endpoint is negative. Zero failing endpoints is the only TNS that signs anything off.
import sys
# SAMPLE endpoint report (ps), in the shape of the two-endpoint netlist above.
# Hand-written path slacks at a sample required time of 350 ps.
BEFORE = {
"y_deep": [-52, -47, -39, -28, -15, -6],
"y_side": [180, 195],
}
AFTER = {
"y_deep": [5, 12, 24],
"y_side": [180, 195],
}
def wns(endpoints):
return min(min(paths) for paths in endpoints.values())
def tns(endpoints):
total = 0
for paths in endpoints.values():
worst = min(paths)
if worst < 0:
total += worst
return total
def tns_all_paths(endpoints):
return sum(s for paths in endpoints.values() for s in paths if s < 0)
if wns(BEFORE) != -52:
print("FAIL: WNS " + str(wns(BEFORE)) + " want -52")
sys.exit(1)
if tns(BEFORE) != -52:
print("FAIL: TNS " + str(tns(BEFORE)) + " want -52")
sys.exit(1)
if tns_all_paths(BEFORE) != -187:
print("FAIL: all-paths sum " + str(tns_all_paths(BEFORE)) + " want -187")
sys.exit(1)
if tns_all_paths(BEFORE) == tns(BEFORE):
print("FAIL: all-paths sum should differ from per-endpoint TNS here")
sys.exit(1)
if wns(AFTER) != 5:
print("FAIL: fixed WNS " + str(wns(AFTER)) + " want 5")
sys.exit(1)
if tns(AFTER) != 0:
print("FAIL: fixed TNS " + str(tns(AFTER)) + " want 0")
sys.exit(1)
print("PASS: WNS -52 ps and TNS -52 ps; summing all paths gives -187 ps instead")WNS and TNS move for different reasons, so read them as a pair. The table below covers the four common patterns; each row is a reading rule, not a measurement.
WNS | TNS | Reading |
|---|---|---|
Improving | Improving | Effort works across the failing endpoints. |
Improving | Flat | One endpoint is fixed while many still fail; broaden the effort. |
Flat | Improving | Shared logic helps many endpoints but the worst one resists; attack it directly. |
Flat | Flat | Effort is spent out; restructure or stop. |
The script's second run replays the same design under heavy.constr at 0.2 pF per output. The recorded worst arrival rises from 432 ps to 572 ps at area 23: the same logic, later everywhere, because every endpoint now drives four times the capacitance. Against the sample required time of 380 ps the worst slack falls by exactly that rise, hand-derived below from the two recorded arrivals. A fixed target plus a heavier load is a tighter constraint wearing a disguise, and the endpoint totals are where the disguise shows.
worst slack at required 380 ps, hand-derived from the recorded worst arrivals
typical load: 380 - 432 = -52 ps
heavy load: 380 - 572 = -192 ps
shift = 140 ps worse on the worst endpointOnly y_deep fails, with a worst slack of -52 ps. y_side is positive and contributes nothing, so the design's total shortfall is 52 ps across one endpoint.
TNS sums worst slacks per endpoint, not paths: y_deep's worst is now +5 and y_side's is +180, and neither is negative. One fixed endpoint zeroes the total.
Cell counts, areas, worst arrivals and the netlist picture on this page come from one Yosys 0.70 run of the script shown, recorded by the course checker (scripts/tools/run-rtl-testbenches.mjs with COURSE=synth): ABC maps a two-endpoint design against sized.lib with typical and heavy loads. Endpoint slacks are a hand-written labelled sample, because no open tool here prints per-endpoint slack; the program computes WNS, per-endpoint TNS and the all-paths counter-example from that sample with asserts. The library is fictional, so absolute figures are teaching units.