# Memory Leak Report — Job Tracker **Date:** 2026-07-05 **Investigator role:** Senior Performance Engineer (memory/browser internals/full-stack) **Verdict:** **No confirmed memory leak.** One *resource-release correctness* bug (over-eager blob-URL revocation) was found and fixed; it is the opposite of a leak. See [ROOT_CAUSE_ANALYSIS.md](ROOT_CAUSE_ANALYSIS.md) and [PERFORMANCE_IMPROVEMENTS.md](PERFORMANCE_IMPROVEMENTS.md). > Method & honesty note. The app is a data-driven SPA that renders only after the backend answers > `/auth/config` + `/auth/me`; headless (no backend/DB) it sits on a "Loading…" screen, so live > DevTools heap-snapshot/allocation-timeline profiling of populated screens was **not** performed in this > environment. Evidence here is therefore **static code analysis of every known leak vector** plus the > existing automated test suite. Where a runtime confirmation is still advisable, it is called out > explicitly. Per the mission's Final Rule, nothing below is reported as a leak unless the code path > actually retains memory — and none did. --- ## Phase 1–2 — Does a leak exist? Can it be reproduced? No leak was reproduced or evidenced. The classic React/browser leak vectors were each checked in code and found to have correct teardown. "Memory grows while using the app" (the usual trigger for this kind of investigation) is explained by **expected behaviour** — MUI/emulator caches, route-level component state, and delayed GC — not by retained graphs. There is no growing global collection, no unremoved listener, no uncleared timer, and no real-time connection to leak. ## Phase 3 / 3.5 — Vector-by-vector evidence | Vector | Finding | Evidence | Verdict | |---|---|---|---| | **Timers / intervals** | Both `setInterval`s clear on cleanup | `App.tsx:154-155` (reminders, 60s → `clearInterval`); `ProfilePage.tsx:319-323` (extraction poll, 4s → `clearInterval`) | ✅ no leak | | **`setTimeout`** | Used only for one-shot object-URL revokes | `BackupCard.tsx:29`, `Attachments.tsx:193`, `ImportExportJobs.tsx:21` | ✅ no leak | | **Event listeners** | Every `addEventListener` has a matching `removeEventListener` in the effect cleanup | `App.tsx:174-175` (auth-changed), `App.tsx:185-186` (keydown), `CropImageDialog.tsx:114-124` (mouse/touch drag ×4) | ✅ no leak | | **Object URLs (media)** | Created URLs are revoked on cleanup/timeout | `CropImageDialog.tsx:59/65`, `Attachments.tsx:111/181/193/201`, `BackupCard.tsx:18/29`, `ImportExportJobs.tsx:16/21`, `JobDetailsDialog.tsx:507/514`, `ProfilePage.tsx` (see fix) | ✅ no leak (1 over-revoke bug fixed) | | **Observers** | None used | grep: no `ResizeObserver` / `IntersectionObserver` / `MutationObserver` in `src/` | ✅ n/a | | **WebSocket / SSE / SignalR** | None used | grep: no `new WebSocket` / `EventSource` / SignalR client anywhere | ✅ n/a | | **Signal/event subscriptions** | Only the `window` `"auth-changed"` custom event; unsubscribed on cleanup | `App.tsx:157-176` | ✅ no leak | | **Global/module state (client)** | No module-level mutable collection that grows unbounded | grep for module-scope `Map`/array caches — none accumulating | ✅ no leak | | **Client caches (localStorage)** | Bounded keys (prefs, columns, saved views); no per-event append | `App.tsx`, `SettingsView.tsx`, `SavedViewsMenu.tsx`, `themePrefs.ts` | ✅ no leak | | **React effects w/o cleanup** | All effects reviewed return cleanup where they acquire resources | see rows above | ✅ no leak | | **Server static collections** | All `static` collections are **fixed lookup tables** or **method return types**, never growing fields | `AttachmentsController`, `AuthController`, `ProfileCvController`, `HumanLanguageCatalog`, `StructuredCvProfileJson` | ✅ no leak | | **Server `IMemoryCache`** | Bounded: OAuth state entries expire in 15 min and are removed on consume | `GmailOAuthService.cs:72` (`TimeSpan.FromMinutes(15)`), `:133-138` (`TryGetValue`+`Remove`) | ✅ no leak | | **AI service (Python) caches** | `cachetools.TTLCache` (bounded by TTL + maxsize) | `tools/summarizer/app.py:4` | ✅ no leak | | **Server timers / background** | Hosted services use scoped DI + `PeriodicTimer`/delays; no accumulating handlers | `FollowUpReminderHostedService`, `RulesHostedService`, `JobEnrichmentHostedService`, etc. | ✅ no leak | ## Phase 3.5 — Repeated/duplicate work audit - **Reminders poll** (`App.tsx:151`, every 60s): correct URL `/jobapplications/reminders`, cheap, cleaned up. (An earlier read rendered the path with backslashes — a display artifact; the source uses forward slashes. **No bug.**) - **Extraction-run poll** (`ProfilePage.tsx:315-324`, every 4s): effect deps `[extractionRuns, loadProfile]` and `extractionRuns` changes each poll, so the interval is torn down + recreated every 4s while a run is active. **Not a leak** (cleanup runs); benign churn that self-terminates when runs finish. Minor — see [PERFORMANCE_IMPROVEMENTS.md](PERFORMANCE_IMPROVEMENTS.md). - No duplicate subscriptions, no retry storms, no infinite render loops observed. ## Phase 4 — Root cause No leak → no leak root cause. The single defect found is an *over-release* (revoking blob URLs still in use), root-caused in [ROOT_CAUSE_ANALYSIS.md](ROOT_CAUSE_ANALYSIS.md). ## Phase 5 — Fix `fix(profile): revoke CV-preview blob URLs on unmount, not on every change` (commit `eed9b1f`). Smallest change: track the carousel in a ref and revoke only on unmount. ## Phase 6 — Verification `profile-page.test.tsx` passes **5/5** with an adequate test timeout after the fix. The broader suite's intermittent timeouts are a **pre-existing** flakiness of the heavy RTL suites (verified: they fail identically on the clean tree; three of them don't touch `ProfilePage`). ## Phase 7 — Regression audit Swept all object-URL, timer, and listener sites (table above). No other instance of the over-revoke pattern, and no missing-cleanup pattern, was found. ## Remaining risks / recommendations - Live heap-snapshot profiling on a **populated** session (real backend) is still worth doing once, to confirm the static conclusion under real navigation — see [PERFORMANCE_IMPROVEMENTS.md](PERFORMANCE_IMPROVEMENTS.md). - Keep the disciplined cleanup pattern (this codebase is already good at it). ## Security-audit note (standing instruction) The single code change is a client-side blob-URL revocation-timing fix: no auth/authz surface, no new user input, no data exposure, no injection vector, no secret handling. Nothing for the security lens to flag. Existing protections (HttpOnly-cookie + CSRF auth, SSRF blocklist, global query-filter tenancy) are untouched.