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.
233 lines
5.3 KiB
Go
233 lines
5.3 KiB
Go
package backup
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const float64EqualityThreshold = 1e-6
|
|
|
|
func almostEqual(a, b float64) bool {
|
|
if math.IsNaN(a) || math.IsNaN(b) {
|
|
panic("almostEqual passed a NaN")
|
|
}
|
|
return math.Abs(a-b) <= float64EqualityThreshold
|
|
}
|
|
|
|
func TestEstimatorDefault(t *testing.T) {
|
|
var start time.Time
|
|
e := newRateEstimator(start)
|
|
r := e.rate(start)
|
|
if math.IsNaN(r) || r != 0 {
|
|
t.Fatalf("e.Rate == %v, want zero", r)
|
|
}
|
|
r = e.rate(start.Add(time.Hour))
|
|
if math.IsNaN(r) || r != 0 {
|
|
t.Fatalf("e.Rate == %v, want zero", r)
|
|
}
|
|
}
|
|
|
|
func TestEstimatorSimple(t *testing.T) {
|
|
var when time.Time
|
|
type testcase struct {
|
|
bytes uint64
|
|
when time.Duration
|
|
rate float64
|
|
}
|
|
|
|
cases := []testcase{
|
|
{0, 0, 0},
|
|
{1, time.Second, 1},
|
|
{60, time.Second, 60},
|
|
{60, time.Minute, 1},
|
|
}
|
|
for _, c := range cases {
|
|
e := newRateEstimator(when)
|
|
e.recordBytes(when.Add(time.Second), c.bytes)
|
|
rate := e.rate(when.Add(c.when))
|
|
if !almostEqual(rate, c.rate) {
|
|
t.Fatalf("e.Rate == %v, want %v (testcase %+v)", rate, c.rate, c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBucketWidth(t *testing.T) {
|
|
var when time.Time
|
|
|
|
// Recording byte transfers within a bucket width's time window uses one
|
|
// bucket.
|
|
e := newRateEstimator(when)
|
|
e.recordBytes(when, 1)
|
|
e.recordBytes(when.Add(bucketWidth-time.Nanosecond), 1)
|
|
if e.buckets.Len() != 1 {
|
|
t.Fatalf("e.buckets.Len() is %d, want 1", e.buckets.Len())
|
|
}
|
|
b := e.buckets.Back().Value.(*rateBucket)
|
|
if b.totalBytes != 2 {
|
|
t.Fatalf("b.totalBytes is %d, want 2", b.totalBytes)
|
|
}
|
|
if b.end != when.Add(bucketWidth) {
|
|
t.Fatalf("b.end is %v, want %v", b.end, when.Add(bucketWidth))
|
|
}
|
|
|
|
// Recording a byte outside the bucket width causes another bucket.
|
|
e.recordBytes(when.Add(bucketWidth), 1)
|
|
if e.buckets.Len() != 2 {
|
|
t.Fatalf("e.buckets.Len() is %d, want 2", e.buckets.Len())
|
|
}
|
|
b = e.buckets.Back().Value.(*rateBucket)
|
|
if b.totalBytes != 1 {
|
|
t.Fatalf("b.totalBytes is %d, want 1", b.totalBytes)
|
|
}
|
|
if b.end != when.Add(2*bucketWidth) {
|
|
t.Fatalf("b.end is %v, want %v", b.end, when.Add(bucketWidth))
|
|
}
|
|
|
|
// Recording a byte after a longer delay creates a sparse bucket list.
|
|
e.recordBytes(when.Add(time.Hour+time.Millisecond), 7)
|
|
if e.buckets.Len() != 3 {
|
|
t.Fatalf("e.buckets.Len() is %d, want 3", e.buckets.Len())
|
|
}
|
|
b = e.buckets.Back().Value.(*rateBucket)
|
|
if b.totalBytes != 7 {
|
|
t.Fatalf("b.totalBytes is %d, want 7", b.totalBytes)
|
|
}
|
|
if b.end != when.Add(time.Hour+time.Millisecond+time.Second) {
|
|
t.Fatalf("b.end is %v, want %v", b.end, when.Add(time.Hour+time.Millisecond+time.Second))
|
|
}
|
|
}
|
|
|
|
type chunk struct {
|
|
repetitions uint64 // repetition count
|
|
bytes uint64 // byte count (every second)
|
|
}
|
|
|
|
func applyChunk(c chunk, t time.Time, e *rateEstimator) time.Time {
|
|
for i := uint64(0); i < c.repetitions; i++ {
|
|
e.recordBytes(t, c.bytes)
|
|
t = t.Add(time.Second)
|
|
}
|
|
return t
|
|
}
|
|
|
|
func applyChunks(chunks []chunk, t time.Time, e *rateEstimator) time.Time {
|
|
for _, c := range chunks {
|
|
t = applyChunk(c, t, e)
|
|
}
|
|
return t
|
|
}
|
|
|
|
func TestEstimatorResponsiveness(t *testing.T) {
|
|
type testcase struct {
|
|
description string
|
|
chunks []chunk
|
|
rate float64
|
|
}
|
|
|
|
cases := []testcase{
|
|
{
|
|
"1000 bytes/sec over one second",
|
|
[]chunk{
|
|
{1, 1000},
|
|
},
|
|
1000,
|
|
},
|
|
{
|
|
"1000 bytes/sec over one minute",
|
|
[]chunk{
|
|
{60, 1000},
|
|
},
|
|
1000,
|
|
},
|
|
{
|
|
"1000 bytes/sec for 10 seconds, then 2000 bytes/sec for 10 seconds",
|
|
[]chunk{
|
|
{10, 1000},
|
|
{10, 2000},
|
|
},
|
|
1500,
|
|
},
|
|
{
|
|
"1000 bytes/sec for one minute, then 2000 bytes/sec for one minute",
|
|
[]chunk{
|
|
{60, 1000},
|
|
{60, 2000},
|
|
},
|
|
1500,
|
|
},
|
|
{
|
|
"rate doubles after 30 seconds",
|
|
[]chunk{
|
|
{30, minRateEstimatorBytes},
|
|
{90, 2 * minRateEstimatorBytes},
|
|
},
|
|
minRateEstimatorBytes * 1.75,
|
|
},
|
|
{
|
|
"rate doubles after 31 seconds",
|
|
[]chunk{
|
|
{31, minRateEstimatorBytes},
|
|
{90, 2 * minRateEstimatorBytes},
|
|
},
|
|
// The expected rate is the same as the prior test case because the
|
|
// first second has rolled off the estimator.
|
|
minRateEstimatorBytes * 1.75,
|
|
},
|
|
{
|
|
"rate doubles after 90 seconds",
|
|
[]chunk{
|
|
{90, minRateEstimatorBytes},
|
|
{90, 2 * minRateEstimatorBytes},
|
|
},
|
|
// The expected rate is the same as the prior test case because the
|
|
// first 60 seconds have rolled off the estimator.
|
|
minRateEstimatorBytes * 1.75,
|
|
},
|
|
{
|
|
"rate doubles for two full minutes",
|
|
[]chunk{
|
|
{60, minRateEstimatorBytes},
|
|
{120, 2 * minRateEstimatorBytes},
|
|
},
|
|
2 * minRateEstimatorBytes,
|
|
},
|
|
{
|
|
"rate falls to zero",
|
|
[]chunk{
|
|
{30, minRateEstimatorBytes},
|
|
{30, 0},
|
|
},
|
|
minRateEstimatorBytes / 2,
|
|
},
|
|
{
|
|
"rate falls to zero for extended time",
|
|
[]chunk{
|
|
{60, 1000},
|
|
{300, 0},
|
|
},
|
|
1000 * 60 / (60 + 300.0),
|
|
},
|
|
{
|
|
"rate falls to zero for extended time (from high rate)",
|
|
[]chunk{
|
|
{2 * minRateEstimatorBuckets, minRateEstimatorBytes},
|
|
{300, 0},
|
|
},
|
|
// Expect that only minRateEstimatorBuckets buckets are used in the
|
|
// rate estimate.
|
|
minRateEstimatorBytes * minRateEstimatorBuckets /
|
|
(minRateEstimatorBuckets + 300.0),
|
|
},
|
|
}
|
|
|
|
for i, c := range cases {
|
|
var w time.Time
|
|
e := newRateEstimator(w)
|
|
w = applyChunks(c.chunks, w, e)
|
|
r := e.rate(w)
|
|
if !almostEqual(r, c.rate) {
|
|
t.Fatalf("e.Rate == %f, want %f (testcase %d %+v)", r, c.rate, i, c)
|
|
}
|
|
}
|
|
}
|