Performance: ItemRow extraction + scheduler single-pass + selectedIds memo fix

Major optimizations to reduce UI lag with large queues (5000+ items):

1. ItemRow extracted to its own memoized component (renderer)
   Previously every package re-render mapped all its items inline,
   producing N×M re-renders per state update. Now each item-row only
   re-renders when ITS specific data changes, with custom equality on
   the visible fields (status, progress, speed, fullStatus, etc.).
   Also adds stable useCallback handlers per item.

2. PackageCard stats consolidated into single useMemo (renderer)
   Replaces 5 separate filter()/some() + 2 reduce() calls (O(7N)) with
   one O(N) pass collecting all aggregates (done/failed/cancelled/
   extracted/extracting/activeProgress/extractingProgress).

3. selectedIds memo comparator fixed (renderer)
   Custom equality now checks if selection state changed for items in
   THIS package only. Previously any selection anywhere broke memo on
   all 200+ visible PackageCards.

4. Scheduler single-pass queue presence (main)
   New getQueuePresence() returns hasImmediate + hasDelayed in one
   iteration. Replaces hasQueuedItems() + hasDelayedQueuedItems() that
   each scanned packages independently. Saves one full O(n) iteration
   per scheduler tick.

No functional changes. All 565 tests green.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe
2026-04-19 13:07:42 +02:00
co-authored by Claude Opus 4.6
parent c1edb07009
commit 3c9894c7b0
2 changed files with 229 additions and 143 deletions
+23 -39
View File
@@ -7407,7 +7407,9 @@ export class DownloadManager extends EventEmitter {
this.runGlobalStallWatchdog(now);
const downloadsComplete = this.activeTasks.size === 0 && !this.hasQueuedItems() && !this.hasDelayedQueuedItems();
// Single-pass queue presence check (saves one full O(n) iteration per tick)
const queuePresence = this.activeTasks.size === 0 ? this.getQueuePresence(now) : { hasImmediate: true, hasDelayed: false };
const downloadsComplete = this.activeTasks.size === 0 && !queuePresence.hasImmediate && !queuePresence.hasDelayed;
const postProcessComplete = this.packagePostProcessTasks.size === 0;
if (downloadsComplete && (postProcessComplete || this.settings.autoExtractWhenStopped)) {
this.finishRun();
@@ -7617,56 +7619,38 @@ export class DownloadManager extends EventEmitter {
return null;
}
private hasQueuedItems(): boolean {
const now = nowMs();
/** Single-pass alternative to hasQueuedItems + hasDelayedQueuedItems.
* Returns both flags so the scheduler termination check needs only ONE
* iteration over packages/items per tick instead of two separate scans. */
private getQueuePresence(now = nowMs()): { hasImmediate: boolean; hasDelayed: boolean } {
let hasImmediate = false;
let hasDelayed = false;
for (const packageId of this.session.packageOrder) {
const pkg = this.session.packages[packageId];
if (!pkg || pkg.cancelled || !pkg.enabled) {
continue;
}
if (this.runPackageIds.size > 0 && !this.runPackageIds.has(packageId)) {
continue;
}
if (!pkg || pkg.cancelled || !pkg.enabled) continue;
if (this.runPackageIds.size > 0 && !this.runPackageIds.has(packageId)) continue;
for (const itemId of pkg.itemIds) {
const item = this.session.items[itemId];
if (!item) {
continue;
}
if (!item) continue;
if (item.status !== "queued" && item.status !== "reconnect_wait") continue;
const retryAfter = this.retryAfterByItem.get(itemId) || 0;
if (retryAfter > now) {
continue;
}
if (item.status === "queued" || item.status === "reconnect_wait") {
return true;
hasDelayed = true;
} else {
hasImmediate = true;
}
if (hasImmediate && hasDelayed) return { hasImmediate, hasDelayed };
}
}
return false;
return { hasImmediate, hasDelayed };
}
private hasQueuedItems(): boolean {
return this.getQueuePresence().hasImmediate;
}
private hasDelayedQueuedItems(): boolean {
const now = nowMs();
for (const [itemId, readyAt] of this.retryAfterByItem.entries()) {
if (readyAt <= now) {
continue;
}
const item = this.session.items[itemId];
if (!item) {
continue;
}
if (item.status !== "queued" && item.status !== "reconnect_wait") {
continue;
}
const pkg = this.session.packages[item.packageId];
if (!pkg || pkg.cancelled || !pkg.enabled) {
continue;
}
if (this.runPackageIds.size > 0 && !this.runPackageIds.has(item.packageId)) {
continue;
}
return true;
}
return false;
return this.getQueuePresence().hasDelayed;
}
private countQueuedItems(): number {