diff --git a/Sources/SwiftNetwork/Protocols/IPProtocol.swift b/Sources/SwiftNetwork/Protocols/IPProtocol.swift index c5347cd..6dd586c 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 ad51e2a..de561e7 100644 --- a/Sources/SwiftNetwork/QUIC/Ack.swift +++ b/Sources/SwiftNetwork/QUIC/Ack.swift @@ -551,7 +551,7 @@ struct Ack: ~Copyable, PrefixedLoggable, NonCopyableTimerUser { mutating 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) @@ -590,7 +590,7 @@ struct Ack: ~Copyable, PrefixedLoggable, NonCopyableTimerUser { isAckSet: Bool, setAckFrame: (PacketNumberSpace, consuming QUICFrame, Bool) -> Void, ecnCounter: ECNCounter?, - now: NetworkClock.Instant = .now + now: NetworkClock.Instant ) -> Bool { var shouldSend = false if isAckSet { @@ -1237,7 +1237,8 @@ extension Ack { mutating func buildForTesting( for packetNumberSpace: PacketNumberSpace, setAckFrame: (PacketNumberSpace, consuming QUICFrame, Bool) -> Void, - ecnCounter: ECNCounter? = nil + ecnCounter: ECNCounter? = nil, + now: NetworkClock.Instant ) -> Int { var size = 0 let delayExponent = localDelayExponent @@ -1249,7 +1250,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 99b1932..b8c2829 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 ac3271b..28334cf 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 77fd016..130b166 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 eebbf4d..9af9ea1 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 de3ff7c..e6a84f6 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") { + 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/Prague.swift b/Sources/SwiftNetwork/QUIC/Prague.swift index af4d5ef..02f999a 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/QLog.swift b/Sources/SwiftNetwork/QUIC/QLog.swift index 1b25cba..f07f7c9 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 0418715..dadf12b 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) { firedAt in + self.ack.timerFired(timeNow: firedAt) } 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) { 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") { + migration.timerID = timer.insert(description: "Migration", timerNow: self.now) { _ in self.migration.timerFired(connection: self) } @@ -1135,7 +1135,8 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, if let qLog { qLog.parametersSet( owner: owner, - transportParameters: transportParameters + transportParameters: transportParameters, + timestamp: self.now ) } #endif @@ -1313,7 +1314,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,8 +1367,9 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, idleTimerID = timer.insert( description: "Idle timeout", - fromNow: idleTimeout - ) { + fromNow: idleTimeout, + timerNow: self.now + ) { _ in self.idleTimeoutFired() } @@ -1410,7 +1412,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,8 +2755,9 @@ 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 + ) { _ in self.drain() } } @@ -2834,7 +2837,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 +2877,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) { _ in self.keepaliveHandler() } } @@ -3670,7 +3673,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, let tagSize = protector.getTagSize(for: keyState) if isPacing { - let now = NetworkClock.Instant.now + let now = self.now let lastAckElicitingPacketSentTimestamp = lastAckElicitingPacketSentTimestamp // lastAckElicitingPacketSentTimestamp can be in the future for kernel packet pacing. if now > lastAckElicitingPacketSentTimestamp { @@ -3985,7 +3988,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) @@ -4004,9 +4007,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 @@ -4415,7 +4418,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 @@ -4534,7 +4537,8 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, if let qLog { qLog.recoveryUpdated( ptoCount: 0, - inRecovery: nil + inRecovery: nil, + timestamp: self.now ) } #endif @@ -4560,7 +4564,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 ab0ec48..7a2b315 100644 --- a/Sources/SwiftNetwork/QUIC/QUICPath.swift +++ b/Sources/SwiftNetwork/QUIC/QUICPath.swift @@ -365,7 +365,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 } @@ -626,7 +626,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 @@ -686,7 +686,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) @@ -710,7 +717,8 @@ extension QUICPath { bytesLost: bytesLost, largestLostSentTime: largestLostSentTime, mss: mss, - smoothedRTT: smoothedRTT + smoothedRTT: smoothedRTT, + now: parentProtocol.now ) ?? false } @@ -771,6 +779,7 @@ extension QUICPath { largestAckedSentTime: largestAckedSentTime, mss: mss, smoothedRTT: smoothedRTT, + now: parentProtocol.now, qlog: qlog ) #if !NETWORK_EMBEDDED @@ -783,6 +792,7 @@ extension QUICPath { largestAckedSentTime: largestAckedSentTime, mss: mss, smoothedRTT: smoothedRTT, + now: parentProtocol.now, qlog: qlog ) case .prague(var prague): @@ -794,6 +804,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 e36e15d..8a3db2e 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 @@ -989,7 +990,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 @@ -1255,7 +1256,7 @@ struct Recovery: ~Copyable, PrefixedLoggable, NonCopyableTimerUser { qLog.recoveryUpdated( ptoCount: UInt64(path.recoveryState.PTOCount), inRecovery: nil, - timestamp: .now + timestamp: connection.now ) } #endif @@ -1285,7 +1286,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 @@ -1498,7 +1499,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 diff --git a/Sources/SwiftNetwork/QUIC/Timer.swift b/Sources/SwiftNetwork/QUIC/Timer.swift index af26a63..9c50828 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 @@ -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,8 +112,8 @@ final class Timer: PrefixedLoggable { func insert( description: String, fromNow: NetworkDuration = .zero, - timerNow: NetworkClock.Instant = .now, - closure: @escaping () -> Void + timerNow: NetworkClock.Instant, + closure: @escaping (NetworkClock.Instant) -> Void ) -> TimerID { let identifier = nextID var entry = TimerEntry(identifier: nextID, description: description, closure: closure) @@ -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 @@ -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/AckTests.swift b/Tests/QUICTests/AckTests.swift index 6dc043e..3c1ec92 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 ) } + + mutating func append(packetNumberSpace: PacketNumberSpace, packetNumber: PacketNumber) { + self.append( + packetNumberSpace: packetNumberSpace, + packetNumber: packetNumber, + now: .testBase + ) + } + + mutating 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, *) @@ -888,7 +916,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 +953,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 af68ed3..7840ae2 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 2fe3d6e..1c4ad6d 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 f8523e8..cc91ba3 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/TestClock.swift b/Tests/QUICTests/TestClock.swift new file mode 100644 index 0000000..a27eba0 --- /dev/null +++ b/Tests/QUICTests/TestClock.swift @@ -0,0 +1,37 @@ +//===----------------------------------------------------------------------===// +// +// 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) + } +} + +#endif diff --git a/Tests/QUICTests/TimerTests.swift b/Tests/QUICTests/TimerTests.swift index 0f244e1..9305643 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,12 @@ 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)) - { + { _ in semaphore.signal() } XCTAssertEqual(timer.nextDeadline, .init(milliseconds: 1500)) @@ -146,7 +146,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 +156,7 @@ final class TimerTests: XCTestCase { description: "two", fromNow: .microseconds(1_000_000), timerNow: .init(microseconds: 2) - ) { + ) { _ in semaphore.signal() } @@ -185,7 +185,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 +195,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 +229,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 +242,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)))