Skip to content

Performance: Logs page unbounded buffer + full re-render per record #522

@nitrobass24

Description

@nitrobass24

The Logs page appends to an uncapped array and forces a synchronous full change-detection pass on every SSE log record, with no virtualization and track $index. During DEBUG logging on an active sync this is unbounded memory growth and steadily rising per-record render cost.

1. Live log: unbounded records + detectChanges() per record

Severity: medium · Effort: quick · Dimension: performance · Verdict: confirmed

Location: src/angular/src/app/pages/logs/logs-page.component.ts:44,59-77,103-109

Corrected/extra locations: src/angular/src/app/pages/logs/logs-page.component.ts:69-70 (append+detectChanges) and logs-page.component.html:58-74 (uncapped @for, track $index)

Impact
records is never capped: this.records = [...this.records, record] on every 'log-record' SSE event, then this.changeDetector.detectChanges() forces a synchronous full CD pass. The template renders @for (record of records; track $index) with NO virtualization, so the DOM grows one

per log line forever. With DEBUG logging during an active sync the backend can emit many records/second; over a long-lived session this is unbounded memory growth and steadily increasing per-record render cost (full array re-create + detectChanges + ngAfterViewChecked getBoundingClientRect on every record). track $index also defeats DOM node reuse on any prepend/trim.

Recommended fix
Cap records to a ring buffer (e.g. keep last 1000-2000, slice on overflow) and render with CDK virtual scroll instead of a plain @for. Use a stable track key (record timestamp+seq) rather than $index. Optionally batch incoming records (throttle/auditTime) before detectChanges so bursts collapse into one render.


2. (dup-context) records grow unbounded with detectChanges per record

Severity: low · Effort: quick · Dimension: performance · Verdict: unverified

Location: src/angular/src/app/pages/logs/logs-page.component.ts:44, 60-77

Impact
On a busy server with the Logs page left open, records accumulates without bound; each new record copies the entire array (O(n) per event) and forces a synchronous full change-detection pass, so cost grows quadratically over time and the tab's memory climbs steadily. Only mitigated by navigating away (LogService.logs$ is a non-replay Subject, so records reset on remount).

Recommended fix
Cap the retained records to a sane window (e.g. last 1000–5000) by slicing the array when it exceeds the limit, mirroring a ring-buffer. Consider trackBy on the rendered list to reduce DOM churn.


3. Log history list uses track $index → full DOM rebuild on filter

Severity: low · Effort: quick · Dimension: performance · Verdict: unverified

Location: src/angular/src/app/pages/logs/logs-page.component.html:29-42

Impact
@for (entry of historyRecords; track $index) means Angular keys rows by position. fetchHistory returns up to 500 entries; on every search/level change the whole historyRecords array is replaced and, because tracking is by index, Angular re-renders/re-binds all up-to-500

nodes instead of reusing them. With debounced search this still rebuilds the full 500-row list per committed query.

Recommended fix
Track by a stable identity (e.g. entry.timestamp + entry.message, or a synthetic id) so unchanged rows are reused. For 500 rows consider the same virtual-scroll treatment as the live log.


Acceptance criteria

  • Live and history log lists are capped to a ring buffer (e.g. last 1000–2000)
  • Rendered with CDK virtual scroll instead of a plain @for
  • A stable track key (timestamp+seq) replaces track $index
  • Incoming records are batched (throttle/auditTime) before change detection

From the codebase audit — see REVIEW.md. Generated by the codebase-audit workflow; findings adversarially verified against the code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    auditFindings from the codebase audit (REVIEW.md)javascriptPull requests that update javascript codeoptimizationPerformance and size optimization

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions