diff --git a/Sources/SwiftNetwork/Connection/DataTransferSnapshot.swift b/Sources/SwiftNetwork/Connection/DataTransferSnapshot.swift index ebbcc7e..94e72c3 100644 --- a/Sources/SwiftNetwork/Connection/DataTransferSnapshot.swift +++ b/Sources/SwiftNetwork/Connection/DataTransferSnapshot.swift @@ -12,8 +12,9 @@ // //===----------------------------------------------------------------------===// -#if !NETWORK_EMBEDDED -struct DataTransferSnapshot: Equatable { +@_spi(ProtocolProvider) +@available(Network 0.1.0, *) +public struct DataTransferSnapshot: Equatable { var interfaceIndex: UInt64? var receivedIPPacketCount: UInt64 = 0 @@ -49,4 +50,3 @@ struct DataTransferSnapshot: Equatable { var migrationToOtherCount: UInt64 = 0 var migrationToFallbackCount: UInt64 = 0 } -#endif diff --git a/Sources/SwiftNetwork/Connection/NetworkMetrics.swift b/Sources/SwiftNetwork/Connection/NetworkMetrics.swift new file mode 100644 index 0000000..2e486bd --- /dev/null +++ b/Sources/SwiftNetwork/Connection/NetworkMetrics.swift @@ -0,0 +1,27 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@_spi(ProtocolProvider) +@available(Network 0.1.0, *) +public enum RequestedNetworkMetrics { + case protocolEstablishmentReports + case dataTransferSnapshot +} + +@_spi(ProtocolProvider) +@available(Network 0.1.0, *) +public enum NetworkMetrics { + case protocolEstablishmentReports([ProtocolEstablishmentReport]) + case dataTransferSnapshot(DataTransferSnapshot) +} diff --git a/Sources/SwiftNetwork/Connection/ProtocolEstablishmentReport.swift b/Sources/SwiftNetwork/Connection/ProtocolEstablishmentReport.swift new file mode 100644 index 0000000..5288f41 --- /dev/null +++ b/Sources/SwiftNetwork/Connection/ProtocolEstablishmentReport.swift @@ -0,0 +1,104 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +@_spi(ProtocolProvider) +@available(Network 0.1.0, *) +public enum ClientAccurateECNState: UInt32, Equatable { + case ecnInvalid = 0 + case ecnFeatureDisabled = 1 + case ecnFeatureEnabled = 2 + case classicECNAvailable = 3 // TCP only + case ecnNotAvailable = 4 + case ecnNegotiationBlackholed = 5 + case ecnAccurateECNBleachingDetected = 6 // TCP only + case ecnNegotiationSuccess = 7 + case ecnNegotiationSuccessECTManglingDetected = 8 // TCP only + case ecnNegotiationSuccessECTBleachingDetected = 9 // TCP only +} + +@_spi(ProtocolProvider) +@available(Network 0.1.0, *) +public enum ServerAccurateECNState: UInt32, Equatable { + case ecnInvalid = 0 + case ecnFeatureDisabled = 1 + case ecnFeatureEnabled = 2 + case noECNRequested = 3 + case classicEcnRequested = 4 + case ecnRequested = 5 + case ecnNegotiationBlackholed = 6 + case ecnAccurateECNBleachingDetected = 7 + case ecnNegotiationSuccess = 8 + case ecnNegotiationSuccessECTManglingDetected = 9 + case ecnNegotiationSuccessECTBleachingDetected = 10 +} + +@_spi(ProtocolProvider) +@available(Network 0.1.0, *) +public struct ProtocolEstablishmentReport: Equatable { + let handshakeMilliseconds: NetworkDuration + let handshakeRTTMilliseconds: NetworkDuration + let protocolIdentifier: ProtocolIdentifier + let clientAccurateECNState: ClientAccurateECNState + let serverAccurateECNState: ServerAccurateECNState + + init( + handshakeMilliseconds: NetworkDuration, + handshakeRTTMilliseconds: NetworkDuration, + protocolIdentifier: ProtocolIdentifier, + clientAccurateECNState: ClientAccurateECNState = .ecnInvalid, + serverAccurateECNState: ServerAccurateECNState = .ecnInvalid + ) { + self.handshakeMilliseconds = handshakeMilliseconds + self.handshakeRTTMilliseconds = handshakeRTTMilliseconds + self.protocolIdentifier = protocolIdentifier + self.clientAccurateECNState = clientAccurateECNState + self.serverAccurateECNState = serverAccurateECNState + } + + struct Flags: OptionSet { + init(rawValue: Self.RawValue) { + self.rawValue = rawValue + } + var rawValue: UInt8 + static let l4sEnabled = Flags(rawValue: 1 << 0) + static let quicMigrationSupported = Flags(rawValue: 1 << 1) + static let quicStatelessResetReceived = Flags(rawValue: 1 << 2) + static let quicStatelessResetDuringPathProbe = Flags(rawValue: 1 << 3) + } + private var flags = Flags() + var l4sEnabled: Bool { + get { flags.contains(.l4sEnabled) } + set { if newValue { flags.insert(.l4sEnabled) } else { flags.remove(.l4sEnabled) } } + } + var quicMigrationSupported: Bool { + get { flags.contains(.quicMigrationSupported) } + set { if newValue { flags.insert(.quicMigrationSupported) } else { flags.remove(.quicMigrationSupported) } } + } + var quicStatelessResetReceived: Bool { + get { flags.contains(.quicStatelessResetReceived) } + set { + if newValue { flags.insert(.quicStatelessResetReceived) } else { flags.remove(.quicStatelessResetReceived) } + } + } + var quicStatelessResetDuringPathProbe: Bool { + get { flags.contains(.quicStatelessResetDuringPathProbe) } + set { + if newValue { + flags.insert(.quicStatelessResetDuringPathProbe) + } else { + flags.remove(.quicStatelessResetDuringPathProbe) + } + } + } +} diff --git a/Sources/SwiftNetwork/Protocols/BottomProtocol.swift b/Sources/SwiftNetwork/Protocols/BottomProtocol.swift index 450e745..3cbaaa8 100644 --- a/Sources/SwiftNetwork/Protocols/BottomProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/BottomProtocol.swift @@ -66,6 +66,12 @@ public protocol BottomProtocolHandler: ~Copyable, OutboundDataHandler { /// The metadata state for this protocol. var metadata: AbstractProtocolMetadata? { get } #endif + + /// Update this protocols contribution to a data transfer snapshot. + func updateDataTransferSnapshot(_ snapshot: inout DataTransferSnapshot) + + /// Fetch this protocols establishment report entry + var protocolEstablishmentReport: ProtocolEstablishmentReport? { get } } @_spi(ProtocolProvider) @@ -245,6 +251,22 @@ extension BottomProtocolHandler where Self: ~Copyable { return nil } + public func getMetrics( + _ from: ProtocolInstanceReference, + requestedNetworkMetric: RequestedNetworkMetrics + ) -> NetworkMetrics? { + do { try validate(upper: from, #function) } catch { return nil } + switch requestedNetworkMetric { + case .protocolEstablishmentReports: + guard let report = protocolEstablishmentReport else { return nil } + return .protocolEstablishmentReports([report]) + case .dataTransferSnapshot: + var snapshot = DataTransferSnapshot() + updateDataTransferSnapshot(&snapshot) + return .dataTransferSnapshot(snapshot) + } + } + #if !NETWORK_EMBEDDED public func getOptions(from parameters: Parameters) -> ProtocolOptions? { parameters.protocolOptions(for: self.reference) @@ -279,6 +301,10 @@ extension BottomProtocolHandler where Self: ~Copyable { #if !NETWORK_EMBEDDED public var metadata: AbstractProtocolMetadata? { nil } #endif + + public func updateDataTransferSnapshot(_ snapshot: inout DataTransferSnapshot) {} + + public var protocolEstablishmentReport: ProtocolEstablishmentReport? { nil } } extension BottomProtocolHandler where Self: ~Copyable, UpperProtocol == InboundDatagramLinkage { diff --git a/Sources/SwiftNetwork/Protocols/HarnessProtocols.swift b/Sources/SwiftNetwork/Protocols/HarnessProtocols.swift index 057129d..429a8c1 100644 --- a/Sources/SwiftNetwork/Protocols/HarnessProtocols.swift +++ b/Sources/SwiftNetwork/Protocols/HarnessProtocols.swift @@ -926,6 +926,12 @@ public class NewFlowHarness NetworkMetrics? { + fromExternal { + lower.invokeGetMetrics(reference, requestedNetworkMetric: requestedNetworkMetric) + } + } } @_spi(ProtocolProvider) diff --git a/Sources/SwiftNetwork/Protocols/ManyToManyProtocol.swift b/Sources/SwiftNetwork/Protocols/ManyToManyProtocol.swift index 735b6e2..eaa1235 100644 --- a/Sources/SwiftNetwork/Protocols/ManyToManyProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/ManyToManyProtocol.swift @@ -61,6 +61,8 @@ public protocol ManyToManyProtocolHandler: ListenerHandler, LoggableProtocol { func teardown(flow: MultiplexedFlowIdentifier) func handleApplicationEvent(flow: MultiplexedFlowIdentifier, event: ApplicationEvent) -> HandleNetworkEventResult func getMetadata

