15

My application was crashing with out-of-memory exceptions and sometimes other exceptions probably also caused by running out of memory.

I reproduced the problem with this simple code:

  for (int i = 0; i < 100000; i++)
    var bmp = new RenderTargetBitmap(256, 256, 96, 96, PixelFormats.Default);

In theory this code should not crash because the bitmaps should be automatically garbage collected, but it crashes consistently when running in 32 bit mode.

The problem can be fixed like this:

  for (int i = 0; i < 100000; i++)
  {
    var bmp = new RenderTargetBitmap(256, 256, 96, 96, PixelFormats.Default);
    if (i % 500 == 0)
    {
      GC.Collect();
      GC.WaitForPendingFinalizers();
    }
  }

Of course this solution is contrary to the common wisdom that you shouldn't explicitly call GC.Collect, but I suspect that this is a scenario where it does actually make sense.

Can anyone offer any informed insight into this? Is there a better way of solving the problem?

MetaMapper
  • 968
  • 8
  • 22
  • 3
    It's more likely to be garbage collected when the method that allocates it returns. Try allocating your bitmap in a subroutine. Also, the GC is optimized for real-world scenarios. Your example is a bug in the code, not a real-world scenario. In other words, you should not be doing work by continually allocating objects. – Brannon Mar 25 '14 at 15:32
  • It's too bad `RenderTargetBitmap` doesn't implement `IDisposable`. Are you sure it uses unmanaged memory internally? It seems like if it did, this would be a violation of Microsoft's own guidelines on when to use `IDisposable`. – NathanAldenSr Mar 25 '14 at 15:40
  • It might be useful to post what, specifically, is throwing OutOfMemoryException. Is it something in WPF or something in the runtime itself? – NathanAldenSr Mar 25 '14 at 15:42
  • Thanks Brannon, the code I posted is simplified to illustrate the problem I'm having in my real-world application, which does in fact allocate the bitmap in a subroutine. My real-world scenario requires that a lot of bitmaps are created and disposed of at high speed. The example is not a 'bug in the code'. – MetaMapper Mar 25 '14 at 15:46
  • If the RenderTargetBitmap is allocated at the same size and parameters every time, you should be able to reuse it (or a fixed buffer of them). – Brannon Mar 25 '14 at 15:48
  • Thanks Nathan, yes I think you are right, the problem is that RenderTargetBitmap does not implement IDisposable. The WinForms version of the program does not have the problem because the Bitmap class provides a Dispose() method. – MetaMapper Mar 25 '14 at 15:50
  • Thanks Brannon, yes I had thought of using a pool of RenderTargetBitmaps, and I think that would solve the problem, but the WaitForPendingFinalizers solution is a lot easier to implement. – MetaMapper Mar 25 '14 at 15:59
  • In this simplified example the error is: An unhandled exception of type 'System.Runtime.InteropServices.COMException' occurred in PresentationCore.dll. Additional information: MILERR_WIN32ERROR (Exception from HRESULT: 0x88980003). In the real program out-of-memory exceptions are thrown in various places. – MetaMapper Mar 25 '14 at 16:01
  • +1: nice question with compact sample demonstrating the problem. – Alexei Levenkov Mar 25 '14 at 16:23
  • @Brannon As suggested, I tried creating a pool of reusable bitmaps to solve this problem. Unfortunately, this did not work in my scenario because the bitmaps are created on a background thread (for performance reasons), and to pass them to the UI thread they must be frozen. But of course, when they are frozen they can't be reused :( – MetaMapper Apr 04 '14 at 13:33

1 Answers1

12

RenderTargetBitmap most likely has a native resource(s) associated with it. You've got plenty of managed memory (GC gets called every X bytes allocated) - the managed objects probably don't have enough memory use on their own to be interesting at all. So it must be the unmanaged part - I expect that it has a DirectX texture (or something similar) underlying, which will only be released when finalizers are executed.

However, since there's never enough managed memory pressure, the GC doesn't actually get called at all, and the native resources will not be released.

