NetworkMetrics.swift 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. //
  2. // NetworkMetrics.swift
  3. // Kingfisher
  4. //
  5. // Created by FunnyValentine on 2025/07/25.
  6. //
  7. // Copyright (c) 2025 Wei Wang <onevcat@gmail.com>
  8. //
  9. // Permission is hereby granted, free of charge, to any person obtaining a copy
  10. // of this software and associated documentation files (the "Software"), to deal
  11. // in the Software without restriction, including without limitation the rights
  12. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  13. // copies of the Software, and to permit persons to whom the Software is
  14. // furnished to do so, subject to the following conditions:
  15. //
  16. // The above copyright notice and this permission notice shall be included in
  17. // all copies or substantial portions of the Software.
  18. //
  19. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  20. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  21. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  22. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  23. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  24. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  25. // THE SOFTWARE.
  26. import Foundation
  27. /// Represents the network performance metrics collected during an image download task.
  28. public struct NetworkMetrics: Sendable {
  29. /// The original URLSessionTaskMetrics for advanced use cases.
  30. public let rawMetrics: URLSessionTaskMetrics
  31. /// The duration of the actual image retrieval (excluding redirects).
  32. public let retrieveImageDuration: TimeInterval?
  33. /// The total time from request start to completion (including redirects).
  34. public let totalRequestDuration: TimeInterval
  35. /// The time it took to perform DNS lookup.
  36. public let domainLookupDuration: TimeInterval?
  37. /// The time it took to establish the TCP connection.
  38. public let connectDuration: TimeInterval?
  39. /// The time it took to perform TLS handshake.
  40. public let secureConnectionDuration: TimeInterval?
  41. /// The number of bytes sent in the request body.
  42. public let requestBodyBytesSent: Int64
  43. /// The number of bytes received in the response body.
  44. public let responseBodyBytesReceived: Int64
  45. /// The HTTP response status code, if available.
  46. public let httpStatusCode: Int?
  47. /// The number of redirects that occurred during the request.
  48. public let redirectCount: Int
  49. /// Creates a NetworkMetrics instance from URLSessionTaskMetrics
  50. init?(from urlMetrics: URLSessionTaskMetrics) {
  51. // Find the first successful transaction (200-299 status) ignoring redirects
  52. // We need to ensure we get metrics from an actual successful download, not from
  53. // intermediate redirects (301/302) which don't represent real download performance
  54. var successfulTransaction: URLSessionTaskTransactionMetrics?
  55. for transaction in urlMetrics.transactionMetrics {
  56. if let httpResponse = transaction.response as? HTTPURLResponse,
  57. (200...299).contains(httpResponse.statusCode) {
  58. successfulTransaction = transaction
  59. break
  60. }
  61. }
  62. // make sure we have a valid successful transaction
  63. guard let successfulTransaction else {
  64. return nil
  65. }
  66. // Store raw metrics for advanced use cases
  67. self.rawMetrics = urlMetrics
  68. // Calculate the image retrieval duration from the successful transaction
  69. self.retrieveImageDuration = Self.calculateRetrieveImageDuration(from: successfulTransaction)
  70. // Calculate the total request duration from the task interval
  71. self.totalRequestDuration = urlMetrics.taskInterval.duration
  72. // Calculate timing metrics from the successful transaction
  73. self.domainLookupDuration = Self.calculateDomainLookupDuration(from: successfulTransaction)
  74. self.connectDuration = Self.calculateConnectDuration(from: successfulTransaction)
  75. self.secureConnectionDuration = Self.calculateSecureConnectionDuration(from: successfulTransaction)
  76. // Extract data transfer information from the successful transaction
  77. self.requestBodyBytesSent = successfulTransaction.countOfRequestBodyBytesSent
  78. self.responseBodyBytesReceived = successfulTransaction.countOfResponseBodyBytesReceived
  79. // Extract HTTP status code from the successful transaction
  80. self.httpStatusCode = Self.extractHTTPStatusCode(from: successfulTransaction)
  81. // Extract redirect count
  82. self.redirectCount = urlMetrics.redirectCount
  83. }
  84. // MARK: - Private Calculation Methods
  85. /// Calculates DNS lookup duration
  86. /// Formula: domainLookupEndDate - domainLookupStartDate
  87. /// Represents: Time spent resolving domain name to IP address
  88. private static func calculateDomainLookupDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval? {
  89. guard let start = transaction.domainLookupStartDate,
  90. let end = transaction.domainLookupEndDate else { return nil }
  91. return end.timeIntervalSince(start)
  92. }
  93. /// Calculates TCP connection establishment duration
  94. /// Formula: connectEndDate - connectStartDate
  95. /// Represents: Time spent establishing TCP connection to server
  96. private static func calculateConnectDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval? {
  97. guard let start = transaction.connectStartDate,
  98. let end = transaction.connectEndDate else { return nil }
  99. return end.timeIntervalSince(start)
  100. }
  101. /// Calculates TLS/SSL handshake duration
  102. /// Formula: secureConnectionEndDate - secureConnectionStartDate
  103. /// Represents: Time spent performing TLS/SSL handshake for HTTPS connections
  104. private static func calculateSecureConnectionDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval? {
  105. guard let start = transaction.secureConnectionStartDate,
  106. let end = transaction.secureConnectionEndDate else { return nil }
  107. return end.timeIntervalSince(start)
  108. }
  109. /// Calculates the image retrieval duration for a single transaction
  110. /// Formula: responseEndDate - requestStartDate
  111. /// Represents: Time from sending HTTP request to receiving complete image response
  112. private static func calculateRetrieveImageDuration(from transaction: URLSessionTaskTransactionMetrics) -> TimeInterval? {
  113. guard let start = transaction.requestStartDate,
  114. let end = transaction.responseEndDate else {
  115. return nil
  116. }
  117. return end.timeIntervalSince(start)
  118. }
  119. /// Extracts HTTP status code from response
  120. /// Returns: HTTP status code (200, 404, etc.) or nil for non-HTTP responses
  121. private static func extractHTTPStatusCode(from transaction: URLSessionTaskTransactionMetrics) -> Int? {
  122. return (transaction.response as? HTTPURLResponse)?.statusCode
  123. }
  124. }
  125. // MARK: - Convenience Properties
  126. extension NetworkMetrics {
  127. /// The download speed in bytes per second.
  128. ///
  129. /// Calculated as `responseBodyBytesReceived / retrieveImageDuration`.
  130. /// Returns `nil` if the duration is unavailable or zero, or if no data was received.
  131. ///
  132. /// - Note: This uses the actual image retrieval duration, excluding redirects and other overhead,
  133. /// to provide the most accurate representation of the data transfer rate.
  134. public var downloadSpeed: Double? {
  135. guard responseBodyBytesReceived > 0,
  136. let duration = retrieveImageDuration,
  137. duration > 0 else { return nil }
  138. return Double(responseBodyBytesReceived) / duration
  139. }
  140. /// The download speed in megabytes per second (MB/s).
  141. ///
  142. /// This is a convenience property that converts `downloadSpeed` from bytes per second
  143. /// to megabytes per second for easier readability.
  144. ///
  145. /// - Returns: Download speed in MB/s, or `nil` if `downloadSpeed` is unavailable.
  146. public var downloadSpeedMBps: Double? {
  147. guard let speed = downloadSpeed else { return nil }
  148. return speed / (1024 * 1024)
  149. }
  150. }