common: fix precision loss in CalculateETA for sub-ms durations

currently, CalculateETA truncates elapsed time to milliseconds using
elapsed.Milliseconds(). this causes the function to return a zero ETA
for fast operations (<1ms) and effectively discards precision.

this change switches the speed calculation to use elapsed.Seconds()
(float64), ensuring accurate ETA projections for high-throughput,
low-latency updates.

Fixes #33512
This commit is contained in:
bit2swaz 2026-01-03 00:08:14 +05:30
parent b635e0632c
commit 2f4426776e
2 changed files with 66 additions and 8 deletions

View file

@ -21,10 +21,13 @@ import "time"
// CalculateETA calculates the estimated remaining time based on the
// number of finished task, remaining task, and the time cost for finished task.
func CalculateETA(done, left uint64, elapsed time.Duration) time.Duration {
if done == 0 || elapsed.Milliseconds() == 0 {
if done == 0 || elapsed == 0 {
return 0
}
speed := float64(done) / float64(elapsed.Milliseconds())
return time.Duration(float64(left)/speed) * time.Millisecond
speed := float64(done) / elapsed.Seconds()
if speed == 0 {
return 0
}
return time.Duration(float64(left) / speed * float64(time.Second))
}

View file

@ -46,14 +46,69 @@ func TestCalculateETA(t *testing.T) {
name: "@Jolly23 's case",
args: args{done: 16858580, left: 41802252, elapsed: 66179848 * time.Millisecond},
want: 164098440 * time.Millisecond,
// wrong msg: msg="Indexing state history" processed=16858580 left=41802252 elapsed=18h22m59.848s eta=11h36m42.252s
// should be around 45.58 hours
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := CalculateETA(tt.args.done, tt.args.left, tt.args.elapsed); got != tt.want {
t.Errorf("CalculateETA() = %v ms, want %v ms", got.Milliseconds(), tt.want)
got := CalculateETA(tt.args.done, tt.args.left, tt.args.elapsed)
// Allow 1ms tolerance for rounding differences
diff := got - tt.want
if diff < 0 {
diff = -diff
}
if diff > time.Millisecond {
t.Errorf("CalculateETA() = %v, want %v", got, tt.want)
}
})
}
}
// TestCalculateETA_SubMillisecond verifies that CalculateETA correctly handles
// operations completing in less than 1ms without returning zero or panicking.
func TestCalculateETA_SubMillisecond(t *testing.T) {
// Simulate processing 1000 items in 500 microseconds
done := uint64(1000)
left := uint64(1000)
elapsed := 500 * time.Microsecond
eta := CalculateETA(done, left, elapsed)
if eta == 0 {
t.Fatalf("CalculateETA returned 0 for sub-millisecond elapsed time, expected non-zero ETA")
}
// At the same rate, processing another 1000 items should take ~500µs
expectedETA := 500 * time.Microsecond
tolerance := 100 * time.Microsecond
if eta < expectedETA-tolerance || eta > expectedETA+tolerance {
t.Errorf("CalculateETA = %v, expected approximately %v (within %v)", eta, expectedETA, tolerance)
}
}
// TestCalculateETA_EdgeCases verifies correct behavior for boundary conditions
func TestCalculateETA_EdgeCases(t *testing.T) {
tests := []struct {
name string
done uint64
left uint64
elapsed time.Duration
expected time.Duration
}{
{"zero done", 0, 100, time.Second, 0},
{"zero elapsed", 100, 100, 0, 0},
{"zero left", 100, 0, time.Second, 0},
{"instant completion", 1000, 1000, 1 * time.Nanosecond, time.Nanosecond},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := CalculateETA(tt.done, tt.left, tt.elapsed)
if tt.expected == 0 && result != 0 {
t.Errorf("expected 0, got %v", result)
}
if tt.expected != 0 && result == 0 {
t.Errorf("expected non-zero, got 0")
}
})
}