Skip to content

Fix memory leak when opening a save or reloading a pack (#84)#88

Merged
emosaru merged 2 commits into
mainfrom
fix/issue-84-memory-leak
May 10, 2026
Merged

Fix memory leak when opening a save or reloading a pack (#84)#88
emosaru merged 2 commits into
mainfrom
fix/issue-84-memory-leak

Conversation

@emosaru
Copy link
Copy Markdown
Contributor

@emosaru emosaru commented May 10, 2026

Summary

Fixes two distinct memory leaks reported in #84 (large packs, Linux 3.0.3.1):

  • ~600 MB per save-openTrackerState.LoadProgress() created a fresh PackageInstance for the save's (pack, variant) but the old PI was never disposed, leaving its image caches, Lua state, and definitional TrackerState alive indefinitely. Fix: ApplicationModel.LoadProgress() now detects the PI change, migrates the primary state to the new PI via the new PackageInstance.MigrateStateTo(), and disposes the old PI.

  • ~250 MB per reloadPackageLoader.LoadInto() reset the item/location catalogs but left PackageInstance.ImageCache and SourceImageCache intact. Old ImageReference keys from the reset catalogs kept decoded SKBitmaps and IImages alive across every reload. Fix: flush both caches after the catalog resets in PackageLoader.LoadInto().

  • Static cache accumulation (compounding)IconUtility.sPngCache and sAlphaMasks used ConcurrentDictionary with strong IImage references as keys, preventing Avalonia.Bitmap objects from being GC'd even after the PI's ImageCache was cleared. Fix: switch both to ConditionalWeakTable so entries are evicted automatically when the IImage key becomes unreachable.

Files changed

File Change
PackageInstance.cs Add MigrateStateTo() to transfer a state between PIs without disposal
ApplicationModel.cs LoadProgress() detects PI change and disposes old PI
PackageLoader.cs Flush image caches after catalog resets; extract FlushImageCaches() helper
IconUtility.cs sPngCache/sAlphaMasksConditionalWeakTable for automatic eviction

Test plan

  • Open a large pack (e.g. ALBWR tracker), note memory usage (~1.0 GB baseline)
  • Open a save file — confirm memory does not increase by ~600 MB; should stay near baseline after GC
  • Reload the pack several times — confirm memory does not grow ~250 MB per reload
  • Open additional save files repeatedly — confirm memory stays stable
  • Confirm items/images render correctly after each open/reload

🤖 Generated with Claude Code

Two distinct leaks caused memory to grow ~600 MB per save-open and
~250 MB per reload for large packs:

1. LoadProgress leak (~600 MB per open):
   TrackerState.LoadProgress() creates a fresh PackageInstance for the
   save's (pack, variant) but the old PI was never removed from
   ApplicationModel.mPackageInstances or disposed, so its image caches,
   Lua interpreter, and definitional TrackerState accumulated
   indefinitely. Fix:
   - Add PackageInstance.MigrateStateTo() to transfer a state between
     PIs without lifecycle events or disposal.
   - In ApplicationModel.LoadProgress(), detect when the PI changed,
     migrate the state to the new PI, swap mPackageInstances, and
     dispose the old PI (which frees its image caches and definitional
     state).

2. Reload leak (~250 MB per reload):
   PackageLoader.LoadInto() resets the catalogs (Items, Locations, ...)
   but left PackageInstance.ImageCache and SourceImageCache intact. Old
   ImageReference keys from the reset catalogs kept decoded SKBitmaps
   and IImages alive across every Reload. Fix: call FlushImageCaches()
   after the catalog resets so stale decoded images are released before
   the new load populates them.

3. Static IconUtility caches (secondary, compounding):
   sPngCache and sAlphaMasks used ConcurrentDictionary with strong
   references to IImage keys, so Avalonia Bitmaps could never be GC'd
   even after the PI's ImageCache was cleared. Fix: switch both to
   ConditionalWeakTable so entries are evicted automatically when the
   IImage key becomes unreachable.

Closes #84

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@emosaru emosaru requested a review from a team May 10, 2026 19:25
pi.ImageCache.Clear() released the cache references but left the Avalonia
Bitmap objects waiting on the finalizer queue, causing unmanaged bitmap
memory to accumulate across reloads until GC eventually ran. Explicitly
disposing each IImage before clearing mirrors the existing SKBitmap
treatment in SourceImageCache and lets GC reclaim the memory promptly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@emosaru emosaru merged commit f103f45 into main May 10, 2026
3 checks passed
@emosaru emosaru deleted the fix/issue-84-memory-leak branch May 10, 2026 20:14
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.

1 participant