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>
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
# Performance Improvements — Job Tracker
|
||||
|
||||
**Companion to:** [MEMORY_LEAK_REPORT.md](MEMORY_LEAK_REPORT.md) · [ROOT_CAUSE_ANALYSIS.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.
|
||||
|
||||
## Recommended (low-severity, optional)
|
||||
|
||||
### 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. 15–20s) 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.
|
||||
Reference in New Issue
Block a user