diff --git a/eslint.config.mjs b/eslint.config.mjs index 6759a1c..4a53a64 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -20,6 +20,7 @@ export default [ clearTimeout: 'readonly', setInterval: 'readonly', clearInterval: 'readonly', + setImmediate: 'readonly', Buffer: 'readonly', URL: 'readonly', fetch: 'readonly', diff --git a/main.js b/main.js index 6c7f054..4a4feaf 100644 --- a/main.js +++ b/main.js @@ -35,10 +35,36 @@ function getDebugLogPath() { return path.join(baseDir, 'upload-debug.log'); } +// Buffered async writer: debugLog is called hundreds of times per second during +// busy uploads (unhandledRejection traces, progress transitions, folder-monitor +// events). Sync appendFileSync per call blocked the main event loop. We now +// queue lines in memory and flush on a short interval / on process exit. +const _debugLogBuffer = []; +let _debugLogFlushTimer = null; +let _debugLogWriting = false; + +function _flushDebugLog() { + if (_debugLogWriting || _debugLogBuffer.length === 0) return; + const chunk = _debugLogBuffer.join(''); + _debugLogBuffer.length = 0; + _debugLogWriting = true; + fs.appendFile(getDebugLogPath(), chunk, 'utf-8', () => { + _debugLogWriting = false; + // If more lines arrived during the write, flush them next tick. + if (_debugLogBuffer.length) setImmediate(_flushDebugLog); + }); +} + function debugLog(msg) { try { const ts = new Date().toISOString(); - fs.appendFileSync(getDebugLogPath(), `[${ts}] ${msg}\n`, 'utf-8'); + _debugLogBuffer.push(`[${ts}] ${msg}\n`); + if (!_debugLogFlushTimer) { + _debugLogFlushTimer = setTimeout(() => { + _debugLogFlushTimer = null; + _flushDebugLog(); + }, 500); + } } catch {} } @@ -725,6 +751,13 @@ app.on('before-quit', () => { destroyCaptureWindow(); } catch {} destroyDropTargetWindow(); + // Flush pending debug-log buffer synchronously so no lines are lost. + try { + if (_debugLogBuffer.length) { + fs.appendFileSync(getDebugLogPath(), _debugLogBuffer.join(''), 'utf-8'); + _debugLogBuffer.length = 0; + } + } catch {} }); // --- IPC Handlers --- diff --git a/renderer/app.js b/renderer/app.js index ce77638..6c7dfaf 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -60,6 +60,9 @@ const selectedRecentIds = new Set(); // Maintained incrementally — avoids O(n) filter() scans every 250ms in the status bar. let _sessionDoneCount = 0; let _sessionErrorCount = 0; +// O(1) dedup for maybeAddSessionFile (was O(n) sessionFilesData.some). +// Huge with thousands of rows × thousands of incoming results. +const _sessionFileKeys = new Set(); // --- Init --- async function init() { @@ -93,9 +96,13 @@ async function init() { window.api.onUpdateAvailable(showUpdateBanner); window.api.onUpdateProgress(handleUpdateProgress); - // Upload event listeners — with debug logging to file + // Upload event listeners — debug log only on state transitions; the 'uploading' + // tick fires 4×/sec per active job and an IPC roundtrip per event would + // backlog the renderer↔main channel with hundreds of messages/sec. window.api.onUploadProgress((data) => { - window.api.debugLog('RX upload-progress: ' + data.status + ' ' + data.hoster + ' ' + (data.fileName || '')); + if (data.status !== 'uploading') { + window.api.debugLog('RX upload-progress: ' + data.status + ' ' + data.hoster + ' ' + (data.fileName || '')); + } handleProgress(data); }); window.api.onUploadBatchDone((data) => { @@ -103,7 +110,10 @@ async function init() { handleBatchDone(data); }); window.api.onUploadStats((data) => { - window.api.debugLog('RX upload-stats: state=' + data.state + ' active=' + data.activeJobs); + // Stats fire every second per upload session — skip while uploading. + if (data.state !== 'uploading') { + window.api.debugLog('RX upload-stats: state=' + data.state + ' active=' + data.activeJobs); + } handleStats(data); }); window.api.onShutdownCountdown(handleShutdownCountdown); @@ -1005,11 +1015,18 @@ function _renderVirtualRows(tbody) { tbody.innerHTML = html; } +// Coalesce rapid scroll events (a fast trackpad fling fires dozens) into one +// render per frame. rAF keeps the scroll thread cheap. +let _queueScrollQueued = false; function _onQueueScroll() { - if (_sortedJobsCache.length >= 200) { + if (_queueScrollQueued) return; + if (_sortedJobsCache.length < 200) return; + _queueScrollQueued = true; + requestAnimationFrame(() => { + _queueScrollQueued = false; const tbody = document.getElementById('queueBody'); if (tbody) _renderVirtualRows(tbody); - } + }); } const _collatorDE = new Intl.Collator('de', { sensitivity: 'base', numeric: true }); @@ -1179,6 +1196,7 @@ function deleteSelectedRecentFiles() { sessionFilesData = sessionFilesData.filter(r => { if (!selectedRecentIds.has(r.order)) return true; if (r.isError) removedErr++; else removedDone++; + _sessionFileKeys.delete(`${r.link}\u0001${r.filename}\u0001${r.host}`); return false; }); _sessionDoneCount = Math.max(0, _sessionDoneCount - removedDone); @@ -1191,6 +1209,7 @@ function clearAllRecentFiles() { if (sessionFilesData.length === 0) return; if (!confirm(`Wirklich alle ${sessionFilesData.length} Links aus diesem Panel entfernen?`)) return; sessionFilesData = []; + _sessionFileKeys.clear(); _sessionDoneCount = 0; _sessionErrorCount = 0; selectedRecentIds.clear(); @@ -1949,7 +1968,9 @@ function maybeAddSessionFile(job) { if (job.status === 'done' && job.result) { const link = job.result.download_url || job.result.embed_url || ''; if (!link) return; - if (!sessionFilesData.some((row) => row.link === link && row.filename === job.fileName && row.host === job.hoster)) { + const dedupKey = `${link}\u0001${job.fileName}\u0001${job.hoster}`; + if (!_sessionFileKeys.has(dedupKey)) { + _sessionFileKeys.add(dedupKey); sessionFilesData.push({ date: dt.text, dateTs: dt.ts,