1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
|
// TransferRateEstimator
// Dustin Mierau • @mierau
// MIT License
import Foundation
/// Transfer rate estimator using exponential moving average (EMA)
///
/// Tracks transfer speed and estimates time remaining. Designed to smooth out
/// network jitter and provide stable estimates after collecting enough samples.
///
/// Example:
/// ```swift
/// var estimator = TransferRateEstimator(total: fileSize)
///
/// while transferring {
/// let chunk = try await receiveData()
/// let progress = estimator.update(bytes: chunk.count)
/// print("Speed: \(progress.bytesPerSecond ?? 0) B/s, ETA: \(progress.estimatedTimeRemaining ?? 0)s")
/// }
/// ```
public struct TransferRateEstimator {
/// Total bytes to transfer (nil if unknown)
public let total: Int?
/// Exponential moving average of transfer rate (bytes/second)
private var emaBytesPerSecond: Double = 0
/// Smoothing factor for EMA (0 < alpha ≤ 1)
/// Higher = more responsive to recent changes, lower = more smoothing
private let alpha: Double
/// Number of samples collected
private var sampleCount: Int = 0
/// Timestamp of first sample (for elapsed time calculation)
private var startTime: ContinuousClock.Instant?
/// Timestamp of last update (for calculating sample duration)
private var lastUpdateTime: ContinuousClock.Instant?
/// Minimum elapsed time before trusting estimates (seconds)
private let minElapsedTime: TimeInterval
/// Minimum number of samples before trusting estimates
private let minSamples: Int
/// Current number of bytes transferred
public private(set) var transferred: Int = 0
/// Create a new transfer rate estimator
///
/// - Parameters:
/// - total: Total bytes to transfer (nil if unknown)
/// - alpha: EMA smoothing factor (default: 0.2). Range: 0.0-1.0
/// - minElapsedTime: Minimum elapsed time before estimates are reliable (default: 2.0s)
/// - minSamples: Minimum samples before estimates are reliable (default: 4)
public init(
total: Int? = nil,
alpha: Double = 0.2,
minElapsedTime: TimeInterval = 2.0,
minSamples: Int = 8
) {
precondition(alpha > 0 && alpha <= 1, "alpha must be in range (0, 1]")
precondition(minSamples >= 0, "minSamples must be >= 0")
self.total = total
self.alpha = alpha
self.minElapsedTime = minElapsedTime
self.minSamples = minSamples
}
public mutating func update(total: Int) -> NetSocket.FileProgress {
return self.update(bytes: max(0, total - self.transferred))
}
/// Update the estimator with a new data sample
///
/// Automatically calculates the duration since the last update.
///
/// - Parameter bytes: Number of bytes transferred in this sample
/// - Returns: Current progress with speed and ETA estimates
public mutating func update(bytes: Int) -> NetSocket.FileProgress {
let clock = ContinuousClock()
let now = clock.now
// Record start time on first sample
if self.startTime == nil {
self.startTime = now
}
// Calculate duration since last update
let duration = self.lastUpdateTime.map { now - $0 } ?? .zero
self.lastUpdateTime = now
// Update transferred count
self.transferred += bytes
// Calculate instantaneous rate for this sample
let seconds: Double = duration / .seconds(1.0)
if seconds > 0 {
let instantRate = Double(bytes) / seconds
self.sampleCount += 1
// Update EMANetSocket
if self.emaBytesPerSecond == 0 {
self.emaBytesPerSecond = instantRate
} else {
self.emaBytesPerSecond += self.alpha * (instantRate - self.emaBytesPerSecond)
}
}
// Determine if we have enough data to trust the estimate
let elapsed = self.startTime.map { now - $0 } ?? .zero
let elapsedSeconds: Double = elapsed / .seconds(1.0)
let haveEstimate = (elapsedSeconds >= self.minElapsedTime || self.sampleCount >= self.minSamples) && self.emaBytesPerSecond > 0
// Calculate ETA if we have both an estimate and a known total
let eta: TimeInterval?
if haveEstimate, let total = self.total {
let remaining = total - self.transferred
eta = remaining > 0 ? TimeInterval(Double(remaining) / self.emaBytesPerSecond) : 0
} else {
eta = nil
}
return NetSocket.FileProgress(
sent: self.transferred,
total: self.total,
bytesPerSecond: haveEstimate ? self.emaBytesPerSecond : nil,
estimatedTimeRemaining: eta
)
}
}
|