Skip to content

Add Smart Memory Management and DevTools Enhancements#38

Draft
JaymeFernandes wants to merge 2 commits intoAvaloniaUtils:masterfrom
JaymeFernandes:devtools-fix
Draft

Add Smart Memory Management and DevTools Enhancements#38
JaymeFernandes wants to merge 2 commits intoAvaloniaUtils:masterfrom
JaymeFernandes:devtools-fix

Conversation

@JaymeFernandes
Copy link

@JaymeFernandes JaymeFernandes commented Feb 14, 2026

This PR introduces improvements to AsyncImageLoader.Avalonia, focusing on memory management and developer support:

  • Added BitmapCacheCoordinator and BitmapStore services for memory management, including reference counting, LRU, and automatic cleanup of unused bitmaps.

  • Created the AsyncImageLoader DevTools, a window that can be opened with Ctrl+I. The DevTools displays:

    • All HTTP requests made, including status and errors;
    • Items currently stored in the BitmapStore, allowing developers to see the state of memory managed by the coordinator.
  • Improved DevTools UI, providing better visibility of logs and memory information.

These changes give developers more transparency over memory usage and allow monitoring of HTTP requests, improving debugging and application stability.

image

I noticed that RamCachedWebImageLoader was missing the ProvideImageAsync(string url, IStorageProvider? storageProvider = null) implementation, which caused the memory cache to be ignored whenever this version should have been used.

Additionally, since BaseWebImageLoader implements both IAsyncImageLoader and IAdvancedAsyncImageLoader, loader is IAdvancedAsyncImageLoader always evaluates to true, so the “advanced” branch was always executed.

This update:

  • Implements the missing ProvideImageAsync with IStorageProvider?.
  • Ensures that both basic and advanced paths use the memory cache consistently, preventing potential memory leaks.

The logic itself remains unchanged—this simply adds the missing method and ensures proper caching across both paths.

public override async Task<Bitmap?> ProvideImageAsync(string url, IStorageProvider? storageProvider = null) {
    var bitmap = await _memoryCache.GetOrAdd(url, LoadAsync)
        .ConfigureAwait(false);

    if (bitmap == null) _memoryCache.TryRemove(url, out _);
    return bitmap;
}

Sorry for opening a new PR by mistake—I accidentally closed the previous one. Here is the final version, including all the updates you requested earlier.

@JaymeFernandes
Copy link
Author

I hope you like how everything turned out!

@SKProCH SKProCH self-requested a review February 15, 2026 11:33
Copy link
Member

@SKProCH SKProCH left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, refrain from using AI so much. There is many places with a confusing logic, which AI can't understand. This requires a manual approach

return new Bitmap(url);

if (storageProvider is null) return null;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you add spaces here? The .editorconfig in the project should remove spaces from empty lines.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s how my IDE is configured, it automatically formats the indentation that way, but I’ve reformatted it to match your requested style.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but I’ve reformatted it to match your requested style.

No, you are not? The file isn't modified from the moment of making my comment. Probably you forgot to push your changes?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I try to avoid that, but sometimes I just miss a change.

}

#if NETSTANDARD2_1
protected sealed override async Task SaveToGlobalCache(string url, byte[] imageBytes) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, format the code

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ve already fixed it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, its still has the wrong indentation level


namespace AsyncImageLoader;

public class LoggingHandler : DelegatingHandler
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably should be internal

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is meant to centralize all requests so the developer can have a clear overview of which ones occurred and whether they succeeded. It helps during development.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but the class should be internal.


---

### **New Smart Loaders**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need to separate loaders, make it a one list

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept it this way to avoid confusing users who are already using your library. Since this mechanism is new, I believe it’s better not to mix it with the existing behavior, at least for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes no difference whether the user is already using the library or not. There is no point in separating them, it does not provide any additional information to the user.