The weird thing is that RenderTargetBitmap isn't an IDisposable. That means you can't properly dispose of the native resources ASAP. So, it's more like a bug in WPF than in .NET itself.

That's just an assumption, though.

To address a comment, the GC most definitely doesn't wait for the method to exit first. Replacing RenderTargetBitmap with byte[] shows this working correctly when native resources aren't involved.

EDIT: I finally managed to find this in the BCL source code. To dispose of the native resources of RenderTargetBitmap, you have to call Clear. It will be freed eventually even without that (the native resources are on a safe handle), but if you're only allocating and deallocating RenderTargetBitmap, you're going to run out of texture / native memory long before you even get GC to run. So to answer your real-life question, simply call Clear on the bitmap when it's not needed anymore, and it should not hog memory anymore.

July 2015:

It seems that the original bug has been fixed - looking through 4.5.2 sources, the memory pressure is correctly applied and allocating tons of RenderTargetBitmaps should now cause GC to collect properly. Still no IDisposable implementation, though.

Luaan
  • 62,244
  • 7
  • 97
  • 116
  • I don't think the Clear method disposes of the native resources, it just sets the pixels to black. The sample code still crashes in the same way even if I call Clear. Apart from that, your answer is very interesting, I hadn't considered that GC might not be getting called at all. – MetaMapper Mar 25 '14 at 16:21
  • +1 - good explanation on why there is no GC in this case. Note that it is ok to explicitly call `GC.Collect` in case like this - you know that there is no managed heap pressure, but huge native allocations and you are at good place where you know more about memory allocations than GC algorithm([discussion](http://stackoverflow.com/questions/478167/when-is-it-acceptable-to-call-gc-collect)) – Alexei Levenkov Mar 25 '14 at 16:26
  • I've done some more tests, and it seems that the problem is not caused by the GC failing to run, but is caused by the finalizers not running quickly enough - calling GC.Collect() alone does not fix the problem. – MetaMapper Mar 25 '14 at 16:28
  • @MetaMapper - if it is the case you need a lot of automatic GC - finalizers run on full GC (gen 2), just having GC for gen 0/gen 1 (which would happen first) is not enough. – Alexei Levenkov Mar 25 '14 at 16:31
  • 1
    @MetaMapper Yeah, that's quite possible, they're being called on a separate thread. `WaitForPendingFinalizers` might be enough in that case. Just make sure your finalizers aren't deadlocking. And you're right about the `Clear`. Seems like a huge oversight on part of the WPF team, there *should* have been a `Dispose` :/ – Luaan Mar 25 '14 at 16:32
  • Calling GC.Collect(2, GCCollectionMode.Forced); doesn't fix the problem. You have to call WaitForPendingFinalizers as well. – MetaMapper Mar 25 '14 at 16:36
  • 1
    @MetaMapper Yes, that's expected. `GC.Collect()` actually does a Gen-2 collect, Alexei was talking about automatic GCs, not your `GC.Collect()` call. If the finalizers can't execute fast enough, you have to use `WaitForPendingFinalizers` after the collect, because finalizers don't block your threads (thankfully). – Luaan Mar 25 '14 at 16:38
  • Thank you for pointing to this, Clear() call worked well for me. But I used it after Freeze() call and this seems enough to release all resources. I rendered 100k images without any issues. – Andrey Aug 28 '14 at 06:05
  • @Luaan: You should edit your edit to correct your answer. Clear just [sets the pixels to black](https://msdn.microsoft.com/en-us/library/system.windows.media.imaging.rendertargetbitmap.clear). – Nick Westgate Jul 07 '15 at 02:27
  • @NickWestgate True, MSDN says that, but that's not all that important. The thing is, `Clear` works. Going through the BCL sources now, it seems that the original bug has been fixed - as far as I can tell, the memory pressure is now applied correctly with the native handle. I'll update the answer to reflect this, although it'd be useful to also know which version of .NET framework fixed this. Too bad Reference Source doesn't have history :) – Luaan Jul 07 '15 at 06:23