feat: implement upload speed calculation and ETA estimation (#2677)

pull/2715/head
M A E R Y O 2023-09-15 07:41:36 +09:00 committed by GitHub
parent 36af01daa6
commit ecdd684bf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 138 additions and 19 deletions

View File

@ -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];
} }
}
} }

View File

@ -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);
} }
}, },

View File

@ -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;

View File

@ -11,6 +11,8 @@ const state = {
progress: [], progress: [],
queue: [], queue: [],
uploads: {}, uploads: {},
speedMbyte: 0,
eta: 0,
}; };
const mutations = { const mutations = {

View File

@ -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;
}, },
}; };