5

I've read multiple threads about this, but still cannot find anything that works.

I'm writing program for basically browsing database of images. I have a ListView with DataTemplate:

<DataTemplate>
        <Grid Width="Auto" Height="Auto" >
            <Image VerticalAlignment="Center" Source="{Binding IsAsync=True,
 Converter={StaticResource Converter},
 ConverterParameter={x:Static viewModel:SearchViewModel.MiniaturesHeight}}"
 Grid.RowSpan="2"  Stretch="None" Margin="5" 
Height="{Binding Source={StaticResource Locator}, Path=MiniaturesHeight}" 
Width="{Binding Source={StaticResource Locator}, Path=MiniaturesHeight}"
 RenderOptions.BitmapScalingMode="NearestNeighbor" />
             <TextBlock Text="{Binding Name}" Margin="5" />
        </Grid>
    </DataTemplate>

In converter I receive object and make URL from it's content. My problem is that I need to display 100 images per page, the whole database is for example 40k images. I would like to allow the user to click through all the pages without StackOveflowException. Unfortunately, each time I change page, memory usage increases and won't go down, even if I wait a long time.

Program itself uses around 60mb of RAM, but after changing page 5 times it's 150MB and steadily goes up.

This was my first converter:

  public class ObjectToUrl : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null)
                return DependencyProperty.UnsetValue;

            var obj = value as MyObject;

            return "base url" + obj.prop1;


        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return DependencyProperty.UnsetValue;
        }
    }

Then I've found, that WPF is caching by default all images passed to Image control using InternetExplorer caching options. This was a problem for me, since I wanted an easy way to update image on screen when other user would change something. So I changed my converter to use the most standard technique for that to disable caching:

  public class ObjectToUrl : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            if (value == null)
                return DependencyProperty.UnsetValue;

            var obj = value as MyObject;

            var url = "base url" + obj.prop1;

            try
            {                
                var bmp = new BitmapImage();


                bmp.BeginInit();
                bmp.CacheOption = BitmapCacheOption.None;
                bmp.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
                bmp.UriSource = new Uri(url);
                bmp.EndInit();
                return bmp;
            }
            catch
            {
                return DependencyProperty.UnsetValue;
            }

        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return DependencyProperty.UnsetValue;
        }
    }

This works in exactly the same way, with exception, that if I remove and add item to list bound to ListView, it loads refreshed image.

I still have problem with memory leak. Any ideas?