(flow: MultiplexedFlowIdentifier) -> ProtocolMetadata

? where P: NetworkProtocol + func updateDataTransferSnapshot(flow: MultiplexedFlowIdentifier, _ snapshot: inout DataTransferSnapshot) + var protocolEstablishmentReport: ProtocolEstablishmentReport? { get } // MARK: Per-path events to implement func handleConnectedEvent(path: MultiplexingPathIdentifier) @@ -291,6 +293,31 @@ extension ManyToManyProtocolHandler { getMetadata(flow: .allFlows) } + public func updateDataTransferSnapshot(flow: MultiplexedFlowIdentifier, _ snapshot: inout DataTransferSnapshot) {} + public var protocolEstablishmentReport: ProtocolEstablishmentReport? { nil } + + public func getMetrics( + flow: MultiplexedFlowIdentifier, + requestedNetworkMetric: RequestedNetworkMetrics + ) -> NetworkMetrics? { + switch requestedNetworkMetric { + case .protocolEstablishmentReports: + guard let report = protocolEstablishmentReport else { return nil } + return .protocolEstablishmentReports([report]) + case .dataTransferSnapshot: + var snapshot = DataTransferSnapshot() + updateDataTransferSnapshot(flow: flow, &snapshot) + return .dataTransferSnapshot(snapshot) + } + } + + public func getMetrics( + _ from: ProtocolInstanceReference, + requestedNetworkMetric: RequestedNetworkMetrics + ) -> NetworkMetrics? { + getMetrics(flow: .allFlows, requestedNetworkMetric: requestedNetworkMetric) + } + public func handleInboundDataAvailableEvent(path: MultiplexingPathIdentifier) {} public func handleOutboundRoomAvailableEvent(path: MultiplexingPathIdentifier) {} @@ -1211,6 +1238,14 @@ extension MultiplexedFlow { return parentProtocol.getMetadata(flow: identifier) } + public func getMetrics( + _ from: ProtocolInstanceReference, + requestedNetworkMetric: RequestedNetworkMetrics + ) -> NetworkMetrics? { + do { try validate(upper: from, #function) } catch { return nil } + return parentProtocol.getMetrics(flow: identifier, requestedNetworkMetric: requestedNetworkMetric) + } + fileprivate func deliverConnectedEvent() { if upper.isDetached { // Enqueue pending event instead of delivering immediately. diff --git a/Sources/SwiftNetwork/Protocols/OneToOneProtocol.swift b/Sources/SwiftNetwork/Protocols/OneToOneProtocol.swift index 1b0a784..bb49273 100644 --- a/Sources/SwiftNetwork/Protocols/OneToOneProtocol.swift +++ b/Sources/SwiftNetwork/Protocols/OneToOneProtocol.swift @@ -89,6 +89,12 @@ public protocol OneToOneProtocolHandler: ~Copyable, OutboundDataHandler, Inbound /// Protocols that don't handle events should initialize this to `true`. /// The stack may set this to `false` explicitly, after which you shouldn't set it back to `true`. var passthroughEvents: Bool { get set } + + /// Update this protocols data transfer snapshot. + func updateDataTransferSnapshot(_ snapshot: inout DataTransferSnapshot) + + /// Fetch this protocols establishment report entry + var protocolEstablishmentReport: ProtocolEstablishmentReport? { get } } @_spi(ProtocolProvider) @@ -379,6 +385,36 @@ extension OneToOneProtocolHandler where Self: ~Copyable { #endif } + public func getMetrics( + _ from: ProtocolInstanceReference, + requestedNetworkMetric: RequestedNetworkMetrics + ) -> NetworkMetrics? { + do { try validate(upper: from, #function) } catch { return nil } + let lowerMetrics = lower.invokeGetMetrics( + effectiveSelfReference, + requestedNetworkMetric: requestedNetworkMetric + ) + switch requestedNetworkMetric { + case .protocolEstablishmentReports: + var reports = [ProtocolEstablishmentReport]() + if case .protocolEstablishmentReports(let protocolEstablishmentReports) = lowerMetrics { + reports = protocolEstablishmentReports + } + if let currentProtocolEstablishmentReport = protocolEstablishmentReport { + reports.append(currentProtocolEstablishmentReport) + } + return .protocolEstablishmentReports(reports) + case .dataTransferSnapshot: + if case .dataTransferSnapshot(var snapshot) = lowerMetrics { + updateDataTransferSnapshot(&snapshot) + return .dataTransferSnapshot(snapshot) + } + var snapshot = DataTransferSnapshot() + updateDataTransferSnapshot(&snapshot) + return .dataTransferSnapshot(snapshot) + } + } + public func tlsOptions(from parameters: Parameters) -> ProtocolOptions? { parameters.tlsOptions(for: self.reference) } @@ -429,6 +465,10 @@ extension OneToOneProtocolHandler where Self: ~Copyable { public mutating func handleApplicationEvent(_ event: ApplicationEvent) -> HandleNetworkEventResult { .unconsumed } + + public func updateDataTransferSnapshot(_ snapshot: inout DataTransferSnapshot) {} + + public var protocolEstablishmentReport: ProtocolEstablishmentReport? { nil } } extension OneToOneProtocolHandler where Self: ~Copyable { diff --git a/Sources/SwiftNetwork/Protocols/ProtocolControlHandlers.swift b/Sources/SwiftNetwork/Protocols/ProtocolControlHandlers.swift index f325637..0cf04e1 100644 --- a/Sources/SwiftNetwork/Protocols/ProtocolControlHandlers.swift +++ b/Sources/SwiftNetwork/Protocols/ProtocolControlHandlers.swift @@ -142,6 +142,10 @@ public protocol LowerProtocolHandler: ~Copyable, ProtocolInstance mutating func handleApplicationEvent(_ from: ProtocolInstanceReference, event: ApplicationEvent) func getMetadata(_ from: ProtocolInstanceReference) -> ProtocolMetadata

