perf: O(1) lookups for selection buttons, applySummaryResults, file-drop dedup; batched upload log
Four more wins targeting batch-heavy paths:
- updateQueueActionButtons replaced three O(n) queueJobs.some() scans
with a single O(|selection|) pass over selectedJobIds, using the
existing _jobIndexById map. Selection change cost on a 1000-job
queue drops from ~3000 comparisons to |selection|.
- applySummaryResults built a (fileName+hoster)→job Map once per call
instead of running queueJobs.find() per result. Big batches
(hundreds of files × multiple hosters) no longer scale O(n²).
- addPathsToQueue and the folder-monitor auto-queue path built their
dedup Set up front instead of running .find() per incoming path.
Picking a folder with thousands of files now dedups in O(n+m)
instead of O(n×m).
- appendUploadLog became async + buffered like debugLog. A burst of
20 files completing within a second becomes one fs.appendFile
instead of 20 fs.appendFileSync that each blocked the main event
loop. Fallback ladder (primary → Desktop → userData) is preserved;
pending buffer flushes synchronously on before-quit.
This commit is contained in:
parent
73e7190913
commit
879f6ade0e
113
main.js
113
main.js
@ -159,54 +159,70 @@ function getSafeDesktopDir() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let _uploadLogFallbackWarned = false;
|
let _uploadLogFallbackWarned = false;
|
||||||
|
// Buffer upload-log lines so a burst of completing jobs (e.g. 20 files finishing
|
||||||
|
// within a second) becomes one file write instead of 20 sync writes.
|
||||||
|
const _uploadLogBuffer = [];
|
||||||
|
let _uploadLogFlushTimer = null;
|
||||||
|
let _uploadLogWriting = false;
|
||||||
|
|
||||||
|
function _resolveUploadLogTarget() {
|
||||||
|
// Try primary → desktop → userData, mirror the original fallback ladder.
|
||||||
|
const primary = getLogFilePath();
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(primary), { recursive: true });
|
||||||
|
return { path: primary, isFallback: false };
|
||||||
|
} catch (err) {
|
||||||
|
debugLog(`uploadLog primary dir unavailable (${err.message})`);
|
||||||
|
}
|
||||||
|
const desktop = getSafeDesktopDir();
|
||||||
|
if (desktop) {
|
||||||
|
try {
|
||||||
|
const p = buildFallbackLogName(desktop);
|
||||||
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||||
|
return { path: p, isFallback: true };
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const p = buildFallbackLogName(app.getPath('userData'));
|
||||||
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||||
|
return { path: p, isFallback: true };
|
||||||
|
} catch (err) {
|
||||||
|
debugLog(`uploadLog: no writable target (${err.message})`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _flushUploadLog() {
|
||||||
|
if (_uploadLogWriting || _uploadLogBuffer.length === 0) return;
|
||||||
|
const target = _resolveUploadLogTarget();
|
||||||
|
if (!target) { _uploadLogBuffer.length = 0; return; }
|
||||||
|
const chunk = _uploadLogBuffer.join('');
|
||||||
|
_uploadLogBuffer.length = 0;
|
||||||
|
_uploadLogWriting = true;
|
||||||
|
fs.appendFile(target.path, chunk, 'utf-8', (err) => {
|
||||||
|
_uploadLogWriting = false;
|
||||||
|
if (err) {
|
||||||
|
debugLog(`uploadLog append failed: ${err.message}`);
|
||||||
|
} else if (target.isFallback && !_uploadLogFallbackWarned) {
|
||||||
|
_uploadLogFallbackWarned = true;
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.webContents.send('upload-log-fallback', { fallbackPath: target.path });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_uploadLogBuffer.length) setImmediate(_flushUploadLog);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function appendUploadLog(hoster, link, fileName) {
|
function appendUploadLog(hoster, link, fileName) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const pad = (n) => String(n).padStart(2, '0');
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
const dateStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
const dateStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
||||||
const line = `${dateStr}|${hoster}|${link}||${fileName}|\n`;
|
_uploadLogBuffer.push(`${dateStr}|${hoster}|${link}||${fileName}|\n`);
|
||||||
|
if (!_uploadLogFlushTimer) {
|
||||||
const tryWrite = (p) => {
|
_uploadLogFlushTimer = setTimeout(() => {
|
||||||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
_uploadLogFlushTimer = null;
|
||||||
fs.appendFileSync(p, line, 'utf-8');
|
_flushUploadLog();
|
||||||
};
|
}, 500);
|
||||||
|
|
||||||
try {
|
|
||||||
tryWrite(getLogFilePath());
|
|
||||||
return;
|
|
||||||
} catch (err) {
|
|
||||||
debugLog(`appendUploadLog primary failed (${err.message}); trying desktop fallback`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback 1: current user's Desktop (visible, easy to find).
|
|
||||||
const desktop = getSafeDesktopDir();
|
|
||||||
if (desktop) {
|
|
||||||
try {
|
|
||||||
const fallbackPath = buildFallbackLogName(desktop);
|
|
||||||
tryWrite(fallbackPath);
|
|
||||||
if (!_uploadLogFallbackWarned) {
|
|
||||||
_uploadLogFallbackWarned = true;
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.webContents.send('upload-log-fallback', { fallbackPath });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
} catch (err) {
|
|
||||||
debugLog(`appendUploadLog desktop fallback failed (${err.message}); trying userData`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback 2: userData (always writable by the current user).
|
|
||||||
try {
|
|
||||||
const fallbackPath = buildFallbackLogName(app.getPath('userData'));
|
|
||||||
tryWrite(fallbackPath);
|
|
||||||
if (!_uploadLogFallbackWarned) {
|
|
||||||
_uploadLogFallbackWarned = true;
|
|
||||||
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
||||||
mainWindow.webContents.send('upload-log-fallback', { fallbackPath });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
debugLog(`appendUploadLog all fallbacks failed: ${err.message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -751,13 +767,20 @@ app.on('before-quit', () => {
|
|||||||
destroyCaptureWindow();
|
destroyCaptureWindow();
|
||||||
} catch {}
|
} catch {}
|
||||||
destroyDropTargetWindow();
|
destroyDropTargetWindow();
|
||||||
// Flush pending debug-log buffer synchronously so no lines are lost.
|
// Flush pending log buffers synchronously so no lines are lost.
|
||||||
try {
|
try {
|
||||||
if (_debugLogBuffer.length) {
|
if (_debugLogBuffer.length) {
|
||||||
fs.appendFileSync(getDebugLogPath(), _debugLogBuffer.join(''), 'utf-8');
|
fs.appendFileSync(getDebugLogPath(), _debugLogBuffer.join(''), 'utf-8');
|
||||||
_debugLogBuffer.length = 0;
|
_debugLogBuffer.length = 0;
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
try {
|
||||||
|
if (_uploadLogBuffer.length) {
|
||||||
|
const target = _resolveUploadLogTarget();
|
||||||
|
if (target) fs.appendFileSync(target.path, _uploadLogBuffer.join(''), 'utf-8');
|
||||||
|
_uploadLogBuffer.length = 0;
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- IPC Handlers ---
|
// --- IPC Handlers ---
|
||||||
|
|||||||
@ -130,13 +130,16 @@ async function init() {
|
|||||||
if (fmHosters.length > 0) {
|
if (fmHosters.length > 0) {
|
||||||
// Pre-selected hosters: set them as active selection and add directly to queue
|
// Pre-selected hosters: set them as active selection and add directly to queue
|
||||||
selectedUploadHosters = fmHosters.slice();
|
selectedUploadHosters = fmHosters.slice();
|
||||||
|
const existing = new Set();
|
||||||
|
for (const f of selectedFiles) existing.add(f.path);
|
||||||
|
for (const f of _pendingFiles) existing.add(f.path);
|
||||||
const newFiles = [];
|
const newFiles = [];
|
||||||
for (const p of files) {
|
for (const p of files) {
|
||||||
if (!selectedFiles.find(f => f.path === p) && !_pendingFiles.find(f => f.path === p)) {
|
if (existing.has(p)) continue;
|
||||||
|
existing.add(p);
|
||||||
const name = p.split('\\').pop().split('/').pop();
|
const name = p.split('\\').pop().split('/').pop();
|
||||||
newFiles.push({ path: p, name, size: null });
|
newFiles.push({ path: p, name, size: null });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (newFiles.length > 0) {
|
if (newFiles.length > 0) {
|
||||||
const newPaths = new Set(newFiles.map(f => f.path));
|
const newPaths = new Set(newFiles.map(f => f.path));
|
||||||
selectedFiles.push(...newFiles);
|
selectedFiles.push(...newFiles);
|
||||||
@ -646,13 +649,19 @@ async function pickFolder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function addPathsToQueue(paths) {
|
function addPathsToQueue(paths) {
|
||||||
|
// Build path-Set once so dedup is O(1) per candidate instead of O(n+m).
|
||||||
|
// Matters when the user picks a folder with thousands of files.
|
||||||
|
const existing = new Set();
|
||||||
|
for (const f of selectedFiles) existing.add(f.path);
|
||||||
|
for (const f of _pendingFiles) existing.add(f.path);
|
||||||
|
|
||||||
const newFiles = [];
|
const newFiles = [];
|
||||||
for (const p of paths) {
|
for (const p of paths) {
|
||||||
if (!selectedFiles.find(f => f.path === p) && !_pendingFiles.find(f => f.path === p)) {
|
if (existing.has(p)) continue;
|
||||||
|
existing.add(p);
|
||||||
const name = p.split('\\').pop().split('/').pop();
|
const name = p.split('\\').pop().split('/').pop();
|
||||||
newFiles.push({ path: p, name, size: null });
|
newFiles.push({ path: p, name, size: null });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (newFiles.length > 0) {
|
if (newFiles.length > 0) {
|
||||||
_pendingFiles.push(...newFiles);
|
_pendingFiles.push(...newFiles);
|
||||||
openHosterModal();
|
openHosterModal();
|
||||||
@ -687,13 +696,26 @@ function updateStartButton() {
|
|||||||
btn.disabled = uploading || !(hasQueuedJobs || canBuildQueueFromSelection);
|
btn.disabled = uploading || !(hasQueuedJobs || canBuildQueueFromSelection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const _UPLOAD_SELECTION_STATUSES = new Set(['done', 'error', 'aborted', 'skipped']);
|
||||||
|
const _ABORT_SELECTION_STATUSES = new Set(['preview', 'queued', 'getting-server', 'uploading', 'retrying']);
|
||||||
|
|
||||||
function updateQueueActionButtons() {
|
function updateQueueActionButtons() {
|
||||||
updateStartButton();
|
updateStartButton();
|
||||||
|
|
||||||
const hasSelection = selectedJobIds.size > 0;
|
const hasSelection = selectedJobIds.size > 0;
|
||||||
const hasUploadSelection = queueJobs.some((job) => selectedJobIds.has(job.id) && ['done', 'error', 'aborted', 'skipped'].includes(job.status));
|
// Single pass over the (usually small) selection set instead of three O(n)
|
||||||
const hasAbortSelection = queueJobs.some((job) => selectedJobIds.has(job.id) && ['preview', 'queued', 'getting-server', 'uploading', 'retrying'].includes(job.status));
|
// scans over the entire queue. For 1000 jobs × 3 scans this drops the
|
||||||
const hasStartableSelection = queueJobs.some((job) => selectedJobIds.has(job.id) && isStartableQueueStatus(job.status));
|
// selection-change cost from ~3000 checks to |selection|.
|
||||||
|
let hasUploadSelection = false, hasAbortSelection = false, hasStartableSelection = false;
|
||||||
|
for (const id of selectedJobIds) {
|
||||||
|
const job = _jobIndexById.get(id);
|
||||||
|
if (!job) continue;
|
||||||
|
const s = job.status;
|
||||||
|
if (!hasUploadSelection && _UPLOAD_SELECTION_STATUSES.has(s)) hasUploadSelection = true;
|
||||||
|
if (!hasAbortSelection && _ABORT_SELECTION_STATUSES.has(s)) hasAbortSelection = true;
|
||||||
|
if (!hasStartableSelection && isStartableQueueStatus(s)) hasStartableSelection = true;
|
||||||
|
if (hasUploadSelection && hasAbortSelection && hasStartableSelection) break;
|
||||||
|
}
|
||||||
const hasMovableSelection = hasSelection && !uploading;
|
const hasMovableSelection = hasSelection && !uploading;
|
||||||
|
|
||||||
const startSelectedBtn = document.getElementById('startSelectedBtn');
|
const startSelectedBtn = document.getElementById('startSelectedBtn');
|
||||||
@ -1990,9 +2012,16 @@ function maybeAddSessionFile(job) {
|
|||||||
|
|
||||||
function applySummaryResults(summary) {
|
function applySummaryResults(summary) {
|
||||||
const files = Array.isArray(summary?.files) ? summary.files : [];
|
const files = Array.isArray(summary?.files) ? summary.files : [];
|
||||||
|
// Build a (fileName + hoster) → job map once so the per-result lookup is O(1)
|
||||||
|
// instead of O(|queueJobs|). Big batches (hundreds of files × multiple hosters)
|
||||||
|
// otherwise become O(n²).
|
||||||
|
const jobByKey = new Map();
|
||||||
|
for (const j of queueJobs) {
|
||||||
|
jobByKey.set(`${j.fileName}\u0001${j.hoster}`, j);
|
||||||
|
}
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
for (const result of file.results || []) {
|
for (const result of file.results || []) {
|
||||||
const job = queueJobs.find((entry) => entry.fileName === file.name && entry.hoster === result.hoster);
|
const job = jobByKey.get(`${file.name}\u0001${result.hoster}`);
|
||||||
if (!job) continue;
|
if (!job) continue;
|
||||||
if (result.status === 'done') {
|
if (result.status === 'done') {
|
||||||
job.status = 'done';
|
job.status = 'done';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user