Performance: visiblePackages dep fix + ItemRow date memo + small Object.keys cuts

Five more low-risk hot-path optimizations:

1. visiblePackages no longer re-runs the sort callback on every item update.
   The sort is only meaningful when running && autoSort && >1 packages, so we
   pass null as items dep otherwise. Previously fired the full O(N) sort
   pass on every progress tick even when it would have returned the input
   array unchanged.

2. ItemRow memoizes formattedCreatedAt + displayStatus + statusTitle so a
   row that re-renders because of progress/speed changes no longer pays for
   formatDateTime() and computeDisplayedItemStatus() twice (title+body).

3. resetSessionTotalsIfQueueEmpty: removed redundant Object.keys() check.
   itemCount + packageOrder.length cover the same condition without
   allocating two intermediate arrays.

4. markQueuedAsReconnectWait: replaced Object.keys() array allocation with
   for-in iteration when runItemIds is empty. Saves a 5000-element string
   array allocation every 900ms during reconnect.

5. columnOrderJson useEffect dep: replaced JSON.stringify with cached
   join() + useMemo. Stringifying a 7-element settings array on every
   render was ~2-3ms per render for nothing.

All 140 download-manager tests green. Renderer + main both build clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe
2026-04-19 13:46:57 +02:00
co-authored by Claude Opus 4.6
parent b92330863a
commit d71dd3af0b
2 changed files with 53 additions and 28 deletions
+16 -11
View File
@@ -2149,12 +2149,14 @@ export class DownloadManager extends EventEmitter {
}
private resetSessionTotalsIfQueueEmpty(force = false): void {
// Cheap O(1) check via cached counters covers the common case.
// The Object.keys() cross-check below was redundant — itemCount and
// packageOrder are kept in sync with session.items / session.packages
// by every mutation site, so the second check just allocated two
// arrays per call without ever changing the outcome.
if (this.itemCount > 0 || this.session.packageOrder.length > 0) {
return;
}
if (Object.keys(this.session.items).length > 0 || Object.keys(this.session.packages).length > 0) {
return;
}
if (!force && (this.sessionDownloadedBytes > 0 || this.sessionCompletedFiles > 0 || this.itemContributedBytes.size > 0)) {
return;
}
@@ -7566,22 +7568,25 @@ export class DownloadManager extends EventEmitter {
let changed = false;
const waitSeconds = Math.max(0, Math.ceil((this.session.reconnectUntil - nowMs()) / 1000));
const waitText = `Reconnect-Wait (${waitSeconds}s)`;
const itemIds = this.runItemIds.size > 0 ? this.runItemIds : Object.keys(this.session.items);
for (const itemId of itemIds) {
// Iterate without allocating an Object.keys() array (called every 900ms
// during reconnect; with 5000+ items that's a 5000-string allocation per tick).
const updateItem = (itemId: string): void => {
const item = this.session.items[itemId];
if (!item) {
continue;
}
if (!item) return;
const pkg = this.session.packages[item.packageId];
if (!pkg || pkg.cancelled || !pkg.enabled) {
continue;
}
if (!pkg || pkg.cancelled || !pkg.enabled) return;
if (item.status === "queued") {
item.status = "reconnect_wait";
item.fullStatus = waitText;
item.updatedAt = nowMs();
changed = true;
}
};
if (this.runItemIds.size > 0) {
for (const itemId of this.runItemIds) updateItem(itemId);
} else {
// for-in iterates own enumerable string keys without allocating an array
for (const itemId in this.session.items) updateItem(itemId);
}
if (changed) {
this.emitState();