feat(diag): comprehensive high-concurrency measurement build
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) <noreply@anthropic.com>
This commit is contained in:
parent
ad87e36a8f
commit
53c3448836
@ -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;
|
||||
|
||||
22
main.js
22
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 {}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user