perf: final sweep — hot-path allocation, cached log target, sort-header skip
Last round of targeted wins:
- upload-manager progress callback was allocating a fresh
{ jobId, speedKbs, bytesUploaded } object on every fs stream chunk
(hundreds of times per second per active job). Now a single entry
is created at job start and mutated in place — zero allocations
on the steady-state progress tick.
- upload-manager stats timer's two separate activeJobs.values()
scans (globalSpeedKbs + inProgressBytes) merged into one pass.
- clouddrop-upload.js reuses a single Buffer.allocUnsafe(chunkSize)
across all chunks, taking subarray() only for the tail chunk.
A 1 GB upload no longer allocates 64× 16 MB = 1 GB of short-lived
buffers — real GC relief during many-file batches.
- _resolveUploadLogTarget is now cached; the fallback ladder runs
once per session (or when the user changes the log path / daily-log
date rolls), not on every 500ms flush.
- renderRecentUploadsPanel skips updateRecentSortHeaders on the
append-only fast path — sort state hasn't changed, headers don't
need recomputing.
This commit is contained in:
parent
c73108afff
commit
2d8b3f1bf9
@ -155,9 +155,13 @@ class ClouddropUploader {
|
|||||||
const totalChunks = initPayload.totalChunks || Math.ceil(fileSize / chunkSize);
|
const totalChunks = initPayload.totalChunks || Math.ceil(fileSize / chunkSize);
|
||||||
if (!sessionId) throw new Error('Clouddrop: Keine sessionId von /upload/init');
|
if (!sessionId) throw new Error('Clouddrop: Keine sessionId von /upload/init');
|
||||||
|
|
||||||
// 2. Read file and PUT chunks sequentially
|
// 2. Read file and PUT chunks sequentially.
|
||||||
|
// Reuse a single buffer for all chunks (only the last chunk may be smaller,
|
||||||
|
// in which case we slice a view). Avoids 64× 16 MB allocations on a 1 GB
|
||||||
|
// file — real GC pressure during busy uploads.
|
||||||
const fd = fs.openSync(filePath, 'r');
|
const fd = fs.openSync(filePath, 'r');
|
||||||
let bytesSent = 0;
|
let bytesSent = 0;
|
||||||
|
const reusableBuf = Buffer.allocUnsafe(chunkSize);
|
||||||
try {
|
try {
|
||||||
for (let i = 0; i < totalChunks; i++) {
|
for (let i = 0; i < totalChunks; i++) {
|
||||||
if (signal && signal.aborted) throw new Error('Aborted');
|
if (signal && signal.aborted) throw new Error('Aborted');
|
||||||
@ -165,8 +169,10 @@ class ClouddropUploader {
|
|||||||
const offset = i * chunkSize;
|
const offset = i * chunkSize;
|
||||||
const remaining = fileSize - offset;
|
const remaining = fileSize - offset;
|
||||||
const thisChunkSize = Math.min(chunkSize, remaining);
|
const thisChunkSize = Math.min(chunkSize, remaining);
|
||||||
const buf = Buffer.alloc(thisChunkSize);
|
fs.readSync(fd, reusableBuf, 0, thisChunkSize, offset);
|
||||||
fs.readSync(fd, buf, 0, thisChunkSize, offset);
|
const body = thisChunkSize === chunkSize
|
||||||
|
? reusableBuf
|
||||||
|
: reusableBuf.subarray(0, thisChunkSize);
|
||||||
|
|
||||||
if (throttle) await throttle.consume(thisChunkSize, signal);
|
if (throttle) await throttle.consume(thisChunkSize, signal);
|
||||||
|
|
||||||
@ -174,7 +180,7 @@ class ClouddropUploader {
|
|||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
dispatcher: clouddropAgent,
|
dispatcher: clouddropAgent,
|
||||||
signal,
|
signal,
|
||||||
body: buf,
|
body,
|
||||||
headers: this._headers({
|
headers: this._headers({
|
||||||
'Content-Type': 'application/octet-stream',
|
'Content-Type': 'application/octet-stream',
|
||||||
'Content-Length': String(thisChunkSize)
|
'Content-Length': String(thisChunkSize)
|
||||||
|
|||||||
@ -361,7 +361,11 @@ class UploadManager extends EventEmitter {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.activeJobs.set(uploadId, { jobId, speedKbs: 0, bytesUploaded: 0 });
|
// 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 };
|
||||||
|
this.activeJobs.set(uploadId, activeEntry);
|
||||||
|
|
||||||
let lastEmitTime = 0;
|
let lastEmitTime = 0;
|
||||||
const PROGRESS_EMIT_INTERVAL = 250; // ms – throttle UI updates
|
const PROGRESS_EMIT_INTERVAL = 250; // ms – throttle UI updates
|
||||||
@ -377,7 +381,8 @@ class UploadManager extends EventEmitter {
|
|||||||
lastSpeedTime = now;
|
lastSpeedTime = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.activeJobs.set(uploadId, { jobId, speedKbs: currentSpeedKbs, bytesUploaded });
|
activeEntry.speedKbs = currentSpeedKbs;
|
||||||
|
activeEntry.bytesUploaded = bytesUploaded;
|
||||||
|
|
||||||
// Throttle progress emissions to reduce IPC + rendering overhead
|
// Throttle progress emissions to reduce IPC + rendering overhead
|
||||||
if (now - lastEmitTime < PROGRESS_EMIT_INTERVAL) return;
|
if (now - lastEmitTime < PROGRESS_EMIT_INTERVAL) return;
|
||||||
@ -582,18 +587,17 @@ class UploadManager extends EventEmitter {
|
|||||||
_startStatsTimer() {
|
_startStatsTimer() {
|
||||||
if (this.statsInterval) clearInterval(this.statsInterval);
|
if (this.statsInterval) clearInterval(this.statsInterval);
|
||||||
this.statsInterval = setInterval(() => {
|
this.statsInterval = setInterval(() => {
|
||||||
|
// Single pass over active jobs instead of two.
|
||||||
let globalSpeedKbs = 0;
|
let globalSpeedKbs = 0;
|
||||||
let activeCount = 0;
|
let activeCount = 0;
|
||||||
|
let inProgressBytes = 0;
|
||||||
for (const job of this.activeJobs.values()) {
|
for (const job of this.activeJobs.values()) {
|
||||||
globalSpeedKbs += job.speedKbs || 0;
|
globalSpeedKbs += job.speedKbs || 0;
|
||||||
|
inProgressBytes += job.bytesUploaded || 0;
|
||||||
activeCount++;
|
activeCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
const elapsed = Math.round((Date.now() - this.startTime) / 1000);
|
const elapsed = Math.round((Date.now() - this.startTime) / 1000);
|
||||||
let inProgressBytes = 0;
|
|
||||||
for (const job of this.activeJobs.values()) {
|
|
||||||
inProgressBytes += job.bytesUploaded || 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.emit('stats', {
|
this.emit('stats', {
|
||||||
state: this.running ? (this.stopAfterActive ? 'stopping' : 'uploading') : 'idle',
|
state: this.running ? (this.stopAfterActive ? 'stopping' : 'uploading') : 'idle',
|
||||||
|
|||||||
29
main.js
29
main.js
@ -165,12 +165,33 @@ const _uploadLogBuffer = [];
|
|||||||
let _uploadLogFlushTimer = null;
|
let _uploadLogFlushTimer = null;
|
||||||
let _uploadLogWriting = false;
|
let _uploadLogWriting = false;
|
||||||
|
|
||||||
|
// Cache the resolved upload-log target across flushes — mkdirSync + path
|
||||||
|
// assembly on every 500ms flush during uploads is wasted work once we've
|
||||||
|
// confirmed a writable directory. Invalidated when the user changes the log
|
||||||
|
// path or when the daily-log date rolls over.
|
||||||
|
let _cachedUploadLogTarget = null;
|
||||||
|
let _cachedUploadLogKey = '';
|
||||||
|
|
||||||
|
function _invalidateUploadLogTargetCache() {
|
||||||
|
_cachedUploadLogTarget = null;
|
||||||
|
_cachedUploadLogKey = '';
|
||||||
|
}
|
||||||
|
|
||||||
function _resolveUploadLogTarget() {
|
function _resolveUploadLogTarget() {
|
||||||
// Try primary → desktop → userData, mirror the original fallback ladder.
|
|
||||||
const primary = getLogFilePath();
|
const primary = getLogFilePath();
|
||||||
|
const key = `${primary}|${_dailyLogDate || ''}`;
|
||||||
|
if (_cachedUploadLogKey === key && _cachedUploadLogTarget) return _cachedUploadLogTarget;
|
||||||
|
|
||||||
|
const commit = (t) => {
|
||||||
|
_cachedUploadLogTarget = t;
|
||||||
|
_cachedUploadLogKey = key;
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try primary → desktop → userData, mirror the original fallback ladder.
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(path.dirname(primary), { recursive: true });
|
fs.mkdirSync(path.dirname(primary), { recursive: true });
|
||||||
return { path: primary, isFallback: false };
|
return commit({ path: primary, isFallback: false });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debugLog(`uploadLog primary dir unavailable (${err.message})`);
|
debugLog(`uploadLog primary dir unavailable (${err.message})`);
|
||||||
}
|
}
|
||||||
@ -179,13 +200,13 @@ function _resolveUploadLogTarget() {
|
|||||||
try {
|
try {
|
||||||
const p = buildFallbackLogName(desktop);
|
const p = buildFallbackLogName(desktop);
|
||||||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||||
return { path: p, isFallback: true };
|
return commit({ path: p, isFallback: true });
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const p = buildFallbackLogName(app.getPath('userData'));
|
const p = buildFallbackLogName(app.getPath('userData'));
|
||||||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||||||
return { path: p, isFallback: true };
|
return commit({ path: p, isFallback: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
debugLog(`uploadLog: no writable target (${err.message})`);
|
debugLog(`uploadLog: no writable target (${err.message})`);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -3264,6 +3264,7 @@ function renderRecentUploadsPanel() {
|
|||||||
&& rows.length > _recentLastRenderedLen
|
&& rows.length > _recentLastRenderedLen
|
||||||
&& tbody.querySelectorAll('.recent-file-row').length === _recentLastRenderedLen;
|
&& tbody.querySelectorAll('.recent-file-row').length === _recentLastRenderedLen;
|
||||||
|
|
||||||
|
let wasAppendOnly = false;
|
||||||
if (dateDescAppendOnly) {
|
if (dateDescAppendOnly) {
|
||||||
// Fast path: only new rows (date desc puts newest on top) — insert them
|
// Fast path: only new rows (date desc puts newest on top) — insert them
|
||||||
// at the top without rebuilding the 5000-row tbody below.
|
// at the top without rebuilding the 5000-row tbody below.
|
||||||
@ -3271,6 +3272,7 @@ function renderRecentUploadsPanel() {
|
|||||||
let html = '';
|
let html = '';
|
||||||
for (let i = 0; i < added; i++) html += _buildRecentRowHtml(rows[i]);
|
for (let i = 0; i < added; i++) html += _buildRecentRowHtml(rows[i]);
|
||||||
tbody.insertAdjacentHTML('afterbegin', html);
|
tbody.insertAdjacentHTML('afterbegin', html);
|
||||||
|
wasAppendOnly = true;
|
||||||
} else {
|
} else {
|
||||||
tbody.innerHTML = rows.map(_buildRecentRowHtml).join('');
|
tbody.innerHTML = rows.map(_buildRecentRowHtml).join('');
|
||||||
}
|
}
|
||||||
@ -3316,7 +3318,8 @@ function renderRecentUploadsPanel() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateRecentSortHeaders();
|
// Sort headers only change when the sort state changes — skip on appends.
|
||||||
|
if (!wasAppendOnly) updateRecentSortHeaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderHistoryTable(container) {
|
function renderHistoryTable(container) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user