restic/internal/ui/backup/rate_estimator.go
2023-06-08 20:24:21 +02:00

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 estimator 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
}