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.
This commit is contained in:
Sucukdeluxe
2026-08-20 05:23:59 +02:00
parent 08f017e8c4
commit 10528a2696
5 changed files with 59 additions and 21 deletions
+5 -16
View File
@@ -163,8 +163,9 @@ const ARCHIVE_SETTLE_MAX_WAIT_MS = 5000;
const MAX_SAME_DIRECT_URL_ATTEMPTS = 3; const MAX_SAME_DIRECT_URL_ATTEMPTS = 3;
const MAX_HTTP416_FRESH_RESTARTS = 2; const MAX_HTTP416_FRESH_RESTARTS = 2;
const HTTP416_FRESH_RESTART_DELAY_MS = 8000; const HTTP416_FRESH_RESTART_DELAY_MS = 8000;
const DOWNLOAD_LIVE_UPDATE_INTERVAL_MS = 750;
function getHttp416FreshRestartDelayMs(): number { function getHttp416FreshRestartDelayMs(): number {
const fromEnv = Number(process.env.RD_HTTP416_FRESH_RESTART_DELAY_MS ?? NaN); const fromEnv = Number(process.env.RD_HTTP416_FRESH_RESTART_DELAY_MS ?? NaN);
@@ -6561,14 +6562,7 @@ export class DownloadManager extends EventEmitter {
if (this.stateEmitTimer) { if (this.stateEmitTimer) {
return; return;
} }
const itemCount = this.itemCount; const emitDelay = this.session.running ? DOWNLOAD_LIVE_UPDATE_INTERVAL_MS : 200;
const emitDelay = this.session.running
? itemCount >= 1500
? 700
: itemCount >= 250
? 500
: 150
: 200;
this.stateEmitTimer = setTimeout(() => { this.stateEmitTimer = setTimeout(() => {
this.stateEmitTimer = null; this.stateEmitTimer = null;
this.lastStateEmitAt = nowMs(); this.lastStateEmitAt = nowMs();
@@ -10579,12 +10573,7 @@ export class DownloadManager extends EventEmitter {
let windowStarted = nowMs(); let windowStarted = nowMs();
let lastPackageLogAt = 0; let lastPackageLogAt = 0;
let lastLoggedPercent = -1; let lastLoggedPercent = -1;
const itemCount = this.itemCount; const uiUpdateIntervalMs = DOWNLOAD_LIVE_UPDATE_INTERVAL_MS;
const uiUpdateIntervalMs = itemCount >= 1500
? 700
: itemCount >= 250
? 500
: 120;
let lastUiEmitAt = 0; let lastUiEmitAt = 0;
const stallTimeoutMs = getDownloadStallTimeoutMs(); const stallTimeoutMs = getDownloadStallTimeoutMs();
const drainTimeoutMs = Math.max(30000, Math.min(300000, stallTimeoutMs > 0 ? stallTimeoutMs * 12 : 120000)); const drainTimeoutMs = Math.max(30000, Math.min(300000, stallTimeoutMs > 0 ? stallTimeoutMs * 12 : 120000));
+3 -3
View File
@@ -880,7 +880,7 @@ const historyRetentionLabels: Record<RendererSettings["historyRetentionMode"], s
const AUTO_RENDER_PACKAGE_LIMIT = 260; const AUTO_RENDER_PACKAGE_LIMIT = 260;
export function getSnapshotRenderDelay(itemCount: number, running: boolean, activeTab: MainView): number { export function getSnapshotRenderDelay(itemCount: number, running: boolean, activeTab: MainView): number {
let delay = itemCount >= 250 ? 0 : 100; let delay = running && activeTab === "downloads" ? 0 : itemCount >= 250 ? 0 : 100;
if (!running) delay = Math.min(delay, 200); if (!running) delay = Math.min(delay, 200);
if (activeTab !== "downloads") delay = Math.max(delay, 800); if (activeTab !== "downloads") delay = Math.max(delay, 800);
return delay; return delay;
@@ -1272,7 +1272,7 @@ const BandwidthChart = memo(function BandwidthChart({ running, paused, speedHist
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches; const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const interval = setInterval(() => { const interval = setInterval(() => {
drawChart(); drawChart();
}, reducedMotion ? 1000 : 500); }, reducedMotion ? 1000 : 750);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [drawChart, running, paused]); }, [drawChart, running, paused]);
@@ -1374,7 +1374,7 @@ const DownloadSpeedSparkline = memo(function DownloadSpeedSparkline({ speedBps,
draw(); draw();
}; };
const id = window.setInterval(tick, 500); const id = window.setInterval(tick, 750);
return () => window.clearInterval(id); return () => window.clearInterval(id);
}, []); }, []);
+12 -1
View File
@@ -50,10 +50,21 @@ describe("desktop shell", () => {
expect(getSnapshotRenderDelay(2_470, true, "statistics")).toBe(800); 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); 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", () => { it("keeps application menus mounted for animated opening and closing", () => {
const source = readFileSync(new URL("../src/renderer/App.tsx", import.meta.url), "utf8"); 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"); const css = readFileSync(new URL("../src/renderer/styles.css", import.meta.url), "utf8");
+38
View File
@@ -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", () => { describe("resolveUnrestrictTimeoutBudgetMs", () => {
it("covers the complete provider plan and each later Real-Debrid account attempt", () => { it("covers the complete provider plan and each later Real-Debrid account attempt", () => {
const settings = { const settings = {
+1 -1
View File
@@ -507,7 +507,7 @@ describe("bandwidth chart palette", () => {
expect(chartBlock).toContain('role="img"'); expect(chartBlock).toContain('role="img"');
expect(chartBlock).toContain('aria-label="Bandbreitenverlauf der letzten 60 Sekunden"'); expect(chartBlock).toContain('aria-label="Bandbreitenverlauf der letzten 60 Sekunden"');
expect(chartBlock).toContain('window.matchMedia("(prefers-reduced-motion: reduce)")'); 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", () => { it("asks for confirmation before deleting all saved download statistics", () => {