RenderTargetBitmap GDI handle leak in Master-Details view RenderTargetBitmap GDI handle leak in Master-Details view wpf wpf

RenderTargetBitmap GDI handle leak in Master-Details view


TL;DR: fixed. See the bottom. Read on for my journey of discovery and all the wrong alleys I went down!

I've done some poking around with this, and I don't think it's leaking as such. If I beef up the GC by putting this either side of the loop in Images:

GC.Collect();GC.WaitForPendingFinalizers();GC.Collect();

You can step (slowly) down the list and see no change in the GDI handles after a few seconds.Indeed, checking with MemoryProfiler confirms this - no .net or GDI objects leak when moving slowly from item to item.

You do get into trouble moving quickly down the list - I saw process memory heading past 1.5G and the GDI object climbing to 10000 when it hit a wall. Every time MakeImage was called after that, a COM error was thrown and nothing useful could be done for the process:

A first chance exception of type 'System.Runtime.InteropServices.COMException' occurred in PresentationCore.dllA first chance exception of type 'System.Runtime.InteropServices.COMException' occurred in PresentationCore.dllA first chance exception of type 'System.Reflection.TargetInvocationException' occurred in mscorlib.dllSystem.Windows.Data Error: 8 : Cannot save value from target back to source. BindingExpression:Path=SelectedMasterItem; DataItem='MasterViewModel' (HashCode=28657291); target element is 'ListBox' (Name=''); target property is 'SelectedItem' (type 'Object') COMException:'System.Runtime.InteropServices.COMException (0x88980003): Exception from HRESULT: 0x88980003   at System.Windows.Media.Imaging.RenderTargetBitmap.FinalizeCreation()

This, I think explains why you see so many RenderTargetBitmaps hanging around. It also suggests to me a mitigation strategy - assuming it's a framework/GDI bug. Try to push the render code (RenderImage) into a domain which will allow the underlying COM component to be restarted. Initially, I'd try a thread in it's own apartment (SetApartmentState(ApartmentState.STA)) and if that didn't work, I'd try an AppDomain.

However, it'd be easier to try to deal with the source of the problem, which is allocating so many images so quickly, because even if I get it up to 9000 GDI handles and wait a bit, the count falls right back down to the baseline after the next change (it seems to me as there's some idle processing in the COM object which needs a few seconds of nothing, and then another change to release all of it's handles)

I don't think there are any easy fixes for this - I've tried adding a sleep to slow the movement down, and even calling ComponentDispatched.RaiseIdle() - neither of these have any effect. If I had to make it work this way, I'd be trying to run the GDI processing in a restartable way (and dealing with the errors which would occur) or changing the UI.

Depending on the requirements in the detail view, and most importantly, the visibility and size of the images in the right hand side, you could take advantage of the ability of the ItemsControl to virtualise your list (but you probably have to at least define the height and number of the contained images so it can manage the scrollbars properly). I suggest returning an ObservableCollection of images, rather than an IEnumerable.

In fact, having just tested that, this code appears to make the problem go away:

public ObservableCollection<ImageSource> Images{    get     {        return new ObservableCollection<ImageSource>(ImageSources);    }}IEnumerable<ImageSource> ImageSources{    get    {        var random = new Random(seed);        for (int i = 0; i < 150; i++)        {            yield return MakeImage(random);        }    }}

The main thing this gives the runtime, as far as I can see, is the number of items (which the enumerable, obviously, does not) meaning that it neither has to enumerate it multiple times, or guess (!). I can run up and down the list with my finger on the cursor key without this blowing 10k handles, even with 1000 MasterItems, so it looks good to me. (My code has no explicit GC either)


If you clone into a simpler bitmap type (and freeze) it won't use up as many gdi handles, but it's slower.There's cloning via serialization in an answer to How achieve Image.Clone() in WPF?"