fix: SpeedTracker uses monotonic bytes + 15s smoothing for accurate speed display
Speed display was wildly inaccurate during parallel multipart uploads because: 1. Parallel chunk progress events caused non-monotonic byte counts 2. The 5-second smoothing window was too short for bursty uploads 3. Speed dropped to near-zero between chunk boundaries Fixes: - Enforce monotonic byte tracking via _peak guard - Extend sliding window from 5s to 15s - Cache last good speed value for display continuity - Require >= 1s of data before calculating (was 0.5s)
This commit is contained in:
parent
5113adb635
commit
2e3ac69f97
1 changed files with 16 additions and 6 deletions
|
|
@ -1135,28 +1135,38 @@ async function startUpload() {
|
||||||
// Falls back to server-proxied chunked upload if presigned fails.
|
// Falls back to server-proxied chunked upload if presigned fails.
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// Upload speed tracker — smoothed speed + ETA
|
// Upload speed tracker — smoothed speed + ETA
|
||||||
|
// Uses monotonically-increasing cumulative bytes with a 15-second sliding
|
||||||
|
// window so that parallel chunk uploads don't cause wild speed fluctuations.
|
||||||
class SpeedTracker {
|
class SpeedTracker {
|
||||||
constructor(totalBytes) {
|
constructor(totalBytes) {
|
||||||
this.total = totalBytes;
|
this.total = totalBytes;
|
||||||
this.startTime = Date.now();
|
this.startTime = Date.now();
|
||||||
this.samples = []; // {time, bytes}
|
this.samples = []; // {time, bytes} — cumulative bytes only
|
||||||
this.uploaded = 0;
|
this.uploaded = 0;
|
||||||
|
this._peak = 0; // highest cumulative value seen (monotonic guard)
|
||||||
|
this._lastSpeed = 0; // cache last good speed for display continuity
|
||||||
}
|
}
|
||||||
update(bytes) {
|
update(bytes) {
|
||||||
|
// Enforce monotonic: with parallel chunks, in-flight progress can
|
||||||
|
// temporarily report a lower total when a new chunk starts from 0.
|
||||||
|
if (bytes < this._peak) return;
|
||||||
|
this._peak = bytes;
|
||||||
this.uploaded = bytes;
|
this.uploaded = bytes;
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
this.samples.push({ time: now, bytes });
|
this.samples.push({ time: now, bytes });
|
||||||
// Keep last 5 seconds of samples for smoothing
|
// 15-second sliding window — long enough to smooth out inter-chunk gaps
|
||||||
const cutoff = now - 5000;
|
const cutoff = now - 15000;
|
||||||
this.samples = this.samples.filter(s => s.time >= cutoff);
|
this.samples = this.samples.filter(s => s.time >= cutoff);
|
||||||
}
|
}
|
||||||
speed() {
|
speed() {
|
||||||
if (this.samples.length < 2) return 0;
|
if (this.samples.length < 2) return this._lastSpeed;
|
||||||
const first = this.samples[0];
|
const first = this.samples[0];
|
||||||
const last = this.samples[this.samples.length - 1];
|
const last = this.samples[this.samples.length - 1];
|
||||||
const dt = (last.time - first.time) / 1000;
|
const dt = (last.time - first.time) / 1000;
|
||||||
if (dt < 0.5) return 0;
|
if (dt < 1) return this._lastSpeed; // need >= 1 s of data
|
||||||
return (last.bytes - first.bytes) / dt;
|
const s = (last.bytes - first.bytes) / dt;
|
||||||
|
if (s > 0) this._lastSpeed = s;
|
||||||
|
return this._lastSpeed;
|
||||||
}
|
}
|
||||||
eta() {
|
eta() {
|
||||||
const s = this.speed();
|
const s = this.speed();
|
||||||
Loading…
Reference in a new issue