From d04779d0ac641a6f392fb19c6746611efd4a37b3 Mon Sep 17 00:00:00 2001 From: xRangerDE Date: Fri, 20 Feb 2026 21:36:23 +0100 Subject: [PATCH] Optimize renderer scheduling and batched logging pipeline (v4.1.5) --- typescript-version/package-lock.json | 4 +- typescript-version/package.json | 2 +- typescript-version/src/index.html | 4 +- typescript-version/src/main.ts | 57 +++++++++++- typescript-version/src/renderer-settings.ts | 21 +++-- typescript-version/src/renderer-streamers.ts | 77 +++++++++++----- typescript-version/src/renderer.ts | 92 +++++++++++++++++++- 7 files changed, 219 insertions(+), 38 deletions(-) diff --git a/typescript-version/package-lock.json b/typescript-version/package-lock.json index 1230c26..b0bc31d 100644 --- a/typescript-version/package-lock.json +++ b/typescript-version/package-lock.json @@ -1,12 +1,12 @@ { "name": "twitch-vod-manager", - "version": "4.1.4", + "version": "4.1.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "twitch-vod-manager", - "version": "4.1.4", + "version": "4.1.5", "license": "MIT", "dependencies": { "axios": "^1.6.0", diff --git a/typescript-version/package.json b/typescript-version/package.json index e05339c..4c6e5e9 100644 --- a/typescript-version/package.json +++ b/typescript-version/package.json @@ -1,6 +1,6 @@ { "name": "twitch-vod-manager", - "version": "4.1.4", + "version": "4.1.5", "description": "Twitch VOD Manager - Download Twitch VODs easily", "main": "dist/main.js", "author": "xRangerDE", diff --git a/typescript-version/src/index.html b/typescript-version/src/index.html index cfb7ae9..73f98e6 100644 --- a/typescript-version/src/index.html +++ b/typescript-version/src/index.html @@ -457,7 +457,7 @@

Updates

-

Version: v4.1.4

+

Version: v4.1.5

@@ -502,7 +502,7 @@
Nicht verbunden - v4.1.4 + v4.1.5 diff --git a/typescript-version/src/main.ts b/typescript-version/src/main.ts index b4a5409..fac1038 100644 --- a/typescript-version/src/main.ts +++ b/typescript-version/src/main.ts @@ -8,7 +8,7 @@ import { autoUpdater } from 'electron-updater'; // ========================================== // CONFIG & CONSTANTS // ========================================== -const APP_VERSION = '4.1.4'; +const APP_VERSION = '4.1.5'; const UPDATE_CHECK_URL = 'http://24-music.de/version.json'; // Paths @@ -28,6 +28,8 @@ const DEFAULT_PERFORMANCE_MODE: PerformanceMode = 'balanced'; const QUEUE_SAVE_DEBOUNCE_MS = 250; const MIN_FREE_DISK_BYTES = 128 * 1024 * 1024; const TOOL_PATH_REFRESH_TTL_MS = 10 * 1000; +const DEBUG_LOG_FLUSH_INTERVAL_MS = 1000; +const DEBUG_LOG_BUFFER_FLUSH_LINES = 48; const CACHE_CLEANUP_INTERVAL_MS = 60 * 1000; const MAX_LOGIN_TO_USER_ID_CACHE_ENTRIES = 4096; const MAX_VOD_LIST_CACHE_ENTRIES = 512; @@ -379,6 +381,8 @@ let ffmpegPathCache: string | null = null; let ffprobePathCache: string | null = null; let bundledToolPathSignature = ''; let bundledToolPathRefreshedAt = 0; +let debugLogFlushTimer: NodeJS.Timeout | null = null; +let pendingDebugLogLines: string[] = []; // ========================================== // TOOL PATHS @@ -502,8 +506,47 @@ async function runPreflight(autoFix = false): Promise { return result; } +function flushPendingDebugLogLines(): void { + if (!pendingDebugLogLines.length) { + return; + } + + try { + const payload = pendingDebugLogLines.join(''); + pendingDebugLogLines = []; + fs.appendFileSync(DEBUG_LOG_FILE, payload); + } catch { + // ignore debug log errors + } +} + +function startDebugLogFlushTimer(): void { + if (debugLogFlushTimer) { + return; + } + + debugLogFlushTimer = setInterval(() => { + flushPendingDebugLogLines(); + }, DEBUG_LOG_FLUSH_INTERVAL_MS); + + debugLogFlushTimer.unref?.(); +} + +function stopDebugLogFlushTimer(flush = true): void { + if (debugLogFlushTimer) { + clearInterval(debugLogFlushTimer); + debugLogFlushTimer = null; + } + + if (flush) { + flushPendingDebugLogLines(); + } +} + function readDebugLog(lines = 200): string { try { + flushPendingDebugLogLines(); + if (!fs.existsSync(DEBUG_LOG_FILE)) { return 'Debug-Log ist leer.'; } @@ -863,7 +906,14 @@ function appendDebugLog(message: string, details?: unknown): void { const payload = details === undefined ? '' : ` | ${typeof details === 'string' ? details : JSON.stringify(details)}`; - fs.appendFileSync(DEBUG_LOG_FILE, `[${ts}] ${message}${payload}\n`); + + pendingDebugLogLines.push(`[${ts}] ${message}${payload}\n`); + + if (pendingDebugLogLines.length >= DEBUG_LOG_BUFFER_FLUSH_LINES) { + flushPendingDebugLogLines(); + } else { + startDebugLogFlushTimer(); + } } catch { // ignore debug log errors } @@ -3133,6 +3183,7 @@ ipcMain.handle('save-video-dialog', async (_, defaultName: string) => { app.whenReady().then(() => { refreshBundledToolPaths(true); startMetadataCacheCleanup(); + startDebugLogFlushTimer(); createWindow(); appendDebugLog('startup-tools-check-skipped', 'Deferred to first use'); @@ -3146,6 +3197,7 @@ app.whenReady().then(() => { app.on('window-all-closed', () => { stopMetadataCacheCleanup(); cleanupMetadataCaches('shutdown'); + stopDebugLogFlushTimer(true); if (currentProcess) { currentProcess.kill(); @@ -3160,5 +3212,6 @@ app.on('window-all-closed', () => { app.on('before-quit', () => { stopMetadataCacheCleanup(); cleanupMetadataCaches('shutdown'); + stopDebugLogFlushTimer(true); flushQueueSave(); }); diff --git a/typescript-version/src/renderer-settings.ts b/typescript-version/src/renderer-settings.ts index 8421ff1..b4dea2b 100644 --- a/typescript-version/src/renderer-settings.ts +++ b/typescript-version/src/renderer-settings.ts @@ -1,3 +1,5 @@ +let lastRuntimeMetricsOutput = ''; + async function connect(): Promise { const hasCredentials = Boolean((config.client_id ?? '').toString().trim() && (config.client_secret ?? '').toString().trim()); if (!hasCredentials) { @@ -73,9 +75,11 @@ function applyTemplatePreset(preset: string): void { validateFilenameTemplates(); } -async function refreshRuntimeMetrics(): Promise { +async function refreshRuntimeMetrics(showLoading = true): Promise { const output = byId('runtimeMetricsOutput'); - output.textContent = UI_TEXT.static.runtimeMetricsLoading; + if (showLoading) { + output.textContent = UI_TEXT.static.runtimeMetricsLoading; + } try { const metrics = await window.api.getRuntimeMetrics(); @@ -92,9 +96,16 @@ async function refreshRuntimeMetrics(): Promise { `${UI_TEXT.static.runtimeMetricUpdated}: ${new Date(metrics.timestamp).toLocaleString(currentLanguage === 'en' ? 'en-US' : 'de-DE')}` ]; - output.textContent = lines.join('\n'); + const nextOutput = lines.join('\n'); + if (nextOutput !== lastRuntimeMetricsOutput) { + output.textContent = nextOutput; + lastRuntimeMetricsOutput = nextOutput; + } } catch { - output.textContent = UI_TEXT.static.runtimeMetricsError; + if (lastRuntimeMetricsOutput !== UI_TEXT.static.runtimeMetricsError) { + output.textContent = UI_TEXT.static.runtimeMetricsError; + lastRuntimeMetricsOutput = UI_TEXT.static.runtimeMetricsError; + } } } @@ -131,7 +142,7 @@ function toggleRuntimeMetricsAutoRefresh(enabled: boolean): void { if (enabled) { runtimeMetricsAutoRefreshTimer = window.setInterval(() => { - void refreshRuntimeMetrics(); + void refreshRuntimeMetrics(false); }, 2000); } } diff --git a/typescript-version/src/renderer-streamers.ts b/typescript-version/src/renderer-streamers.ts index 05203f5..52d75cf 100644 --- a/typescript-version/src/renderer-streamers.ts +++ b/typescript-version/src/renderer-streamers.ts @@ -1,4 +1,31 @@ let selectStreamerRequestId = 0; +let vodRenderTaskId = 0; +const VOD_RENDER_CHUNK_SIZE = 64; + +function buildVodCardHtml(vod: VOD, streamer: string): string { + const thumb = vod.thumbnail_url.replace('%{width}', '320').replace('%{height}', '180'); + const date = formatUiDate(vod.created_at); + const escapedTitle = vod.title.replace(/'/g, "\\'").replace(/\"/g, '"'); + const safeDisplayTitle = escapeHtml(vod.title || UI_TEXT.vods.untitled); + + return ` +
+ +
+
${safeDisplayTitle}
+
+ ${date} + ${vod.duration} + ${formatUiNumber(vod.view_count)} ${UI_TEXT.vods.views} +
+
+
+ + +
+
+ `; +} function renderStreamers(): void { const list = byId('streamerList'); @@ -92,36 +119,40 @@ async function selectStreamer(name: string, forceRefresh = false): Promise function renderVODs(vods: VOD[] | null | undefined, streamer: string): void { const grid = byId('vodGrid'); + const renderTaskId = ++vodRenderTaskId; + + const scheduleNextChunk = (nextStartIndex: number): void => { + const delayMs = document.hidden ? 16 : 0; + window.setTimeout(() => { + renderChunk(nextStartIndex); + }, delayMs); + }; if (!vods || vods.length === 0) { grid.innerHTML = `

${UI_TEXT.vods.noResultsTitle}

${UI_TEXT.vods.noResultsText}

`; return; } - grid.innerHTML = vods.map((vod: VOD) => { - const thumb = vod.thumbnail_url.replace('%{width}', '320').replace('%{height}', '180'); - const date = formatUiDate(vod.created_at); - const escapedTitle = vod.title.replace(/'/g, "\\'").replace(/\"/g, '"'); - const safeDisplayTitle = escapeHtml(vod.title || UI_TEXT.vods.untitled); + grid.innerHTML = ''; - return ` -
- -
-
${safeDisplayTitle}
-
- ${date} - ${vod.duration} - ${formatUiNumber(vod.view_count)} ${UI_TEXT.vods.views} -
-
-
- - -
-
- `; - }).join(''); + const renderChunk = (startIndex: number): void => { + if (renderTaskId !== vodRenderTaskId) { + return; + } + + const chunk = vods.slice(startIndex, startIndex + VOD_RENDER_CHUNK_SIZE); + if (!chunk.length) { + return; + } + + grid.insertAdjacentHTML('beforeend', chunk.map((vod) => buildVodCardHtml(vod, streamer)).join('')); + + if (startIndex + chunk.length < vods.length) { + scheduleNextChunk(startIndex + chunk.length); + } + }; + + renderChunk(0); } async function refreshVODs(): Promise { diff --git a/typescript-version/src/renderer.ts b/typescript-version/src/renderer.ts index a5255f4..11d0b03 100644 --- a/typescript-version/src/renderer.ts +++ b/typescript-version/src/renderer.ts @@ -1,3 +1,9 @@ +const QUEUE_SYNC_FAST_MS = 900; +const QUEUE_SYNC_DEFAULT_MS = 1800; +const QUEUE_SYNC_IDLE_MS = 4500; +const QUEUE_SYNC_HIDDEN_MS = 9000; +const QUEUE_SYNC_RECENT_ACTIVITY_WINDOW_MS = 15000; + async function init(): Promise { config = await window.api.getConfig(); const language = setLanguage((config.language as string) || 'en'); @@ -34,6 +40,7 @@ async function init(): Promise { window.api.onQueueUpdated((q: QueueItem[]) => { queue = mergeQueueState(Array.isArray(q) ? q : []); renderQueue(); + markQueueActivity(); }); window.api.onQueueDuplicateSkipped((payload) => { @@ -57,16 +64,19 @@ async function init(): Promise { item.totalBytes = progress.totalBytes; item.progressStatus = progress.status; renderQueue(); + markQueueActivity(); }); window.api.onDownloadStarted(() => { downloading = true; updateDownloadButtonState(); + markQueueActivity(); }); window.api.onDownloadFinished(() => { downloading = false; updateDownloadButtonState(); + markQueueActivity(); }); window.api.onCutProgress((percent: number) => { @@ -98,12 +108,71 @@ async function init(): Promise { validateFilenameTemplates(); void refreshRuntimeMetrics(); - setInterval(() => { - void syncQueueAndDownloadState(); - }, 2000); + document.addEventListener('visibilitychange', () => { + scheduleQueueSync(document.hidden ? 600 : 150); + }); + + scheduleQueueSync(QUEUE_SYNC_DEFAULT_MS); } let toastHideTimer: number | null = null; +let queueSyncTimer: number | null = null; +let queueSyncInFlight = false; +let lastQueueActivityAt = Date.now(); + +function markQueueActivity(): void { + lastQueueActivityAt = Date.now(); +} + +function hasActiveQueueWork(): boolean { + return queue.some((item) => item.status === 'pending' || item.status === 'downloading' || item.status === 'paused'); +} + +function getNextQueueSyncDelayMs(): number { + if (document.hidden) { + return QUEUE_SYNC_HIDDEN_MS; + } + + if (downloading || queue.some((item) => item.status === 'downloading')) { + return QUEUE_SYNC_FAST_MS; + } + + if (hasActiveQueueWork()) { + return QUEUE_SYNC_DEFAULT_MS; + } + + const idleForMs = Date.now() - lastQueueActivityAt; + return idleForMs > QUEUE_SYNC_RECENT_ACTIVITY_WINDOW_MS ? QUEUE_SYNC_IDLE_MS : QUEUE_SYNC_DEFAULT_MS; +} + +function scheduleQueueSync(delayMs = getNextQueueSyncDelayMs()): void { + if (queueSyncTimer) { + clearTimeout(queueSyncTimer); + queueSyncTimer = null; + } + + queueSyncTimer = window.setTimeout(() => { + queueSyncTimer = null; + void runQueueSyncCycle(); + }, Math.max(300, Math.floor(delayMs))); +} + +async function runQueueSyncCycle(): Promise { + if (queueSyncInFlight) { + scheduleQueueSync(400); + return; + } + + queueSyncInFlight = true; + try { + await syncQueueAndDownloadState(); + } catch { + // ignore transient IPC errors and retry on next cycle + } finally { + queueSyncInFlight = false; + scheduleQueueSync(); + } +} function showAppToast(message: string, type: 'info' | 'warn' = 'info'): void { let toast = document.getElementById('appToast'); @@ -161,6 +230,18 @@ function mergeQueueState(nextQueue: QueueItem[]): QueueItem[] { }); } +function getQueueStateFingerprint(items: QueueItem[]): string { + return items.map((item) => [ + item.id, + item.status, + Math.round((Number(item.progress) || 0) * 10), + item.currentPart || 0, + item.totalParts || 0, + item.last_error || '', + item.progressStatus || '' + ].join(':')).join('|'); +} + function updateDownloadButtonState(): void { const btn = byId('btnStart'); const hasPaused = queue.some((item) => item.status === 'paused'); @@ -169,8 +250,13 @@ function updateDownloadButtonState(): void { } async function syncQueueAndDownloadState(): Promise { + const previousFingerprint = getQueueStateFingerprint(queue); const latestQueue = await window.api.getQueue(); queue = mergeQueueState(Array.isArray(latestQueue) ? latestQueue : []); + const nextFingerprint = getQueueStateFingerprint(queue); + if (nextFingerprint !== previousFingerprint) { + markQueueActivity(); + } renderQueue(); const backendDownloading = await window.api.isDownloading();