* [SmartDiskImageLoader](https://github.com/AvaloniaUtils/AsyncImageLoader.Avalonia/blob/master/AsyncImageLoader.Avalonia/Loaders/SmartDiskImageLoader.cs) – inherits `SmartImageLoader` and adds **disk caching**

`RamCachedWebImageLoader` are used by default.
> Note: **automatic memory cleanup cannot be disabled anymore**. This is now the default behavior and only works for loaders inheriting from `SmartImageLoader`. Older loaders (`RamCachedWebImageLoader`, `DiskCachedWebImageLoader`) do not have this mechanism. No newline at end of file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Memory cleanup not related to loaders, isn't it?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I introduced an interface inheritance to establish this relationship and explicitly indicate whether support is available or not. It doesn’t solve the problem completely, but it mitigates a significant part of it. With future updates, I believe the system will become even more robust in handling memory management.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least this is can be disabled now

@JaymeFernandes
Copy link
Author

I’ve completed the update. Could you please review it? Before anything else, I’d appreciate it if you could test the update to compare it with the previous version.

@SKProCH
Copy link
Member

SKProCH commented Feb 22, 2026

Please respond to all comments with fixes or comments, so we would eventually close all of them, since half of them are not touched in you commit from my quick observation

@JaymeFernandes
Copy link
Author

All done, I believe I addressed everything. If I accidentally missed any comment, please feel free to flag it again.

Copy link
Member

@SKProCH SKProCH left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please, stop using and relying on AI so much. I've asked you about replying on comments, cuz you missed half of it, not so you could generate an AI that everything's fixed when it's not.
I don't mind a simple "Done" or something like that as a reply to a comment; I don't need formalities. I need good code at the end. I've reopened some of them, so please also check it out.

About the memory cleanup, ICoordinatedImageLoader and so on - for now I've don't see any specific reason, why we actually need to introduce the specific loaders, the only thing they do different - is just adding bitmap BitmapStore, and doesn't use it anywhere.
Currently, i suppose that we can just remove them and add/check BitmapStore and everything related in Load method of AdvancedImage, instead of adding a toggle that doesn't work with some loaders, and specific loaders that does nothing.

In previous PR iterations, smart loaders makes sense, cuz they keeping some mostly used images in a cache. In current - it doesn't make any sense.

The features should be separated - bitmap disposing cleanup when AdvancedImage leaves the virtual tree - is the one feature, advanced caching in smart loaders - the other one.

And also, please properly reformat the files. If your IDE doesn't handle the .editorconfig - please use dotnet format

public string? Source {
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
set
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is can be a lambda expression


if (GetSource(imageBrush) != newValue) return;
imageBrush.Source = bitmap;
if (!cts.Token.IsCancellationRequested && GetSource(imageBrush) == newValue)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it still exists


private static async void OnSourceChanged(Image sender, AvaloniaPropertyChangedEventArgs args) {
private static async void OnSourceChanged(Image sender, AvaloniaPropertyChangedEventArgs args)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but please keep formatting in the same style, defined in .editorconfig. If your IDE doesn't use settings from .editorconfig - use dotnet format


---

### **New Smart Loaders**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes no difference whether the user is already using the library or not. There is no point in separating them, it does not provide any additional information to the user.


namespace AsyncImageLoader;

public class LoggingHandler : DelegatingHandler
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but the class should be internal.

IsLoading = true;

if (CurrentImage is ImageWrapper wrapper)
wrapper.Dispose();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should dispose wrapper before, since if source is null and fallback image set, it will replace the image without disposing previous one

return entry;
}

var bitmap = await loader
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IAdvancedImageLoader check and call forgotten

public sealed class ImageWrapper : IImage {
public IImage ImageImplementation { get; }
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) {
if(!_isInsideVirtualizingPanel)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic doesn't depends on having the virtualizing panel parent. And all logic below too

Or i've just missing something, but since _isInsideVirtualizingPanel check supposenly never worked anyway...


protected override void OnDataContextChanged(EventArgs e) {
if(_isInsideVirtualizingPanel)
ReleaseImage();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is handled by our usual Source changed logic, no need to do this, as I already commented

* [SmartDiskImageLoader](https://github.com/AvaloniaUtils/AsyncImageLoader.Avalonia/blob/master/AsyncImageLoader.Avalonia/Loaders/SmartDiskImageLoader.cs) – inherits `SmartImageLoader` and adds **disk caching**

`RamCachedWebImageLoader` are used by default.
> Note: **automatic memory cleanup cannot be disabled anymore**. This is now the default behavior and only works for loaders inheriting from `SmartImageLoader`. Older loaders (`RamCachedWebImageLoader`, `DiskCachedWebImageLoader`) do not have this mechanism. No newline at end of file
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least this is can be disabled now

@JaymeFernandes
Copy link
Author

I did not use AI to generate the implementation. I only used it to help translate my comments into English, since I’m not very confident writing in English.

I understand your points about separating responsibilities and simplifying the structure. However, at the moment I don’t have more time or energy to continue iterating on this PR.

If the code does not meet your expectations or the standards you consider appropriate, please feel free to adjust, simplify, or finalize it as you see fit.

Thanks for the feedback.

@SKProCH
Copy link
Member

SKProCH commented Mar 2, 2026

I'm not sure that I've actually have so much time to address all this comments by myself. Probably designing the new modular loaders approach while keeping in mind features from this PR would be better, than addressing this.

So, currently marking this PR as a draft, if somebody wants to finish it in current state - it would be fine.

@SKProCH SKProCH marked this pull request as draft March 2, 2026 17:13
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.

2 participants