From 385e7ad35bab316a970f49d5e12a1dea14e9c6bd Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Thu, 20 Aug 2026 10:26:37 -0400 Subject: [PATCH 1/6] Congestion control and loss detection take the instant from their caller Each of them read `NetworkClock.Instant.now` where it needed a time, so one inbound batch sampled the clock repeatedly and figures that should have agreed did not. The `= .now` defaults on `Timer` and `Recovery.findLostPacket` are the worst of it: they let a caller take a time without saying which one it meant, and loss detection ended up comparing recorded deadlines against an instant sampled later than the one its timer fired at. `CongestionControlProtocol` and its three implementations now take `now`, `QUICPath` supplies the instant `QUICConnection` already caches for the batch, and the defaults are gone so a caller has to name what it means. `NetworkClock.Instant.testBase` and two `Ack` conveniences over it keep the ACK-bookkeeping callsites free of `now:` noise. `IPProtocol.processInbound` resolves one instant per inbound batch, lazily, so a stack that never asks for receive timestamps pays nothing. This changes what a receive timestamp means: the frames of a batch share one instant where each used to carry its own, so inter-arrival within a batch now reads zero. `CubicTests.testCubicCongestionLimited` and its Prague twin assert an exact congestion window. Seeded from the system clock they had dated send times into their own future, so every loss opened a new recovery period instead of one per round. --- .../SwiftNetwork/Protocols/IPProtocol.swift | 78 ++++++--- Sources/SwiftNetwork/QUIC/Ack.swift | 9 +- .../SwiftNetwork/QUIC/CongestionControl.swift | 40 +++-- Sources/SwiftNetwork/QUIC/Cubic.swift | 26 +-- Sources/SwiftNetwork/QUIC/Ledbat.swift | 13 +- Sources/SwiftNetwork/QUIC/Migration.swift | 6 +- Sources/SwiftNetwork/QUIC/PMTUD.swift | 2 +- Sources/SwiftNetwork/QUIC/Prague.swift | 30 ++-- .../SwiftNetwork/QUIC/QUICConnection.swift | 32 ++-- Sources/SwiftNetwork/QUIC/QUICPath.swift | 19 ++- Sources/SwiftNetwork/QUIC/Recovery.swift | 4 +- Sources/SwiftNetwork/QUIC/Timer.swift | 8 +- Tests/QUICTests/AckTests.swift | 6 +- Tests/QUICTests/CubicTests.swift | 146 ++++++++++------- Tests/QUICTests/LedbatTests.swift | 121 +++++++------- Tests/QUICTests/PragueTests.swift | 148 ++++++++++-------- Tests/QUICTests/QUICTestClock.swift | 67 ++++++++ 17 files changed, 485 insertions(+), 270 deletions(-) create mode 100644 Tests/QUICTests/QUICTestClock.swift diff --git a/Sources/SwiftNetwork/Protocols/IPProtocol.swift b/Sources/SwiftNetwork/Protocols/IPProtocol.swift index c5347cdc..6dd586c8 100644 --- a/Sources/SwiftNetwork/Protocols/IPProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/IPProtocol.swift @@ -463,7 +463,8 @@ public struct IPProtocol: NetworkProtocol { mutating func appendReassembledPackets( _ log: borrowing NetworkLoggerState, - reassembled: inout FrameArray + reassembled: inout FrameArray, + now: NetworkClock.Instant ) { guard let empty = reassemblyState?.inputReassemblyFrames.isEmpty, !empty else { return @@ -623,7 +624,7 @@ public struct IPProtocol: NetworkProtocol { } newFrame.metadataComplete = true if self.flags.calculateReceiveTime { - newFrame.timestamp = Frame.FrameTimestamp.receiveTime(.now) + newFrame.timestamp = Frame.FrameTimestamp.receiveTime(now) } reassembled.add(frame: newFrame) @@ -637,13 +638,14 @@ public struct IPProtocol: NetworkProtocol { _ log: borrowing NetworkLoggerState, ipID: UInt16, reassembled: inout FrameArray, - forceFlush: Bool + forceFlush: Bool, + now: NetworkClock.Instant ) { let hasAccumulatedFragments = reassemblyState?.inputReassemblyFrames.isEmpty == false let isNewID = reassemblyState?.reassemblyID != ipID if hasAccumulatedFragments && (isNewID || forceFlush) { - appendReassembledPackets(log, reassembled: &reassembled) + appendReassembledPackets(log, reassembled: &reassembled, now: now) // Only discard buffered fragments when the IP ID changes if isNewID && !forceFlush { var dropped = 0 @@ -671,7 +673,11 @@ public struct IPProtocol: NetworkProtocol { } } - mutating func processInboundFrames(_ log: borrowing NetworkLoggerState, _ inboundFrames: inout FrameArray) { + mutating func processInboundFrames( + _ log: borrowing NetworkLoggerState, + _ inboundFrames: inout FrameArray, + now: NetworkClock.Instant + ) { let localAddress: UInt32 = self.localAddress.addressValue let remoteAddress: UInt32 = self.remoteAddress.addressValue let mask = (0xF000_0000 as UInt32).bigEndian @@ -784,7 +790,7 @@ public struct IPProtocol: NetworkProtocol { break } if self.flags.calculateReceiveTime { - frame.timestamp = Frame.FrameTimestamp.receiveTime(.now) + frame.timestamp = Frame.FrameTimestamp.receiveTime(now) } if self.flags.receiveHopLimit { frame.hopLimit = ttl @@ -842,7 +848,13 @@ public struct IPProtocol: NetworkProtocol { continue } - processReassembly(log, ipID: identifier, reassembled: &reassembledFragments, forceFlush: false) + processReassembly( + log, + ipID: identifier, + reassembled: &reassembledFragments, + forceFlush: false, + now: now + ) let currentFragmentCount = reassemblyState?.inputReassemblyFrames.count ?? 0 guard currentFragmentCount < IPMaxFragmentCount else { frame.finalize(success: false) @@ -896,7 +908,7 @@ public struct IPProtocol: NetworkProtocol { } self.counters.rxPackets += 1 } - processReassembly(log, ipID: 0, reassembled: &reassembledFragments, forceFlush: true) + processReassembly(log, ipID: 0, reassembled: &reassembledFragments, forceFlush: true, now: now) processedFrames.add(frames: reassembledFragments) inboundFrames.add(frames: processedFrames) } @@ -1245,7 +1257,8 @@ public struct IPProtocol: NetworkProtocol { mutating func appendReassembledPackets( _ log: borrowing NetworkLoggerState, - reassembled: inout FrameArray + reassembled: inout FrameArray, + now: NetworkClock.Instant ) { guard let empty = reassemblyState?.inputReassemblyFrames.isEmpty, !empty else { return @@ -1381,7 +1394,7 @@ public struct IPProtocol: NetworkProtocol { newFrame.hopLimit = firstHopLimit } if self.flags.calculateReceiveTime { - newFrame.timestamp = Frame.FrameTimestamp.receiveTime(.now) + newFrame.timestamp = Frame.FrameTimestamp.receiveTime(now) } newFrame.metadataComplete = true reassembled.add(frame: newFrame) @@ -1396,13 +1409,14 @@ public struct IPProtocol: NetworkProtocol { _ log: borrowing NetworkLoggerState, fragmentID: UInt32, reassembled: inout FrameArray, - forceFlush: Bool + forceFlush: Bool, + now: NetworkClock.Instant ) { let hasAccumulatedFragments = reassemblyState?.inputReassemblyFrames.isEmpty == false let isNewID = reassemblyState?.reassemblyID != fragmentID if hasAccumulatedFragments && (isNewID || forceFlush) { - appendReassembledPackets(log, reassembled: &reassembled) + appendReassembledPackets(log, reassembled: &reassembled, now: now) // Only discard buffered fragments when the IP ID change if isNewID && !forceFlush { var dropped = 0 @@ -1426,7 +1440,11 @@ public struct IPProtocol: NetworkProtocol { } } - mutating func processInboundFrames(_ log: borrowing NetworkLoggerState, _ inboundFrames: inout FrameArray) { + mutating func processInboundFrames( + _ log: borrowing NetworkLoggerState, + _ inboundFrames: inout FrameArray, + now: NetworkClock.Instant + ) { let localAddress = self.localAddress.addressValue let remoteAddress = self.remoteAddress.addressValue @@ -1558,7 +1576,7 @@ public struct IPProtocol: NetworkProtocol { break } if self.flags.calculateReceiveTime { - frame.timestamp = Frame.FrameTimestamp.receiveTime(.now) + frame.timestamp = Frame.FrameTimestamp.receiveTime(now) } if self.flags.receiveHopLimit { frame.hopLimit = hopLimit @@ -1639,7 +1657,8 @@ public struct IPProtocol: NetworkProtocol { log, fragmentID: fragmentID, reassembled: &reassembledFragments, - forceFlush: false + forceFlush: false, + now: now ) let currentFragmentCount = reassemblyState?.inputReassemblyFrames.count ?? 0 @@ -1685,7 +1704,13 @@ public struct IPProtocol: NetworkProtocol { } self.counters.rxPackets += 1 } - processReassembly(log, fragmentID: 0, reassembled: &reassembledFragments, forceFlush: true) + processReassembly( + log, + fragmentID: 0, + reassembled: &reassembledFragments, + forceFlush: true, + now: now + ) processedFrames.add(frames: reassembledFragments) inboundFrames.add(frames: processedFrames) } @@ -2015,7 +2040,12 @@ public struct IPProtocol: NetworkProtocol { guard var inboundFrames, !inboundFrames.isEmpty else { return nil } - IPInstance.processInbound(&self.instanceType, log: self.log, frames: &inboundFrames) + IPInstance.processInbound( + &self.instanceType, + log: self.log, + frames: &inboundFrames, + now: NetworkClock.Instant.now + ) guard !inboundFrames.isEmpty else { log.error("Dropped inbound packets, checking for more") continue @@ -2060,18 +2090,26 @@ public struct IPProtocol: NetworkProtocol { try invokeSendDatagrams(datagrams) } + /// - Parameter now: Read lazily, and only when something will use it. Every consumer of + /// this instant is behind `calculateReceiveTime`, so reading the clock unconditionally + /// would charge a clock read to every inbound batch of a stack that never asks for + /// receive timestamps. Resolved once here, so the frames of a batch still share one + /// instant. @inline(__always) private static func processInbound( _ instanceType: inout IPInstanceType, log: borrowing NetworkLoggerState, - frames: inout FrameArray + frames: inout FrameArray, + now: @autoclosure () -> NetworkClock.Instant ) { switch instanceType { case .ipv4(var instance): - instance.processInboundFrames(log, &frames) + let receiveTime = instance.flags.calculateReceiveTime ? now() : .zero + instance.processInboundFrames(log, &frames, now: receiveTime) instanceType = .ipv4(instance) case .ipv6(var instance): - instance.processInboundFrames(log, &frames) + let receiveTime = instance.flags.calculateReceiveTime ? now() : .zero + instance.processInboundFrames(log, &frames, now: receiveTime) instanceType = .ipv6(instance) } } diff --git a/Sources/SwiftNetwork/QUIC/Ack.swift b/Sources/SwiftNetwork/QUIC/Ack.swift index 3234d609..59c9ec56 100644 --- a/Sources/SwiftNetwork/QUIC/Ack.swift +++ b/Sources/SwiftNetwork/QUIC/Ack.swift @@ -559,7 +559,7 @@ final class Ack: PrefixedLoggable, TimerUser { func append( packetNumberSpace: PacketNumberSpace, packetNumber: PacketNumber, - now: NetworkClock.Instant = .now + now: NetworkClock.Instant ) { withAckSpace(packetNumberSpace: packetNumberSpace) { ackSpace in ackSpace.append(packetNumber, packetNumberSpace: packetNumberSpace, now: now) @@ -598,7 +598,7 @@ final class Ack: PrefixedLoggable, TimerUser { isAckSet: Bool, setAckFrame: (PacketNumberSpace, consuming QUICFrame, Bool) -> Void, ecnCounter: ECNCounter?, - now: NetworkClock.Instant = .now + now: NetworkClock.Instant ) -> Bool { var shouldSend = false if isAckSet { @@ -1244,7 +1244,8 @@ extension Ack { func buildForTesting( for packetNumberSpace: PacketNumberSpace, setAckFrame: (PacketNumberSpace, consuming QUICFrame, Bool) -> Void, - ecnCounter: ECNCounter? = nil + ecnCounter: ECNCounter? = nil, + now: NetworkClock.Instant ) -> Int { var size = 0 withAckSpace(packetNumberSpace: packetNumberSpace) { ackSpace in @@ -1254,7 +1255,7 @@ extension Ack { delaySize: delaySize, setAckFrame: setAckFrame, ecnCounter: ecnCounter, - now: .now + now: now ) return true } diff --git a/Sources/SwiftNetwork/QUIC/CongestionControl.swift b/Sources/SwiftNetwork/QUIC/CongestionControl.swift index 99b1932d..b8c28294 100644 --- a/Sources/SwiftNetwork/QUIC/CongestionControl.swift +++ b/Sources/SwiftNetwork/QUIC/CongestionControl.swift @@ -92,18 +92,19 @@ enum CongestionControl { path: QUICPath?, mss: Int, packetsLost: Bool, + now: NetworkClock.Instant, qlog: QLog? = nil ) { switch self { case .cubic(algorithm: var cubic): - cubic.ackEnd(rtt: rtt, path: path, mss: mss, packetsLost: packetsLost, qlog: qlog) + cubic.ackEnd(rtt: rtt, path: path, mss: mss, packetsLost: packetsLost, now: now, qlog: qlog) self = .cubic(algorithm: cubic) #if !NETWORK_EMBEDDED case .ledbat(algorithm: var ledbat): - ledbat.ackEnd(rtt: rtt, path: path, mss: mss, packetsLost: packetsLost, qlog: qlog) + ledbat.ackEnd(rtt: rtt, path: path, mss: mss, packetsLost: packetsLost, now: now, qlog: qlog) self = .ledbat(algorithm: ledbat) case .prague(algorithm: var prague): - prague.ackEnd(rtt: rtt, path: path, mss: mss, packetsLost: packetsLost, qlog: qlog) + prague.ackEnd(rtt: rtt, path: path, mss: mss, packetsLost: packetsLost, now: now, qlog: qlog) self = .prague(algorithm: prague) #endif } @@ -145,7 +146,8 @@ enum CongestionControl { bytesLost: Int, largestLostSentTime: NetworkClock.Instant, mss: Int, - smoothedRTT: NetworkDuration + smoothedRTT: NetworkDuration, + now: NetworkClock.Instant ) -> Bool { switch self { case .cubic(algorithm: var cubic): @@ -153,7 +155,8 @@ enum CongestionControl { bytesLost: bytesLost, largestLostSentTime: largestLostSentTime, mss: mss, - smoothedRTT: smoothedRTT + smoothedRTT: smoothedRTT, + now: now ) self = .cubic(algorithm: cubic) return reducedCongestionWindow @@ -163,7 +166,8 @@ enum CongestionControl { bytesLost: bytesLost, largestLostSentTime: largestLostSentTime, mss: mss, - smoothedRTT: smoothedRTT + smoothedRTT: smoothedRTT, + now: now ) self = .ledbat(algorithm: ledbat) return reducedCongestionWindow @@ -172,7 +176,8 @@ enum CongestionControl { bytesLost: bytesLost, largestLostSentTime: largestLostSentTime, mss: mss, - smoothedRTT: smoothedRTT + smoothedRTT: smoothedRTT, + now: now ) self = .prague(algorithm: prague) return reducedCongestionWindow @@ -329,11 +334,12 @@ protocol CongestionControlProtocol: PrefixedLoggable { path: QUICPath?, mss: Int, packetsLost: Bool, + now: NetworkClock.Instant, qlog: QLog? ) mutating func spuriousRetransmit(qlog: QLog?) mutating func idleTimeout(mss: Int, qlog: QLog?) - mutating func enterRecovery(mss: Int, qlog: QLog?) + mutating func enterRecovery(mss: Int, now: NetworkClock.Instant, qlog: QLog?) mutating func processECN( path: QUICPath?, ceCount: Int, @@ -343,6 +349,7 @@ protocol CongestionControlProtocol: PrefixedLoggable { largestAckedSentTime: NetworkClock.Instant, mss: Int, smoothedRTT: NetworkDuration, + now: NetworkClock.Instant, qlog: QLog? ) mutating func packetLost( @@ -351,9 +358,15 @@ protocol CongestionControlProtocol: PrefixedLoggable { largestLostSentTime: NetworkClock.Instant, mss: Int, smoothedRTT: NetworkDuration, + now: NetworkClock.Instant, qlog: QLog? ) -> Bool - mutating func linkFlowControl(largestAckSentTime: NetworkClock.Instant, mss: Int, qlog: QLog?) + mutating func linkFlowControl( + largestAckSentTime: NetworkClock.Instant, + mss: Int, + now: NetworkClock.Instant, + qlog: QLog? + ) mutating func persistentCongestion(mss: Int, qlog: QLog?) mutating func mssChanged(mss: Int, qlog: QLog?) mutating func packetDiscarded(bytesSent: Int, qlog: QLog?) @@ -469,22 +482,24 @@ extension CongestionControlProtocol { mutating func congestionEvent( sentTime: NetworkClock.Instant, mss: Int, + now: NetworkClock.Instant, qlog: QLog? = nil ) -> Bool { // If the packet was sent before recovery started, do nothing if packetInRecovery(sentTime: sentTime) { return false } // Enter recovery if the packet was sent // after start of the previous recovery period - enterRecovery(mss: mss, qlog: qlog) + enterRecovery(mss: mss, now: now, qlog: qlog) return true } mutating func linkFlowControl( largestAckSentTime: NetworkClock.Instant, mss: Int, + now: NetworkClock.Instant, qlog: QLog? = nil ) { - congestionEvent(sentTime: largestAckSentTime, mss: mss, qlog: qlog) + congestionEvent(sentTime: largestAckSentTime, mss: mss, now: now, qlog: qlog) log.debug( "Link was flow controlled, reduced congestion window is \(congestionWindow) bytes" ) @@ -526,8 +541,7 @@ extension CongestionControlProtocol { } } - mutating func revalidateCongestionWindow(smoothedRTT: NetworkDuration) -> Bool { - let now = NetworkClock.Instant.now + mutating func revalidateCongestionWindow(smoothedRTT: NetworkDuration, now: NetworkClock.Instant) -> Bool { if pipeAckSampleEnd == .zero { pipeAckNewRound(target: now.advanced(by: smoothedRTT)) } diff --git a/Sources/SwiftNetwork/QUIC/Cubic.swift b/Sources/SwiftNetwork/QUIC/Cubic.swift index ac3271bd..28334cfd 100644 --- a/Sources/SwiftNetwork/QUIC/Cubic.swift +++ b/Sources/SwiftNetwork/QUIC/Cubic.swift @@ -141,8 +141,7 @@ struct Cubic: CongestionControlProtocol, CubicLikeProtocol { #endif } - private mutating func getTarget(mss: Int, smoothedRTT: NetworkDuration) -> UInt64 { - let now = NetworkClock.Instant.now + private mutating func getTarget(mss: Int, smoothedRTT: NetworkDuration, now: NetworkClock.Instant) -> UInt64 { if epochStart == .zero { // If we exit slow start without any packet // loss, CUBIC switches to CA where t is the elapsed @@ -189,11 +188,12 @@ struct Cubic: CongestionControlProtocol, CubicLikeProtocol { private mutating func processAckCongestionAvoidance( bytesAcked: UInt64, smoothedRTT: NetworkDuration, - mss: Int + mss: Int, + now: NetworkClock.Instant ) { totalAcked += bytesAcked // compute W(t+RTT) - let WCubicNext = getTarget(mss: mss, smoothedRTT: smoothedRTT) + let WCubicNext = getTarget(mss: mss, smoothedRTT: smoothedRTT, now: now) updateTCPWindow(bytesAcked: bytesAcked, mss: mss) if congestionWindow < WCubicNext { // Either concave or convex region @@ -245,22 +245,23 @@ struct Cubic: CongestionControlProtocol, CubicLikeProtocol { largestLostSentTime: NetworkClock.Instant, mss: Int, smoothedRTT: NetworkDuration, + now: NetworkClock.Instant, qlog: QLog? = nil ) -> Bool { decrementBytesInFlight(UInt64(bytesLost)) let reducedCongestionWindow = congestionEvent( sentTime: largestLostSentTime, mss: mss, + now: now, qlog: qlog ) updatePacerState(path: path, smoothedRTT: smoothedRTT) return reducedCongestionWindow } - mutating func enterRecovery(mss: Int, qlog: QLog? = nil) { + mutating func enterRecovery(mss: Int, now: NetworkClock.Instant, qlog: QLog? = nil) { log.datapath("Entering Recovery: current cwin=\(congestionWindow)") - let timeNow = NetworkClock.Instant.now - recoveryStartTime = timeNow + recoveryStartTime = now lastMaxCongestionWindow = maxCongestionWindow maxCongestionWindow = congestionWindow congestionWindow = UInt64(Double(lossFlightSize) * Cubic.beta) @@ -284,7 +285,7 @@ struct Cubic: CongestionControlProtocol, CubicLikeProtocol { // Note that K = 0 if we enter congestion avoidance without loss. setK(mss: mss) // Set the start of current congestion avoidance and the origin point - epochStart = timeNow + epochStart = now originPoint = maxCongestionWindow // Reset tcpCongestionWindow to be in sync with cubic tcpCongestionWindow = congestionWindow @@ -300,6 +301,7 @@ struct Cubic: CongestionControlProtocol, CubicLikeProtocol { path: QUICPath? = nil, mss: Int, packetsLost: Bool, + now: NetworkClock.Instant, qlog: QLog? = nil ) { if packetsLost { @@ -312,7 +314,7 @@ struct Cubic: CongestionControlProtocol, CubicLikeProtocol { return } let smoothedRTT = rtt.smoothedRTT - if !revalidateCongestionWindow(smoothedRTT: smoothedRTT) { + if !revalidateCongestionWindow(smoothedRTT: smoothedRTT, now: now) { bytesAcked = 0 return } @@ -325,7 +327,8 @@ struct Cubic: CongestionControlProtocol, CubicLikeProtocol { processAckCongestionAvoidance( bytesAcked: bytesAcked, smoothedRTT: smoothedRTT, - mss: mss + mss: mss, + now: now ) } // Should be a minimum of 2*MSS @@ -345,6 +348,7 @@ struct Cubic: CongestionControlProtocol, CubicLikeProtocol { largestAckedSentTime: NetworkClock.Instant, mss: Int, smoothedRTT: NetworkDuration, + now: NetworkClock.Instant, qlog: QLog? = nil ) { if _slowPath(ceCount < ecnCECounter) { @@ -381,7 +385,7 @@ struct Cubic: CongestionControlProtocol, CubicLikeProtocol { // Haven't elapsed one RTT yet from last CWR return } - congestionEvent(sentTime: largestAckedSentTime, mss: mss, qlog: qlog) + congestionEvent(sentTime: largestAckedSentTime, mss: mss, now: now, qlog: qlog) // Update pacer state as congestionWindow has changed updatePacerState(path: path, smoothedRTT: smoothedRTT) // Start new round for CWR diff --git a/Sources/SwiftNetwork/QUIC/Ledbat.swift b/Sources/SwiftNetwork/QUIC/Ledbat.swift index 77fd016c..130b166e 100644 --- a/Sources/SwiftNetwork/QUIC/Ledbat.swift +++ b/Sources/SwiftNetwork/QUIC/Ledbat.swift @@ -87,19 +87,21 @@ struct Ledbat: CongestionControlProtocol, CubicLikeProtocol { largestLostSentTime: NetworkClock.Instant, mss: Int, smoothedRTT: NetworkDuration, + now: NetworkClock.Instant, qlog: QLog? = nil ) -> Bool { decrementBytesInFlight(UInt64(bytesLost)) let reducedCongestionWindow = congestionEvent( sentTime: largestLostSentTime, mss: mss, + now: now, qlog: qlog ) return reducedCongestionWindow } - mutating func enterRecovery(mss: Int, qlog: QLog? = nil) { - recoveryStartTime = .now + mutating func enterRecovery(mss: Int, now: NetworkClock.Instant, qlog: QLog? = nil) { + recoveryStartTime = now prevCongestionWindow = congestionWindow congestionWindow = UInt64(Double(lossFlightSize) * Ledbat.beta) if _slowPath(congestionWindow < Ledbat.minCongestionWindow(mss)) { @@ -117,6 +119,7 @@ struct Ledbat: CongestionControlProtocol, CubicLikeProtocol { path: QUICPath? = nil, mss: Int, packetsLost: Bool, + now: NetworkClock.Instant, qlog: QLog? = nil ) { guard packetsLost == false else { @@ -129,7 +132,7 @@ struct Ledbat: CongestionControlProtocol, CubicLikeProtocol { return } let smoothedRTT = rtt.smoothedRTT - if !revalidateCongestionWindow(smoothedRTT: smoothedRTT) { + if !revalidateCongestionWindow(smoothedRTT: smoothedRTT, now: now) { bytesAcked = 0 return } @@ -140,7 +143,6 @@ struct Ledbat: CongestionControlProtocol, CubicLikeProtocol { return } let qDelay = currentRTT - baseRTT - let now = NetworkClock.Instant.now // Slowdown period - first slowdown // is 2RTT after we exit initial slow start. // Subsequent slowdowns are after 9 times the @@ -238,6 +240,7 @@ struct Ledbat: CongestionControlProtocol, CubicLikeProtocol { largestAckedSentTime: NetworkClock.Instant, mss: Int, smoothedRTT: NetworkDuration, + now: NetworkClock.Instant, qlog: QLog? = nil ) { if _slowPath(ceCount < ecnCECounter) { @@ -269,7 +272,7 @@ struct Ledbat: CongestionControlProtocol, CubicLikeProtocol { /* Haven't elapsed one RTT yet from last CWR */ return } - congestionEvent(sentTime: largestAckedSentTime, mss: mss) + congestionEvent(sentTime: largestAckedSentTime, mss: mss, now: now) // Start new round for CWR self.largestSentPN = largestSentPN diff --git a/Sources/SwiftNetwork/QUIC/Migration.swift b/Sources/SwiftNetwork/QUIC/Migration.swift index c36439b9..18f9e15d 100644 --- a/Sources/SwiftNetwork/QUIC/Migration.swift +++ b/Sources/SwiftNetwork/QUIC/Migration.swift @@ -30,7 +30,7 @@ struct Migration: ~Copyable { private func sendPendingChallenges( connection: QUICConnection, - now: NetworkClock.Instant = NetworkClock.Instant.now + now: NetworkClock.Instant ) { connection.applyToAllPaths { path in if path.hasPendingItems(now: now) { @@ -45,7 +45,7 @@ struct Migration: ~Copyable { return } - let now = NetworkClock.Instant.now + let now = connection.now sendPendingChallenges(connection: connection, now: now) var firstChallengeTime: NetworkClock.Instant? @@ -92,7 +92,7 @@ struct Migration: ~Copyable { func timerFired(connection: QUICConnection) { connection.log.debug("Migration timer fired") - sendPendingChallenges(connection: connection) + sendPendingChallenges(connection: connection, now: connection.now) } func migrate(to path: QUICPath, connection: QUICConnection) { diff --git a/Sources/SwiftNetwork/QUIC/PMTUD.swift b/Sources/SwiftNetwork/QUIC/PMTUD.swift index 7253f889..23c72cb8 100644 --- a/Sources/SwiftNetwork/QUIC/PMTUD.swift +++ b/Sources/SwiftNetwork/QUIC/PMTUD.swift @@ -131,7 +131,7 @@ struct PMTUDState: ~Copyable { if timerID == nil { let pathID = path.identifier - timerID = connection.timer.insert(description: "PMTUD") { + timerID = connection.timer.insert(description: "PMTUD", timerNow: connection.now) { let innerPath = connection.path(for: pathID) guard let innerPath else { return } innerPath.pmtudState.timerFired(timeNow: connection.now, path: innerPath) diff --git a/Sources/SwiftNetwork/QUIC/Prague.swift b/Sources/SwiftNetwork/QUIC/Prague.swift index af4d5ef3..02f999aa 100644 --- a/Sources/SwiftNetwork/QUIC/Prague.swift +++ b/Sources/SwiftNetwork/QUIC/Prague.swift @@ -161,8 +161,11 @@ struct Prague: CongestionControlProtocol, CubicLikeProtocol { cubicK = K } - private mutating func getCubicTarget(mss: Int, smoothedRTT: NetworkDuration) -> UInt64 { - let now = NetworkClock.Instant.now + private mutating func getCubicTarget( + mss: Int, + smoothedRTT: NetworkDuration, + now: NetworkClock.Instant + ) -> UInt64 { if cubicEpochStart == .zero { // If we exit slow start without any packet loss, CUBIC switches to CA // where t is the elapsed time since the beginning of the current CA. @@ -211,12 +214,13 @@ struct Prague: CongestionControlProtocol, CubicLikeProtocol { private mutating func cubicProcessAckCA( bytesAcked: UInt64, smoothedRTT: NetworkDuration, - mss: Int + mss: Int, + now: NetworkClock.Instant ) { cubicAcked += bytesAcked // compute W(t+RTT) - let wCubicNext = getCubicTarget(mss: mss, smoothedRTT: smoothedRTT) + let wCubicNext = getCubicTarget(mss: mss, smoothedRTT: smoothedRTT, now: now) updateRenoCongestionWindow(bytesAcked: bytesAcked, mss: mss) @@ -323,10 +327,9 @@ struct Prague: CongestionControlProtocol, CubicLikeProtocol { sentTime <= recoveryStartTime } - mutating func enterRecovery(mss: Int, qlog: QLog? = nil) { + mutating func enterRecovery(mss: Int, now: NetworkClock.Instant, qlog: QLog? = nil) { log.datapath("Entering Recovery: current cwin=\(congestionWindow)") - let timeNow = NetworkClock.Instant.now - recoveryStartTime = timeNow + recoveryStartTime = now cubicLastMaxCongestionWindow = cubicMaxCongestionWindow cubicMaxCongestionWindow = congestionWindow @@ -354,7 +357,7 @@ struct Prague: CongestionControlProtocol, CubicLikeProtocol { // Note that K = 0 if we enter CA without loss. setCubicK(mss: mss) // Set the start of current CA and the origin point - cubicEpochStart = timeNow + cubicEpochStart = now cubicOriginPoint = cubicMaxCongestionWindow // Reset renoCongestionWindow to be in sync with Prague renoCongestionWindow = congestionWindow @@ -462,6 +465,7 @@ struct Prague: CongestionControlProtocol, CubicLikeProtocol { private mutating func pragueCongestionEvent( sentTime: NetworkClock.Instant, mss: Int, + now: NetworkClock.Instant, qlog: QLog? = nil ) -> Bool { // If the packet was sent before recovery started, do nothing @@ -469,7 +473,7 @@ struct Prague: CongestionControlProtocol, CubicLikeProtocol { return false } - enterRecovery(mss: mss, qlog: qlog) + enterRecovery(mss: mss, now: now, qlog: qlog) return true } @@ -480,12 +484,14 @@ struct Prague: CongestionControlProtocol, CubicLikeProtocol { largestLostSentTime: NetworkClock.Instant, mss: Int, smoothedRTT: NetworkDuration, + now: NetworkClock.Instant, qlog: QLog? = nil ) -> Bool { decrementBytesInFlight(UInt64(bytesLost)) let reducedCongestionWindow = pragueCongestionEvent( sentTime: largestLostSentTime, mss: mss, + now: now, qlog: qlog ) updatePacerState(path: path, smoothedRTT: smoothedRTT) @@ -497,6 +503,7 @@ struct Prague: CongestionControlProtocol, CubicLikeProtocol { path: QUICPath? = nil, mss: Int, packetsLost: Bool, + now: NetworkClock.Instant, qlog: QLog? = nil ) { if packetsLost { @@ -511,7 +518,7 @@ struct Prague: CongestionControlProtocol, CubicLikeProtocol { } let smoothedRTT = rtt.smoothedRTT - if !revalidateCongestionWindow(smoothedRTT: smoothedRTT) { + if !revalidateCongestionWindow(smoothedRTT: smoothedRTT, now: now) { bytesAcked = 0 return } @@ -522,7 +529,7 @@ struct Prague: CongestionControlProtocol, CubicLikeProtocol { if reducedDueToCE { pragueCAAfterCE(bytesAcked: bytesAcked, mss: mss) } else { - cubicProcessAckCA(bytesAcked: bytesAcked, smoothedRTT: smoothedRTT, mss: mss) + cubicProcessAckCA(bytesAcked: bytesAcked, smoothedRTT: smoothedRTT, mss: mss, now: now) } } @@ -543,6 +550,7 @@ struct Prague: CongestionControlProtocol, CubicLikeProtocol { largestAckedSentTime: NetworkClock.Instant, mss: Int, smoothedRTT: NetworkDuration, + now: NetworkClock.Instant, qlog: QLog? = nil ) { if _slowPath(ceCount < ecnCECounter) { diff --git a/Sources/SwiftNetwork/QUIC/QUICConnection.swift b/Sources/SwiftNetwork/QUIC/QUICConnection.swift index 0f7255f7..deca15e9 100644 --- a/Sources/SwiftNetwork/QUIC/QUICConnection.swift +++ b/Sources/SwiftNetwork/QUIC/QUICConnection.swift @@ -447,13 +447,13 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, self.setMetadataHandlers() self.timer = Timer(reference: self.reference, timerReference: timerReference, logPrefixer: logPrefixer) - let ackTimerID = timer.insert(description: "ACK") { - self.ack.timerFired(timeNow: .now) + let ackTimerID = timer.insert(description: "ACK", timerNow: self.now) { + self.ack.timerFired(timeNow: self.now) } self.ack = Ack(connection: self, timerID: ackTimerID, logPrefixer: logPrefixer) - let recoveryTimerID = timer.insert(description: "Recovery") { - self.recovery.timerFired(timeNow: .now) + let recoveryTimerID = timer.insert(description: "Recovery", timerNow: self.now) { + self.recovery.timerFired(timeNow: self.now) } self.recovery = Recovery( connection: self, @@ -461,7 +461,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, logPrefixer: logPrefixer ) - migration.timerID = timer.insert(description: "Migration") { + migration.timerID = timer.insert(description: "Migration", timerNow: self.now) { self.migration.timerFired(connection: self) } @@ -1313,7 +1313,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, } else { if state == .idle { // Record handshake start time start - handshakeStartTime = .now + handshakeStartTime = self.now // Start idle timer to terminate unresponded to connection guard clientStartIdleTimer() else { @@ -1366,7 +1366,8 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, idleTimerID = timer.insert( description: "Idle timeout", - fromNow: idleTimeout + fromNow: idleTimeout, + timerNow: self.now ) { self.idleTimeoutFired() } @@ -1410,7 +1411,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, // - client: initial packet sent // Check when the last activity was recorded - let now = NetworkClock.Instant.now + let now = self.now guard now >= lastPacketReceivedTimestamp else { log.fault("Now should not be less than lastPacketReceivedTimestamp") return @@ -2753,7 +2754,8 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, withCurrentPath { path in _ = timer.insert( description: "draining", - fromNow: path.recoveryState.getMaxPTODrainTime(idleTimeout: self.idleTimeout) + fromNow: path.recoveryState.getMaxPTODrainTime(idleTimeout: self.idleTimeout), + timerNow: self.now ) { self.drain() } @@ -2834,7 +2836,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, // We try to delay the keep-alive by some delta amount // depending on when we last received a valid packet // from the remote side. - let now = NetworkClock.Instant.now + let now = self.now if _slowPath(now < lastPacketReceivedTimestamp) { log.fault("Bogus lastPacketReceivedTimestamp") return @@ -2874,7 +2876,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, minIdleTime = .milliseconds(min(idleTimeoutLocal, idleTimeoutRemote)) } if keepaliveTimerID == nil { - keepaliveTimerID = timer.insert(description: "keepalive") { + keepaliveTimerID = timer.insert(description: "keepalive", timerNow: self.now) { self.keepaliveHandler() } } @@ -3429,7 +3431,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, let tagSize = protector.getTagSize(for: keyState) if isPacing { - let now = NetworkClock.Instant.now + let now = self.now // lastAckElicitingPacketSentTimestamp can be in the future for kernel packet pacing. if now > lastAckElicitingPacketSentTimestamp { let idleTime = lastAckElicitingPacketSentTimestamp.duration(to: now) @@ -3742,7 +3744,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, private func log(packet: inout Packet, coalesced: Bool = false, outbound: Bool) { #if !NETWORK_EMBEDDED if Logger.swiftNetworkDatapathLoggingEnabled { - let now = NetworkClock.Instant.now + let now = self.now var delta: NetworkDuration = .milliseconds(0) if lastShorthandTimestamp != .zero { delta = lastShorthandTimestamp.duration(to: now) @@ -4172,7 +4174,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, } } - handshakeDuration = handshakeStartTime.duration(to: .now) + handshakeDuration = handshakeStartTime.duration(to: self.now) var currentRTT: NetworkDuration = .milliseconds(0) if let currentPath = currentPath { currentRTT = currentPath.rtt.smoothedRTT @@ -4300,7 +4302,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, } public func wakeup() { - self.timer.timerFired() + self.timer.timerFired(timeNow: self.now) } func setupStreamID(isUnidirectional: Bool, isServer: Bool) -> QUICStreamID? { diff --git a/Sources/SwiftNetwork/QUIC/QUICPath.swift b/Sources/SwiftNetwork/QUIC/QUICPath.swift index 2b9cda3c..93ceaf03 100644 --- a/Sources/SwiftNetwork/QUIC/QUICPath.swift +++ b/Sources/SwiftNetwork/QUIC/QUICPath.swift @@ -363,7 +363,7 @@ public final class QUICPath: MultiplexingDatagramPath, Equatable ) } - func updateBDP(length: Int, now: NetworkClock.Instant = NetworkClock.Instant.now) { + func updateBDP(length: Int, now: NetworkClock.Instant) { if bdp.timestamp == .zero { bdp.timestamp = now } @@ -610,7 +610,7 @@ public final class QUICPath: MultiplexingDatagramPath, Equatable return } log.debug("Valid path challenge response received: \(data)") - let now = NetworkClock.Instant.now + let now = parentProtocol.now let responseDuration = pendingOutboundChallenge.sentTime.duration(to: now) pendingOutboundChallenges.removeAll() challengesSent = 0 @@ -655,7 +655,14 @@ extension QUICPath { @inline(__always) func congestionControlAckEnd(rtt: borrowing RTT, path: QUICPath?, mss: Int, packetsLost: Bool, qlog: QLog? = nil) { - congestionControl?.ackEnd(rtt: rtt, path: self, mss: mss, packetsLost: packetsLost, qlog: qlog) + congestionControl?.ackEnd( + rtt: rtt, + path: self, + mss: mss, + packetsLost: packetsLost, + now: parentProtocol.now, + qlog: qlog + ) } @inline(__always) @@ -679,7 +686,8 @@ extension QUICPath { bytesLost: bytesLost, largestLostSentTime: largestLostSentTime, mss: mss, - smoothedRTT: smoothedRTT + smoothedRTT: smoothedRTT, + now: parentProtocol.now ) ?? false } @@ -740,6 +748,7 @@ extension QUICPath { largestAckedSentTime: largestAckedSentTime, mss: mss, smoothedRTT: smoothedRTT, + now: parentProtocol.now, qlog: qlog ) #if !NETWORK_EMBEDDED @@ -752,6 +761,7 @@ extension QUICPath { largestAckedSentTime: largestAckedSentTime, mss: mss, smoothedRTT: smoothedRTT, + now: parentProtocol.now, qlog: qlog ) case .prague(var prague): @@ -763,6 +773,7 @@ extension QUICPath { largestAckedSentTime: largestAckedSentTime, mss: mss, smoothedRTT: smoothedRTT, + now: parentProtocol.now, qlog: qlog ) #endif diff --git a/Sources/SwiftNetwork/QUIC/Recovery.swift b/Sources/SwiftNetwork/QUIC/Recovery.swift index 1b3700e9..e7879cca 100644 --- a/Sources/SwiftNetwork/QUIC/Recovery.swift +++ b/Sources/SwiftNetwork/QUIC/Recovery.swift @@ -988,7 +988,7 @@ struct Recovery: ~Copyable, PrefixedLoggable, NonCopyableTimerUser { mutating func findLostPacket( pnSpace: PacketNumberSpace? = nil, path: QUICPath? = nil, - timeNow: NetworkClock.Instant = NetworkClock.Instant.now, + timeNow: NetworkClock.Instant, connection: QUICConnection ) -> Bool { var packetLost = false @@ -1281,7 +1281,7 @@ struct Recovery: ~Copyable, PrefixedLoggable, NonCopyableTimerUser { ) if lossTime != .zero { log.datapath("Recovery timer fired, finding lost packets") - findLostPacket(connection: connection) + findLostPacket(timeNow: timeNow, connection: connection) } else { log.datapath("Recovery timer fired, PTO") connection.withCurrentPath { path in diff --git a/Sources/SwiftNetwork/QUIC/Timer.swift b/Sources/SwiftNetwork/QUIC/Timer.swift index af26a63f..c3493279 100644 --- a/Sources/SwiftNetwork/QUIC/Timer.swift +++ b/Sources/SwiftNetwork/QUIC/Timer.swift @@ -59,7 +59,7 @@ private struct TimerEntry: ~Copyable { mutating func disable() { deadline = .zero } - mutating func schedule(fromNow: NetworkDuration, timerNow: NetworkClock.Instant = .now) { + mutating func schedule(fromNow: NetworkDuration, timerNow: NetworkClock.Instant) { precondition(fromNow != .zero) self.deadline = timerNow.advanced(by: fromNow) } @@ -112,7 +112,7 @@ final class Timer: PrefixedLoggable { func insert( description: String, fromNow: NetworkDuration = .zero, - timerNow: NetworkClock.Instant = .now, + timerNow: NetworkClock.Instant, closure: @escaping () -> Void ) -> TimerID { let identifier = nextID @@ -237,7 +237,7 @@ final class Timer: PrefixedLoggable { func reschedule( identifier: TimerID, fromNow: NetworkDuration, - timerNow: NetworkClock.Instant = .now + timerNow: NetworkClock.Instant ) { guard let index = find(identifier) else { return @@ -255,7 +255,7 @@ final class Timer: PrefixedLoggable { } } - public func timerFired(timeNow: NetworkClock.Instant = .now) { + public func timerFired(timeNow: NetworkClock.Instant) { // Timer fired means the kernel woke us up. wakeup = .idle diff --git a/Tests/QUICTests/AckTests.swift b/Tests/QUICTests/AckTests.swift index b76bb1a1..819b6f07 100644 --- a/Tests/QUICTests/AckTests.swift +++ b/Tests/QUICTests/AckTests.swift @@ -888,7 +888,8 @@ final class AckTests: XCTestCase { for: .applicationData, isAckSet: false, setAckFrame: testSetAckFrame, - ecnCounter: nil + ecnCounter: nil, + now: .testBase ) wait(for: [pingExpectation], timeout: 2.0) XCTAssertTrue( @@ -924,7 +925,8 @@ final class AckTests: XCTestCase { for: .applicationData, isAckSet: false, setAckFrame: testSetAckFrame, - ecnCounter: nil + ecnCounter: nil, + now: .testBase ) wait(for: [pingExpectation], timeout: 2.0) // Verify that a PING frame was NOT requested diff --git a/Tests/QUICTests/CubicTests.swift b/Tests/QUICTests/CubicTests.swift index af68ed37..7840ae24 100644 --- a/Tests/QUICTests/CubicTests.swift +++ b/Tests/QUICTests/CubicTests.swift @@ -52,11 +52,11 @@ final class CubicTests: XCTestCase { func testCubicReset() { XCTAssertEqual(cubic.availableCongestionWindow, defaultCongestionWindow) - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase cubic.packetSent(bytesSent: 1000) cubic.ackBegin() cubic.packetsAcked(bytesAcked: 1000, sentTime: time) - cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(cubic.availableCongestionWindow, 13000) cubic.reset(mss: Constants.initialMSS) XCTAssertEqual(cubic.availableCongestionWindow, defaultCongestionWindow) @@ -65,13 +65,14 @@ final class CubicTests: XCTestCase { func testCubicLostPackets() { XCTAssertEqual(cubic.availableCongestionWindow, defaultCongestionWindow) /* "Send" some packets and declare them lost */ - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase cubic.packetSent(bytesSent: 1000) cubic.packetLost( bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) XCTAssertEqual(cubic.availableCongestionWindow, 8400) /* See if we can send another packet */ @@ -81,7 +82,7 @@ final class CubicTests: XCTestCase { func testCubicSlowStart() { rtt.smoothedRTT = .microseconds(0) /* "Send" some packets and declare one of them lost */ - var time = NetworkClock.Instant.now + var time = NetworkClock.Instant.testBase cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) @@ -94,20 +95,21 @@ final class CubicTests: XCTestCase { cubic.packetsAcked(bytesAcked: 1000, sentTime: time) cubic.packetsAcked(bytesAcked: 1000, sentTime: time) cubic.packetsAcked(bytesAcked: 1000, sentTime: time) - cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) cubic.packetLost( bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) XCTAssertEqual(cubic.availableCongestionWindow, 11900) /* Make sure that another successful packet doesn't cause us to continue slow start */ - time = NetworkClock.Instant.now.advanced(by: .microseconds(100)) + time = NetworkClock.Instant.testBase.advanced(by: .microseconds(100)) cubic.packetSent(bytesSent: 1000) cubic.ackBegin() cubic.packetsAcked(bytesAcked: 1000, sentTime: time) - cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(cubic.availableCongestionWindow, 11953) } @@ -115,7 +117,7 @@ final class CubicTests: XCTestCase { rtt.smoothedRTT = .microseconds(0) XCTAssertEqual(cubic.availableCongestionWindow, defaultCongestionWindow) /* Test that CE counts will reduce the congestion window immediately and move CUBIC to Congestion avoidance */ - var time = NetworkClock.Instant.now + var time = NetworkClock.Instant.testBase cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) @@ -136,15 +138,16 @@ final class CubicTests: XCTestCase { largestAckedPN: 5, largestAckedSentTime: time, mss: mss, - smoothedRTT: rtt.smoothedRTT + smoothedRTT: rtt.smoothedRTT, + now: time ) - cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(cubic.availableCongestionWindow, 8400) - time = NetworkClock.Instant.now.advanced(by: .microseconds(100)) + time = NetworkClock.Instant.testBase.advanced(by: .microseconds(100)) cubic.packetSent(bytesSent: 1000) cubic.ackBegin() cubic.packetsAcked(bytesAcked: 1000, sentTime: time) - cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) /* congestion window grows during congestion avoidance */ XCTAssertEqual(cubic.availableCongestionWindow, 8475) } @@ -153,7 +156,7 @@ final class CubicTests: XCTestCase { rtt.smoothedRTT = .microseconds(0) XCTAssertEqual(cubic.availableCongestionWindow, defaultCongestionWindow) /* Test that CE counts will reduce congestion window, enter congestion window recovery and after that we don't decrease congestion window for 1RTT even we receive new CE counts */ - var time = NetworkClock.Instant.now + var time = NetworkClock.Instant.testBase cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) @@ -172,11 +175,12 @@ final class CubicTests: XCTestCase { largestAckedPN: 3, largestAckedSentTime: time, mss: mss, - smoothedRTT: rtt.smoothedRTT + smoothedRTT: rtt.smoothedRTT, + now: time ) /* availableCongestionWindow = congestionWindow - bytesInFlight = 8400 - 2000 = 6400 */ XCTAssertEqual(cubic.availableCongestionWindow, 6400) - time = NetworkClock.Instant.now.advanced(by: .microseconds(100)) + time = NetworkClock.Instant.testBase.advanced(by: .microseconds(100)) cubic.ackBegin() cubic.packetsAcked(bytesAcked: 1000, sentTime: time) cubic.packetsAcked(bytesAcked: 1000, sentTime: time) @@ -187,9 +191,10 @@ final class CubicTests: XCTestCase { largestAckedPN: 5, largestAckedSentTime: time, mss: mss, - smoothedRTT: rtt.smoothedRTT + smoothedRTT: rtt.smoothedRTT, + now: time ) - cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) /* congestion window is the same 8400, bytes in flight has reduced to 0 */ XCTAssertEqual(cubic.availableCongestionWindow, 8400) } @@ -197,7 +202,7 @@ final class CubicTests: XCTestCase { func testCubicAckDuringRecovery() { rtt.smoothedRTT = .microseconds(0) /* "Send" some packets and declare one of them lost */ - var time = NetworkClock.Instant.now + var time = NetworkClock.Instant.testBase cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) @@ -214,21 +219,22 @@ final class CubicTests: XCTestCase { bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) - cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: true) + cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: true, now: time) XCTAssertEqual(cubic.availableCongestionWindow, 8400) - time = NetworkClock.Instant.now.advanced(by: .microseconds(100)) + time = NetworkClock.Instant.testBase.advanced(by: .microseconds(100)) cubic.packetSent(bytesSent: 1000) cubic.ackBegin() cubic.packetsAcked(bytesAcked: 1000, sentTime: time) - cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(cubic.availableCongestionWindow, 8475) } func testCubicIdleTimeout() { XCTAssertEqual(cubic.availableCongestionWindow, defaultCongestionWindow) - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) @@ -242,7 +248,7 @@ final class CubicTests: XCTestCase { cubic.packetsAcked(bytesAcked: 1000, sentTime: time) cubic.packetsAcked(bytesAcked: 1000, sentTime: time) cubic.packetsAcked(bytesAcked: 1000, sentTime: time) - cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(cubic.availableCongestionWindow, 18000) cubic.idleTimeout(mss: mss) XCTAssertEqual(cubic.availableCongestionWindow, defaultCongestionWindow) @@ -258,7 +264,15 @@ final class CubicTests: XCTestCase { } func testCubicCongestionLimited() { - var time = NetworkClock.Instant.now + // `sentTime` is when the packets went out; `detectedAt` is when their loss was noticed, + // which is necessarily later. Dating a send after the recovery period it is compared + // against re-enters recovery on every loss instead of once per round. + // + // RFC 9002 Section 7.3.2 allows one reduction per recovery period, and Appendix B opens a + // new period only for a packet sent after the current one started. Three rounds of sending + // therefore give three reductions. + var sentTime = NetworkClock.Instant.testBase + var detectedAt = sentTime.advanced(by: .microseconds(100)) cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) @@ -266,65 +280,75 @@ final class CubicTests: XCTestCase { cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) cubic.ackBegin() - cubic.packetsAcked(bytesAcked: 1000, sentTime: time) - cubic.packetsAcked(bytesAcked: 1000, sentTime: time) - cubic.packetsAcked(bytesAcked: 1000, sentTime: time) - cubic.packetsAcked(bytesAcked: 1000, sentTime: time) - cubic.packetsAcked(bytesAcked: 1000, sentTime: time) + cubic.packetsAcked(bytesAcked: 1000, sentTime: sentTime) + cubic.packetsAcked(bytesAcked: 1000, sentTime: sentTime) + cubic.packetsAcked(bytesAcked: 1000, sentTime: sentTime) + cubic.packetsAcked(bytesAcked: 1000, sentTime: sentTime) + cubic.packetsAcked(bytesAcked: 1000, sentTime: sentTime) cubic.packetLost( bytesLost: 1000, - largestLostSentTime: time, + largestLostSentTime: sentTime, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: detectedAt ) XCTAssertEqual(cubic.availableCongestionWindow, 8400) - time = NetworkClock.Instant.now.advanced(by: .microseconds(1000)) + sentTime = sentTime.advanced(by: .microseconds(1000)) + detectedAt = sentTime.advanced(by: .microseconds(100)) cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) cubic.packetLost( bytesLost: 1000, - largestLostSentTime: time, + largestLostSentTime: sentTime, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: detectedAt ) cubic.packetLost( bytesLost: 1000, - largestLostSentTime: time, + largestLostSentTime: sentTime, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: detectedAt ) cubic.packetLost( bytesLost: 1000, - largestLostSentTime: time, + largestLostSentTime: sentTime, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: detectedAt ) cubic.packetLost( bytesLost: 1000, - largestLostSentTime: time, + largestLostSentTime: sentTime, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: detectedAt ) - time = NetworkClock.Instant.now.advanced(by: .microseconds(2000)) + sentTime = sentTime.advanced(by: .microseconds(1000)) + detectedAt = sentTime.advanced(by: .microseconds(100)) cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) cubic.packetLost( bytesLost: 1000, - largestLostSentTime: time, + largestLostSentTime: sentTime, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: detectedAt ) cubic.packetLost( bytesLost: 1000, - largestLostSentTime: time, + largestLostSentTime: sentTime, mss: mss, - smoothedRTT: .microseconds(0) - ) - XCTAssert( - (cubic.availableCongestionWindow >= 2400) && (cubic.availableCongestionWindow < 3000) + smoothedRTT: .microseconds(0), + now: detectedAt ) + // One reduction per round, three rounds: 12000 -> 8400 -> 5880 -> 4116, each step + // `UInt64(Double(window) * Cubic.beta)`. Written out rather than as `pow(beta, 3)`, which + // is 0.34299999999999997 and truncates to 4115; the reductions compound one at a time. + XCTAssertEqual(cubic.availableCongestionWindow, 4116) XCTAssertFalse(cubic.canSend(packetLength: 10000)) } @@ -335,7 +359,7 @@ final class CubicTests: XCTestCase { } func testCubicSpuriousRetransmit() { - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) @@ -344,7 +368,8 @@ final class CubicTests: XCTestCase { bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) cubic.spuriousRetransmit() XCTAssertEqual(cubic.availableCongestionWindow, 9000) @@ -352,7 +377,7 @@ final class CubicTests: XCTestCase { /* Tests that we can enter CA without any loss after idle period */ func testCubicCongestionAvoidance() { - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) @@ -363,9 +388,10 @@ final class CubicTests: XCTestCase { bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) - cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: true) + cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: true, now: time) XCTAssertEqual(cubic.availableCongestionWindow, 8400) cubic.idleTimeout(mss: mss) XCTAssertEqual(cubic.availableCongestionWindow, 8400) @@ -376,7 +402,7 @@ final class CubicTests: XCTestCase { cubic.packetsAcked(bytesAcked: 1200, sentTime: time) cubic.packetsAcked(bytesAcked: 1200, sentTime: time) cubic.packetsAcked(bytesAcked: 1200, sentTime: time) - cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) /* Enter CA */ XCTAssertEqual(cubic.availableCongestionWindow, 12000) for _ in 0..<12 { @@ -386,7 +412,7 @@ final class CubicTests: XCTestCase { for _ in 0..<12 { cubic.packetsAcked(bytesAcked: 1000, sentTime: time) } - cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(cubic.availableCongestionWindow, 13200) } @@ -400,13 +426,13 @@ final class CubicTests: XCTestCase { XCTAssertTrue(dataTransferSnapshot.transportCongestionWindow > 0) XCTAssertTrue(dataTransferSnapshot.transportSlowStartThreshold > 0) let existingCongestionWindow = dataTransferSnapshot.transportCongestionWindow - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase cubic.packetSent(bytesSent: 1000) cubic.packetSent(bytesSent: 1000) cubic.ackBegin() cubic.packetsAcked(bytesAcked: 1000, sentTime: time) cubic.packetsAcked(bytesAcked: 1000, sentTime: time) - cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + cubic.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) cubic.filloutDataTransferSnapshot(dataTransferSnapshot: &dataTransferSnapshot) XCTAssertEqual( diff --git a/Tests/QUICTests/LedbatTests.swift b/Tests/QUICTests/LedbatTests.swift index 2fe3d6e1..1c4ad6d3 100644 --- a/Tests/QUICTests/LedbatTests.swift +++ b/Tests/QUICTests/LedbatTests.swift @@ -55,11 +55,11 @@ final class LedbatTests: XCTestCase { rtt.adjustedRTT = .milliseconds(120) rtt.smoothedRTT = .milliseconds(100) - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase ledbat.packetSent(bytesSent: 1000) ledbat.ackBegin() ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(ledbat.availableCongestionWindow, 2900) ledbat.reset(mss: Constants.initialMSS) XCTAssertEqual(ledbat.availableCongestionWindow, defaultCongestionWindow) @@ -71,7 +71,7 @@ final class LedbatTests: XCTestCase { rtt.adjustedRTT = .milliseconds(120) rtt.smoothedRTT = .milliseconds(100) - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase /* Send to increase cwnd */ ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) @@ -82,7 +82,7 @@ final class LedbatTests: XCTestCase { ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(ledbat.availableCongestionWindow, 4400) /* Send a packet and declare them lost */ ledbat.packetSent(bytesSent: 1000) @@ -90,7 +90,8 @@ final class LedbatTests: XCTestCase { bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) XCTAssertEqual(ledbat.availableCongestionWindow, 2400) /* See if we can send another packet */ @@ -104,7 +105,7 @@ final class LedbatTests: XCTestCase { rtt.smoothedRTT = .milliseconds(100) /* Send some packets to increase cwnd */ - var time = NetworkClock.Instant.now + var time = NetworkClock.Instant.testBase ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) @@ -116,7 +117,7 @@ final class LedbatTests: XCTestCase { ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(ledbat.availableCongestionWindow, 4900) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) @@ -126,26 +127,27 @@ final class LedbatTests: XCTestCase { bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) XCTAssertEqual(ledbat.availableCongestionWindow, 2450) /* Additive increase during CA */ - time = NetworkClock.Instant.now.advanced(by: .microseconds(100)) + time = NetworkClock.Instant.testBase.advanced(by: .microseconds(100)) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) ledbat.ackBegin() ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(ledbat.availableCongestionWindow, 2939) /* Mulitplicative decrease during CA */ /* Current RTT = 180ms */ rtt.adjustedRTT = .milliseconds(180) - time = NetworkClock.Instant.now.advanced(by: .microseconds(100)) + time = NetworkClock.Instant.testBase.advanced(by: .microseconds(100)) ledbat.packetSent(bytesSent: 1000) ledbat.ackBegin() ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(ledbat.availableCongestionWindow, 2606) } @@ -156,7 +158,7 @@ final class LedbatTests: XCTestCase { XCTAssertEqual(ledbat.availableCongestionWindow, defaultCongestionWindow) /* Lets increase the window first to go higher than MIN_CWND */ - var time = NetworkClock.Instant.now + var time = NetworkClock.Instant.testBase ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) @@ -170,10 +172,10 @@ final class LedbatTests: XCTestCase { ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(ledbat.availableCongestionWindow, 5400) - time = NetworkClock.Instant.now.advanced(by: .microseconds(100)) + time = NetworkClock.Instant.testBase.advanced(by: .microseconds(100)) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) @@ -194,16 +196,17 @@ final class LedbatTests: XCTestCase { largestAckedPN: 5, largestAckedSentTime: time, mss: mss, - smoothedRTT: rtt.smoothedRTT + smoothedRTT: rtt.smoothedRTT, + now: time ) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(ledbat.availableCongestionWindow, 2700) - time = NetworkClock.Instant.now.advanced(by: .microseconds(200)) + time = NetworkClock.Instant.testBase.advanced(by: .microseconds(200)) ledbat.packetSent(bytesSent: 1000) ledbat.ackBegin() ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) /* cwnd grows during congestion avoidance */ XCTAssertEqual(ledbat.availableCongestionWindow, 2922) } @@ -214,7 +217,7 @@ final class LedbatTests: XCTestCase { rtt.smoothedRTT = .milliseconds(100) XCTAssertEqual(ledbat.availableCongestionWindow, defaultCongestionWindow) /* Lets increase the window first to go higher than MIN_CWND */ - var time = NetworkClock.Instant.now + var time = NetworkClock.Instant.testBase ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) @@ -228,11 +231,11 @@ final class LedbatTests: XCTestCase { ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(ledbat.availableCongestionWindow, 5400) /* Test that CE counts will reduce cwnd, enter CWR and after that we don't decrease cwnd for 1RTT even we receive new CE counts */ - time = NetworkClock.Instant.now.advanced(by: .microseconds(100)) + time = NetworkClock.Instant.testBase.advanced(by: .microseconds(100)) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) @@ -251,9 +254,10 @@ final class LedbatTests: XCTestCase { largestAckedPN: 3, largestAckedSentTime: time, mss: mss, - smoothedRTT: rtt.smoothedRTT + smoothedRTT: rtt.smoothedRTT, + now: time ) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) /* allowed cwnd = cwnd - bytes_in_flight = 2700 - 2000 = 700 */ XCTAssertEqual(ledbat.availableCongestionWindow, 700) @@ -268,9 +272,10 @@ final class LedbatTests: XCTestCase { largestAckedPN: 5, largestAckedSentTime: time, mss: mss, - smoothedRTT: rtt.smoothedRTT + smoothedRTT: rtt.smoothedRTT, + now: time ) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) /* cwnd is same 2700, bytes in flight has reduced to 0 */ XCTAssertEqual(ledbat.availableCongestionWindow, 2700) } @@ -280,7 +285,7 @@ final class LedbatTests: XCTestCase { rtt.adjustedRTT = .milliseconds(120) rtt.smoothedRTT = .milliseconds(100) /* "Send" some packets and declare one of them lost */ - var time = NetworkClock.Instant.now + var time = NetworkClock.Instant.testBase ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) @@ -297,15 +302,16 @@ final class LedbatTests: XCTestCase { bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: true) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: true, now: time) XCTAssertEqual(ledbat.availableCongestionWindow, 2400) - time = NetworkClock.Instant.now.advanced(by: .microseconds(100)) + time = NetworkClock.Instant.testBase.advanced(by: .microseconds(100)) ledbat.packetSent(bytesSent: 1000) ledbat.ackBegin() ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(ledbat.availableCongestionWindow, 2650) } @@ -314,7 +320,7 @@ final class LedbatTests: XCTestCase { /* SRTT = 100ms, base RTT = 100ms network RTT = 120ms */ rtt.adjustedRTT = .milliseconds(120) rtt.smoothedRTT = .milliseconds(100) - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) @@ -328,7 +334,7 @@ final class LedbatTests: XCTestCase { ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(ledbat.availableCongestionWindow, 5400) ledbat.idleTimeout(mss: mss) XCTAssertEqual(ledbat.availableCongestionWindow, defaultCongestionWindow) @@ -350,7 +356,7 @@ final class LedbatTests: XCTestCase { /* SRTT = 100ms, base RTT = 100ms network RTT = 120ms */ rtt.adjustedRTT = .milliseconds(120) rtt.smoothedRTT = .milliseconds(100) - var time = NetworkClock.Instant.now + var time = NetworkClock.Instant.testBase ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) @@ -367,10 +373,11 @@ final class LedbatTests: XCTestCase { bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) XCTAssertEqual(ledbat.availableCongestionWindow, 2400) - time = NetworkClock.Instant.now.advanced(by: .microseconds(1000)) + time = NetworkClock.Instant.testBase.advanced(by: .microseconds(1000)) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) @@ -379,40 +386,46 @@ final class LedbatTests: XCTestCase { bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) ledbat.packetLost( bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) ledbat.packetLost( bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) ledbat.packetLost( bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) - time = NetworkClock.Instant.now.advanced(by: .microseconds(2000)) + time = NetworkClock.Instant.testBase.advanced(by: .microseconds(2000)) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) ledbat.packetLost( bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) ledbat.packetLost( bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) XCTAssertEqual(ledbat.availableCongestionWindow, 2400) XCTAssertFalse(ledbat.canSend(packetLength: 3000)) @@ -428,18 +441,19 @@ final class LedbatTests: XCTestCase { /* SRTT = 100ms, base RTT = 100ms network RTT = 120ms */ rtt.adjustedRTT = .milliseconds(120) rtt.smoothedRTT = .milliseconds(100) - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase ledbat.packetSent(bytesSent: 1000) ledbat.ackBegin() ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(ledbat.availableCongestionWindow, 2900) ledbat.packetSent(bytesSent: 1000) ledbat.packetLost( bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) ledbat.spuriousRetransmit() XCTAssertEqual(ledbat.availableCongestionWindow, 2900) @@ -450,7 +464,7 @@ final class LedbatTests: XCTestCase { /* SRTT = 100ms, base RTT = 100ms network RTT = 120ms */ rtt.adjustedRTT = .milliseconds(120) rtt.smoothedRTT = .milliseconds(100) - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) ledbat.packetSent(bytesSent: 1000) @@ -461,9 +475,10 @@ final class LedbatTests: XCTestCase { bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: true) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: true, now: time) XCTAssertEqual(ledbat.availableCongestionWindow, 2400) ledbat.idleTimeout(mss: mss) XCTAssertEqual(ledbat.availableCongestionWindow, 2400) @@ -472,7 +487,7 @@ final class LedbatTests: XCTestCase { ledbat.ackBegin() ledbat.packetsAcked(bytesAcked: 1200, sentTime: time) ledbat.packetsAcked(bytesAcked: 1200, sentTime: time) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) /* Enter CA */ XCTAssertEqual(ledbat.availableCongestionWindow, 3000) for _ in 0..<3 { @@ -482,7 +497,7 @@ final class LedbatTests: XCTestCase { for _ in 0..<3 { ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) } - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(ledbat.availableCongestionWindow, 3600) } @@ -497,11 +512,11 @@ final class LedbatTests: XCTestCase { XCTAssertTrue(dataTransferSnapshot.transportSlowStartThreshold > 0) rtt.adjustedRTT = .milliseconds(120) rtt.smoothedRTT = .milliseconds(100) - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase ledbat.packetSent(bytesSent: 1000) ledbat.ackBegin() ledbat.packetsAcked(bytesAcked: 1000, sentTime: time) - ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + ledbat.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) ledbat.filloutDataTransferSnapshot(dataTransferSnapshot: &dataTransferSnapshot) XCTAssertEqual(dataTransferSnapshot.transportCongestionWindow, 2900) diff --git a/Tests/QUICTests/PragueTests.swift b/Tests/QUICTests/PragueTests.swift index f8523e80..cc91ba3c 100644 --- a/Tests/QUICTests/PragueTests.swift +++ b/Tests/QUICTests/PragueTests.swift @@ -52,11 +52,11 @@ final class PragueTests: XCTestCase { func testPragueReset() { XCTAssertEqual(prague.availableCongestionWindow, defaultCongestionWindow) - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase prague.packetSent(bytesSent: 1000) prague.ackBegin() prague.packetsAcked(bytesAcked: 1000, sentTime: time) - prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(prague.availableCongestionWindow, 13000) prague.reset(mss: Constants.initialMSS) XCTAssertEqual(prague.availableCongestionWindow, defaultCongestionWindow) @@ -65,13 +65,14 @@ final class PragueTests: XCTestCase { func testPragueLostPackets() { XCTAssertEqual(prague.availableCongestionWindow, defaultCongestionWindow) /* "Send" some packets and declare them lost */ - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase prague.packetSent(bytesSent: 1000) prague.packetLost( bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) XCTAssertEqual(prague.availableCongestionWindow, 8400) /* See if we can send another packet */ @@ -81,7 +82,7 @@ final class PragueTests: XCTestCase { func testPragueSlowStart() { rtt.smoothedRTT = .microseconds(10) /* "Send" some packets and declare one of them lost */ - var time = NetworkClock.Instant.now + var time = NetworkClock.Instant.testBase prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) @@ -94,20 +95,21 @@ final class PragueTests: XCTestCase { prague.packetsAcked(bytesAcked: 1000, sentTime: time) prague.packetsAcked(bytesAcked: 1000, sentTime: time) prague.packetsAcked(bytesAcked: 1000, sentTime: time) - prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) prague.packetLost( bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) XCTAssertEqual(prague.availableCongestionWindow, 11900) /* Make sure that another successful packet doesn't cause us to continue slow start */ - time = NetworkClock.Instant.now.advanced(by: .microseconds(100)) + time = NetworkClock.Instant.testBase.advanced(by: .microseconds(100)) prague.packetSent(bytesSent: 1000) prague.ackBegin() prague.packetsAcked(bytesAcked: 1000, sentTime: time) - prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(prague.availableCongestionWindow, 11953) } @@ -115,7 +117,7 @@ final class PragueTests: XCTestCase { rtt.smoothedRTT = .milliseconds(15) XCTAssertEqual(prague.availableCongestionWindow, defaultCongestionWindow) /* Test that CE counts will reduce the congestion window immediately and move Prague to Congestion avoidance */ - var time = NetworkClock.Instant.now + var time = NetworkClock.Instant.testBase prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) @@ -136,17 +138,18 @@ final class PragueTests: XCTestCase { largestAckedPN: 5, largestAckedSentTime: time, mss: mss, - smoothedRTT: rtt.smoothedRTT + smoothedRTT: rtt.smoothedRTT, + now: time ) - prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) // cwnd after reduction = 6313 and after AI increase for 5 unmarked packets = 6826 XCTAssertEqual(prague.availableCongestionWindow, 6826) - time = NetworkClock.Instant.now.advanced(by: .microseconds(100)) + time = NetworkClock.Instant.testBase.advanced(by: .microseconds(100)) prague.packetSent(bytesSent: 1000) prague.ackBegin() prague.packetsAcked(bytesAcked: 1000, sentTime: time) - prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) /* congestion window grows during congestion avoidance */ XCTAssertEqual(prague.availableCongestionWindow, 6924) } @@ -155,7 +158,7 @@ final class PragueTests: XCTestCase { rtt.smoothedRTT = .milliseconds(15) XCTAssertEqual(prague.availableCongestionWindow, defaultCongestionWindow) /* Test that CE counts will reduce congestion window, enter CWR and after that we don't decrease congestion window for 1RTT even we receive new CE counts */ - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) @@ -174,9 +177,10 @@ final class PragueTests: XCTestCase { largestAckedPN: 3, largestAckedSentTime: time, mss: mss, - smoothedRTT: rtt.smoothedRTT + smoothedRTT: rtt.smoothedRTT, + now: time ) - prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) // cwnd after decrease = 6282, after AI increase = 6582 // allowed cwnd = cwnd - bytes_in_flight = 6582 - 2000 = 4582 XCTAssertEqual(prague.availableCongestionWindow, 4582) @@ -191,9 +195,10 @@ final class PragueTests: XCTestCase { largestAckedPN: 5, largestAckedSentTime: time, mss: mss, - smoothedRTT: rtt.smoothedRTT + smoothedRTT: rtt.smoothedRTT, + now: time ) - prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) // cwnd is 6664 after AI for 1 unmarked packet XCTAssertEqual(prague.availableCongestionWindow, 6664) } @@ -201,7 +206,7 @@ final class PragueTests: XCTestCase { func testPragueAckDuringRecovery() { rtt.smoothedRTT = .microseconds(10) /* "Send" some packets and declare one of them lost */ - var time = NetworkClock.Instant.now + var time = NetworkClock.Instant.testBase prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) @@ -218,21 +223,22 @@ final class PragueTests: XCTestCase { bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) - prague.ackEnd(rtt: rtt, mss: mss, packetsLost: true) + prague.ackEnd(rtt: rtt, mss: mss, packetsLost: true, now: time) XCTAssertEqual(prague.availableCongestionWindow, 8400) - time = NetworkClock.Instant.now.advanced(by: .microseconds(100)) + time = NetworkClock.Instant.testBase.advanced(by: .microseconds(100)) prague.packetSent(bytesSent: 1000) prague.ackBegin() prague.packetsAcked(bytesAcked: 1000, sentTime: time) - prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(prague.availableCongestionWindow, 8475) } func testPragueIdleTimeout() { XCTAssertEqual(prague.availableCongestionWindow, defaultCongestionWindow) - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) @@ -246,7 +252,7 @@ final class PragueTests: XCTestCase { prague.packetsAcked(bytesAcked: 1000, sentTime: time) prague.packetsAcked(bytesAcked: 1000, sentTime: time) prague.packetsAcked(bytesAcked: 1000, sentTime: time) - prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(prague.availableCongestionWindow, 18000) prague.idleTimeout(mss: mss) XCTAssertEqual(prague.availableCongestionWindow, defaultCongestionWindow) @@ -262,7 +268,13 @@ final class PragueTests: XCTestCase { } func testPragueCongestionLimited() { - var time = NetworkClock.Instant.now + // See `CubicTests.testCubicCongestionLimited`: `sentTime` is when the packets went + // out, `detectedAt` when their loss was noticed. The old test pushed the send times + // ahead of the system clock, so every loss re-entered recovery. RFC 9002 + // Section 7.3.2 allows one reduction per recovery period; three rounds of sending + // give three reductions. + var sentTime = NetworkClock.Instant.testBase + var detectedAt = sentTime.advanced(by: .microseconds(100)) prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) @@ -270,65 +282,75 @@ final class PragueTests: XCTestCase { prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) prague.ackBegin() - prague.packetsAcked(bytesAcked: 1000, sentTime: time) - prague.packetsAcked(bytesAcked: 1000, sentTime: time) - prague.packetsAcked(bytesAcked: 1000, sentTime: time) - prague.packetsAcked(bytesAcked: 1000, sentTime: time) - prague.packetsAcked(bytesAcked: 1000, sentTime: time) + prague.packetsAcked(bytesAcked: 1000, sentTime: sentTime) + prague.packetsAcked(bytesAcked: 1000, sentTime: sentTime) + prague.packetsAcked(bytesAcked: 1000, sentTime: sentTime) + prague.packetsAcked(bytesAcked: 1000, sentTime: sentTime) + prague.packetsAcked(bytesAcked: 1000, sentTime: sentTime) prague.packetLost( bytesLost: 1000, - largestLostSentTime: time, + largestLostSentTime: sentTime, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: detectedAt ) XCTAssertEqual(prague.availableCongestionWindow, 8400) - time = NetworkClock.Instant.now.advanced(by: .microseconds(1000)) + sentTime = sentTime.advanced(by: .microseconds(1000)) + detectedAt = sentTime.advanced(by: .microseconds(100)) prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) prague.packetLost( bytesLost: 1000, - largestLostSentTime: time, + largestLostSentTime: sentTime, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: detectedAt ) prague.packetLost( bytesLost: 1000, - largestLostSentTime: time, + largestLostSentTime: sentTime, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: detectedAt ) prague.packetLost( bytesLost: 1000, - largestLostSentTime: time, + largestLostSentTime: sentTime, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: detectedAt ) prague.packetLost( bytesLost: 1000, - largestLostSentTime: time, + largestLostSentTime: sentTime, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: detectedAt ) - time = NetworkClock.Instant.now.advanced(by: .microseconds(2000)) + sentTime = sentTime.advanced(by: .microseconds(1000)) + detectedAt = sentTime.advanced(by: .microseconds(100)) prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) prague.packetLost( bytesLost: 1000, - largestLostSentTime: time, + largestLostSentTime: sentTime, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: detectedAt ) prague.packetLost( bytesLost: 1000, - largestLostSentTime: time, + largestLostSentTime: sentTime, mss: mss, - smoothedRTT: .microseconds(0) - ) - XCTAssert( - (prague.availableCongestionWindow >= 2400) && (prague.availableCongestionWindow < 3000) + smoothedRTT: .microseconds(0), + now: detectedAt ) + // One reduction per round, three rounds: 12000 -> 8400 -> 5880 -> 4116, each step + // `UInt64(Double(window) * Prague.beta)`. Written out rather than as `pow(beta, 3)`, which + // is 0.34299999999999997 and truncates to 4115; the reductions compound one at a time. + XCTAssertEqual(prague.availableCongestionWindow, 4116) XCTAssertFalse(prague.canSend(packetLength: 10000)) } @@ -339,7 +361,7 @@ final class PragueTests: XCTestCase { } func testPragueSpuriousRetransmit() { - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) @@ -348,7 +370,8 @@ final class PragueTests: XCTestCase { bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) prague.spuriousRetransmit() XCTAssertEqual(prague.availableCongestionWindow, 9000) @@ -356,7 +379,7 @@ final class PragueTests: XCTestCase { /* Tests that we can enter CA without any loss after idle period */ func testPragueCongestionAvoidance() { - var time = NetworkClock.Instant.now + var time = NetworkClock.Instant.testBase prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) @@ -367,13 +390,14 @@ final class PragueTests: XCTestCase { bytesLost: 1000, largestLostSentTime: time, mss: mss, - smoothedRTT: .microseconds(0) + smoothedRTT: .microseconds(0), + now: time ) - prague.ackEnd(rtt: rtt, mss: mss, packetsLost: true) + prague.ackEnd(rtt: rtt, mss: mss, packetsLost: true, now: time) XCTAssertEqual(prague.availableCongestionWindow, 8400) prague.idleTimeout(mss: mss) XCTAssertEqual(prague.availableCongestionWindow, 8400) - time = NetworkClock.Instant.now + time = NetworkClock.Instant.testBase prague.packetSent(bytesSent: 1200) prague.packetSent(bytesSent: 1200) prague.packetSent(bytesSent: 1200) @@ -381,7 +405,7 @@ final class PragueTests: XCTestCase { prague.packetsAcked(bytesAcked: 1200, sentTime: time) prague.packetsAcked(bytesAcked: 1200, sentTime: time) prague.packetsAcked(bytesAcked: 1200, sentTime: time) - prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) /* Enter CA */ XCTAssertEqual(prague.availableCongestionWindow, 12000) for _ in 0..<12 { @@ -391,7 +415,7 @@ final class PragueTests: XCTestCase { for _ in 0..<12 { prague.packetsAcked(bytesAcked: 1000, sentTime: time) } - prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) XCTAssertEqual(prague.availableCongestionWindow, 13200) } @@ -404,13 +428,13 @@ final class PragueTests: XCTestCase { XCTAssertTrue(dataTransferSnapshot.transportCongestionWindow > 0) XCTAssertTrue(dataTransferSnapshot.transportSlowStartThreshold > 0) let existingCongestionWindow = dataTransferSnapshot.transportCongestionWindow - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase prague.packetSent(bytesSent: 1000) prague.packetSent(bytesSent: 1000) prague.ackBegin() prague.packetsAcked(bytesAcked: 1000, sentTime: time) prague.packetsAcked(bytesAcked: 1000, sentTime: time) - prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false) + prague.ackEnd(rtt: rtt, mss: mss, packetsLost: false, now: time) prague.filloutDataTransferSnapshot(dataTransferSnapshot: &dataTransferSnapshot) XCTAssertEqual( @@ -433,7 +457,7 @@ final class PragueTests: XCTestCase { XCTAssertEqual(path.pacer.burstSize, 10000) XCTAssertEqual(path.congestionControlWindow, 12000) - let time = NetworkClock.Instant.now + let time = NetworkClock.Instant.testBase for _ in 0..<10 { path.congestionControlPacketsSent(bytesSent: 1000) } diff --git a/Tests/QUICTests/QUICTestClock.swift b/Tests/QUICTests/QUICTestClock.swift new file mode 100644 index 00000000..fc0f578a --- /dev/null +++ b/Tests/QUICTests/QUICTestClock.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift open source project +// +// Copyright (c) 2026 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of Swift project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +#if !NETWORK_NO_SWIFT_QUIC + +#if canImport(SwiftNetwork) +@_spi(Essentials) @_spi(ProtocolProvider) @testable import SwiftNetwork +#elseif canImport(Network) +@_spi(Essentials) @_spi(ProtocolProvider) @testable import Network +#endif + +@available(Network 0.1.0, *) +extension NetworkClock.Instant { + /// A fixed instant to seed tests that need one. + /// + /// The congestion controllers only order instants and measure durations between + /// them, so the absolute value is arbitrary, but it has to be *fixed*. Seeding from + /// the real clock makes every duration depend on how long the test itself took to run. + /// + /// Non-zero because much of the stack treats `.zero` as "unset". + static var testBase: NetworkClock.Instant { + NetworkClock.Instant(milliseconds: 1000) + } +} + +/// Test-only conveniences that supply a fixed time. +/// +/// The production signatures deliberately require a time, so that datapath code cannot +/// silently reach for the real clock. Most ACK tests are about packet-number bookkeeping +/// and not about time at all, so restating `now:` several hundred times would be noise. +/// The default here is a *fixed* instant, so those tests stay deterministic. A test +/// whose premise is timing calls the full form and passes its own instant. +@available(Network 0.1.0, *) +extension Ack { + func append(packetNumberSpace: PacketNumberSpace, packetNumber: PacketNumber) { + self.append( + packetNumberSpace: packetNumberSpace, + packetNumber: packetNumber, + now: .testBase + ) + } + + func buildForTesting( + for packetNumberSpace: PacketNumberSpace, + setAckFrame: (PacketNumberSpace, consuming QUICFrame, Bool) -> Void, + ecnCounter: ECNCounter? = nil + ) -> Int { + self.buildForTesting( + for: packetNumberSpace, + setAckFrame: setAckFrame, + ecnCounter: ecnCounter, + now: .testBase + ) + } +} +#endif From a8d0b2222dc4678693898fb0265c08c5d152585c Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Mon, 24 Aug 2026 16:00:24 -0400 Subject: [PATCH 2/6] `QLog` events carry the timestamp of the event The `timestamp: NetworkClock.Instant = .now` defaults stamped an entry when it was written rather than when the thing it describes happened, so entries could be ordered differently from the events. The defaults are gone from the methods whose callers hold an instant, and `Recovery` and `QUICConnection` pass theirs. `congestionControlUpdated` and `logCongestionStateUpdated` keep theirs. Their callers do not hold an instant, and the ordering is not worth threading one down to them; what these two want is a clock to default from rather than an argument. --- Sources/SwiftNetwork/QUIC/QLog.swift | 24 ++++++++++--------- .../SwiftNetwork/QUIC/QUICConnection.swift | 10 ++++---- Sources/SwiftNetwork/QUIC/Recovery.swift | 10 ++++---- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/Sources/SwiftNetwork/QUIC/QLog.swift b/Sources/SwiftNetwork/QUIC/QLog.swift index 1b25cba2..f07f7c94 100644 --- a/Sources/SwiftNetwork/QUIC/QLog.swift +++ b/Sources/SwiftNetwork/QUIC/QLog.swift @@ -633,7 +633,7 @@ final class QLog { } } - func packetSent(_ packet: borrowing Packet, timestamp: NetworkClock.Instant = .now) { + func packetSent(_ packet: borrowing Packet, timestamp: NetworkClock.Instant) { let packetType = PacketType(packet: packet) let packetHeader = PacketHeader(packet: packet) let frameList = EventFrames(packet: packet) @@ -651,7 +651,7 @@ final class QLog { func packetReceived( _ packet: borrowing Packet, coalesced isCoalesced: Bool, - timestamp: NetworkClock.Instant = .now + timestamp: NetworkClock.Instant ) { let packetEvent = EventPacket( packetType: PacketType(packet: packet), @@ -667,7 +667,7 @@ final class QLog { func packetLost( _ packet: borrowing SentPacketRecord, trigger: QLogPacketLostTrigger?, - timestamp: NetworkClock.Instant = .now + timestamp: NetworkClock.Instant ) { let packetEvent = EventPacket( packetType: PacketType(packet: packet), @@ -691,7 +691,7 @@ final class QLog { slowStartThresh: UInt64 = UInt64.max, packetsInFlight: UInt64 = UInt64.max, inRecovery: Bool?, - timestamp: NetworkClock.Instant = .now + timestamp: NetworkClock.Instant ) { let metricEvent = EventMetrics( minRTT: minRTT, @@ -712,7 +712,7 @@ final class QLog { public func recoveryUpdated( ptoCount: UInt64, inRecovery: Bool?, - timestamp: NetworkClock.Instant = .now + timestamp: NetworkClock.Instant ) { metricsUpdated( minRTT: .zero, @@ -756,7 +756,7 @@ final class QLog { smoothedRTT: NetworkDuration, latestRTT: NetworkDuration, rttVariance: NetworkDuration, - timestamp: NetworkClock.Instant = .now + timestamp: NetworkClock.Instant ) { metricsUpdated( minRTT: minRTT, @@ -778,7 +778,7 @@ final class QLog { oldStreamState: QLogStreamState, newStreamState: QLogStreamState, streamSide: QLogStreamSide?, - timestamp: NetworkClock.Instant = .now + timestamp: NetworkClock.Instant ) { if let streamID = stream.streamID, let streamType = stream.streamType { let streamEvent = StreamEvent( @@ -819,7 +819,7 @@ final class QLog { owner: QLogOwner?, oldStreamType: QUICStreamType, newStreamType: QUICStreamType, - timestamp: NetworkClock.Instant = .now + timestamp: NetworkClock.Instant ) { let streamTypeSetEvent = StreamTypeSetEvent( streamID: streamID, @@ -855,7 +855,7 @@ final class QLog { initialMaxStreamsBidirectional: Int?, initialMaxStreamsUnidirectional: Int?, preferredAddress: PreferredAddress?, - timestamp: NetworkClock.Instant = .now + timestamp: NetworkClock.Instant ) { let parametersEvent = EventParametersSet( owner: owner, @@ -885,7 +885,8 @@ final class QLog { public func parametersSet( owner: QLogOwner?, - transportParameters: TransportParameters + transportParameters: TransportParameters, + timestamp: NetworkClock.Instant ) { parametersSet( owner: owner, @@ -923,7 +924,8 @@ final class QLog { TransportParameterTypes.initialMaxStreamsUnidirectional ]?.value, preferredAddress: transportParameters[TransportParameterTypes.preferredAddress]? - .preferredAddress + .preferredAddress, + timestamp: timestamp ) } diff --git a/Sources/SwiftNetwork/QUIC/QUICConnection.swift b/Sources/SwiftNetwork/QUIC/QUICConnection.swift index deca15e9..532833e7 100644 --- a/Sources/SwiftNetwork/QUIC/QUICConnection.swift +++ b/Sources/SwiftNetwork/QUIC/QUICConnection.swift @@ -1135,7 +1135,8 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, if let qLog { qLog.parametersSet( owner: owner, - transportParameters: transportParameters + transportParameters: transportParameters, + timestamp: self.now ) } #endif @@ -3763,9 +3764,9 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, #if QlogOutput if let qLog { if outbound { - qLog.packetSent(packet) + qLog.packetSent(packet, timestamp: self.now) } else { - qLog.packetReceived(packet, coalesced: coalesced) + qLog.packetReceived(packet, coalesced: coalesced, timestamp: self.now) } } #endif @@ -4294,7 +4295,8 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, if let qLog { qLog.recoveryUpdated( ptoCount: 0, - inRecovery: nil + inRecovery: nil, + timestamp: self.now ) } #endif diff --git a/Sources/SwiftNetwork/QUIC/Recovery.swift b/Sources/SwiftNetwork/QUIC/Recovery.swift index e7879cca..9e3c7239 100644 --- a/Sources/SwiftNetwork/QUIC/Recovery.swift +++ b/Sources/SwiftNetwork/QUIC/Recovery.swift @@ -833,7 +833,7 @@ struct Recovery: ~Copyable, PrefixedLoggable, NonCopyableTimerUser { static func logAckElicitingPacketsInFlight(packetCount: Int, connection: QUICConnection) { #if QlogOutput if let qLog = connection.qLog { - qLog.congestionControlUpdated(packetsInFlight: UInt64(packetCount)) + qLog.congestionControlUpdated(packetsInFlight: UInt64(packetCount), timestamp: connection.now) } #endif } @@ -847,7 +847,8 @@ struct Recovery: ~Copyable, PrefixedLoggable, NonCopyableTimerUser { if let qLog = connection.qLog { qLog.packetLost( packet, - trigger: trigger + trigger: trigger, + timestamp: connection.now ) } #endif @@ -1253,7 +1254,7 @@ struct Recovery: ~Copyable, PrefixedLoggable, NonCopyableTimerUser { qLog.recoveryUpdated( ptoCount: UInt64(path.recoveryState.PTOCount), inRecovery: nil, - timestamp: .now + timestamp: connection.now ) } #endif @@ -1494,7 +1495,8 @@ struct Recovery: ~Copyable, PrefixedLoggable, NonCopyableTimerUser { minRTT: sentPath.rtt.minRTT, smoothedRTT: sentPath.rtt.smoothedRTT, latestRTT: sentPath.rtt.latestRTT, - rttVariance: sentPath.rtt.RTTVariance + rttVariance: sentPath.rtt.RTTVariance, + timestamp: connection.now ) } #endif From 07ed46f753ebd46347af9ecd540393a18b26e174 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Mon, 24 Aug 2026 16:00:43 -0400 Subject: [PATCH 3/6] Timer closures receive the instant the timer fired `Timer.timerFired(timeNow:)` compares each deadline against the instant it was given, but called its closures with no argument, so every closure read the clock again. The ACK, recovery and PMTUD handlers therefore ran against an instant strictly later than the one that decided they were due. `TimerEntry.closure` now takes a `NetworkClock.Instant` and `timerFired` passes `timeNow` rather than the threshold-slackened value it compares against. The migration, keepalive, idle and draining handlers ignore it, since none of them measures against a deadline. --- Sources/SwiftNetwork/QUIC/PMTUD.swift | 4 +-- .../SwiftNetwork/QUIC/QUICConnection.swift | 16 +++++----- Sources/SwiftNetwork/QUIC/Timer.swift | 8 ++--- Tests/QUICTests/TimerTests.swift | 29 +++++++++---------- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/Sources/SwiftNetwork/QUIC/PMTUD.swift b/Sources/SwiftNetwork/QUIC/PMTUD.swift index 23c72cb8..74982b21 100644 --- a/Sources/SwiftNetwork/QUIC/PMTUD.swift +++ b/Sources/SwiftNetwork/QUIC/PMTUD.swift @@ -131,10 +131,10 @@ struct PMTUDState: ~Copyable { if timerID == nil { let pathID = path.identifier - timerID = connection.timer.insert(description: "PMTUD", timerNow: connection.now) { + timerID = connection.timer.insert(description: "PMTUD", timerNow: connection.now) { firedAt in let innerPath = connection.path(for: pathID) guard let innerPath else { return } - innerPath.pmtudState.timerFired(timeNow: connection.now, path: innerPath) + innerPath.pmtudState.timerFired(timeNow: firedAt, path: innerPath) } } diff --git a/Sources/SwiftNetwork/QUIC/QUICConnection.swift b/Sources/SwiftNetwork/QUIC/QUICConnection.swift index 532833e7..1a67b6f4 100644 --- a/Sources/SwiftNetwork/QUIC/QUICConnection.swift +++ b/Sources/SwiftNetwork/QUIC/QUICConnection.swift @@ -447,13 +447,13 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, self.setMetadataHandlers() self.timer = Timer(reference: self.reference, timerReference: timerReference, logPrefixer: logPrefixer) - let ackTimerID = timer.insert(description: "ACK", timerNow: self.now) { - self.ack.timerFired(timeNow: self.now) + let ackTimerID = timer.insert(description: "ACK", timerNow: self.now) { firedAt in + self.ack.timerFired(timeNow: firedAt) } self.ack = Ack(connection: self, timerID: ackTimerID, logPrefixer: logPrefixer) - let recoveryTimerID = timer.insert(description: "Recovery", timerNow: self.now) { - self.recovery.timerFired(timeNow: self.now) + let recoveryTimerID = timer.insert(description: "Recovery", timerNow: self.now) { firedAt in + self.recovery.timerFired(timeNow: firedAt) } self.recovery = Recovery( connection: self, @@ -461,7 +461,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, logPrefixer: logPrefixer ) - migration.timerID = timer.insert(description: "Migration", timerNow: self.now) { + migration.timerID = timer.insert(description: "Migration", timerNow: self.now) { _ in self.migration.timerFired(connection: self) } @@ -1369,7 +1369,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, description: "Idle timeout", fromNow: idleTimeout, timerNow: self.now - ) { + ) { _ in self.idleTimeoutFired() } @@ -2757,7 +2757,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, description: "draining", fromNow: path.recoveryState.getMaxPTODrainTime(idleTimeout: self.idleTimeout), timerNow: self.now - ) { + ) { _ in self.drain() } } @@ -2877,7 +2877,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, minIdleTime = .milliseconds(min(idleTimeoutLocal, idleTimeoutRemote)) } if keepaliveTimerID == nil { - keepaliveTimerID = timer.insert(description: "keepalive", timerNow: self.now) { + keepaliveTimerID = timer.insert(description: "keepalive", timerNow: self.now) { _ in self.keepaliveHandler() } } diff --git a/Sources/SwiftNetwork/QUIC/Timer.swift b/Sources/SwiftNetwork/QUIC/Timer.swift index c3493279..9c508289 100644 --- a/Sources/SwiftNetwork/QUIC/Timer.swift +++ b/Sources/SwiftNetwork/QUIC/Timer.swift @@ -46,9 +46,9 @@ private struct TimerEntry: ~Copyable { let identifier: Timer.TimerID var deadline: NetworkClock.Instant = .zero let description: String - let closure: () -> Void + let closure: (NetworkClock.Instant) -> Void - init(identifier: Timer.TimerID, description: String, closure: @escaping () -> Void) { + init(identifier: Timer.TimerID, description: String, closure: @escaping (NetworkClock.Instant) -> Void) { self.identifier = identifier self.description = description self.closure = closure @@ -113,7 +113,7 @@ final class Timer: PrefixedLoggable { description: String, fromNow: NetworkDuration = .zero, timerNow: NetworkClock.Instant, - closure: @escaping () -> Void + closure: @escaping (NetworkClock.Instant) -> Void ) -> TimerID { let identifier = nextID var entry = TimerEntry(identifier: nextID, description: description, closure: closure) @@ -289,7 +289,7 @@ final class Timer: PrefixedLoggable { ) } entries[index].disable() - entries[index].closure() + entries[index].closure(timeNow) ranOne = true } index += 1 diff --git a/Tests/QUICTests/TimerTests.swift b/Tests/QUICTests/TimerTests.swift index 0f244e1b..de3010c1 100644 --- a/Tests/QUICTests/TimerTests.swift +++ b/Tests/QUICTests/TimerTests.swift @@ -34,7 +34,7 @@ final class TimerTests: XCTestCase { func testOneTimer() { let timer = QUICTimer(timerReference: TimerReference(), logPrefixer: timerTestsLogPrefixer) let semaphore = DispatchSemaphore(value: 0) - let oneId = timer.insert(description: "one", fromNow: .milliseconds(1000), timerNow: .zero) { + let oneId = timer.insert(description: "one", fromNow: .milliseconds(1000), timerNow: .zero) { _ in semaphore.signal() } XCTAssertEqual(timer.nextDeadline, .init(milliseconds: 1000)) @@ -49,7 +49,7 @@ final class TimerTests: XCTestCase { func testReschedule() { let timer = QUICTimer(timerReference: TimerReference(), logPrefixer: timerTestsLogPrefixer) let semaphore = DispatchSemaphore(value: 0) - let oneId = timer.insert(description: "one-reschedule", timerNow: .zero) { + let oneId = timer.insert(description: "one-reschedule", timerNow: .zero) { _ in semaphore.signal() } XCTAssertEqual(timer.nextDeadline, .zero) @@ -66,10 +66,10 @@ final class TimerTests: XCTestCase { func testTwoTimersAtSameTime() throws { let timer = QUICTimer(timerReference: TimerReference(), logPrefixer: timerTestsLogPrefixer) let semaphore = DispatchSemaphore(value: 0) - let oneId = timer.insert(description: "one", fromNow: .milliseconds(1000), timerNow: .zero) { + let oneId = timer.insert(description: "one", fromNow: .milliseconds(1000), timerNow: .zero) { _ in semaphore.signal() } - let twoId = timer.insert(description: "two", fromNow: .milliseconds(1000), timerNow: .zero) { + let twoId = timer.insert(description: "two", fromNow: .milliseconds(1000), timerNow: .zero) { _ in semaphore.signal() } XCTAssertEqual(timer.nextDeadline, .init(milliseconds: 1000)) @@ -90,11 +90,11 @@ final class TimerTests: XCTestCase { func testTwoTimersAtDifferentTimes() throws { let timer = QUICTimer(timerReference: TimerReference(), logPrefixer: timerTestsLogPrefixer) let semaphore = DispatchSemaphore(value: 0) - let oneId = timer.insert(description: "one", fromNow: .milliseconds(2000), timerNow: .zero) { + let oneId = timer.insert(description: "one", fromNow: .milliseconds(2000), timerNow: .zero) { _ in semaphore.signal() } XCTAssertEqual(timer.nextDeadline, .init(milliseconds: 2000)) - let twoId = timer.insert(description: "two", fromNow: .milliseconds(1000), timerNow: .zero) { + let twoId = timer.insert(description: "two", fromNow: .milliseconds(1000), timerNow: .zero) { _ in semaphore.signal() } XCTAssertEqual(timer.nextDeadline, .init(milliseconds: 1000)) @@ -117,12 +117,11 @@ final class TimerTests: XCTestCase { func testRecalculateAfterMissingTimer() throws { let timer = QUICTimer(timerReference: TimerReference(), logPrefixer: timerTestsLogPrefixer) let semaphore = DispatchSemaphore(value: 0) - let oneId = timer.insert(description: "one", fromNow: .milliseconds(1000), timerNow: .zero) { + let oneId = timer.insert(description: "one", fromNow: .milliseconds(1000), timerNow: .zero) { _ in semaphore.signal() } XCTAssertEqual(timer.nextDeadline, .init(milliseconds: 1000)) - let twoId = timer.insert(description: "two", fromNow: .milliseconds(1000), timerNow: .init(milliseconds: 1500)) - { + let twoId = timer.insert(description: "two", fromNow: .milliseconds(1000), timerNow: .init(milliseconds: 1500)) { _ in semaphore.signal() } XCTAssertEqual(timer.nextDeadline, .init(milliseconds: 1500)) @@ -146,7 +145,7 @@ final class TimerTests: XCTestCase { func testRecalculateWithinThreshold() throws { let timer = QUICTimer(timerReference: TimerReference(), logPrefixer: timerTestsLogPrefixer) let semaphore = DispatchSemaphore(value: 0) - let oneId = timer.insert(description: "one", fromNow: .microseconds(1_000_000), timerNow: .zero) { + let oneId = timer.insert(description: "one", fromNow: .microseconds(1_000_000), timerNow: .zero) { _ in semaphore.signal() } XCTAssertEqual(timer.nextDeadline, .init(microseconds: 1_000_000)) @@ -156,7 +155,7 @@ final class TimerTests: XCTestCase { description: "two", fromNow: .microseconds(1_000_000), timerNow: .init(microseconds: 2) - ) { + ) { _ in semaphore.signal() } @@ -185,7 +184,7 @@ final class TimerTests: XCTestCase { let semaphore = DispatchSemaphore(value: 0) // A, in 2s - let idA = timer.insert(description: "A", fromNow: .seconds(2), timerNow: .zero) { + let idA = timer.insert(description: "A", fromNow: .seconds(2), timerNow: .zero) { _ in XCTFail("A was disabled and must not fire") } XCTAssertEqual(timer.nextDeadline, .init(.seconds(2))) @@ -195,7 +194,7 @@ final class TimerTests: XCTestCase { description: "B", fromNow: .seconds(2) + .microseconds(999), timerNow: .zero - ) { + ) { _ in semaphore.signal() } // A was earlier, deadline is unchanged @@ -229,7 +228,7 @@ final class TimerTests: XCTestCase { let timer = QUICTimer(timerReference: TimerReference(), logPrefixer: timerTestsLogPrefixer) let semaphore = DispatchSemaphore(value: 0) - let idA = timer.insert(description: "A", fromNow: .seconds(2), timerNow: .zero) { + let idA = timer.insert(description: "A", fromNow: .seconds(2), timerNow: .zero) { _ in XCTFail("A was cancelled and must not fire") } XCTAssertEqual(timer.nextDeadline, .init(.seconds(2))) @@ -242,7 +241,7 @@ final class TimerTests: XCTestCase { description: "B", fromNow: .seconds(2) + .microseconds(500), timerNow: .zero - ) { + ) { _ in semaphore.signal() } XCTAssertEqual(timer.nextDeadline, .init(.seconds(2) + .microseconds(500))) From c33b6231addaa06a64a2fbc9e7b06511ef430f3a Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Mon, 24 Aug 2026 16:51:40 -0400 Subject: [PATCH 4/6] formatting --- Tests/QUICTests/TimerTests.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Tests/QUICTests/TimerTests.swift b/Tests/QUICTests/TimerTests.swift index de3010c1..9305643b 100644 --- a/Tests/QUICTests/TimerTests.swift +++ b/Tests/QUICTests/TimerTests.swift @@ -121,7 +121,8 @@ final class TimerTests: XCTestCase { semaphore.signal() } XCTAssertEqual(timer.nextDeadline, .init(milliseconds: 1000)) - let twoId = timer.insert(description: "two", fromNow: .milliseconds(1000), timerNow: .init(milliseconds: 1500)) { _ in + let twoId = timer.insert(description: "two", fromNow: .milliseconds(1000), timerNow: .init(milliseconds: 1500)) + { _ in semaphore.signal() } XCTAssertEqual(timer.nextDeadline, .init(milliseconds: 1500)) From f29e5da1728831d1332b8db7101368fa24c75eb9 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Wed, 26 Aug 2026 11:18:05 -0400 Subject: [PATCH 5/6] review comments --- Tests/QUICTests/AckTests.swift | 28 +++++++++++++++++ .../{QUICTestClock.swift => TestClock.swift} | 30 ------------------- 2 files changed, 28 insertions(+), 30 deletions(-) rename Tests/QUICTests/{QUICTestClock.swift => TestClock.swift} (52%) diff --git a/Tests/QUICTests/AckTests.swift b/Tests/QUICTests/AckTests.swift index 819b6f07..c78e760d 100644 --- a/Tests/QUICTests/AckTests.swift +++ b/Tests/QUICTests/AckTests.swift @@ -26,6 +26,13 @@ import XCTest func setAckFrame(_: PacketNumberSpace, _: consuming QUICFrame, _: Bool) { } +/// Test-only conveniences that supply a fixed time. +/// +/// The production signatures deliberately require a time, so that datapath code cannot +/// silently reach for the real clock. Most ACK tests are about packet-number bookkeeping +/// and not about time at all, so restating `now:` several hundred times would be noise. +/// The default here is a *fixed* instant, so those tests stay deterministic. A test +/// whose premise is timing calls the full form and passes its own instant. @available(Network 0.1.0, *) extension Ack { // only required by the test currently @@ -39,6 +46,27 @@ extension Ack { ecnCounter: ecnCounter ) } + + func append(packetNumberSpace: PacketNumberSpace, packetNumber: PacketNumber) { + self.append( + packetNumberSpace: packetNumberSpace, + packetNumber: packetNumber, + now: .testBase + ) + } + + func buildForTesting( + for packetNumberSpace: PacketNumberSpace, + setAckFrame: (PacketNumberSpace, consuming QUICFrame, Bool) -> Void, + ecnCounter: ECNCounter? = nil + ) -> Int { + self.buildForTesting( + for: packetNumberSpace, + setAckFrame: setAckFrame, + ecnCounter: ecnCounter, + now: .testBase + ) + } } @available(Network 0.1.0, *) diff --git a/Tests/QUICTests/QUICTestClock.swift b/Tests/QUICTests/TestClock.swift similarity index 52% rename from Tests/QUICTests/QUICTestClock.swift rename to Tests/QUICTests/TestClock.swift index fc0f578a..a27eba05 100644 --- a/Tests/QUICTests/QUICTestClock.swift +++ b/Tests/QUICTests/TestClock.swift @@ -34,34 +34,4 @@ extension NetworkClock.Instant { } } -/// Test-only conveniences that supply a fixed time. -/// -/// The production signatures deliberately require a time, so that datapath code cannot -/// silently reach for the real clock. Most ACK tests are about packet-number bookkeeping -/// and not about time at all, so restating `now:` several hundred times would be noise. -/// The default here is a *fixed* instant, so those tests stay deterministic. A test -/// whose premise is timing calls the full form and passes its own instant. -@available(Network 0.1.0, *) -extension Ack { - func append(packetNumberSpace: PacketNumberSpace, packetNumber: PacketNumber) { - self.append( - packetNumberSpace: packetNumberSpace, - packetNumber: packetNumber, - now: .testBase - ) - } - - func buildForTesting( - for packetNumberSpace: PacketNumberSpace, - setAckFrame: (PacketNumberSpace, consuming QUICFrame, Bool) -> Void, - ecnCounter: ECNCounter? = nil - ) -> Int { - self.buildForTesting( - for: packetNumberSpace, - setAckFrame: setAckFrame, - ecnCounter: ecnCounter, - now: .testBase - ) - } -} #endif From ef81ae94ae150046035f01199847b1f293d77a77 Mon Sep 17 00:00:00 2001 From: Rick Newton-Rogers Date: Thu, 27 Aug 2026 10:07:02 -0400 Subject: [PATCH 6/6] Mark the `Ack` test conveniences `mutating` `Ack` became a `~Copyable` struct, so the two test-only wrappers over `append` and `buildForTesting` now call mutating members and have to be mutating themselves. `size` alongside them already is. * The wrappers gain `mutating`; the callers already hold `Ack` in a `var`. --- Tests/QUICTests/AckTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/QUICTests/AckTests.swift b/Tests/QUICTests/AckTests.swift index b252fc74..3c1ec92d 100644 --- a/Tests/QUICTests/AckTests.swift +++ b/Tests/QUICTests/AckTests.swift @@ -47,7 +47,7 @@ extension Ack { ) } - func append(packetNumberSpace: PacketNumberSpace, packetNumber: PacketNumber) { + mutating func append(packetNumberSpace: PacketNumberSpace, packetNumber: PacketNumber) { self.append( packetNumberSpace: packetNumberSpace, packetNumber: packetNumber, @@ -55,7 +55,7 @@ extension Ack { ) } - func buildForTesting( + mutating func buildForTesting( for packetNumberSpace: PacketNumberSpace, setAckFrame: (PacketNumberSpace, consuming QUICFrame, Bool) -> Void, ecnCounter: ECNCounter? = nil