? + func getMetrics( + _ from: ProtocolInstanceReference, + requestedNetworkMetric: RequestedNetworkMetrics + ) -> NetworkMetrics? } extension ProtocolInstanceReference { @@ -888,4 +892,43 @@ extension ProtocolInstanceReference { } } } + + public func getMetrics( + _ from: ProtocolInstanceReference, + requestedNetworkMetric: RequestedNetworkMetrics + ) -> NetworkMetrics? { + self.handleCallFromUpperProtocol { + switch self.reference { + case .none: return nil + case .udp(let index): + return context.udpInstances[index].getMetrics(from, requestedNetworkMetric: requestedNetworkMetric) + case .ip(let index): + return context.ipInstances[index].getMetrics(from, requestedNetworkMetric: requestedNetworkMetric) + case .tcp(let instance): return instance.getMetrics(from, requestedNetworkMetric: requestedNetworkMetric) + case .tls(let instance): return instance.getMetrics(from, requestedNetworkMetric: requestedNetworkMetric) + #if !NETWORK_NO_SWIFT_QUIC + case .quic(let instance): return instance.getMetrics(from, requestedNetworkMetric: requestedNetworkMetric) + case .quicStream(let instance): + return instance.getMetrics(from, requestedNetworkMetric: requestedNetworkMetric) + case .quicDatagram(let instance): + return instance.getMetrics(from, requestedNetworkMetric: requestedNetworkMetric) + case .quicCrypto(let instance): + return instance.getMetrics(from, requestedNetworkMetric: requestedNetworkMetric) + #if !NETWORK_NO_TESTING_HARNESS + case .datagramLowerHarness(let instance): + return instance.getMetrics(from, requestedNetworkMetric: requestedNetworkMetric) + case .streamLowerHarness(let instance): + return instance.getMetrics(from, requestedNetworkMetric: requestedNetworkMetric) + #endif + #endif + #if !NETWORK_EMBEDDED + case .custom(let container, let index): + return container.accessLower(at: index) { + $0.getMetrics(from, requestedNetworkMetric: requestedNetworkMetric) + } + #endif + default: fatalError("Protocol cannot accept getMetrics call") + } + } + } } diff --git a/Sources/SwiftNetwork/Protocols/ProtocolLinkage.swift b/Sources/SwiftNetwork/Protocols/ProtocolLinkage.swift index e26f2de..d2699f3 100644 --- a/Sources/SwiftNetwork/Protocols/ProtocolLinkage.swift +++ b/Sources/SwiftNetwork/Protocols/ProtocolLinkage.swift @@ -213,6 +213,13 @@ extension LowerProtocolLinkage { public func invokeGetMetadata(_ from: ProtocolInstanceReference) -> ProtocolMetadata

? { reference.getMetadata(from) } + + public func invokeGetMetrics( + _ from: ProtocolInstanceReference, + requestedNetworkMetric: RequestedNetworkMetrics + ) -> NetworkMetrics? { + reference.getMetrics(from, requestedNetworkMetric: requestedNetworkMetric) + } } @_spi(ProtocolProvider) diff --git a/Sources/SwiftNetwork/QUIC/CongestionControl.swift b/Sources/SwiftNetwork/QUIC/CongestionControl.swift index 4b27a66..b96b352 100644 --- a/Sources/SwiftNetwork/QUIC/CongestionControl.swift +++ b/Sources/SwiftNetwork/QUIC/CongestionControl.swift @@ -281,6 +281,19 @@ enum CongestionControl { #endif } } + + func filloutDataTransferSnapshot(dataTransferSnapshot: inout DataTransferSnapshot) { + switch self { + case .cubic(algorithm: let cubic): + cubic.filloutDataTransferSnapshot(dataTransferSnapshot: &dataTransferSnapshot) + #if !NETWORK_EMBEDDED + case .ledbat(algorithm: let ledbat): + ledbat.filloutDataTransferSnapshot(dataTransferSnapshot: &dataTransferSnapshot) + case .prague(algorithm: let prague): + prague.filloutDataTransferSnapshot(dataTransferSnapshot: &dataTransferSnapshot) + #endif + } + } } protocol CongestionControlProtocol: PrefixedLoggable { diff --git a/Sources/SwiftNetwork/QUIC/Crypto.swift b/Sources/SwiftNetwork/QUIC/Crypto.swift index 1d429bc..cebbe8d 100644 --- a/Sources/SwiftNetwork/QUIC/Crypto.swift +++ b/Sources/SwiftNetwork/QUIC/Crypto.swift @@ -508,6 +508,12 @@ extension QUICCrypto: OutboundStreamHandler { ) {} func getMetadata

(_ from: ProtocolInstanceReference) -> ProtocolMetadata

? where P: NetworkProtocol { nil } + func getMetrics( + _ from: ProtocolInstanceReference, + requestedNetworkMetric: RequestedNetworkMetrics + ) -> NetworkMetrics? { + nil + } func levelForReference(_ from: ProtocolInstanceReference) -> SwiftTLSOptions.EncryptionLevel? { if from == initialLinkage.reference { return .initial } if from == earlyDataLinkage.reference { return .earlyData } diff --git a/Sources/SwiftNetwork/QUIC/Cubic.swift b/Sources/SwiftNetwork/QUIC/Cubic.swift index 363073c..88326c1 100644 --- a/Sources/SwiftNetwork/QUIC/Cubic.swift +++ b/Sources/SwiftNetwork/QUIC/Cubic.swift @@ -432,12 +432,10 @@ struct Cubic: CongestionControlProtocol, CubicLikeProtocol { initPipeAckSamples() } - #if !NETWORK_EMBEDDED func filloutDataTransferSnapshot(dataTransferSnapshot: inout DataTransferSnapshot) { dataTransferSnapshot.transportCongestionWindow = congestionWindow dataTransferSnapshot.transportSlowStartThreshold = slowStartThreshold } - #endif mutating func reset(mss: Int, qlog: QLog? = nil) { congestionWindow = Cubic.initialCongestionWindow(mss) diff --git a/Sources/SwiftNetwork/QUIC/Ledbat.swift b/Sources/SwiftNetwork/QUIC/Ledbat.swift index 9ac96bd..9db4826 100644 --- a/Sources/SwiftNetwork/QUIC/Ledbat.swift +++ b/Sources/SwiftNetwork/QUIC/Ledbat.swift @@ -317,12 +317,10 @@ struct Ledbat: CongestionControlProtocol, CubicLikeProtocol { logUpdate(qlog: qlog) } - #if !NETWORK_EMBEDDED func filloutDataTransferSnapshot(dataTransferSnapshot: inout DataTransferSnapshot) { dataTransferSnapshot.transportCongestionWindow = congestionWindow dataTransferSnapshot.transportSlowStartThreshold = slowStartThreshold } - #endif mutating func reset(mss: Int, qlog: QLog? = nil) { congestionWindow = Ledbat.initialCongestionWindow(mss) diff --git a/Sources/SwiftNetwork/QUIC/Migration.swift b/Sources/SwiftNetwork/QUIC/Migration.swift index f7a2325..94effd7 100644 --- a/Sources/SwiftNetwork/QUIC/Migration.swift +++ b/Sources/SwiftNetwork/QUIC/Migration.swift @@ -129,6 +129,16 @@ struct Migration: ~Copyable { } + func probingPathCount(_ connection: QUICConnection) -> Int { + var probingPaths = 0 + connection.applyToAllPaths { path in + if path.state.isProbing { + probingPaths += 1 + } + } + return probingPaths + } + func handshakeConfirmed(_ connection: QUICConnection) { // TODO: pending migration feature completion } diff --git a/Sources/SwiftNetwork/QUIC/Prague.swift b/Sources/SwiftNetwork/QUIC/Prague.swift index 655a6fc..374e016 100644 --- a/Sources/SwiftNetwork/QUIC/Prague.swift +++ b/Sources/SwiftNetwork/QUIC/Prague.swift @@ -657,12 +657,10 @@ struct Prague: CongestionControlProtocol, CubicLikeProtocol { resetInternal() } - #if !NETWORK_EMBEDDED func filloutDataTransferSnapshot(dataTransferSnapshot: inout DataTransferSnapshot) { dataTransferSnapshot.transportCongestionWindow = congestionWindow dataTransferSnapshot.transportSlowStartThreshold = slowStartThreshold } - #endif mutating func reset(mss: Int, qlog: QLog? = nil) { congestionWindow = Prague.initialCongestionWindow(mss) diff --git a/Sources/SwiftNetwork/QUIC/QUICConnection.swift b/Sources/SwiftNetwork/QUIC/QUICConnection.swift index e00d776..37f2ec0 100644 --- a/Sources/SwiftNetwork/QUIC/QUICConnection.swift +++ b/Sources/SwiftNetwork/QUIC/QUICConnection.swift @@ -238,8 +238,10 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, } } - var handshakeStartTime: NetworkClock.Instant = .zero - var idleTimeout: NetworkDuration = .zero + private(set) var handshakeStartTime: NetworkClock.Instant = .zero + private(set) var handshakeDuration: NetworkDuration = .zero + private(set) var handshakeRTT: NetworkDuration = .zero + private(set) var idleTimeout: NetworkDuration = .zero var keepaliveDuration: NetworkDuration = .zero var keepaliveTimerID: Timer.TimerID? @@ -352,6 +354,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, private(set) var waitingForOutstandingKeepAliveAcks = false private(set) var tlsOptions: SwiftTLSProtocol.Options? private(set) var testSendingShortPackets = false + private(set) var migrationSupported = false // false == IPv6, true == IPv4 private(set) var initialAddressIsIPv4 = false @@ -1022,6 +1025,7 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, log.error("Error inserting remote CID for a preferred address: \(error)") } } + migrationSupported = true migration.addPreferredAddress(preferredAddress) } } @@ -2499,6 +2503,58 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, } #endif + public func updateDataTransferSnapshot(flow: MultiplexedFlowIdentifier, _ snapshot: inout DataTransferSnapshot) { + snapshot.receivedTransportOutOfOrderByteCount = UInt64(clamping: self.stats[.rxOutOfOrderBytes]) + snapshot.sentTransportRetransmittedByteCount = UInt64(clamping: self.stats[.txRetransmittedBytes]) + snapshot.sentTransportECNCapablePacketCount = UInt64(clamping: self.stats[.ecnCapablePacketsSent]) + snapshot.sentTransportECNCapableAckedPacketCount = UInt64(clamping: self.stats[.ecnCapablePacketsAcknowledged]) + snapshot.sentTransportECNCapableMarkedPacketCount = UInt64(clamping: self.stats[.ecnCapablePacketsMarked]) + snapshot.sentTransportECNCapableLostPacketCount = UInt64(clamping: self.stats[.ecnCapablePacketsLost]) + if let path = currentPath { + snapshot.transportMinimumRTT = path.rtt.minRTT + snapshot.transportSmoothedRTT = path.rtt.smoothedRTT + snapshot.transportCurrentRTT = path.rtt.adjustedRTT + snapshot.transportRTTVariance = path.rtt.RTTVariance + path.congestionControlFilloutDataTransferSnapshot(snapshot: &snapshot) + } + } + + public var protocolEstablishmentReport: ProtocolEstablishmentReport? { + var clientAccurateECNState: ClientAccurateECNState = .ecnFeatureDisabled + var l4sEnabled = false + if let path = currentPath { + l4sEnabled = path.l4sEnabled + if let ecnState = path.ecnState?.state { + switch ecnState { + case .probing, .validate: + clientAccurateECNState = .ecnFeatureEnabled + case .manglingDetected: + clientAccurateECNState = .ecnNegotiationSuccessECTManglingDetected + case .handshakeValidationSuccess, .capable: + clientAccurateECNState = .ecnNegotiationSuccess + case .blackholed: + clientAccurateECNState = .ecnNegotiationBlackholed + case .unsupported: + clientAccurateECNState = .ecnNotAvailable + default: + break + } + } + } + var protocolEstablishmentReport = ProtocolEstablishmentReport( + handshakeMilliseconds: handshakeDuration, + handshakeRTTMilliseconds: handshakeRTT, + protocolIdentifier: QUICConnectionProtocol.identifier, + clientAccurateECNState: clientAccurateECNState + ) + protocolEstablishmentReport.l4sEnabled = l4sEnabled + protocolEstablishmentReport.quicMigrationSupported = migrationSupported + protocolEstablishmentReport.quicStatelessResetReceived = (self.stats[.statelessResetReceived] > 0) + protocolEstablishmentReport.quicStatelessResetDuringPathProbe = (self.stats[.statelessResetDuringPathProbe] > 0) + + return protocolEstablishmentReport + } + func setupNewOutboundStream( _ stream: QUICStreamInstance, with protocolOptions: QUICStreamProtocol.Options @@ -3867,7 +3923,11 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, let packetToken = packet.tag, let statelessToken = QUICStatelessResetToken(packetToken) { + self.stats.increment(.statelessResetReceived) if remoteCIDs.find(statelessResetToken: statelessToken) != nil { + if migration.probingPathCount(self) > 0 { + self.stats.increment(.statelessResetDuringPathProbe) + } log.info("received valid stateless reset token") errorToReport = NetworkError.posix(ECONNRESET) close() @@ -4055,6 +4115,8 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, } else if !isServer, self.currentPath?.dcid?.length == 0 { log.info("disabling migration due to zero-length peer CID") migration.disableActiveMigration() + } else { + migrationSupported = true } state.change(to: .connected, logIDString: logPrefixer.logIDString) @@ -4070,12 +4132,13 @@ public final class QUICConnection: ManyToManyApplicationStreamProtocol, } } - let establishmentTime = handshakeStartTime.duration(to: .now) + handshakeDuration = handshakeStartTime.duration(to: .now) var currentRTT: NetworkDuration = .milliseconds(0) if let currentPath = currentPath { currentRTT = currentPath.rtt.smoothedRTT + handshakeRTT = currentRTT } - log.notice("QUIC connection established in \(establishmentTime), RTT \(currentRTT)") + log.notice("QUIC connection established in \(handshakeDuration), RTT \(currentRTT)") self.keyState = .phase0 diff --git a/Sources/SwiftNetwork/QUIC/QUICPath.swift b/Sources/SwiftNetwork/QUIC/QUICPath.swift index da66fc1..5219295 100644 --- a/Sources/SwiftNetwork/QUIC/QUICPath.swift +++ b/Sources/SwiftNetwork/QUIC/QUICPath.swift @@ -757,6 +757,11 @@ extension QUICPath { #endif } } + + @inline(__always) + func congestionControlFilloutDataTransferSnapshot(snapshot: inout DataTransferSnapshot) { + congestionControl?.filloutDataTransferSnapshot(dataTransferSnapshot: &snapshot) + } } #endif diff --git a/Sources/SwiftNetwork/QUIC/Statistics.swift b/Sources/SwiftNetwork/QUIC/Statistics.swift index 88c458d..952dacb 100644 --- a/Sources/SwiftNetwork/QUIC/Statistics.swift +++ b/Sources/SwiftNetwork/QUIC/Statistics.swift @@ -136,11 +136,14 @@ enum QUICStatistic: Int, CaseIterable { case rxNewToken case txDepartureTimestamp + + case statelessResetReceived + case statelessResetDuringPathProbe } struct Statistics: ~Copyable { - private var statisticsArray: [96 of Int] + private var statisticsArray: [98 of Int] init() { statisticsArray = .init(repeating: 0) diff --git a/Tests/SwiftNetworkTests/QUICTestHarness.swift b/Tests/SwiftNetworkTests/QUICTestHarness.swift index 2b71a07..1d90781 100644 --- a/Tests/SwiftNetworkTests/QUICTestHarness.swift +++ b/Tests/SwiftNetworkTests/QUICTestHarness.swift @@ -906,6 +906,7 @@ final class QUICTestHarness { clientOptions: ProtocolOptions = QUICProtocol.options(), serverOptions: ProtocolOptions = QUICProtocol.options(), sendMaxStreamUpdate: Bool = false, + validateMetrics: Bool = false, extraServerCIDs: [(QUICConnectionID, QUICStatelessResetToken)] = .init(), afterHandshake: ((QUICTestHarness) -> Void)? = nil, // Block to run after handshake is complete afterData: ((QUICTestHarness) -> Void)? = nil, // Block to run after handshake is complete @@ -1096,6 +1097,38 @@ final class QUICTestHarness { afterData(self) } + if validateMetrics { + if let serverHarness = state?.serverHarness, let clientHarness = state?.clientHarness { + var clientReports: NetworkMetrics? + var serverReports: NetworkMetrics? + let snapshotExpectation = XCTestExpectation(description: "Wait for QUIC connection to receive metrics") + context.async { + clientReports = clientHarness.getMetrics(requestedNetworkMetric: .dataTransferSnapshot) + serverReports = serverHarness.getMetrics(requestedNetworkMetric: .dataTransferSnapshot) + XCTAssertNotNil(clientReports) + XCTAssertNotNil(serverReports) + snapshotExpectation.fulfill() + } + wait(for: [snapshotExpectation], timeout: 2.0) + // Now validate the protocol establishment report + clientReports = nil + serverReports = nil + let protocolEstablishmentReportExpectation = XCTestExpectation( + description: "Wait for QUIC connection to receive metrics" + ) + context.async { + clientReports = clientHarness.getMetrics(requestedNetworkMetric: .protocolEstablishmentReports) + serverReports = serverHarness.getMetrics(requestedNetworkMetric: .protocolEstablishmentReports) + XCTAssertNotNil(clientReports) + XCTAssertNotNil(serverReports) + protocolEstablishmentReportExpectation.fulfill() + } + wait(for: [protocolEstablishmentReportExpectation], timeout: 2.0) + } else { + XCTFail("There should be saved server and client harnesses") + } + } + // If applicationError is present, act upon that here if let applicationError, let applicationErrorReason { if let serverHarness = state?.serverHarness, let clientHarness = state?.clientHarness { diff --git a/Tests/SwiftNetworkTests/SwiftNetworkQUICHarnessTests.swift b/Tests/SwiftNetworkTests/SwiftNetworkQUICHarnessTests.swift index 056a78f..5249209 100644 --- a/Tests/SwiftNetworkTests/SwiftNetworkQUICHarnessTests.swift +++ b/Tests/SwiftNetworkTests/SwiftNetworkQUICHarnessTests.swift @@ -398,6 +398,15 @@ final class SwiftNetworkQUICHarnessTests: NetTestCase { QUICTestHarness().runQUICTest(streamCount: 4, blockSize: 10240, blockCount: 4) } + func testQUICEcho40KiBMultistreamWithMetrics() { + QUICTestHarness().runQUICTest( + streamCount: 8, + blockSize: 10240, + blockCount: 8, + validateMetrics: true + ) + } + func testQUIC13AutomaticStreams() { let serverOptions = QUICProtocol.options() serverOptions.connectionOptions.initialMaxStreamsBidirectional = 5