Fix Image memory leak in map preview cache via ref-counted leases#1014
Open
Copilot wants to merge 3 commits into
Open
Fix Image memory leak in map preview cache via ref-counted leases#1014Copilot wants to merge 3 commits into
Copilot wants to merge 3 commits into
Conversation
… preview cache Agent-Logs-Url: https://github.com/CnCNet/xna-cncnet-client/sessions/2f5f7709-2ade-4764-9e14-781eb58465ed Co-authored-by: SadPencil <11227602+SadPencil@users.noreply.github.com>
Copilot created this pull request from a session on behalf of
SadPencil
May 15, 2026 20:15
View session
…anagerBase Agent-Logs-Url: https://github.com/CnCNet/xna-cncnet-client/sessions/3badfa49-0b10-4b4f-bb5b-15f2f2b37a7d Co-authored-by: SadPencil <11227602+SadPencil@users.noreply.github.com>
Agent-Logs-Url: https://github.com/CnCNet/xna-cncnet-client/sessions/744a9614-a635-4a90-a471-a4a631d3200a Co-authored-by: SadPencil <11227602+SadPencil@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Map preview
Imageobjects were leaking — never disposed on LRU eviction — and a naive fix would introduce use-after-free: a caller could hold a reference to anImagewhile the cache evicts and disposes it concurrently on the worker thread.This PR solves both problems with a ref-counted lease pattern and reorganises the cache infrastructure into the shared
ClientCoreproject.Problem
MapPreviewCacheManagercachedSixLabors.ImageSharp.Imageobjects in an LRU dictionary. When an entry was evicted it was simply removed from the dictionary without callingImage.Dispose(), leaking native pixel memory. Callers held a direct reference to theImagefrom the cache — there was no mechanism to delay disposal until all callers were done.Solution:
ClientCore.Caching— reusable ref-counted cache infrastructureRefCountedValue<T>(internal)Thread-safe ref-counted wrapper around a cached value. Starts at count = 1 (the cache's own reference).
AcquireLease()atomically increments the count and returns aCacheLease<T>for the caller.Release()atomically decrements; when the count reaches zero thedisposeActionis invoked. Finalizer is intentionally absent — the type holds only managed references andT(e.g.Image) has its own GC backstop.CacheLease<T>(public,IDisposable)The caller-facing handle returned by
ICacheManager.Request(...). Designed forusingblocks.Dispose()is idempotent viaInterlocked.Exchange. Two creation paths:CacheLease(RefCountedValue<T>)) — for items managed by the cache;DisposecallsRefCountedValue<T>.Release().CacheLease<T>.CreateOwned(value, onRelease)) — for values the caller owns outright (e.g. an immediately-loaded preview image not stored in the LRU cache).The two classes have distinct roles and cannot be collapsed into one:
RefCountedValue<T>is the single shared ownership tracker (one per cached entry);CacheLease<T>is the per-caller handle (one perAcquireLease()call).CacheManagerBase<TInput, TOutput>(public abstract)Thread-safe LRU cache with a background worker thread that computes outputs without blocking callers. No
TOutputconstraint — works for any type. Eviction releases the cache's own reference; if a caller still holds a lease the value stays alive until that lease is disposed. Aprotected virtual Action? GetDisposeAction(TOutput?)hook (returningnullby default) lets subclasses inject cleanup without the base class knowing aboutIDisposable.DisposableCacheManagerBase<TInput, TOutput>(public abstract)Thin subclass
where TOutput : IDisposable. OverridesGetDisposeActionto returnvalue.Dispose, ensuring eachImage(or otherIDisposableoutput) is disposed exactly once when its ref count reaches zero.Class hierarchy
Namespace assignment
ICacheManager<TInput, TOutput>ClientCore.CachingClientCoreCacheManagerBase<TInput, TOutput>ClientCore.CachingClientCoreDisposableCacheManagerBase<TInput, TOutput>ClientCore.CachingClientCoreCacheLease<T>ClientCore.CachingClientCoreRefCountedValue<T>ClientCore.CachingClientCoreIMapPreviewCacheManagerDTAClient.Domain.MultiplayerDXMainClientMapPreviewCacheManagerDTAClient.Domain.MultiplayerDXMainClientClientCore.Cachingfollows the existing sub-namespace convention (ClientCore.Statistics,ClientCore.Extensions, etc.) and clearly scopes the folder's purpose. Map-specific types stay inDXMainClientbecause they depend onMap,Image, and other DXMainClient-only types.Thread-safety guarantee
Leases are acquired inside
cacheLock. This means the caller's ref-count increment is visible before any concurrent eviction can callRelease()on the same entry — eliminating the use-after-free window that would exist if the lease were acquired after releasing the lock.Files changed
Added to
ClientCore/Caching/ICacheManager.csCacheManagerBase.csDisposableCacheManagerBase.csCacheLease.csRefCountedValue.csRemoved from
DXMainClient/Domain/Multiplayer/ICacheManager.cs,CacheManagerBase.cs,DisposableCacheManagerBase.cs,CacheLease.cs,RefCountedValue.csUpdated in
DXMainClient/Domain/Multiplayer/IMapPreviewCacheManager.cs—using ClientCore.Caching;MapPreviewCacheManager.cs—using ClientCore.Caching;MapLoader.cs—using ClientCore.Caching;; useCacheLease<Image>.CreateOwnedfor directly-owned images