Files
jobtrackingapp/docs/_archive/performance/MEMORY_LEAK_REPORT.md
T
cesnimda b176a44627 docs: reorganize tree, restore architecture + research from archive, add Phase 0 reports
Active docs/ was stub scaffolding while the real docs sat in docs/_archive/.
Restore and correct them, and record the Phase 0 work.

- docs/architecture/current.md: verified system map (from archived SYSTEM_OVERVIEW,
  9 corrections against code).
- docs/research/competitors.md: sourced competitor analysis (from archived
  PRODUCT_RESEARCH, feature matrix corrected).
- docs/decisions/ADR-002-job-application-model.md: the Job/JobApplication split.
- docs/application-discovery-report.md, docs/implementation-roadmap.md,
  docs/phase-0-foundation-report.md, docs/career-workspace-branch-assessment.md.
- Remove 10 zero-byte placeholder files that advertised content that never existed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:04:32 +02:00

6.5 KiB
Raw Blame History

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 and 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 12 — 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 setIntervals 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.
  • 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.

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.
  • 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.