feat: implement upload speed calculation and ETA estimation (#2677)
parent
36af01daa6
commit
ecdd684bf1
|
@ -6,6 +6,11 @@ import { fetchURL } from "./utils";
|
||||||
|
|
||||||
const RETRY_BASE_DELAY = 1000;
|
const RETRY_BASE_DELAY = 1000;
|
||||||
const RETRY_MAX_DELAY = 20000;
|
const RETRY_MAX_DELAY = 20000;
|
||||||
|
const SPEED_UPDATE_INTERVAL = 1000;
|
||||||
|
const ALPHA = 0.2;
|
||||||
|
const ONE_MINUS_ALPHA = 1 - ALPHA;
|
||||||
|
const RECENT_SPEEDS_LIMIT = 5;
|
||||||
|
const MB_DIVISOR = 1024 * 1024;
|
||||||
const CURRENT_UPLOAD_LIST = {};
|
const CURRENT_UPLOAD_LIST = {};
|
||||||
|
|
||||||
export async function upload(
|
export async function upload(
|
||||||
|
@ -35,23 +40,48 @@ export async function upload(
|
||||||
"X-Auth": store.state.jwt,
|
"X-Auth": store.state.jwt,
|
||||||
},
|
},
|
||||||
onError: function (error) {
|
onError: function (error) {
|
||||||
|
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
||||||
|
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
||||||
|
}
|
||||||
delete CURRENT_UPLOAD_LIST[filePath];
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
reject("Upload failed: " + error);
|
reject("Upload failed: " + error);
|
||||||
},
|
},
|
||||||
onProgress: function (bytesUploaded) {
|
onProgress: function (bytesUploaded) {
|
||||||
// Emulate ProgressEvent.loaded which is used by calling functions
|
let fileData = CURRENT_UPLOAD_LIST[filePath];
|
||||||
// loaded is specified in bytes (https://developer.mozilla.org/en-US/docs/Web/API/ProgressEvent/loaded)
|
fileData.currentBytesUploaded = bytesUploaded;
|
||||||
|
|
||||||
|
if (!fileData.hasStarted) {
|
||||||
|
fileData.hasStarted = true;
|
||||||
|
fileData.lastProgressTimestamp = Date.now();
|
||||||
|
|
||||||
|
fileData.interval = setInterval(() => {
|
||||||
|
calcProgress(filePath);
|
||||||
|
}, SPEED_UPDATE_INTERVAL);
|
||||||
|
}
|
||||||
if (typeof onupload === "function") {
|
if (typeof onupload === "function") {
|
||||||
onupload({ loaded: bytesUploaded });
|
onupload({ loaded: bytesUploaded });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess: function () {
|
onSuccess: function () {
|
||||||
|
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
||||||
|
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
||||||
|
}
|
||||||
delete CURRENT_UPLOAD_LIST[filePath];
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
resolve();
|
resolve();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
CURRENT_UPLOAD_LIST[filePath] = {
|
||||||
|
upload: upload,
|
||||||
|
recentSpeeds: [],
|
||||||
|
initialBytesUploaded: 0,
|
||||||
|
currentBytesUploaded: 0,
|
||||||
|
currentAverageSpeed: 0,
|
||||||
|
lastProgressTimestamp: null,
|
||||||
|
sumOfRecentSpeeds: 0,
|
||||||
|
hasStarted: false,
|
||||||
|
interval: null,
|
||||||
|
};
|
||||||
upload.start();
|
upload.start();
|
||||||
CURRENT_UPLOAD_LIST[filePath] = upload;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,20 +123,73 @@ function isTusSupported() {
|
||||||
return tus.isSupported === true;
|
return tus.isSupported === true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function abortUpload(filePath) {
|
function computeETA(state) {
|
||||||
const upload = CURRENT_UPLOAD_LIST[filePath];
|
if (state.speedMbyte === 0) {
|
||||||
if (upload) {
|
return Infinity;
|
||||||
upload.abort();
|
|
||||||
delete CURRENT_UPLOAD_LIST[filePath];
|
|
||||||
}
|
}
|
||||||
|
const totalSize = state.sizes.reduce((acc, size) => acc + size, 0);
|
||||||
|
const uploadedSize = state.progress.reduce(
|
||||||
|
(acc, progress) => acc + progress,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const remainingSize = totalSize - uploadedSize;
|
||||||
|
const speedBytesPerSecond = state.speedMbyte * 1024 * 1024;
|
||||||
|
return remainingSize / speedBytesPerSecond;
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeGlobalSpeedAndETA() {
|
||||||
|
let totalSpeed = 0;
|
||||||
|
let totalCount = 0;
|
||||||
|
|
||||||
|
for (let filePath in CURRENT_UPLOAD_LIST) {
|
||||||
|
totalSpeed += CURRENT_UPLOAD_LIST[filePath].currentAverageSpeed;
|
||||||
|
totalCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalCount === 0) return { speed: 0, eta: Infinity };
|
||||||
|
|
||||||
|
const averageSpeed = totalSpeed / totalCount;
|
||||||
|
const averageETA = computeETA(store.state.upload, averageSpeed);
|
||||||
|
|
||||||
|
return { speed: averageSpeed, eta: averageETA };
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcProgress(filePath) {
|
||||||
|
let fileData = CURRENT_UPLOAD_LIST[filePath];
|
||||||
|
|
||||||
|
let elapsedTime = (Date.now() - fileData.lastProgressTimestamp) / 1000;
|
||||||
|
let bytesSinceLastUpdate =
|
||||||
|
fileData.currentBytesUploaded - fileData.initialBytesUploaded;
|
||||||
|
let currentSpeed = bytesSinceLastUpdate / MB_DIVISOR / elapsedTime;
|
||||||
|
|
||||||
|
if (fileData.recentSpeeds.length >= RECENT_SPEEDS_LIMIT) {
|
||||||
|
fileData.sumOfRecentSpeeds -= fileData.recentSpeeds.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
fileData.recentSpeeds.push(currentSpeed);
|
||||||
|
fileData.sumOfRecentSpeeds += currentSpeed;
|
||||||
|
|
||||||
|
let avgRecentSpeed =
|
||||||
|
fileData.sumOfRecentSpeeds / fileData.recentSpeeds.length;
|
||||||
|
fileData.currentAverageSpeed =
|
||||||
|
ALPHA * avgRecentSpeed + ONE_MINUS_ALPHA * fileData.currentAverageSpeed;
|
||||||
|
|
||||||
|
const { speed, eta } = computeGlobalSpeedAndETA();
|
||||||
|
store.commit("setUploadSpeed", speed);
|
||||||
|
store.commit("setETA", eta);
|
||||||
|
|
||||||
|
fileData.initialBytesUploaded = fileData.currentBytesUploaded;
|
||||||
|
fileData.lastProgressTimestamp = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function abortAllUploads() {
|
export function abortAllUploads() {
|
||||||
for (let filePath in CURRENT_UPLOAD_LIST) {
|
for (let filePath in CURRENT_UPLOAD_LIST) {
|
||||||
const upload = CURRENT_UPLOAD_LIST[filePath];
|
if (CURRENT_UPLOAD_LIST[filePath].interval) {
|
||||||
if (upload) {
|
clearInterval(CURRENT_UPLOAD_LIST[filePath].interval);
|
||||||
upload.abort();
|
}
|
||||||
|
if (CURRENT_UPLOAD_LIST[filePath].upload) {
|
||||||
|
CURRENT_UPLOAD_LIST[filePath].upload.abort(true);
|
||||||
|
}
|
||||||
delete CURRENT_UPLOAD_LIST[filePath];
|
delete CURRENT_UPLOAD_LIST[filePath];
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -7,6 +7,10 @@
|
||||||
<div class="card floating">
|
<div class="card floating">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
|
<h2>{{ $t("prompts.uploadFiles", { files: filesInUploadCount }) }}</h2>
|
||||||
|
<div class="upload-info">
|
||||||
|
<div class="upload-speed">{{ uploadSpeed.toFixed(2) }} MB/s</div>
|
||||||
|
<div class="upload-eta">{{ formattedETA }} remaining</div>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
class="action"
|
class="action"
|
||||||
@click="abortAll"
|
@click="abortAll"
|
||||||
|
@ -61,19 +65,39 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters(["filesInUpload", "filesInUploadCount"]),
|
...mapGetters([
|
||||||
...mapMutations(['resetUpload']),
|
"filesInUpload",
|
||||||
|
"filesInUploadCount",
|
||||||
|
"uploadSpeed",
|
||||||
|
"eta",
|
||||||
|
]),
|
||||||
|
...mapMutations(["resetUpload"]),
|
||||||
|
formattedETA() {
|
||||||
|
if (!this.eta || this.eta === Infinity) {
|
||||||
|
return "--:--:--";
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalSeconds = this.eta;
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
totalSeconds %= 3600;
|
||||||
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
|
const seconds = Math.round(totalSeconds % 60);
|
||||||
|
|
||||||
|
return `${hours.toString().padStart(2, "0")}:${minutes
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggle: function () {
|
toggle: function () {
|
||||||
this.open = !this.open;
|
this.open = !this.open;
|
||||||
},
|
},
|
||||||
abortAll() {
|
abortAll() {
|
||||||
if (confirm(this.$t('upload.abortUpload'))) {
|
if (confirm(this.$t("upload.abortUpload"))) {
|
||||||
abortAllUploads();
|
abortAllUploads();
|
||||||
buttons.done('upload');
|
buttons.done("upload");
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.$store.commit('resetUpload');
|
this.$store.commit("resetUpload");
|
||||||
this.$store.commit("setReload", true);
|
this.$store.commit("setReload", true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -49,6 +49,8 @@ const getters = {
|
||||||
currentPromptName: (_, getters) => {
|
currentPromptName: (_, getters) => {
|
||||||
return getters.currentPrompt?.prompt;
|
return getters.currentPrompt?.prompt;
|
||||||
},
|
},
|
||||||
|
uploadSpeed: (state) => state.upload.speedMbyte,
|
||||||
|
eta: (state) => state.upload.eta,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default getters;
|
export default getters;
|
||||||
|
|
|
@ -11,6 +11,8 @@ const state = {
|
||||||
progress: [],
|
progress: [],
|
||||||
queue: [],
|
queue: [],
|
||||||
uploads: {},
|
uploads: {},
|
||||||
|
speedMbyte: 0,
|
||||||
|
eta: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mutations = {
|
const mutations = {
|
||||||
|
|
|
@ -97,12 +97,20 @@ const mutations = {
|
||||||
state.clipboard.key = "";
|
state.clipboard.key = "";
|
||||||
state.clipboard.items = [];
|
state.clipboard.items = [];
|
||||||
},
|
},
|
||||||
|
setUploadSpeed: (state, value) => {
|
||||||
|
state.upload.speedMbyte = value;
|
||||||
|
},
|
||||||
|
setETA(state, value) {
|
||||||
|
state.upload.eta = value;
|
||||||
|
},
|
||||||
resetUpload(state) {
|
resetUpload(state) {
|
||||||
state.upload.uploads = {};
|
state.upload.uploads = {};
|
||||||
state.upload.queue = [];
|
state.upload.queue = [];
|
||||||
state.upload.progress = [];
|
state.upload.progress = [];
|
||||||
state.upload.sizes = [];
|
state.upload.sizes = [];
|
||||||
state.upload.id = 0;
|
state.upload.id = 0;
|
||||||
|
state.upload.speedMbyte = 0;
|
||||||
|
state.upload.eta = 0;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue