RenderTargetBitmap GDI handle leak in Master-Detai

2020-06-09 08:03发布

问题:

I have an app with a Master-Details view. When you select an item from the 'master' list, it populates the 'details' area with some images (created via RenderTargetBitmap).

Each time I select a different master item from the list, the number of GDI handles in use by my app (as reported in Process Explorer) goes up - and eventually falls over (or sometimes locks up) at 10,000 GDI handles in use.

I'm at a loss on how to fix this, so any suggestions on what I'm doing wrong (or just suggestions on how to get more information) would be greatly appreciated.

I've simplified my app down to the following in a new WPF Application (.NET 4.0) called "DoesThisLeak":

In MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        ViewModel = new MasterViewModel();
        InitializeComponent();
    }

    public MasterViewModel ViewModel { get; set; }
}

public class MasterViewModel : INotifyPropertyChanged
{
    private MasterItem selectedMasterItem;

    public IEnumerable<MasterItem> MasterItems
    {
        get
        {
            for (int i = 0; i < 100; i++)
            {
                yield return new MasterItem(i);
            }
        }
    }

    public MasterItem SelectedMasterItem
    {
        get { return selectedMasterItem; }
        set
        {
            if (selectedMasterItem != value)
            {
                selectedMasterItem = value;

                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("SelectedMasterItem"));
                }
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

public class MasterItem
{
    private readonly int seed;

    public MasterItem(int seed)
    {
        this.seed = seed;
    }

    public IEnumerable<ImageSource> Images
    {
        get
        {
            GC.Collect(); // Make sure it's not the lack of collections causing the problem

            var random = new Random(seed);

            for (int i = 0; i < 150; i++)
            {
                yield return MakeImage(random);
            }
        }
    }

    private ImageSource MakeImage(Random random)
    {
        const int size = 180;
        var drawingVisual = new DrawingVisual();
        using (DrawingContext drawingContext = drawingVisual.RenderOpen())
        {
            drawingContext.DrawRectangle(Brushes.Red, null, new Rect(random.NextDouble() * size, random.NextDouble() * size, random.NextDouble() * size, random.NextDouble() * size));
        }

        var bitmap = new RenderTargetBitmap(size, size, 96, 96, PixelFormats.Pbgra32);
        bitmap.Render(drawingVisual);
        bitmap.Freeze();
        return bitmap;
    }
}

In MainWindow.xaml

<Window x:Class="DoesThisLeak.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="900" Width="1100"
        x:Name="self">
  <Grid DataContext="{Binding ElementName=self, Path=ViewModel}">
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="210"/>
      <ColumnDefinition Width="*"/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <ListBox Grid.Column="0" ItemsSource="{Binding MasterItems}" SelectedItem="{Binding SelectedMasterItem}"/>

    <ItemsControl Grid.Column="1" ItemsSource="{Binding Path=SelectedMasterItem.Images}">
      <ItemsControl.ItemTemplate>
        <DataTemplate>
          <Image Source="{Binding}"/>
        </DataTemplate>
      </ItemsControl.ItemTemplate>
    </ItemsControl>
  </Grid>
</Window>

You can reproduce the problem if you click on the first item in the list, then hold down the Down cursor key.

From looking at !gcroot in WinDbg with SOS, I can't find anything keeping those RenderTargetBitmap objects alive, but if I do !dumpheap -type System.Windows.Media.Imaging.RenderTargetBitmap it still shows a few thousand of them that haven't been collected yet.

回答1:

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.dll
A first chance exception of type 'System.Runtime.InteropServices.COMException' occurred in PresentationCore.dll
A first chance exception of type 'System.Reflection.TargetInvocationException' occurred in mscorlib.dll
System.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)



回答2:

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?"



回答3:

Try using the solution described here: RenderTargetBitmap.Render() throws OutOfMemoryException when rendering large visuals.

Update: Also, take a look at RenderTargetBitmap Memory Leak.