Every now and again (especially when training a model), I need to have a guesstimate as to how long a "step" takes, and how long the process will take, so I wrote myself a little piece of code that does that. Because I've had the question multiple times (and because I think everyone codes their own after a while), here's mine. Feel free to use it
/// Structure that keeps track of the time it takes to complete steps, to average or estimate the remaining time
public struct TimeRecord {
/// The number of steps to keep for averaging. 5 is a decent default, increase or decrease as needed
/// Minimum for average is 2, obvioulsy
public var smoothing: Int = 5 {
didSet {
smoothing = max(smoothing, 2) // minimum 2 values
}
}
/// dates for the steps
private var dates : [Date] = []
/// formatter for debug print and/or display
private var formatter = DateComponentsFormatter()
public var formatterStyle : DateComponentsFormatter.UnitsStyle {
didSet {
formatter.allowedUnits = [.hour, .minute, .second, .nanosecond]
formatter.unitsStyle = formatterStyle
}
}
public init(smoothing s: Int = 5, style fs: DateComponentsFormatter.UnitsStyle = .positional) {
smoothing = max(s, 2)
formatterStyle = fs
formatter = DateComponentsFormatter()
// not available everywhere
// formatter.allowedUnits = [.hour, .minute, .second, .nanosecond]
formatter.allowedUnits = [.hour, .minute, .second]
formatter.zeroFormattingBehavior = .pad
formatter.unitsStyle = fs
}
/// adds the record for a step
/// - param d: the date of the step. If unspecified, current date is taken
mutating func addRecord(_ d: Date? = nil) {
if let d = d { dates.append(d) }
else { dates.append(Date()) }
while(dates.count > smoothing) { dates.remove(at: 0) }
}
/// gives the average delta between two steps (in seconds)
var averageDelta : Double {
if dates.count <= 1 { return 0.0 }
var totalTime = 0.0
for i in 1..<dates.count {
totalTime += dates[i].timeIntervalSince(dates[i-1])
}
return totalTime/Double(dates.count)
}
/// gives the average delta between two steps in human readable form
/// - see formatterStyle for options, default is "02:46:40"
var averageDeltaHumanReadable : String {
let delta = averageDelta
return formatter.string(from: delta) ?? ""
}
/// given a number of remaining steps, gives an estimate of the time left on the process (in s)
func estimatedTimeRemaining(_ steps: Int) -> Double {
return Double(steps) * averageDelta
}
/// given a number of remaining steps, gives an estimate of the time left on the process in human readable form
/// - see formatterStyle for options, default is "02:46:40"
func estimatedTimeRemainingHumanReadable(_ steps: Int) -> String {
let delta = estimatedTimeRemaining(steps)
return formatter.string(from: delta) ?? ""
}
}
When I train a model, I tend to use it that way:
// prepare model
var tt = TimeRecord()
tt.addRecord()
while currentEpoch < maxEpochs {
// train the model
tt.addRecord()
if currentEpoch > 0 && currentEpoch % 5 == 0 {
print(tt.averageDeltaHumanReadable + " per epoch, "
+ tt.(estimatedTimeRemainingHumanReadable(maxEpochs - currentEpoch) + " remaining"
)
}
}