@@ -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
11711199impl 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