Krzysztof Skowronek
  • 2,796
  • 1
  • 13
  • 29
  • 1
    BitmapCacheOption is not related to caching of image resources loaded from remote URIs. It should be set to OnLoad here. That said, your `MiniaturesHeight` converter parameter implies that you somehow reduce the height of the loaded bitmaps. You should pass that parameter to the `DecodePixelHeigth` property of the BitmapImage. – Clemens Mar 02 '18 at 15:48
  • What is the ListBox ItemsSource binding? Is it ObservableCollection? – Yury Schkatula Mar 02 '18 at 15:51
  • @Clemens MiniaturesHeight is passed in URL and API returns scaled images based on parameter and I don't want ANY caching – Krzysztof Skowronek Mar 02 '18 at 21:40
  • @YurySchkatula It is ReactiveList from ReactiveUI framework – Krzysztof Skowronek Mar 02 '18 at 21:40
  • 2
    It's not convincing that you have a memory leak. Did you call GC.Collect()? Garbage collector might decide it's not worth it to run, because there is plenty of free memory. – Evk Mar 06 '18 at 12:31
  • 2
    WPF has a bunch of dirty little secrets that would probably shock a lot of experienced .NET programmers if they knew about it. This is one of them. BitmapImage is a wrapper for unmanaged code (WIC) that uses lots of unmanaged memory, but does not have a Dispose() method. Or in other words, there is no way you can control its memory usage directly, not the Image control either. What they did about it is something nobody wants to know and it is very, very ugly. But you can easily do what it does, call GC.Collect(). Or leave it up to the Smart Guys to get it right, 150 MB is peanuts. – Hans Passant Mar 06 '18 at 12:57
  • @HansPassant did they have any reasoning to do it like that? – Evk Mar 06 '18 at 15:54
  • 1
    Not something they shared afaik. But probably training wheels from .NET 1.0, programmers only learn to use Dispose() the hard way. – Hans Passant Mar 06 '18 at 16:04
  • @Evk you are right. I wrote in GC.Collect every 30 secs and it is impossible to get over 130MB of RAM usage - everything is nicely collected. Also, without that, I'va managed to get up to 300MB and only then GC started reducing the memory and it hovers around 200MB. If you want the bounty, please change your comment into an answer :) – Krzysztof Skowronek Mar 06 '18 at 16:19
  • 2
    If anyone is interested in the "something nobody wants to know and it is very, very ugly" that @HansPassant mentioned, I believe that [this question](https://stackoverflow.com/questions/36044796) and related questions goe into some of the details – zastrowm Mar 07 '18 at 03:14
  • @HansPassant it turns out in last versions of WPF they removed that ugly hack and now `BitmapSource` just calls `GC.AddMemoryPressure` (no explicit `Dispose` still though). – Evk Mar 07 '18 at 10:47

2 Answers2

4

Increasing memory does not always indicate memory leak. Since .NET is garbage collected environment - GC decides itself when to run, based on its own heuristics. Part of that heuristics might be total amount of memory consumed by your application and total amount of available memory. Say you have 8GB available memory, and your application consumes 150MB. GC might think - why bother? After all, memory exists to be used, not to stay free all the time.

So to ensure that you have a memory leak - you might try to call GC.Collect periodically and see if that helps to reclaim memory. If yes - then you don't have a leak. If no - then you need to run profiler and figure out what's going on in more details. In any way - DO NOT leave that GC.Collect in your code after you figured out there is no memory leak. In very rare and specific cases it might be worth to leave it, but in general - just let GC do its job and reclaim memory when it sees fit. There is high chance it knows when to do this better than you.

The case with BitmapImage is a bit more complicated though. It's a wrapper around unmanaged resource, and all such wrappers should provide a Dispose method, so that caller can immediately reclaim unmanaged memory used by it (because, unlike managed memory - unmanaged usually can be reclaimed immediately, there is no garbage collector managing it).

For whatever historical reason, BitmapImage (BitmapSource) does not provide such method (at least not public, probably you can reach it via reflection). However, unmanaged resource pointer is wrapped into SafeHandle which has finalizer. In addition to that - BitmapSource calls GC.AddMemoryPressure (at least in modern .NET versions) to notify garbage collector that it holds X bytes of unmanaged memory.

That means GC knows exactly how much memory is consumed by BitmapImage, even though large part of this memory is unmanaged, and can account for that when deciding when to run garbage collection. When BitmapImage is collected - it's SafeHandle finalizer runs and unmanaged memory is reclaimed.

Long story short: it should be fine to just do nothing in your situation.

Evk
  • 98,527
  • 8
  • 141
  • 191
  • i dont understand why the developer of .net is not given a choice to free memory when not needed. For high memory demanding applications, and also if u have lot of other processes to start as in linux, it is essential to use only that is necessary and release what is unnecessary. It would be really nice to let programmer access GC to release specific list of memory. But sadly microsoft dont think abt it much.. – na th Mar 07 '18 at 07:17
  • My app runs mostly on rather powerfull machines (CAD designers use it), with some exceptions. My dev machine has 16GB of RAM, so in fact, GC could have decided not to collect/finalize all those image, because of too much free memory. Long story short, WPF is not as bugged :P – Krzysztof Skowronek Mar 07 '18 at 18:09
0

Add this after EndInit():

bmp.Freeze();

Changed Handlers on Unfrozen Freezables may Keep Objects Alive

l33t
  • 18,692
  • 16
  • 103
  • 180
  • I already tried that - if I do that, images don't show in ListView – Krzysztof Skowronek Mar 06 '18 at 16:08
  • That's weird. It should work! Are your urls online images? Anyway, perhaps you can freeze the image when `SourceUpdated` fires. Or when download completes (i.e. `DownloadCompleted`). – l33t Mar 06 '18 at 17:00
  • It turnes out that there is no leak, just GC comes into play quite late. – Krzysztof Skowronek Mar 06 '18 at 18:01
  • @KrzysztofSkowronek .. Yes Image control does not release the memory... even calling GC.Collect does not collect immediately... Bad design from Microsoft. Probably we need a custom image control which draws directx directly... which wont be that trivial. :( – na th Mar 06 '18 at 20:37