.NET Memory issues loading ~40 images, memory not reclaimed, potentially due to LOH fragmentation
Asked Answered
C

2

36

Well, this is my first foray into memory profiling a .NET app (CPU tuning I have done) and I am hitting a bit of a wall here.

I have a view in my app which loads 40 images (max) per page, each running about ~3MB. The max number of pages is 10. Seeing as I don't want to keep 400 images or 1.2GB in memory at once, I set each image to null when the page is changed.

Now, at first I thought that I must just have stale references to these images. I downloaded ANTS profiler (great tool BTW) and ran a few tests. The object lifetime graph tells me that I don't have any references to these images other than the single reference in the parent class (which is by design, also confirmed by meticulously combing through my code):

enter image description here

The parent class SlideViewModelBase sticks around forever in a cache, but the MacroImage property is set to null when the page is changed. I don't see any indication that these objects should be kept around longer than expected.

I next took a look at the large object heap and memory usage in general. After looking at three pages of images I have 691.9MB of unmanaged memory allocated and 442.3MB on the LOH. System.Byte[], which comes from my System.Drawing.Bitmap to BitmapImage conversion is taking pretty much all of the LOH space. Here is my conversion code:

public static BitmapSource ToBmpSrc( this Bitmap b )
{
    var bi = new BitmapImage();
    var ms = new MemoryStream();
    bi.CacheOption = BitmapCacheOption.OnLoad;
    b.Save( ms,  ImageFormat.Bmp );
    ms.Position = 0;
    bi.BeginInit();
    ms.Seek( 0, SeekOrigin.Begin );
    bi.StreamSource = ms;
    bi.EndInit();
    return bi;
}

I am having a hard time finding where all of that unmanaged memory is going. I suspected the System.Drawing.Bitmap objects at first, but ANTS doesn't show them sticking around, and I also ran a test where I made absolutely sure that all of them were disposed and it didn't make a difference. So I haven't yet figured out where all of that unmanaged memory is coming from.

My two current theories are:

  1. LOH fragmentation. If I navigate away from the paged view and click a couple of buttons about half of the ~1.5GB is reclaimed. Still too much, but interesting nonetheless.
  2. Some weird WPF binding thing. We do use databinding to display these images and I am no expert in regards to the ins and outs of how these WPF controls work.

If anyone has any theories or profiling tips I would be extremely grateful as (of course) we are on a tight deadline and I am scrambling a bit to get this final part done and working. I think I've been spoiled by tracking down memory leaks in C++ ... who woulda' thought?

If you need more info or would like me to try something else please ask. Sorry about the wall-o-text here, I tried to keep it as concise as possible.

Cholera answered 7/6, 2011 at 21:32 Comment(7)
How about disposing of the MemoryStream?Brace
Unfortunately that won't work. BitmapCacheOption.OnLoad should get rid of the stream per the docs, and I have tried manually disposing it with no luck. Thanks though, I should have added that.Cholera
@Yuriy Faktorovich: Another problem is that, if I manually dispose it even when using the OnLoad option, the image does not appear. The object is valid, but the image data is gone.Cholera
Images in memory in WPF are great fun, fragmentation sounds quite possible.Alves
@H.B.: You're telling me... yeesh. I love this place though, saved me who knows how many more hours of work.Cholera
@EdS.: The beautiful power of combined knowledge :)Alves
@EdS: It makes me happy when we can help solve each others issues. Seems like BitmapImageSource might need another 'adjustment' in .NET 4 SP1.Sorcim
S
39

This blog post appears to descibe what you are seeing, and the proposed solution was to create an implementation of Stream that wraps another stream.

The Dispose method of this wrapper class needs to release the wrapped stream, so that it can be garbage collected. Once the BitmapImage is initialised with this wrapper stream, the wrapper stream can be disposed, releasing the underlying stream, and allowing the large byte array itself to be freed.

The BitmapImage keeps a reference to the source stream so it keeps the MemoryStream object alive. Unfortunately, even though MemoryStream.Dispose has been invoked, it doesn't release the byte array that the memory stream wraps. So, in this case, bitmap is referencing stream, which is referencing buffer, which may be taking up a lot of space on the large object heap. There isn't a true memory leak; when there are no more references to bitmap, all these objects will (eventually) be garbage collected. But since bitmap has already made its own private copy of the image (for rendering), it seems rather wasteful to have the now-unnecessary original copy of the bitmap still in memory.

Also, what version of .NET are you using? Prior to .NET 3.5 SP1, there was a known issue where a BitmapImage could cause a memory leak. The workaround was to call Freeze on the BitmapImage.

Sorcim answered 7/6, 2011 at 21:45 Comment(10)
And that is the exact problem, I wish I could throw you 1000 more points. I assumed that the underlying stream was not the issue after I manually disposed of it in one of my tests, but I guess that's what I get for assuming that I know what the hell I am doing =D. Thanks again.Cholera
@EdS.: Actually you could just award a bounty, that use is even suggested on the blog.Alves
@H.B.: Well, that answer came in pretty quickly after a I asked the question, 13 minutes after to be exact.Cholera
@EdS.: It's not running away though :) (also you probably have seen comments like "thanks, will accept in 15-x minutes" before, right?)Alves
Brilliant article here on this topic: samsaffron.com/archive/2011/10/28/…Diatomaceous
Yes! This has solved a nightmare issue for me - I can't thank you enough :-)Derive
I'm getting this in .NET 4.6.1. Is this solution applicable?Lissa
@SuperJMN: In. NET 4.5.1 and above you could use 'GCSettings.LargeObjectHeapCompactionMode' on a background thread periodically. This setting explicitly compacts and clears out garbage from LOH on the next run and resets it to the default after collection. Normally it's not compacted automatically. msdn.microsoft.com/en-us/library/…Kenaz
Both links are dead. Any idea where to find that content?Finnougric
a copy here: web.archive.org/web/20100823040205/http://code.logos.com/blog/…Godson
M
2

Where are you closing and disposing the memory stream? It could be that the GC has to work a lot harder to free the resources by moving the up several generations before executing the destructors on the object (which generally call the dispose if you forgot to do it).

In your case, you can't dispose the memory stream until you are done with the image. When you want them unloaded, loop through the images and try disposing the memory stream.

Monolithic answered 7/6, 2011 at 21:41 Comment(2)
Yes, Yuri got me to try something else just now. I had tried Disposing of the stream at the very beginning, but the problem is that the image is, well... blank. The image data is gone. I thought that using the OnLoad option would get rid of it for me per the docs, but I think I misunderstood its meaning. I did at one point dispose of the StreamSource when clearning the images and, at that point, it had no visible affect. I will look at that more deeply again though as that stream is the issue.Cholera
And yep, I saw just what Oppositional is describing below.Cholera

© 2022 - 2024 — McMap. All rights reserved.