From 10528a2696440d18808d80010a729d206dc0bd4e Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Thu, 20 Aug 2026 05:23:59 +0200 Subject: [PATCH] perf(ui): enforce 750 ms live update cadence Use one 750 ms interval for running download snapshots, progress updates, the bandwidth chart, and the header speed sparkline across every queue size. Remove the extra active-download renderer delay and add timing regressions for small, medium, and large queues. --- src/main/download-manager.ts | 21 +++++-------------- src/renderer/App.tsx | 6 +++--- tests/app-shell.test.tsx | 13 +++++++++++- tests/download-manager.test.ts | 38 ++++++++++++++++++++++++++++++++++ tests/statistics-view.test.tsx | 2 +- 5 files changed, 59 insertions(+), 21 deletions(-) diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 829ddae..57203b1 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -163,8 +163,9 @@ const ARCHIVE_SETTLE_MAX_WAIT_MS = 5000; const MAX_SAME_DIRECT_URL_ATTEMPTS = 3; -const MAX_HTTP416_FRESH_RESTARTS = 2; -const HTTP416_FRESH_RESTART_DELAY_MS = 8000; +const MAX_HTTP416_FRESH_RESTARTS = 2; +const HTTP416_FRESH_RESTART_DELAY_MS = 8000; +const DOWNLOAD_LIVE_UPDATE_INTERVAL_MS = 750; function getHttp416FreshRestartDelayMs(): number { const fromEnv = Number(process.env.RD_HTTP416_FRESH_RESTART_DELAY_MS ?? NaN); @@ -6561,14 +6562,7 @@ export class DownloadManager extends EventEmitter { if (this.stateEmitTimer) { return; } - const itemCount = this.itemCount; - const emitDelay = this.session.running - ? itemCount >= 1500 - ? 700 - : itemCount >= 250 - ? 500 - : 150 - : 200; + const emitDelay = this.session.running ? DOWNLOAD_LIVE_UPDATE_INTERVAL_MS : 200; this.stateEmitTimer = setTimeout(() => { this.stateEmitTimer = null; this.lastStateEmitAt = nowMs(); @@ -10579,12 +10573,7 @@ export class DownloadManager extends EventEmitter { let windowStarted = nowMs(); let lastPackageLogAt = 0; let lastLoggedPercent = -1; - const itemCount = this.itemCount; - const uiUpdateIntervalMs = itemCount >= 1500 - ? 700 - : itemCount >= 250 - ? 500 - : 120; + const uiUpdateIntervalMs = DOWNLOAD_LIVE_UPDATE_INTERVAL_MS; let lastUiEmitAt = 0; const stallTimeoutMs = getDownloadStallTimeoutMs(); const drainTimeoutMs = Math.max(30000, Math.min(300000, stallTimeoutMs > 0 ? stallTimeoutMs * 12 : 120000)); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 2ba13f7..563f2fb 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -880,7 +880,7 @@ const historyRetentionLabels: Record= 250 ? 0 : 100; + let delay = running && activeTab === "downloads" ? 0 : itemCount >= 250 ? 0 : 100; if (!running) delay = Math.min(delay, 200); if (activeTab !== "downloads") delay = Math.max(delay, 800); return delay; @@ -1272,7 +1272,7 @@ const BandwidthChart = memo(function BandwidthChart({ running, paused, speedHist const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const interval = setInterval(() => { drawChart(); - }, reducedMotion ? 1000 : 500); + }, reducedMotion ? 1000 : 750); return () => clearInterval(interval); }, [drawChart, running, paused]); @@ -1374,7 +1374,7 @@ const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ speedBps, draw(); }; - const id = window.setInterval(tick, 500); + const id = window.setInterval(tick, 750); return () => window.clearInterval(id); }, []); diff --git a/tests/app-shell.test.tsx b/tests/app-shell.test.tsx index 1aa99b7..6719d7a 100644 --- a/tests/app-shell.test.tsx +++ b/tests/app-shell.test.tsx @@ -50,10 +50,21 @@ describe("desktop shell", () => { expect(getSnapshotRenderDelay(2_470, true, "statistics")).toBe(800); }); - it("renders medium download queues immediately after their calm half-second snapshot", () => { + it("renders medium download queues immediately after their calm 750 ms snapshot", () => { expect(getSnapshotRenderDelay(661, true, "downloads")).toBe(0); }); + it("does not add renderer delay after the 750 ms manager cadence for a small active queue", () => { + expect(getSnapshotRenderDelay(69, true, "downloads")).toBe(0); + }); + + it("redraws the header speed sparkline on the same 750 ms cadence", () => { + const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8"); + const sparklineBlock = source.slice(source.indexOf("const DownloadSpeedSparkline"), source.indexOf("const initialCollectorTabs")); + + expect(sparklineBlock).toContain("window.setInterval(tick, 750)"); + }); + it("keeps application menus mounted for animated opening and closing", () => { const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8"); const css = readFileSync(new URL("../src/renderer/styles.css", import.meta.url), "utf8"); diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index b71e5d8..d6b4b7d 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -45,6 +45,44 @@ describe("runWithLimitedConcurrency", () => { }); }); +describe("download live update cadence", () => { + it.each([69, 661, 2_470])("emits a running queue snapshot no sooner than 750 ms for %i items", async (itemCount) => { + vi.useFakeTimers(); + try { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-live-cadence-")); + tempDirs.push(root); + const session = emptySession(); + session.running = true; + const manager = new DownloadManager(defaultSettings(), session, createStoragePaths(path.join(root, "state"))); + const internal = manager as unknown as { + emitState: () => void; + itemCount: number; + session: typeof session; + stateEmitTimer: NodeJS.Timeout | null; + }; + internal.itemCount = itemCount; + internal.session.running = true; + if (internal.stateEmitTimer) { + clearTimeout(internal.stateEmitTimer); + internal.stateEmitTimer = null; + } + let emitted = 0; + manager.on("state", () => { + emitted += 1; + }); + + internal.emitState(); + await vi.advanceTimersByTimeAsync(749); + expect(emitted).toBe(0); + + await vi.advanceTimersByTimeAsync(1); + expect(emitted).toBe(1); + } finally { + vi.useRealTimers(); + } + }); +}); + describe("resolveUnrestrictTimeoutBudgetMs", () => { it("covers the complete provider plan and each later Real-Debrid account attempt", () => { const settings = { diff --git a/tests/statistics-view.test.tsx b/tests/statistics-view.test.tsx index 6b864ca..33c51cf 100644 --- a/tests/statistics-view.test.tsx +++ b/tests/statistics-view.test.tsx @@ -507,7 +507,7 @@ describe("bandwidth chart palette", () => { expect(chartBlock).toContain('role="img"'); expect(chartBlock).toContain('aria-label="Bandbreitenverlauf der letzten 60 Sekunden"'); expect(chartBlock).toContain('window.matchMedia("(prefers-reduced-motion: reduce)")'); - expect(chartBlock).toContain("reducedMotion ? 1000 : 500"); + expect(chartBlock).toContain("reducedMotion ? 1000 : 750"); }); it("asks for confirmation before deleting all saved download statistics", () => {