From 53c3448836511d8771c3607e2a469e5348f8dbeb Mon Sep 17 00:00:00 2001 From: Administrator Date: Sun, 21 Jun 2026 16:23:43 +0200 Subject: [PATCH] feat(diag): comprehensive high-concurrency measurement build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User asked to measure everything so we can pinpoint the lag/ECONNRESET at 70 concurrent uploads. All additive instrumentation, no upload-behavior change. main process: the eventloop-delay log line now also reports cpu=X%core (process.cpuUsage delta over wall time; >100% means multiple cores), rss, per-hoster live connection distribution (active-by-hoster), cumulative transient (ECONNRESET-class) error count, and pending count — pulled from a new UploadManager.getDiagnostics(). This shows whether the main thread is CPU-bound and which hoster is oversubscribed. renderer: a PerformanceObserver('longtask') plus a requestAnimationFrame frame-time monitor log 'renderer-perf' every 5s while uploading — fps, jankFrames (>33ms), worstFrame, longtask count and max. This is the direct renderer ground truth (real frame rate under load), which the component-timing harness could not capture. Low fps / high jank => renderer is blocked; ~60fps while it still feels laggy => the cpu/eld numbers in the same log decide CPU-bound vs IO-bound in the main process. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/upload-manager.js | 21 +++++++++++++++++---- main.js | 22 +++++++++++++++++++++- renderer/app.js | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 5 deletions(-) diff --git a/lib/upload-manager.js b/lib/upload-manager.js index d067b77..fc5fb9d 100644 --- a/lib/upload-manager.js +++ b/lib/upload-manager.js @@ -34,10 +34,11 @@ class UploadManager extends EventEmitter { this.stopAfterActive = false; this.statsInterval = null; this.startTime = 0; - this.activeJobs = new Map(); // uploadId -> { jobId, speedKbs, bytesUploaded } + this.activeJobs = new Map(); // uploadId -> { jobId, speedKbs, bytesUploaded, hoster } this.jobAbortControllers = new Map(); // jobId -> AbortController this.cancelledJobIds = new Set(); this.sessionBytes = 0; + this._transientErrorTotal = 0; this.lastStartTime = {}; // hoster -> timestamp of last upload start this.intervalLocks = {}; // hoster -> Promise chain for serialized interval waits this.globalThrottle = null; @@ -81,6 +82,17 @@ class UploadManager extends EventEmitter { return this.activeJobs.size; } + getDiagnostics() { + const activeByHoster = {}; + for (const v of this.activeJobs.values()) { + const h = v && v.hoster ? v.hoster : 'unknown'; + activeByHoster[h] = (activeByHoster[h] || 0) + 1; + } + let pending = 0; + for (const sem of Object.values(this.semaphores)) pending += (sem && sem.pending) || 0; + return { activeByHoster, transientErrors: this._transientErrorTotal, pending, active: this.activeJobs.size }; + } + clearFailedAccount(hoster, accountId) { return this._failedAccounts.delete(`${hoster}:${accountId}`); } @@ -625,7 +637,7 @@ class UploadManager extends EventEmitter { // Mutate this single object on each progress callback instead of // allocating a fresh one — callback fires on every stream chunk // (hundreds/sec per active job). - const activeEntry = { jobId, speedKbs: 0, bytesUploaded: 0 }; + const activeEntry = { jobId, speedKbs: 0, bytesUploaded: 0, hoster: task.hoster }; this.activeJobs.set(uploadId, activeEntry); let lastEmitTime = 0; @@ -686,6 +698,7 @@ class UploadManager extends EventEmitter { return; } catch (err) { this.activeJobs.delete(uploadId); + if (this._isTransientNetworkError(err)) this._transientErrorTotal++; const isSpeedRestart = speedAbort && speedAbort.signal.aborted && !signal.aborted; if (!signal.aborted && !isSpeedRestart) { @@ -938,7 +951,7 @@ class UploadManager extends EventEmitter { let lastBytes = 0; let lastSpeedTime = jobStart; let currentSpeedKbs = 0; - const activeEntry = { jobId, speedKbs: 0, bytesUploaded: 0 }; + const activeEntry = { jobId, speedKbs: 0, bytesUploaded: 0, hoster: task.hoster }; this.activeJobs.set(uploadId, activeEntry); let lastEmitTime = 0; @@ -1074,7 +1087,7 @@ class UploadManager extends EventEmitter { let lastBytes = 0; let lastSpeedTime = jobStart; let currentSpeedKbs = 0; - const activeEntry = { jobId, speedKbs: 0, bytesUploaded: 0 }; + const activeEntry = { jobId, speedKbs: 0, bytesUploaded: 0, hoster: task.hoster }; this.activeJobs.set(uploadId, activeEntry); let lastEmitTime = 0; const PROGRESS_EMIT_INTERVAL = 250; diff --git a/main.js b/main.js index aeb168c..ea5308e 100644 --- a/main.js +++ b/main.js @@ -30,6 +30,8 @@ const { createAgent } = require('./lib/diagnostics-agent'); const _eventLoopDelay = monitorEventLoopDelay({ resolution: 10 }); _eventLoopDelay.enable(); let _eldLastLog = 0; +let _lastCpu = process.cpuUsage(); +let _lastCpuT = Date.now(); let mainWindow; let _lastImportPath = null; @@ -205,7 +207,25 @@ function _maybeLogEventLoopDelay(activeJobs) { const top = Object.entries(hist).sort((a, b) => b[1] - a[1]).slice(0, 6).map(([k, v]) => `${k}:${v}`).join(','); resStr = ` resources=${info.length} {${top}}`; } catch {} - logInfo('perf', `eventloop-delay active=${activeJobs} mean=${mean}ms p99=${p99}ms max=${max}ms stddev=${stddev}ms threadpool=${process.env.UV_THREADPOOL_SIZE}${resStr}`); + let cpuStr = ''; + try { + const d = process.cpuUsage(_lastCpu); + const wall = now - _lastCpuT; + const pct = wall > 0 ? Math.round((d.user + d.system) / 1000 / wall * 100) : 0; + const rss = Math.round(process.memoryUsage().rss / 1048576); + cpuStr = ` cpu=${pct}%core rss=${rss}MB`; + _lastCpu = process.cpuUsage(); + _lastCpuT = now; + } catch {} + let upStr = ''; + try { + if (uploadManager && typeof uploadManager.getDiagnostics === 'function') { + const d = uploadManager.getDiagnostics(); + const byHoster = Object.entries(d.activeByHoster || {}).map(([h, c]) => `${h.replace(/\..*$/, '')}:${c}`).join(','); + upStr = ` active-by-hoster={${byHoster}} transient-errs=${d.transientErrors || 0} pending=${d.pending || 0}`; + } + } catch {} + logInfo('perf', `eventloop-delay active=${activeJobs} mean=${mean}ms p99=${p99}ms max=${max}ms stddev=${stddev}ms threadpool=${process.env.UV_THREADPOOL_SIZE}${cpuStr}${resStr}${upStr}`); _eventLoopDelay.reset(); } catch {} } diff --git a/renderer/app.js b/renderer/app.js index 2a7856e..9b469db 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -18,6 +18,37 @@ let config = { hosters: {}, hosterSettings: {}, globalSettings: {} }; let hosterSettings = {}; let uploading = false; let healthCheckRunning = false; + +let _rLongTasks = 0, _rLongTaskMax = 0, _rFrameLast = 0, _rFrameWorst = 0, _rFrameCount = 0, _rFrameJank = 0, _rPerfLastLog = 0, _rPerfWindowStart = 0; +try { + if (window.PerformanceObserver) { + new window.PerformanceObserver((list) => { + for (const e of list.getEntries()) { _rLongTasks++; if (e.duration > _rLongTaskMax) _rLongTaskMax = e.duration; } + }).observe({ entryTypes: ['longtask'] }); + } +} catch {} +function _rFrameTick(ts) { + if (_rFrameLast) { const d = ts - _rFrameLast; _rFrameCount++; if (d > _rFrameWorst) _rFrameWorst = d; if (d > 33) _rFrameJank++; } + _rFrameLast = ts; + requestAnimationFrame(_rFrameTick); +} +requestAnimationFrame(_rFrameTick); +function _resetRendererPerf() { + _rPerfWindowStart = Date.now(); + _rFrameCount = 0; _rFrameJank = 0; _rFrameWorst = 0; _rLongTasks = 0; _rLongTaskMax = 0; +} +function _maybeLogRendererPerf(activeJobs) { + const now = Date.now(); + if (!_rPerfWindowStart) _rPerfWindowStart = now; + if (now - _rPerfLastLog < 5000) return; + const winSec = (now - _rPerfWindowStart) / 1000; + const fps = winSec > 0 ? Math.round(_rFrameCount / winSec) : 0; + if (window.api && window.api.debugLog) { + window.api.debugLog(`renderer-perf active=${activeJobs} fps=${fps} jankFrames=${_rFrameJank} worstFrame=${Math.round(_rFrameWorst)}ms longtasks=${_rLongTasks} maxTask=${Math.round(_rLongTaskMax)}ms`); + } + _rPerfLastLog = now; + _resetRendererPerf(); +} let accountStatuses = {}; // { accountId: { status: 'ok'|'warn'|'error'|'checking'|'unchecked', message: '' } } let editingAccountId = null; // null = adding, string = editing account by ID let autoHealthCheckEnabled = true; @@ -2539,6 +2570,12 @@ function _handleStatsImpl(data) { updateStatusBar(); updateStatsPanel(); + if (data.state === 'uploading' && (data.activeJobs || 0) > 0) { + _maybeLogRendererPerf(data.activeJobs); + } else { + _resetRendererPerf(); + } + // Track run time if (data.state === 'uploading' || data.state === 'stopping') { if (!statsStartTime) {