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:
co-authored by
Claude Opus 4.6
parent
c1edb07009
commit
3c9894c7b0
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user