Files
jobtrackingapp/docs/performance/PERFORMANCE_IMPROVEMENTS.md
T
cesnimda 39266c0935 docs(perf): memory-leak investigation reports (no leak found)
Evidence-based investigation across every leak vector (timers, listeners, object
URLs, observers, websockets, static server collections, IMemoryCache, Python
caches). Verdict: no confirmed memory leak — the codebase has disciplined
cleanup. One resource-release correctness bug (over-eager blob-URL revocation in
the CV carousel) was found and fixed (eed9b1f).

Adds docs/performance/: MEMORY_LEAK_REPORT.md, ROOT_CAUSE_ANALYSIS.md,
PERFORMANCE_IMPROVEMENTS.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 14:59:28 +02:00

4.0 KiB
Raw Blame History

Performance Improvements — Job Tracker

Companion to: MEMORY_LEAK_REPORT.md · ROOT_CAUSE_ANALYSIS.md

Changes made (this pass)

Change File Effect Verified
Revoke CV-preview blob URLs on unmount only (ref-based), not on every carousel change job-tracker-ui/src/pages/ProfilePage.tsx Fixes broken previews on multi-template decks; still frees URLs on unmount profile-page.test.tsx 5/5

Context: this was the only defect found in a full resource audit. The codebase already practises disciplined cleanup (timers cleared, listeners removed, object URLs revoked), so there was no leak to fix — see the main report.

1. Stabilise the extraction-run poll — minor

ProfilePage.tsx:315-324 recreates its 4s interval on every poll because extractionRuns is in the deps and changes each tick. It's harmless (cleanup runs; it stops when runs finish) but churns. If touched: poll on a stable trigger (e.g. a boolean hasActiveRuns in deps, or read runs from a ref inside the interval) so the interval is created once per active-window.

2. One live heap-snapshot pass on a populated session — verification, not a fix

The static audit is strong, but a single DevTools confirmation closes the loop:

  1. Run the real stack (backend on :5202 + a seeded DB) and sign in.
  2. DevTools → Memory → take a heap snapshot.
  3. Navigate /dashboard → /jobs → open a job dialog → close → /profile → build a CV deck → back, ×5.
  4. Force GC, take a second snapshot, Comparison view.
  5. Expect: node/listener/detached counts return to baseline (sawtooth), not monotonic growth. Sort retained size by constructor; look for Detached HTMLElement, growing Array/Map, or listener counts that never fall.

Also cheap and useful: performance.memory.usedJSHeapSize (Chromium) logged across the loop, or a Playwright script that repeats the navigation and asserts heap stays bounded.

3. Guard async setState after unmount — defensive, not a current leak

Several components await api…().then(setState). React 18 no-ops setState on unmounted components (just a dev warning historically), so this is not a leak, but for long CV/AI calls consider an AbortController on the request (cancels the in-flight network work on unmount) — improves responsiveness and avoids wasted work more than memory.

Prevention — keep leaks from creeping in

  • Lint: enable react-hooks/exhaustive-deps (surfaces the exact wrong-deps class that caused the one bug here) and consider react-hooks/react-compiler checks.
  • Rule of thumb: any effect that acquires a resource (listener, timer, object URL, observer, subscription, connection) must return a cleanup that releases exactly that resource. "Release once on unmount" ⇒ empty-deps effect + a ref for current state — never a value in the deps array.
  • Object URLs: pair every createObjectURL with a revokeObjectURL in the same owner; prefer revoking on unmount/replace, never on unrelated re-renders.
  • Server caches: every IMemoryCache.Set must carry an absolute/sliding expiration (as GmailOAuthService correctly does); if the app grows to heavy caching, set a SizeLimit.
  • No unbounded static state: keep static collections to fixed lookup tables (as today); never accumulate per-request data in a static field.
  • CI: the heavy RTL suites are timeout-flaky under load — raising testTimeout (e.g. 1520s) or reducing jest worker contention would make regressions (including any future leak-guard tests) reliably visible instead of hidden behind flakes.

Security-audit note (standing instruction)

The applied change carries no security surface (client-side URL lifetime only). The recommendations above introduce none either; if #3 (AbortController) is implemented, ensure aborted requests don't leave partial writes — not applicable to the read-only CV export/preview calls here.