forked from TrueCloudLab/restic
0372c7ef04
The ETA restic displays was based on a rate computed across the entire backup operation. Often restic can progress at uneven rates. In the worst case, restic progresses over most of the backup at a very high rate and then finds new data to back up. The displayed ETA is then unrealistic and never adapts. Restic now estimates the transfer rate based on a sliding window, with the goal of adapting to observed changes in rate. To avoid wild changes in the estimate, several heuristics are used to keep the sliding window wide enough to be relatively stable.
98 lines
3 KiB
Go
98 lines
3 KiB
Go
package backup
|
|
|
|
import (
|
|
"container/list"
|
|
"time"
|
|
)
|
|
|
|
// rateBucket represents a one second window of recorded progress.
|
|
type rateBucket struct {
|
|
totalBytes uint64
|
|
end time.Time // the end of the time window, exclusive
|
|
}
|
|
|
|
// rateEstimator represents an estimate of the time to complete an operation.
|
|
type rateEstimator struct {
|
|
buckets *list.List
|
|
start time.Time
|
|
totalBytes uint64
|
|
}
|
|
|
|
// newRateEstimator returns an esimator initialized to a presumed start time.
|
|
func newRateEstimator(start time.Time) *rateEstimator {
|
|
return &rateEstimator{buckets: list.New(), start: start}
|
|
}
|
|
|
|
// See trim(), below.
|
|
const (
|
|
bucketWidth = time.Second
|
|
minRateEstimatorBytes = 100 * 1000 * 1000
|
|
minRateEstimatorBuckets = 20
|
|
minRateEstimatorMinutes = 2
|
|
)
|
|
|
|
// trim removes the oldest history from the estimator assuming a given
|
|
// current time.
|
|
func (r *rateEstimator) trim(now time.Time) {
|
|
// The estimator retains byte transfer counts over a two minute window.
|
|
// However, to avoid removing too much history when transfer rates are
|
|
// low, the estimator also retains a minimum number of processed bytes
|
|
// across a minimum number of buckets. An operation that is processing a
|
|
// significant number of bytes per second will typically retain only a
|
|
// two minute window's worth of information. One that is making slow
|
|
// progress, such as one being over a rate limited connection, typically
|
|
// observes bursts of updates as infrequently as every ten or twenty
|
|
// seconds, in which case the other limiters will kick in. This heuristic
|
|
// avoids wildly fluctuating estimates over rate limited connections.
|
|
start := now.Add(-minRateEstimatorMinutes * time.Minute)
|
|
|
|
for e := r.buckets.Front(); e != nil; e = r.buckets.Front() {
|
|
if r.buckets.Len() <= minRateEstimatorBuckets {
|
|
break
|
|
}
|
|
b := e.Value.(*rateBucket)
|
|
if b.end.After(start) {
|
|
break
|
|
}
|
|
total := r.totalBytes - b.totalBytes
|
|
if total < minRateEstimatorBytes {
|
|
break
|
|
}
|
|
r.start = b.end
|
|
r.totalBytes = total
|
|
r.buckets.Remove(e)
|
|
}
|
|
}
|
|
|
|
// recordBytes records the transfer of a number of bytes at a given
|
|
// time. Times passed in successive calls should advance monotonically (as
|
|
// is the case with time.Now().
|
|
func (r *rateEstimator) recordBytes(now time.Time, bytes uint64) {
|
|
if bytes == 0 {
|
|
return
|
|
}
|
|
var tail *rateBucket
|
|
if r.buckets.Len() > 0 {
|
|
tail = r.buckets.Back().Value.(*rateBucket)
|
|
}
|
|
if tail == nil || !tail.end.After(now) {
|
|
// The new bucket holds measurements in the time range [now .. now+1sec).
|
|
tail = &rateBucket{end: now.Add(bucketWidth)}
|
|
r.buckets.PushBack(tail)
|
|
}
|
|
tail.totalBytes += bytes
|
|
r.totalBytes += bytes
|
|
r.trim(now)
|
|
}
|
|
|
|
// rate returns an estimated bytes per second rate at a given time, or zero
|
|
// if there is not enough data to compute a rate.
|
|
func (r *rateEstimator) rate(now time.Time) float64 {
|
|
r.trim(now)
|
|
if !r.start.Before(now) {
|
|
return 0
|
|
}
|
|
elapsed := float64(now.Sub(r.start)) / float64(time.Second)
|
|
rate := float64(r.totalBytes) / elapsed
|
|
return rate
|
|
}
|