Post #20596
34
UnigramDev/Unigram/composer-typing-performance • a6396fa • 6 files, +325/-79
Move the SendFilesPopup thumbnails off the models
StorageMedia._preview was assigned in four places and released in none, and
the models outlive the popup — SendFileExecute hands them to the send loop —
so every decoded thumbnail stayed alive for the whole send. A photo costs
roughly a megabyte and a video twice that, so a large drop retained tens of
megabytes with nothing left to free them.
Preview/Refresh/RefreshAsync are gone. The decode moves verbatim into
StorageThumbnailCache, owned by the popup, which pushes into the ImageBrush
instead of being pulled through a binding. Entries are dropped as their album
container is recycled, so the live set is now bounded by what the ListView has
realized rather than by how many files were picked, and cleared outright on
unload.
The cache also coalesces: Refresh() was async void fired from the Preview
getter with no in-flight guard, and _preview stayed null across the await, so
every container that asked before the first decode returned started another
decode of the same file — and the album panel rebuilds all of its children on
every UpdatePanel.
A decode that finishes after its entry was released, invalidated or cleared is
dropped rather than cached. That is what stops a post-recycle result from
resurrecting a bitmap nothing is left to release, and a pre-crop image from
landing after Invalidate.
The StorageVideo.Refresh override goes with it. It re-ran LoadPreview() after
a crop, which only feeds MaxCompression/Compression — dead, since every
original* field it reads is assigned solely in commented-out lines. The
constructor still runs it, so the initial state is unchanged.
Closing the popup still cannot stop a decode already under way: neither
BitmapImage.SetSourceAsync nor the video path takes a cancellation token. Only
the retention is fixed here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram
Move the SendFilesPopup thumbnails off the models
StorageMedia._preview was assigned in four places and released in none, and
the models outlive the popup — SendFileExecute hands them to the send loop —
so every decoded thumbnail stayed alive for the whole send. A photo costs
roughly a megabyte and a video twice that, so a large drop retained tens of
megabytes with nothing left to free them.
Preview/Refresh/RefreshAsync are gone. The decode moves verbatim into
StorageThumbnailCache, owned by the popup, which pushes into the ImageBrush
instead of being pulled through a binding. Entries are dropped as their album
container is recycled, so the live set is now bounded by what the ListView has
realized rather than by how many files were picked, and cleared outright on
unload.
The cache also coalesces: Refresh() was async void fired from the Preview
getter with no in-flight guard, and _preview stayed null across the await, so
every container that asked before the first decode returned started another
decode of the same file — and the album panel rebuilds all of its children on
every UpdatePanel.
A decode that finishes after its entry was released, invalidated or cleared is
dropped rather than cached. That is what stops a post-recycle result from
resurrecting a bitmap nothing is left to release, and a pre-crop image from
landing after Invalidate.
The StorageVideo.Refresh override goes with it. It re-ran LoadPreview() after
a crop, which only feeds MaxCompression/Compression — dead, since every
original* field it reads is assigned solely in commented-out lines. The
constructor still runs it, so the initial state is unchanged.
Closing the popup still cannot stop a decode already under way: neither
BitmapImage.SetSourceAsync nor the video path takes a cancellation token. Only
the retention is fixed here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#unigram