5555TcpPrrRecovery::EnterRecovery (Ptr<TcpSocketState> tcb,
5656 uint32_t dupAckCount [[maybe_unused]] ,
5757 uint32_t unAckDataCount,
58- uint32_t deliveredBytes)
58+ uint32_t deliveredBytes,
59+ uint32_t bytesSacked)
5960{
60- NS_LOG_FUNCTION (this << tcb << dupAckCount << unAckDataCount);
61+ NS_LOG_FUNCTION (this << tcb << dupAckCount << unAckDataCount << deliveredBytes << bytesSacked );
6162
6263 m_prrOut = 0 ;
6364 m_prrDelivered = 0 ;
64- // RFC 9937 Section 6.1 (and RFC 6937 line 296): RecoverFS is the FlightSize
65- // (SND.NXT - SND.UNA) at the start of recovery, NOT the RFC 6675 pipe
66- // (FlightSize - sacked - lost). unAckDataCount carries SND.NXT - SND.UNA.
65+ // RFC 9937 Section 6.1 (and RFC 6937 line 296): RecoverFS is based on the
66+ // FlightSize (SND.NXT - SND.UNA) at the start of recovery, NOT the RFC 6675
67+ // pipe (FlightSize - sacked - lost). unAckDataCount carries SND.NXT - SND.UNA.
6768 // Using the smaller pipe here inflates the proportional send count and makes
68- // PRR overshoot ssThresh at recovery exit. RFC 9937 additionally refines
69- // this base with SACK scoreboard terms (- sacked + newlySacked
70- // + newlyCumAcked); that second-order correction is not applied here because
71- // the recovery-ops interface does not expose the scoreboard. Omitting it is
72- // safe: it can only make RecoverFS larger (more conservative), never smaller.
69+ // PRR overshoot ssThresh at recovery exit.
7370 m_recoveryFlightSize = unAckDataCount;
7471
72+ // RFC 9937 Section 6.1 SACK refinement. Bytes SACKed before entering
73+ // recovery are already delivered and must not be counted as flight to be
74+ // recovered, so they are subtracted; bytes newly SACKed and newly
75+ // cumulatively acknowledged by the triggering ACK are added back:
76+ // RecoverFS = SND.NXT - SND.UNA - (bytes SACKed in scoreboard)
77+ // + (bytes newly SACKed) + (bytes newly cumulatively acked)
78+ // deliveredBytes already carries (newly SACKed + newly cumulatively acked)
79+ // (it is the caller's SACK-derived DeliveredData), and bytesSacked is the
80+ // post-ACK scoreboard SACKed total, so subtracting bytesSacked and adding
81+ // deliveredBytes nets out to removing only the pre-existing SACKed bytes.
82+ // Applied only with SACK: without SACK there is no real scoreboard (the
83+ // reno-sack estimate is handled per-ACK in DoRecovery), so RecoverFS stays
84+ // at the plain FlightSize. Signed math guards a transient underflow, and
85+ // the result is clamped to at least 1 SMSS to keep it a valid divisor.
86+ if (tcb->m_sackEnabled )
87+ {
88+ int64_t recoverFs = static_cast <int64_t >(unAckDataCount) -
89+ static_cast <int64_t >(bytesSacked) +
90+ static_cast <int64_t >(deliveredBytes);
91+ m_recoveryFlightSize =
92+ static_cast <uint32_t >(std::max<int64_t >(recoverFs, tcb->m_segmentSize ));
93+ }
94+
7595 NS_LOG_INFO (" Enter recovery: cWnd " << tcb->m_cWnd << " ssThresh " << tcb->m_ssThresh
7696 << " recoveryFlightSize " << m_recoveryFlightSize
77- << " unAckDataCount " << unAckDataCount);
97+ << " unAckDataCount " << unAckDataCount << " bytesSacked "
98+ << bytesSacked << " deliveredBytes " << deliveredBytes);
7899
79100 DoRecovery (tcb, deliveredBytes, true );
80101}
@@ -89,6 +110,11 @@ TcpPrrRecovery::DoRecovery(Ptr<TcpSocketState> tcb, uint32_t deliveredBytes, boo
89110 // With SACK (the ns-3 default), the caller already supplies the SACK-derived
90111 // DeliveredData (change in SND.UNA plus newly SACKed bytes), so adding a
91112 // segment here would double-count and inflate prr_delivered.
113+ // The m_prrDelivered < m_recoveryFlightSize guard is the RFC's Savage99
114+ // anti-inflation mitigation (RFC 9937 Section 6.2): without SACK, PRR
115+ // disallows incrementing DeliveredData once the total delivered in the
116+ // episode would exceed RecoverFS. This bound is spec-mandated, not
117+ // incidental; do not drop it.
92118 if (isDupAck && !tcb->m_sackEnabled && m_prrDelivered < m_recoveryFlightSize)
93119 {
94120 deliveredBytes += tcb->m_segmentSize ;
0 commit comments