Pinned Instances for GC - Not traceable from my managed code
Asked Answered
L

2

9

So I am using WPF 3.5 with MVVM + DataTemplate method to load 2 views on the GUI. I have observed while memory profiling that items generated as part of items container of items controls are pinned into the memory and doesn't get GCed even after the view is unloaded!

I just ran tests and found out it is reproducible even for the simplest of code... You guys can check for yourself.

XAML:

<Window x:Class="ContentControlVMTest.Window2"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ContentControlVMTest"
        Title="Window2" Height="300" Width="300">
    <DockPanel LastChildFill="True">

        <CheckBox Click="CheckBox_Click" Content="Test1?"
                  DockPanel.Dock="Top" Margin="5"/>

        <ContentControl x:Name="contentControl">
            <ContentControl.Resources>

                <DataTemplate DataType="{x:Type local:Test3}">
                    <TextBlock Text="{Binding C}" Margin="5"/>
                </DataTemplate>

                <DataTemplate DataType="{x:Type local:Test1}">
                    <DockPanel LastChildFill="True" Margin="5">
                        <TextBlock Text="{Binding A}"
                                   DockPanel.Dock="Top"
                                   Margin="5"/>
                        <ListBox ItemsSource="{Binding Bs}"
                                 DisplayMemberPath="B"
                                 Margin="5"/>
                    </DockPanel>
                </DataTemplate>
            </ContentControl.Resources>
        </ContentControl>
    </DockPanel>
</Window>

Code Behind:

public class Test3
{
    public string C { get; set; }
}

public class Test2
{
    public string B { get; set; }
}

public class Test1
{
    public string A { get; set; }

    private List<Test2> _Bs;
    public List<Test2> Bs
    {
        get
        {
            return _Bs;
        }

        set
        {
            _Bs = value;
        }
    }
}

public partial class Window2 : Window
{
    public Window2()
    {
        InitializeComponent();
        this.KeyDown += Window_KeyDown;
    }

    private void Window_KeyDown
            (object sender, System.Windows.Input.KeyEventArgs e)
    {
        if (Keyboard.IsKeyDown(Key.LeftCtrl))
            if (Keyboard.IsKeyDown(Key.LeftShift))
                if (Keyboard.IsKeyDown(Key.LeftAlt))
                    if (Keyboard.IsKeyDown(Key.G))
                    {
                        GC.Collect(2, GCCollectionMode.Forced);
                        GC.WaitForPendingFinalizers();
                        GC.Collect(2, GCCollectionMode.Forced);
                        GC.WaitForPendingFinalizers();
                        GC.Collect(3, GCCollectionMode.Forced);
                        GC.WaitForPendingFinalizers();
                        GC.Collect(3, GCCollectionMode.Forced);
                    }
    }

    private void CheckBox_Click(object sender, RoutedEventArgs e)
    {
        if (((CheckBox)sender).IsChecked.GetValueOrDefault(false))
        {
            var x = new Test1() { A = "Test1 A" };
            x.Bs = new List<Test2>();
            for (int i = 1; i < 10000; i++ )
            {
                x.Bs.Add(new Test2() { B = "Test1 B " + i });
            }
            contentControl.Content = x;
        }
        else
        {
            contentControl.Content = new Test3() { C = "Test3 C" };
        }
    }
}

I perform forced GC by Left Shift + Alt + Ctrl + G. All items for the Test1 or Test3 view and View Model gets dead after they are unloaded correctly. So that is as expected.

But the collection generated in the Test1 model (that has Test2 objects), remains pinned into the memory. And it indicates that the array is the one used by the items container of the listbox because it shows the number of de-virtualized items from the listbox! This pinned array changes it's size when we minimize or restore the view in Test1 view mode! One time it was 16 items and next time it was 69 item when profiled.

enter image description here

This means WPF performs pinning of items generated in items controls! Can anyone explain this? Does this have any signficant drawback?

Thx a lot.

Leontineleontyne answered 21/2, 2012 at 13:10 Comment(5)
Perhaps a CollectionView created for the collection is hanging around.Fredrika
thx for the response. Yes! It is the items collection from the items container. But why would be that hanging around? The view is gone. ListBox is gone. Why would WPF pin collections in the memeory?Leontineleontyne
Could you post the root for the pinned instance please.Directory
@Biran, as I said the root is not existing... its not traceable from any obviousroot! Its Object[] and then <GCHandle> {Pinned}Leontineleontyne
Iguess this has something to do with the optimizations that WPF might be doing. As in, the items from an itemscontrol in a view that was unloaded, are kept pinned in memory for some time by WPF in case if we switch back to the same view, so the WPF engine will utilize the same pinned array to render and load view and the itemscontrol faster... Just a guess... :)Fernyak
B
3

