diff --git a/Sources/SwiftNetwork/QUIC/Cubic.swift b/Sources/SwiftNetwork/QUIC/Cubic.swift index ac3271b..af2ff0d 100644 --- a/Sources/SwiftNetwork/QUIC/Cubic.swift +++ b/Sources/SwiftNetwork/QUIC/Cubic.swift @@ -226,16 +226,21 @@ struct Cubic: CongestionControlProtocol, CubicLikeProtocol { guard let path, path.pacer.enabled else { return } - var rate = congestionWindow + + // A short RTT can round to zero: `RTT.processNewSample` stores the sample as whole + // microseconds, so an ack duration under 500ns becomes 0µs and dividing by it below would + // trap. Fall back to the initial estimate. + let smoothedRTTInMicroseconds = + smoothedRTT.microseconds == 0 ? pacingInitialRTT.microseconds : smoothedRTT.microseconds + // Use 200% rate when in slow start - if congestionWindow < slowStartThreshold { - rate *= 2 - } - // Multiply by USEC_PER_SEC as srtt is in microseconds - rate = (rate * System.Time.USEC_PER_SEC) / UInt64(smoothedRTT.microseconds) - let burst = rate >> burstQueueShift - path.pacer.setRate(rate: rate) - path.pacer.setBurstSize(burstSize: UInt32(truncatingIfNeeded: burst)) + let pacedWindow = congestionWindow < slowStartThreshold ? congestionWindow * 2 : congestionWindow + let rateInBytesPerSecond = + pacedWindow * System.Time.USEC_PER_SEC / UInt64(smoothedRTTInMicroseconds) + let burstSize = rateInBytesPerSecond >> burstQueueShift + + path.pacer.setRate(rate: rateInBytesPerSecond) + path.pacer.setBurstSize(burstSize: UInt32(truncatingIfNeeded: burstSize)) } @discardableResult diff --git a/Sources/SwiftNetwork/QUIC/Prague.swift b/Sources/SwiftNetwork/QUIC/Prague.swift index af4d5ef..1bddf11 100644 --- a/Sources/SwiftNetwork/QUIC/Prague.swift +++ b/Sources/SwiftNetwork/QUIC/Prague.swift @@ -300,23 +300,21 @@ struct Prague: CongestionControlProtocol, CubicLikeProtocol { guard let path, path.pacer.enabled else { return } - var sRTT = smoothedRTT.microseconds - if sRTT == 0 { - sRTT = pacingInitialRTT.microseconds - } - var rate = congestionWindow - // Use 200% rate when in slow start - if congestionWindow < slowStartThreshold { - rate *= 2 - } + // A short RTT can round to zero: `RTT.processNewSample` stores the sample as whole + // microseconds, so an ack duration under 500ns becomes 0µs and dividing by it below would + // trap. Fall back to the initial estimate. + let smoothedRTTInMicroseconds = + smoothedRTT.microseconds == 0 ? pacingInitialRTT.microseconds : smoothedRTT.microseconds - // Multiply by USEC_PER_SEC as sRTT is in microseconds - rate = (rate * System.Time.USEC_PER_SEC) / UInt64(sRTT) - let burst = rate >> burstQueueShift + // Use 200% rate when in slow start + let pacedWindow = congestionWindow < slowStartThreshold ? congestionWindow * 2 : congestionWindow + let rateInBytesPerSecond = + pacedWindow * System.Time.USEC_PER_SEC / UInt64(smoothedRTTInMicroseconds) + let burstSize = rateInBytesPerSecond >> burstQueueShift - path.pacer.setRate(rate: rate) - path.pacer.setBurstSize(burstSize: UInt32(truncatingIfNeeded: burst)) + path.pacer.setRate(rate: rateInBytesPerSecond) + path.pacer.setBurstSize(burstSize: UInt32(truncatingIfNeeded: burstSize)) } private func packetInRecovery(sentTime: NetworkClock.Instant) -> Bool { diff --git a/Tests/QUICTests/CubicTests.swift b/Tests/QUICTests/CubicTests.swift index af68ed3..4470c55 100644 --- a/Tests/QUICTests/CubicTests.swift +++ b/Tests/QUICTests/CubicTests.swift @@ -470,6 +470,32 @@ final class CubicTests: XCTestCase { == Constants.maxBurstIntervalKernelPacing.milliseconds ) } + + /// A smoothed RTT that rounds to zero microseconds must not reach the pacing-rate division; it + /// traps there. + func testCubicPacerSurvivesASubMicrosecondSmoothedRTT() { + let connection = QUICConnection(context: NetworkContext.implicitContext) + let path = QUICPath(parent: connection) + path.pacePackets = true + path.set(interface: nil, priority: 1, isInitial: true) + path.pacer.setInitialState(10_000_000, 10000) + path.pacer.reset() + + rtt.smoothedRTT = .nanoseconds(400) + + // double-check that the rounding happens as expected + XCTAssertEqual(rtt.smoothedRTT.microseconds, 0, "smoothedRTT is expected to round to zero microseconds") + + let time = NetworkClock.Instant.now + path.congestionControlPacketsSent(bytesSent: 1000) + path.congestionControlAckBegin() + path.congestionControlPacketsAcked(bytesAcked: 1000, sentTime: time) + path.congestionControlAckEnd(rtt: rtt, path: path, mss: path.mss, packetsLost: false) + + // Now that we haven't trapped, assert the rate is as expected. One packet + // acked takes the window to 13000, slow start doubles it, and the 100ms fallback divides. + XCTAssertEqual(path.pacer.rate, 26000 * System.Time.USEC_PER_SEC / 100_000) + } } #endif