From 003e14dfe9eb4e0d3fc2a51ac85e3ce22b730e82 Mon Sep 17 00:00:00 2001 From: Administrator Date: Sun, 21 Jun 2026 19:21:41 +0200 Subject: [PATCH] =?UTF-8?q?feat(diagnostics):=20renderer=20interaction=20t?= =?UTF-8?q?iming=20=E2=80=94=20measure=20every=20UI=20switch/click,=20not?= =?UTF-8?q?=20just=20main-thread=20(v3.3.100)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the last measurement gap. The main process was already fully instrumented (every ipcMain handler timed at >=50ms, a 100ms main-thread long-task monitor with channel attribution, config load/serialize timing). Account switches were already covered too: switchAccount is a trivial synchronous Map-set and the rotation work is async, so a switch cannot block the main thread, and any block that did occur would surface in the long-task monitor. The real gap was the renderer side: the renderer-perf line was gated on active uploads (idle clicks were never logged) and only reported an aggregate longtask count — no per-interaction latency and no element attribution. So a switch/sort/tab that janked in the renderer (not main) was invisible. renderer/app.js (additive, self-silencing, wrapped in try/catch): - An Event Timing observer (PerformanceObserver type:'event', durationThreshold:50, buffered) logs `renderer-interaction dur=Xms proc=Yms target=` for every user interaction whose latency exceeds 50ms — always on, idle or under load — and names the element (id / first class / data-action / aria-label / title). This is the direct click->reaction latency the user feels. - The longtask observer now also logs `renderer-longtask dur=Xms` immediately for any single renderer long task >=100ms, regardless of upload state. 405 tests pass; clean Electron boot. Every action — main or renderer, idle or under load — now names itself in the log if it is slow. Co-Authored-By: Claude Opus 4.8 (1M context) --- package.json | 2 +- renderer/app.js | 27 ++++++++++++++++++++++++++- tasks/todo.md | 16 ++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fa4f021..384b86a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "multi-hoster-uploader", - "version": "3.3.99", + "version": "3.3.100", "description": "Upload files to doodstream, voe, vidmoly, byse simultaneously", "main": "main.js", "scripts": { diff --git a/renderer/app.js b/renderer/app.js index 9b469db..bbba33b 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -20,13 +20,38 @@ let uploading = false; let healthCheckRunning = false; let _rLongTasks = 0, _rLongTaskMax = 0, _rFrameLast = 0, _rFrameWorst = 0, _rFrameCount = 0, _rFrameJank = 0, _rPerfLastLog = 0, _rPerfWindowStart = 0; +function _rElLabel(el) { + try { + if (!el || !el.tagName) return '?'; + let s = el.tagName.toLowerCase(); + if (el.id) s += '#' + el.id; + else if (el.className && typeof el.className === 'string') { const c = el.className.trim().split(/\s+/)[0]; if (c) s += '.' + c; } + const a = el.getAttribute && (el.getAttribute('data-action') || el.getAttribute('data-tab') || el.getAttribute('aria-label') || el.getAttribute('title')); + if (a) s += `[${String(a).slice(0, 24)}]`; + return s; + } catch { return '?'; } +} try { if (window.PerformanceObserver) { new window.PerformanceObserver((list) => { - for (const e of list.getEntries()) { _rLongTasks++; if (e.duration > _rLongTaskMax) _rLongTaskMax = e.duration; } + for (const e of list.getEntries()) { + _rLongTasks++; + if (e.duration > _rLongTaskMax) _rLongTaskMax = e.duration; + if (e.duration >= 100 && window.api && window.api.debugLog) window.api.debugLog(`renderer-longtask dur=${Math.round(e.duration)}ms`); + } }).observe({ entryTypes: ['longtask'] }); } } catch {} +try { + if (window.PerformanceObserver) { + new window.PerformanceObserver((list) => { + for (const e of list.getEntries()) { + const proc = Math.round((e.processingEnd || 0) - (e.processingStart || 0)); + if (window.api && window.api.debugLog) window.api.debugLog(`renderer-interaction ${e.name} dur=${Math.round(e.duration)}ms proc=${proc}ms target=${_rElLabel(e.target)}`); + } + }).observe({ type: 'event', durationThreshold: 50, buffered: true }); + } +} catch {} function _rFrameTick(ts) { if (_rFrameLast) { const d = ts - _rFrameLast; _rFrameCount++; if (d > _rFrameWorst) _rFrameWorst = d; if (d > 33) _rFrameJank++; } _rFrameLast = ts; diff --git a/tasks/todo.md b/tasks/todo.md index 8e85c7c..b47bce3 100644 --- a/tasks/todo.md +++ b/tasks/todo.md @@ -1,3 +1,19 @@ +# v3.3.100 — close the LAST measurement gap: renderer interaction timing (switches/clicks) + +User asked "haben wir wirklich ALLES gemessen, auch switches/wechsel?". Audit: main-side was already +fully covered (IPC wrapper ≥50ms on every handler, main-longtask >100ms with lastIpc, config instrument); +account switchAccount is a trivial sync Map-set + the rotation work is async (can't block) → already covered. +The REAL gap was RENDERER-side: renderer-perf was upload-gated (idle clicks unmeasured) and only aggregate +(no per-interaction latency, no element attribution). Closed it (renderer/app.js, additive, self-silencing): +- Event Timing API observer (`type:'event', durationThreshold:50, buffered`) → `renderer-interaction + dur=Xms proc=Yms target=` for EVERY UI interaction ≥50ms (switch/sort-header/tab/button), always-on, + names the element (id/class/data-action/aria-label). The direct "click→reaction" latency. +- Idle renderer-longtask logging: any longtask ≥100ms logged immediately (`renderer-longtask dur=Xms`), + not just during uploads. +405 tests pass, clean boot. Now EVERY action — main or renderer, idle or under load — names itself if slow. + +--- + # v3.3.99 — THE KILL: 38.5MB config-thrash → history split out of the hot config + full instrumentation THE ROOT CAUSE (from v3.3.98's instrument, the real "bread"): electron-config.json was **38.5MB** and got