The problem is being caused by a failure of the binding mechanism to fully release the list items that have actually been bound for display on the screen. That last bit is almost certainly why you're seeing different numbers of "orphaned" instances in different runs. The more you scroll the list, the more problems you generate.

This seems to be related to the same sort of underlying problem as described in a bug report that I submitted over a year ago since the pinning root and pinned instance trees are similar. (To see that kind of detail in a convenient format, you might want to grab a copy of a somewhat fancier memory profiler like ANTS Memory Profiler.)

The really bad news is that your orphaned instances are being pinned past the demise of the window itself, so you probably can't clean them up without the same sort of hack I had to use in the WinForms scenario to force clean-up of the binding privates.

The only bit of good news in all this is that the problem does not occur if you can avoid binding to nested properties. For example, if you add a ToString() override to Test2 to return the value of its B property and remove DisplayMemberPath from you listbox item, the problem will go away. e.g.:

public class Test2
{
    public string B { get; set; }

    public override string ToString()
    {
        return this.B;
    }
}

<ListBox ItemsSource="{Binding Bs}" 
    Margin="5"/>
Bonhomie answered 24/2, 2012 at 20:19 Comment(4)
Ah yes the notorious binding leak issue. This is also true, I have seen it. Though you can fix it by implementing INotifyPropertyChanged on your model. Even if you never call PropertyChanged event, its mere presence fixes the issue in the framework. Also in the binding you can add Mode=OneWay or Mode=OneTime to fix the binding leak issue. Though this isn't always available. Try both, add INotifyPropertyChanged and {Binding B, Mode=OneWay}.Mezzorelievo
@Nicole & Justin, Thx a lot for your help. Using ToString() or OneTime Binding fixed the orphaned Test2 instances. Thx again for that. But a single instance of the array Test2[] still remains pinned and occupies 16 bytes of memory. Although this is much better than having "n" number of Test2 instances pinned to the memory, but still how to remove that? I tried INotifyPropertyChanged for Bs collection too (also tired OneTime binding of Bs) ... the array still stays. Any help here?Leontineleontyne
@justin.m.chase: Thanks for mentioning the INotifyPropertyChanged workaround. It didn't work for my earlier WinForms problems, so I'd completely forgotten that I had run across mention of it for WPF.Bonhomie
@AngelWPF: The 16-byte Test2[] array that you're seeing isn't a leaked instance. It's a static instance created by List<T> for use when the capacity is 0. You could avoid its creation by always passing a non-zero initial capacity to the List<T> constructor, but that's a micro-optimization that I would not recommend.Bonhomie
M
0

In your sample code above I don't see where you are unloading any of the visuals?

But assuming that you are unloading the entire view this is still predictable. The factor you are not taking into account is the Dispatcher. The Dispatcher is a prioritized event queue and for every delegate in that queue it maintains a reference to the objects pointed to by those delegates. Meaning it's very possible for something in your view to be in the queue after an Unloaded event and therefore have a legitimate reference in the GC. You can GC.Collect till you're blue in the face and it won't ever collect objects with remaining references.

So what you have to do is to pump the dispatcher and then call GC.Collect. Something like this:

void Control_Unloaded(object sender, RoutedEventArgs e)
{
  // flush dispatcher
  this.Dispatcher.BeginInvoke(new Action(DoMemoryAnalysis), DispatcherPriority.ContextIdle);
}

private static void DoMemoryAnalysis()
{
  GC.Collect();
  GC.WaitForPendingFinalizers();

  // do memory analysis now.
}

Another really common cause of memory leaks in .net has to do with attaching events and not unattaching them correctly. I don't see you doing this in your sample above but if you are attaching events at all make sure you are unattaching them in Unloaded or Dispose or wherever is most appropriate.

Mezzorelievo answered 23/2, 2012 at 18:4 Comment(2)
The MVVM + DataTemplate model does not hold any view, view model or model objects in memeory coz the view is overwritten when a different target view model is set. My snapshot shows the Test1 View is not live! So that answers your first query... Plus I dont have any leaking event handlers as the root for the Pinned instances is untraceable (to any such event handler) by the profiler. As seen in my code I really dont have any events handled (unless WPF keeps the reference alive for some reason). Plus Pinning instances is always deliberate and my e.g. dont have any such code.Leontineleontyne
WPF itself will put your visual into the dispatcher queue for all kinds of events. You don't have to hook up events in your code for this to happen. I had this exact same problem when working on the PSD importer in Expression Blend. We would open a large psd file, close the dialog and open it again and crash with out of memory exception. How could you be out of memory if the window isn't even open? Its because the window lives on in the dispatcher queue for quite some time. You have to flush the queue and GC collect even though the visual isn't even in the visual tree anymore. Try it.Mezzorelievo

© 2022 - 2024 — McMap. All rights reserved.