From 0a807de8612b996af198086b486ff301678907c6 Mon Sep 17 00:00:00 2001 From: KennyG Date: Mon, 30 Mar 2026 09:24:04 -0400 Subject: [PATCH] Optimize real-time updates in downloads service with throttling to reduce CPU usage --- ui/src/app/app.ts | 11 +++++++--- ui/src/app/services/downloads.service.ts | 27 +++++++++++++++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index 4b0924f..b1b8c1e 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -1,7 +1,7 @@ import { AsyncPipe, DatePipe, KeyValuePipe } from '@angular/common'; import { HttpClient } from '@angular/common/http'; import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, DestroyRef, ElementRef, viewChild, inject, OnDestroy, OnInit } from '@angular/core'; -import { Observable, map, distinctUntilChanged } from 'rxjs'; +import { Observable, map, distinctUntilChanged, auditTime } from 'rxjs'; import { FormsModule } from '@angular/forms'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; @@ -250,8 +250,13 @@ export class App implements AfterViewInit, OnInit, OnDestroy { this.rebuildSortedDone(); this.cdr.markForCheck(); }); - // Subscribe to real-time updates - this.downloads.updated.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => { + // Subscribe to real-time updates (throttled to reduce CPU on large queues). + this.downloads.updated + .pipe( + auditTime(200), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(() => { this.updateMetrics(); this.cdr.markForCheck(); }); diff --git a/ui/src/app/services/downloads.service.ts b/ui/src/app/services/downloads.service.ts index 0a2b6f4..36db9c5 100644 --- a/ui/src/app/services/downloads.service.ts +++ b/ui/src/app/services/downloads.service.ts @@ -36,6 +36,9 @@ export class DownloadsService { ytdlOptionsChanged = new Subject>(); configurationChanged = new Subject>(); updated = new Subject(); + private updateRefreshScheduled = false; + private readonly foregroundRefreshMs = 250; + private readonly backgroundRefreshMs = 30000; configuration: Record = {}; customDirs: Record = {}; @@ -68,7 +71,7 @@ export class DownloadsService { data.checked = !!dl?.checked; data.deleting = !!dl?.deleting; this.queue.set(data.url, data); - this.updated.next(); + this.scheduleUpdatedRefresh(); }); this.socket.fromEvent('completed') .pipe(takeUntilDestroyed()) @@ -117,6 +120,28 @@ export class DownloadsService { }); } + private scheduleUpdatedRefresh() { + // Coalesce high-frequency download progress events into a capped refresh rate. + // requestAnimationFrame (~60fps) is smooth but expensive on large queues. + // We trade a little smoothness for much lower CPU: + // - foreground: ~8fps + // - background: ~1fps + if (this.updateRefreshScheduled) { + return; + } + this.updateRefreshScheduled = true; + + const flush = () => { + this.updateRefreshScheduled = false; + this.updated.next(); + }; + + const delay = document.hidden + ? this.backgroundRefreshMs + : this.foregroundRefreshMs; + setTimeout(() => flush(), delay); + } + handleHTTPError(error: HttpErrorResponse) { const msg = error.error instanceof ErrorEvent ? error.error.message