|
|
@@ -0,0 +1,151 @@
|
|
|
+//
|
|
|
+// NetworkMetrics.swift
|
|
|
+// Kingfisher
|
|
|
+//
|
|
|
+// Created by FunnyValentine on 2025/07/25.
|
|
|
+//
|
|
|
+// Copyright (c) 2025 Wei Wang <onevcat@gmail.com>
|
|
|
+//
|
|
|
+// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
|
+// of this software and associated documentation files (the "Software"), to deal
|
|
|
+// in the Software without restriction, including without limitation the rights
|
|
|
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
+// copies of the Software, and to permit persons to whom the Software is
|
|
|
+// furnished to do so, subject to the following conditions:
|
|
|
+//
|
|
|
+// The above copyright notice and this permission notice shall be included in
|
|
|
+// all copies or substantial portions of the Software.
|
|
|
+//
|
|
|
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
|
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
|
+// THE SOFTWARE.
|
|
|
+
|
|
|
+import Foundation
|
|
|
+
|
|
|
+/// Represents the network performance metrics collected during an image download task.
|
|
|
+public struct NetworkMetrics: Sendable {
|
|
|
+
|
|
|
+ /// The original URLSessionTaskMetrics for advanced use cases.
|
|
|
+ public let rawMetrics: URLSessionTaskMetrics
|
|
|
+
|
|
|
+ /// The duration of the actual image retrieval (excluding redirects).
|
|
|
+ public let retrieveImageDuration: TimeInterval
|
|
|
+
|
|
|
+ /// The total time from request start to completion (including redirects).
|
|
|
+ public let totalRequestDuration: TimeInterval
|
|
|
+
|
|
|
+ /// The time it took to perform DNS lookup.
|
|
|
+ public let domainLookupDuration: TimeInterval?
|
|
|
+
|
|
|
+ /// The time it took to establish the TCP connection.
|
|
|
+ public let connectDuration: TimeInterval?
|
|
|
+
|
|
|
+ /// The time it took to perform TLS handshake.
|
|
|
+ public let secureConnectionDuration: TimeInterval?
|
|
|
+
|
|
|
+ /// The number of bytes sent in the request body.
|
|
|
+ public let requestBodyBytesSent: Int64
|
|
|
+
|
|
|
+ /// The number of bytes received in the response body.
|
|
|
+ public let responseBodyBytesReceived: Int64
|
|
|
+
|
|
|
+ /// The HTTP response status code, if available.
|
|
|
+ public let httpStatusCode: Int?
|
|
|
+
|
|
|
+ /// The number of redirects that occurred during the request.
|
|
|
+ public let redirectCount: Int
|
|
|
+
|
|
|
+ /// Creates a NetworkMetrics instance from URLSessionTaskMetrics
|
|
|
+ init?(from urlMetrics: URLSessionTaskMetrics) {
|
|
|
+ // Find the first successful transaction (200-299 status) ignoring redirects
|
|
|
+ // We need to ensure we get metrics from an actual successful download, not from
|
|
|
+ // intermediate redirects (301/302) which don't represent real download performance
|
|
|
+ var successfulTransaction: URLSessionTaskTransactionMetrics?
|
|
|
+ for transaction in urlMetrics.transactionMetrics {
|
|
|
+ if let httpResponse = transaction.response as? HTTPURLResponse,
|
|
|
+ (200...299).contains(httpResponse.statusCode) {
|
|
|
+ successfulTransaction = transaction
|
|
|
+ break
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // make sure we have a valid successful transaction
|
|
|
+ guard let successfulTransaction else {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ // Store raw metrics for advanced use cases
|
|
|
+ self.rawMetrics = urlMetrics
|
|
|
+
|
|
|
+ // Calculate the image retrieval duration from the successful transaction
|
|
|
+ self.retrieveImageDuration = Self.calculateRetrieveImageDuration(from: successfulTransaction)
|
|
|
+
|
|
|
+ // Calculate the total request duration from the task interval
|
|
|
+ self.totalRequestDuration = urlMetrics.taskInterval.duration
|
|
|
+
|
|
|
+ // Calculate timing metrics from the successful transaction
|
|
|
+ self.domainLookupDuration = Self.calculateDomainLookupDuration(from: successfulTransaction)
|
|
|
+ self.connectDuration = Self.calculateConnectDuration(from: successfulTransaction)
|
|
|
+ self.secureConnectionDuration = Self.calculateSecureConnectionDuration(from: successfulTransaction)
|
|
|
+
|
|
|
+ // Extract data transfer information from the successful transaction
|
|
|
+ self.requestBodyBytesSent = successfulTransaction.countOfRequestBodyBytesSent
|
|
|
+ self.responseBodyBytesReceived = successfulTransaction.countOfResponseBodyBytesReceived
|
|
|
+
|
|
|
+ // Extract HTTP status code from the successful transaction
|
|
|
+ self.httpStatusCode = Self.extractHTTPStatusCode(from: successfulTransaction)
|
|
|
+
|
|
|
+ // Extract redirect count
|
|
|
+ self.redirectCount = urlMetrics.redirectCount
|
|
|
+ }
|
|
|
+
|
|
|
+ // MARK: - Private Calculation Methods
|
|
|
+
|
|
|
+ /// Calculates DNS lookup duration
|
|
|
+ /// Formula: domainLookupEndDate - domainLookupStartDate
|
|
|
+ /// Represents: Time spent resolving domain name to IP address
|
|
|
+ private static func calculateDomainLookupDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval? {
|
|
|
+ guard let start = transaction.domainLookupStartDate,
|
|
|
+ let end = transaction.domainLookupEndDate else { return nil }
|
|
|
+ return end.timeIntervalSince(start)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Calculates TCP connection establishment duration
|
|
|
+ /// Formula: connectEndDate - connectStartDate
|
|
|
+ /// Represents: Time spent establishing TCP connection to server
|
|
|
+ private static func calculateConnectDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval? {
|
|
|
+ guard let start = transaction.connectStartDate,
|
|
|
+ let end = transaction.connectEndDate else { return nil }
|
|
|
+ return end.timeIntervalSince(start)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Calculates TLS/SSL handshake duration
|
|
|
+ /// Formula: secureConnectionEndDate - secureConnectionStartDate
|
|
|
+ /// Represents: Time spent performing TLS/SSL handshake for HTTPS connections
|
|
|
+ private static func calculateSecureConnectionDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval? {
|
|
|
+ guard let start = transaction.secureConnectionStartDate,
|
|
|
+ let end = transaction.secureConnectionEndDate else { return nil }
|
|
|
+ return end.timeIntervalSince(start)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Calculates the image retrieval duration for a single transaction
|
|
|
+ /// Formula: responseEndDate - requestStartDate
|
|
|
+ /// Represents: Time from sending HTTP request to receiving complete image response
|
|
|
+ private static func calculateRetrieveImageDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval {
|
|
|
+ guard let start = transaction.requestStartDate,
|
|
|
+ let end = transaction.responseEndDate else {
|
|
|
+ return 0
|
|
|
+ }
|
|
|
+ return end.timeIntervalSince(start)
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Extracts HTTP status code from response
|
|
|
+ /// Returns: HTTP status code (200, 404, etc.) or nil for non-HTTP responses
|
|
|
+ private static func extractHTTPStatusCode(from transaction: URLSessionTaskTransactionMetrics) -> Int? {
|
|
|
+ return (transaction.response as? HTTPURLResponse)?.statusCode
|
|
|
+ }
|
|
|
+}
|