Skip to content

fix: add LRU eviction to unbounded caches to prevent memory leaks#33

Open
ggfevans wants to merge 3 commits intosteipete:mainfrom
ggfevans:fix/13-memory-leak-unbounded-caches
Open

fix: add LRU eviction to unbounded caches to prevent memory leaks#33
ggfevans wants to merge 3 commits intosteipete:mainfrom
ggfevans:fix/13-memory-leak-unbounded-caches

Conversation

@ggfevans
Copy link

@ggfevans ggfevans commented Jan 31, 2026

Summary

  • Add LRU eviction to ETagCache with 200 entry limit
  • Add LRU eviction to RecentListCache with 50 entry limit per cache type
  • Add @MainActor to RecentListCache for explicit thread safety
  • Add clearAllCaches() method to RecentMenuService for manual cache clearing

Problem

After running for ~2.5 days, RepoBar accumulated ~2GB of memory due to unbounded in-memory caches:

  1. ETagCache: Stored (etag, Data) tuples for every URL ever fetched with no eviction
  2. RecentListCache: 9 separate caches (issues, PRs, releases, commits, discussions, tags, branches, contributors, workflow runs) that stored data per repository key without eviction
  3. recentCommitCounts: Dictionary that grew without bound

Solution

Add LRU (Least Recently Used) eviction to both cache types using an access-order tracking array:

ETagCache

  • Track access order with an array
  • Evict least recently used entries when exceeding 200 entries
  • Move accessed entries to end of order list (most recently used)

RecentListCache

  • Track access order with an array (same pattern as ETagCache)
  • Only evict when inserting new keys (not when updating existing)
  • Evict least recently accessed entries when exceeding 50 entries per cache type
  • Add @MainActor isolation for thread safety

The limits are conservative to maintain good cache hit rates while preventing unbounded growth.

Test plan

  • Build succeeds (swift build)
  • All 257 tests pass (swift test)
  • CodeRabbit review passed (addressed all issues)
  • Memory usage stays bounded after extended runtime (requires multi-day testing)

Fixes #13

🤖 Generated with Claude Code

The app accumulated ~2GB memory after running for 2.5 days due to
unbounded in-memory caches that never evicted old entries.

Changes:
- ETagCache: Add LRU eviction with 200 entry limit. Tracks access order
  and evicts least recently used entries when capacity is reached.
- RecentListCache: Add LRU eviction with 50 entry limit per cache type.
  There are 9 cache instances (issues, PRs, releases, commits, etc.)
  that now evict least recently accessed repository entries.
  Uses access-order array for O(1) eviction (same pattern as ETagCache).
- Add @mainactor to RecentListCache for explicit thread safety.
- Add clearAllCaches() method to RecentMenuService to clear all caches
  including the recentCommitCounts dictionary.

The limits are conservative (200 ETag entries, 50 repos per list type)
to maintain good cache hit rates while preventing unbounded growth.

Fixes steipete#13

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@ggfevans ggfevans force-pushed the fix/13-memory-leak-unbounded-caches branch from 0a5721c to 7e1ecf7 Compare January 31, 2026 02:21
ggfevans and others added 2 commits January 30, 2026 18:24
CodeRabbit identified that the ETagCache save() method would
unnecessarily evict entries when updating an existing key at
capacity. This fix ensures eviction only occurs when inserting
new keys, matching the pattern in RecentListCache.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@ggfevans
Copy link
Author

Hey @steipete I'm curious on your thoughts here in terms of cache count thresholds.

Otherwise this seemed relatively low hanging as well in terms of closing potential memory leaks/unbounded growth.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory leak: 2GB RAM after ~2.5 days of running

1 participant