tags:

views:

81

answers:

2

I'm using the .NET4 WPF DataGrid to show a SQL table containing lots of images.

The XAML code in question is:

...
<DataGridTemplateColumn.CellTemplate>
   <DataTemplate>
      <Image Source="{Binding Converter={StaticResource ImageConverter}, Path=Picture}" Stretch="Uniform" MaxHeight="200" />
   </DataTemplate>
</DataGridTemplateColumn.CellTemplate>
...

And the ImageConverter is written as such:

[ValueConversion(typeof(Binary), typeof(BitmapImage))]
public class ImageConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value != null)
        {
            BitmapImage bi = new BitmapImage();
            bi.BeginInit();
            bi.StreamSource = new System.IO.MemoryStream((value as Binary).ToArray());
            bi.EndInit();
            if (bi.CanFreeze) bi.Freeze();

            return bi;
        }
        else return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

My question is does the code above leak memory?


I have tried to do some profiling about this, but I'm not sure if I'm interpreting the results correctly.

First of all, the SQL table containing the images uses 38MB of disk space (the images should be stored in png format). After loading all the images to the datagrid via LINQ, the app uses some 170+ MB extra RAM. This can probably be contributed to uncompressing the images and the fact that the wpf datagrid is a gigantic memory hog even when virtualizing is enabled. After closing the window the memory usage does not drop. Reopening the window causes another 170+ MB of RAM to be used, making the total memory usage roughly 400MB. If I reopen the window again, then the memory usage drops to about 330MB. Reopening the window again takes the memory usage to 380MB. Reopening again takes it to 270MB. Reopening again takes it to 426MB. So as you can see very fluctuating...

This little bit of testing was done with Win7 x64 and 8GB of RAM (the app is compiled with the Any CPU option).

I did try the same test on a virtual xp machine with <512MB of ram (about 384 if I remember correctly). And everytime I reopened the window I first saw a dramatic drop in memory usage when the window was being loaded and then the same amout of memory reused, so the overall usage was about the same.

I interpret those results such that the GC doesn't bother cleaning up unless there is high pressure on the memory subsystem. But in that case calling GC.Collect() after I close the window should release the majority of the memory? Only that I didn't, I saw only a drop of a 4-6MB in memory usage when I called GC.Collect (I also tried all the possible parameters, including forcing collection on all generations and calling GC.WaitForPendingFinalizers()).

This got me thinking that maybe the unused leaking data is being pushed into the page file. But the page file usage drops and rises acording to the memory usage equaly.

All things considered there ought not to be a memory leak here, but I can't get the memory usage to come down after I have closed the window. I did try to attach some .NET profiler to my process, but thoses profilers are so complex that I can't quite figure out whether the shown image objects are still alive and referenced by something. Or are they dead and the GC simply hasn't cleaned them up...

+1  A: 

In my opinion there is no memory leak in that code. The fact that on the virtual machine memory drops almost immediately means that the GC is doing its job correctly.

One thing I found helpful in a similar situation is to put a button somewhere that calls GC.Collect, and hit it several times. Usually after ~3 times dead objects are "GCollected", if it's not the case you probably have a leak.

Francesco De Vittori
I tried out your suggestion about GC.Collect in a button and got some strange results. First off, I tried GC.Collect() before, right after I closed the Window and that gave no results (perhaps the Window was not out of scope). Now, if I press the button, about 80MB is immediately released, but a 100 or so MB is not. Repeating the procedure of reopening and pressing the button, produces similar results as before - the memory consumption fluctuates a lot. Only that after pressing the button ~80MB is released immediately...Any thoughts? Or is the only option to use a profiler to find out more?
Marko
We've used http://memprofiler.com/ with good success in the past. It's free and not so difficult to use. Btw, if after hitting GC.Collect n times memory is released, this means there is no leak even if n is a very large number. The garbage collector is "lazy" (for good reason) and may decide it's not needed to release resources even if there are unreferenced objects.
Francesco De Vittori
+2  A: 

The way you can find out whether or not the memory is written to the page file is through GC.GetTotalMemory(true). This should return the actual memory consumption of the .NET process including any pages that may have been written to the page file.

This may even be different from e.g. the memory reported by the task manager, but it will report what has actually been allocated by .NET objects.

See http://social.msdn.microsoft.com/Forums/en-US/csharpgeneral/thread/2ebbff38-f881-47ad-a4b4-9c9157fb3b5b for more information concerning the difference in memory consumption reported by the task manager and GetTotalMemory.

My guess is that GC.GetTotalMemory(true) will report that all memory has actually been released after the collection.

Pieter
I'm going to give the answer to Francesco, since he replied first, but your answer is very good too. GC.GetTeotalMemory does report the normal amount of memory my application uses, even while task manager shows some 120MB more. I guess it has something to do with what was said in the link you provided - GC has freed the memory, but not released it to the OS or something like that. Too bad there is no way to force the releasing of unneccessary memory, I'd gladly pay the price of 2-3 seconds GC work. (Reminds me quite a bit of the SQL log file, you have to manually shrink it to relase HDD space)
Marko
Thanks anyway. Too bad that here so much emphases is put on the first answer instead of the right one :).
Pieter
Voted up because I learned about GC.GetTotalMemory (which I didn't know) and because I stole you the bounty ;-)
Francesco De Vittori
Thanks anyway :).
Pieter
It's a bit dissapointing that StackOverflow doesn't allow to set multiple answers, because sometimes the full answer to a question is a sum of more than one. In such a case I tend to reward the first answer, although thinking about it now, your answer is more complete (toggeled the answer in your favour, though I don't know if that will give back reputation)... I'll try to decide more carefully next time ;)
Marko
Nope. Bounty is gone. No problem; thanks anyway.
Pieter