diff --git a/static/js/upload.js b/static/js/upload.js index 9331cc5..d45ec86 100644 --- a/static/js/upload.js +++ b/static/js/upload.js @@ -1,5 +1,7 @@ const FILE_CHUNK_SIZE = 16384; const MAX_FILES = 256; +const SAMPLE_WINDOW = 100; +const STALL_THRESHOLD = 1; let files = []; @@ -8,6 +10,7 @@ let fileIndex = 0; let byteIndex = 0; let bytesSent = 0; let totalBytes = 0; +let timestamps = []; let maxSize = null; @@ -156,6 +159,7 @@ function beginUpload() { fileIndex = 0; byteIndex = 0; bytesSent = 0; + timestamps = []; let websocketUrl = new URL('upload', window.location); websocketUrl.protocol = (window.location.protocol === 'http:') ? 'ws:' : 'wss:'; @@ -249,6 +253,16 @@ function sendData() { socket.send(data); byteIndex = endpoint; bytesSent += data.size; + + // It's ok if the monotonically increasing fields like + // percentage are updating super quickly, but it's awkward for + // rate and ETA + const now = Date.now() / 1000; + if (timestamps.length === 0 || now - timestamps.at(-1)[0] > 1) { + timestamps.push([now, bytesSent]); + if (timestamps.length > SAMPLE_WINDOW) { timestamps.shift(); } + } + updateProgress(); } else { fileIndex += 1; @@ -264,7 +278,22 @@ function updateProgress() { } else { percentage = `${(bytesSent*100/totalBytes).toFixed(1)}%`; } - progress.textContent = `${percentage} (${displaySize(bytesSent)}/${displaySize(totalBytes)})`; + let progressString = `${percentage} - ${displaySize(bytesSent)}/${displaySize(totalBytes)}`; + if (timestamps.length >= 2) { + const start = timestamps.at(0); + const end = timestamps.at(-1); + if (end[0] - start[0] > 0) { + const rate = (end[1] - start[1])/(end[0] - start[0]); + if (rate > STALL_THRESHOLD) { + // Use the value from timestamps rather than bytesSent to avoid awkward UI thrashing + const remaining = (totalBytes - end[1]) / rate; + progressString += ` - ${displaySize(rate)}/s - ${displayTime(remaining)} remaining`; + } else { + progressString += " - stalled"; + } + } + } + progress.textContent = progressString; progressBar.style.backgroundSize = percentage; const fileEntries = Array.from(fileList.children); diff --git a/static/js/util.js b/static/js/util.js index b864d74..def6f80 100644 --- a/static/js/util.js +++ b/static/js/util.js @@ -1,14 +1,35 @@ -const UNITS = [ +const SIZE_UNITS = [ { name: 'GB', size: Math.pow(10, 9) }, { name: 'MB', size: Math.pow(10, 6) }, { name: 'KB', size: Math.pow(10, 3) }, ]; function displaySize(bytes) { - for (const unit of UNITS) { + for (const unit of SIZE_UNITS) { if (bytes >= unit.size) { return `${(bytes / unit.size).toFixed(1)}${unit.name}`; } } return `${bytes}B`; } + +const TIME_UNITS = [ + { name: 'd', size: 86400 }, + { name: 'h', size: 3600 }, + { name: 'm', size: 60 }, + { name: 's', size: 1 }, +]; + +function displayTime(seconds) { + let result = ""; + for (const unit of TIME_UNITS) { + if (seconds >= unit.size || result !== "") { + result += `${Math.floor(seconds / unit.size)}${unit.name}`; + seconds = seconds % unit.size; + } + } + if (result === "") { + result = "0s"; + } + return result; +}