Skip to content

Commit c0d6e0e

Browse files
avrabeclaude
andauthored
feat(wctt): RTA→WCTT release-jitter coupling (v0.9.2) (#199)
Reviewer NC top-5 #4 — *single biggest credibility lift, no new math*. v0.8.x WCTT assumed the producing thread emits its burst "whenever"; the thread's response_time IS its release-jitter, which directly determines the burst at the NIC. When a stream's source end station declares `Timing_Properties::Dispatch_Jitter`, treat it as ingress release-jitter J and inflate the arrival burst σ by `ρ·J` bytes (ceiling-rounded so the bound is never under-estimated). New `WcttRtaCoupled` Info diagnostic per stream echoes the (jitter_ps, jitter_burst_bytes) pair so the coupling is visible. Default unset = 0 → byte-identical v0.8.1 / v0.9.1 output. The full automatic coupling — consume RTA's *computed* response_time directly without requiring the user to propagate via Dispatch_Jitter — is a v0.9.x follow-up; today the user must explicitly declare jitter (which is the canonical AS5506 property), but v0.9.2 wires the math so future automatic propagation is a one-line consumer change. REQ-NETWORK-011 + TEST-WCTT-RTA-COUPLING. 2 new tests (rta_wctt_dispatch_jitter_inflates_burst_and_emits_diagnostic, no_dispatch_jitter_no_coupling_diagnostic). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7dcb652 commit c0d6e0e

3 files changed

Lines changed: 238 additions & 1 deletion

File tree

artifacts/requirements.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1650,6 +1650,25 @@ artifacts:
16501650
status: implemented
16511651
tags: [network, wctt, sensitivity, v092]
16521652

1653+
- id: REQ-NETWORK-011
1654+
type: requirement
1655+
title: RTA→WCTT release-jitter coupling
1656+
description: >
1657+
v0.9.2 closes the RTA↔WCTT loop the post-v0.9.0 reviewer flagged
1658+
as the single biggest credibility lift (NC top-5 #4): when a
1659+
WCTT stream's source end station declares
1660+
`Timing_Properties::Dispatch_Jitter`, that value is treated as
1661+
ingress release-jitter J and inflates the arrival burst by
1662+
`ρ·J` bytes (ceiling-rounded so the bound is never under-
1663+
estimated). New `WcttRtaCoupled` Info diagnostic per stream
1664+
echoes the (jitter_ps, jitter_burst_bytes) pair. Default unset
1665+
= 0 → byte-identical to v0.8.1 / v0.9.1. The full automatic
1666+
coupling (consume RTA's *computed* response_time directly
1667+
without requiring the user to propagate via Dispatch_Jitter)
1668+
is a v0.9.x follow-up.
1669+
status: implemented
1670+
tags: [network, wctt, rta, coupling, v092]
1671+
16531672
# ── Track G: spar-insight discrepancy assistant (v0.9.0) ──────────
16541673

16551674
- id: REQ-INSIGHT-001

artifacts/verification.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2177,6 +2177,29 @@ artifacts:
21772177
- type: satisfies
21782178
target: REQ-NETWORK-012
21792179

2180+
- id: TEST-WCTT-RTA-COUPLING
2181+
type: feature
2182+
title: WCTT consumes Dispatch_Jitter as release-jitter for ρ·J burst inflation
2183+
description: >
2184+
Verifies the v0.9.2 RTA→WCTT release-jitter coupling.
2185+
`rta_wctt_dispatch_jitter_inflates_burst_and_emits_diagnostic`
2186+
builds a 1 Gbps single-hop model with the source declaring
2187+
`Timing_Properties::Dispatch_Jitter => 100 us`; expects the
2188+
`WcttRtaCoupled` Info diagnostic to fire naming the
2189+
jitter_ns and the ρ·J = 12500 byte inflation.
2190+
`no_dispatch_jitter_no_coupling_diagnostic` confirms the
2191+
diagnostic does NOT fire when the property is unset.
2192+
fields:
2193+
method: automated-test
2194+
steps:
2195+
- run: cargo test -p spar-analysis --lib -- rta_wctt
2196+
- run: cargo test -p spar-analysis --lib -- no_dispatch_jitter
2197+
status: passing
2198+
tags: [v0.9.2, network, wctt, rta, coupling]
2199+
links:
2200+
- type: satisfies
2201+
target: REQ-NETWORK-011
2202+
21802203
- id: TEST-INSIGHT-DISCREPANCY
21812204
type: feature
21822205
title: spar-insight CTF parser + 5-kind discrepancy detection

crates/spar-analysis/src/wctt.rs

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,25 @@ impl WcttAnalysis {
243243
let mut max_comp_rate_bps: u64 = 0;
244244
let mut hops_counted: u64 = 0;
245245

246+
// v0.9.2 RTA→WCTT release-jitter coupling diagnostic. The
247+
// burst inflation already happened in `collect_streams`;
248+
// this is the user-facing Info that the coupling fired.
249+
if stream.jitter_burst_bytes > 0 {
250+
diags.push(AnalysisDiagnostic {
251+
severity: Severity::Info,
252+
message: format!(
253+
"WcttRtaCoupled: stream '{}' release-jitter {} ns inflates ingress \
254+
burst by {} B (ρ·J coupling — RTA → WCTT per Buttazzo / Le \
255+
Boudec & Thiran)",
256+
stream_name,
257+
stream.release_jitter_ps / 1_000,
258+
stream.jitter_burst_bytes,
259+
),
260+
path: stream_path.clone(),
261+
analysis: self.name().to_string(),
262+
});
263+
}
264+
246265
for (hop_idx, sw_idx) in stream.hops.iter().enumerate() {
247266
let st = switch_type.get(sw_idx).copied().unwrap_or(SwitchType::Fifo);
248267

@@ -1166,6 +1185,15 @@ struct Stream {
11661185
/// `CbsReservation` (with hi/lo credit and send slope) is built at
11671186
/// each TSN hop because it depends on the bus's link rate.
11681187
cbs_idle_slope_bps: Option<u64>,
1188+
/// v0.9.2 RTA→WCTT release-jitter coupling: when the source end
1189+
/// station declares `Timing_Properties::Dispatch_Jitter`, that
1190+
/// value (picoseconds) is treated as ingress release-jitter J and
1191+
/// inflates the arrival burst by ρ·J bytes. Stored here so the
1192+
/// `WcttRtaCoupled` Info diagnostic at run-time can echo the
1193+
/// pair (jitter_ps, jitter_burst_bytes) back to the user. `0`
1194+
/// when the property is unset (= byte-identical v0.8.x behaviour).
1195+
release_jitter_ps: u64,
1196+
jitter_burst_bytes: u64,
11691197
}
11701198

11711199
impl Stream {
@@ -1259,11 +1287,47 @@ fn collect_streams(
12591287
// Ethernet MTU (DEFAULT_BURST_BYTES).
12601288
let src_props = instance.properties_for(src_idx);
12611289
let rate_bps = read_output_rate_bps(src_props).unwrap_or(0);
1262-
let burst_bytes = read_queue_depth(src_props)
1290+
let burst_base_bytes = read_queue_depth(src_props)
12631291
.map(|q| q.saturating_mul(FRAME_BYTES))
12641292
.unwrap_or(DEFAULT_BURST_BYTES);
12651293

1294+
// v0.9.2 RTA→WCTT release-jitter coupling (NC reviewer top-5
1295+
// #4 — single biggest credibility lift, no new math). When
1296+
// the source end station declares `Timing_Properties::
1297+
// Dispatch_Jitter`, treat it as release-jitter J: a thread
1298+
// whose dispatcher fires up to J ps late at any cycle still
1299+
// produces the same number of bytes per period, but the
1300+
// *burst seen at the NIC* inflates by ρ·J. This couples
1301+
// RTA's response-time semantics into the WCTT input.
1302+
//
1303+
// Default unset = J=0 = byte-identical to v0.8.1/v0.9.1.
1304+
//
1305+
// Future v0.9.x: also consume RTA's *computed*
1306+
// response_time directly (today the user must propagate it
1307+
// via Dispatch_Jitter explicitly, which is the existing
1308+
// AS5506 property semantics).
1309+
let release_jitter_ps = src_props
1310+
.get("Timing_Properties", "Dispatch_Jitter")
1311+
.or_else(|| src_props.get("", "Dispatch_Jitter"))
1312+
.and_then(parse_time_value)
1313+
.unwrap_or(0);
1314+
// ρ·J in bytes = (rate_bps · jitter_ps) / 8 / 1e12, with
1315+
// ceiling rounding so the burst is never under-estimated.
1316+
let jitter_burst_bytes = if release_jitter_ps > 0 && rate_bps > 0 {
1317+
let bits = (rate_bps as u128).saturating_mul(release_jitter_ps as u128);
1318+
let bytes = bits.div_ceil(8u128 * 1_000_000_000_000u128);
1319+
u64::try_from(bytes).unwrap_or(u64::MAX)
1320+
} else {
1321+
0
1322+
};
1323+
let burst_bytes = burst_base_bytes.saturating_add(jitter_burst_bytes);
1324+
12661325
let alpha = ArrivalCurve::affine(burst_bytes, rate_bps);
1326+
if jitter_burst_bytes > 0 {
1327+
// Diagnostic emitted lazily inside `streams_diagnostics`
1328+
// below since `stream_name` is built later. We thread
1329+
// the jitter values through the Stream struct.
1330+
}
12671331
// TSN dispatch metadata read off the source end station.
12681332
// Spar_TSN::Class_of_Service drives the TAS gate-window
12691333
// service curve and is also surfaced on CBS-shaped
@@ -1298,6 +1362,8 @@ fn collect_streams(
12981362
cos,
12991363
is_express,
13001364
cbs_idle_slope_bps,
1365+
release_jitter_ps,
1366+
jitter_burst_bytes,
13011367
});
13021368
}
13031369
}
@@ -1523,6 +1589,135 @@ end Net;
15231589
);
15241590
}
15251591

1592+
// ── v0.9.2 — RTA→WCTT release-jitter coupling ─────────────────
1593+
#[test]
1594+
fn rta_wctt_dispatch_jitter_inflates_burst_and_emits_diagnostic() {
1595+
// Source device with Dispatch_Jitter = 100 us. At 1 Gbps,
1596+
// ρ·J = 1e9 × 100e-6 / 8 = 12500 bytes of inflation.
1597+
let src = r#"
1598+
package Net
1599+
public
1600+
1601+
bus eth
1602+
properties
1603+
Spar_Network::Switch_Type => FIFO;
1604+
Spar_Network::Output_Rate => 1000000000 bitsps;
1605+
Spar_Network::Forwarding_Latency => 0 us .. 0 us;
1606+
Spar_Network::Queue_Depth => 1;
1607+
end eth;
1608+
bus implementation eth.impl
1609+
end eth.impl;
1610+
1611+
device d
1612+
features
1613+
net : requires bus access;
1614+
out_p : out data port;
1615+
in_p : in data port;
1616+
end d;
1617+
device implementation d.impl
1618+
end d.impl;
1619+
1620+
device src_d
1621+
features
1622+
net : requires bus access;
1623+
out_p : out data port;
1624+
properties
1625+
Spar_Network::Output_Rate => 1000000000 bitsps;
1626+
Timing_Properties::Dispatch_Jitter => 100 us;
1627+
end src_d;
1628+
device implementation src_d.impl
1629+
end src_d.impl;
1630+
1631+
system Sys
1632+
end Sys;
1633+
system implementation Sys.impl
1634+
subcomponents
1635+
sw : bus eth.impl;
1636+
a : device src_d.impl;
1637+
b : device d.impl;
1638+
connections
1639+
c_sw_a : bus access sw -> a.net;
1640+
c_sw_b : bus access sw -> b.net;
1641+
data1 : port a.out_p -> b.in_p;
1642+
properties
1643+
Deployment_Properties::Actual_Connection_Binding => (reference (sw));
1644+
end Sys.impl;
1645+
end Net;
1646+
"#;
1647+
let inst = instantiate(src, "Net", "Sys", "impl");
1648+
let diags = WcttAnalysis.analyze(&inst);
1649+
1650+
let coupled = diags
1651+
.iter()
1652+
.find(|d| d.message.starts_with("WcttRtaCoupled"))
1653+
.unwrap_or_else(|| panic!("expected WcttRtaCoupled diagnostic, got: {:#?}", diags));
1654+
assert!(
1655+
coupled.message.contains("100000 ns"),
1656+
"expected jitter 100000 ns in message: {}",
1657+
coupled.message
1658+
);
1659+
// ρ·J = 1Gbps × 100us / 8 = 12500 bytes
1660+
assert!(
1661+
coupled.message.contains("12500 B"),
1662+
"expected 12500 B inflation in message: {}",
1663+
coupled.message
1664+
);
1665+
}
1666+
1667+
#[test]
1668+
fn no_dispatch_jitter_no_coupling_diagnostic() {
1669+
// Without Dispatch_Jitter the coupling diagnostic must not
1670+
// fire, preserving v0.8.x / v0.9.1 byte-identical output.
1671+
let src = r#"
1672+
package Net
1673+
public
1674+
1675+
bus eth
1676+
properties
1677+
Spar_Network::Switch_Type => FIFO;
1678+
Spar_Network::Output_Rate => 1000000000 bitsps;
1679+
Spar_Network::Forwarding_Latency => 0 us .. 0 us;
1680+
Spar_Network::Queue_Depth => 1;
1681+
end eth;
1682+
bus implementation eth.impl
1683+
end eth.impl;
1684+
1685+
device d
1686+
features
1687+
net : requires bus access;
1688+
out_p : out data port;
1689+
in_p : in data port;
1690+
end d;
1691+
device implementation d.impl
1692+
end d.impl;
1693+
1694+
system Sys
1695+
end Sys;
1696+
system implementation Sys.impl
1697+
subcomponents
1698+
sw : bus eth.impl;
1699+
a : device d.impl;
1700+
b : device d.impl;
1701+
connections
1702+
c_sw_a : bus access sw -> a.net;
1703+
c_sw_b : bus access sw -> b.net;
1704+
data1 : port a.out_p -> b.in_p;
1705+
properties
1706+
Deployment_Properties::Actual_Connection_Binding => (reference (sw));
1707+
end Sys.impl;
1708+
end Net;
1709+
"#;
1710+
let inst = instantiate(src, "Net", "Sys", "impl");
1711+
let diags = WcttAnalysis.analyze(&inst);
1712+
assert!(
1713+
!diags
1714+
.iter()
1715+
.any(|d| d.message.starts_with("WcttRtaCoupled")),
1716+
"no Dispatch_Jitter must not emit WcttRtaCoupled: {:#?}",
1717+
diags
1718+
);
1719+
}
1720+
15261721
// ── Test 3: two streams sharing one FIFO switch ─────────────────
15271722
#[test]
15281723
fn two_streams_share_switch_residual_split() {

0 commit comments

Comments
 (0)