From 3c64354a0d27cd71176b83df7c57754cea97bd2d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 10 Jun 2026 08:44:39 +0000 Subject: [PATCH] perf(upload): run multi-file selections through the 10-worker pool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `uploadFiles` sent files one at a time (await per file), so dropping N files meant N sequential round-trips. Reuse the bounded-concurrency pool already proven in `uploadFolderEntries` (CONCURRENCY = 10): independent files now upload up to 10 at a time — ~10x faster for many small files. Per-file progress (XHR → bell), the legacy progress bar, timeout notifications and the quota short-circuit are preserved. On a quota error the pool stops pulling new files while in-flight uploads finish (matching `uploadFolderEntries`) instead of the old hard `break`. https://claude.ai/code/session_01Dp3oWon5GBMVn4j3QXZdgx --- static/js/features/files/fileOperations.js | 39 +++++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/static/js/features/files/fileOperations.js b/static/js/features/files/fileOperations.js index 8cb6a3a7..de7f5251 100644 --- a/static/js/features/files/fileOperations.js +++ b/static/js/features/files/fileOperations.js @@ -349,7 +349,9 @@ const fileOps = { } // Filter out unreadable entries (typically dropped folders/placeholders) + /** @type {File[]} */ const readableFiles = []; + /** @type {string[]} */ const skippedEntries = []; for (const f of originalFiles) { // eslint-disable-next-line no-await-in-loop @@ -386,13 +388,21 @@ const fileOps = { let uploadedCount = 0; let successCount = 0; + let quotaStop = false; - for (let i = 0; i < totalFiles; i++) { - const file = readableFiles[i]; + const targetFolderId = app.currentPath || app.userHomeFolderId; + + /** + * Upload a single readable file by index. Shared counters are + * mutated here; safe because JS runs the workers cooperatively + * (no true parallelism between awaits). + * @param {number} idx + */ + const uploadOneFile = async (idx) => { + if (quotaStop) return; + const file = readableFiles[idx]; const formData = new FormData(); - - const targetFolderId = app.currentPath || app.userHomeFolderId; if (targetFolderId) formData.append('folder_id', targetFolderId); formData.append('file', file); @@ -436,6 +446,8 @@ const fileOps = { }); } if (result.isQuotaError) { + // Stop pulling new files; in-flight uploads still finish. + quotaStop = true; const msg = result.errorMsg || i18n.t('storage_quota_exceeded'); if (notifications) { notifications.addNotification({ @@ -445,10 +457,27 @@ const fileOps = { text: msg }); } - break; } } + }; + + // Pool-based concurrency: keep up to CONCURRENCY uploads in flight + // instead of one at a time (mirrors uploadFolderEntries). Files are + // independent, so this is ~CONCURRENCY× faster for many small files. + const CONCURRENCY = 10; + let nextIdx = 0; + const runNext = async () => { + while (nextIdx < totalFiles && !quotaStop) { + const idx = nextIdx++; + await uploadOneFile(idx); + } + }; + + const workers = []; + for (let w = 0; w < Math.min(CONCURRENCY, totalFiles); w++) { + workers.push(runNext()); } + await Promise.all(workers); // All done this._finishUploadToast(successCount, totalFiles);