Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions Sources/SwiftNetwork/QUIC/Cubic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keep this comment?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I've taken a slightly different approach here. I think the underlying issue once I called it smoothedRTTInMicroseconds is that the units of rate were not clear. I've changed that and I think then the comment is not needed because the code is self-evident.

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
Expand Down
26 changes: 12 additions & 14 deletions Sources/SwiftNetwork/QUIC/Prague.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 26 additions & 0 deletions Tests/QUICTests/CubicTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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