Ayende @ Rahien

It's a girl

Reproducing a WPF memory leak

I have run into WPF memory leaks before, and I thought that I fixed them all, but I started getting more reports from people experiencing large memory usage from NH Prof. That lead me to try to track it down again.

Short story, you can get the reproduction here: http://github.com/ayende/wpf-mem-leak

I left an NH Prof instance running overnight, and was able to confirm that there is a memory leak in there, just by seeing how much more memory it had in task manager. One of the nicest things in Windows 7 is the ability to generate a dump directly from task manager.

I loaded that dump into windbg, loaded sos and mscrowks and set out to figure out what was taking so much memory. The most common way to start a memory leak issue is to start figuring out what is taking all this memory.  For that, the !dumpheap –stat command is great, since it give you memory usage by type and size. Here is what I found:

000007fee95b8a88   364674     11669568 MS.Utility.SingleItemList`1[[System.WeakReference, mscorlib]]
000007feeffe4748    51074     13161552 System.Object[]
000007fee77417a8   728189     17476536 MS.Internal.Data.ValueChangedEventArgs
000007fee95aafe8   364133     17478384 MS.Utility.ThreeItemList`1[[System.WeakReference, mscorlib]]
000007fee95b88c0   728814     17491536 MS.Utility.FrugalObjectList`1[[System.WeakReference, mscorlib]]
000007fee95b8838   728814     23322048 System.Windows.WeakEventManager+ListenerList
0000000000496170    19063     28004120      Free
000007fee7741730   728189     40778584 MS.Internal.Data.ValueChangedEventManager+ValueChangedRecord
000007fef0036c90   736210     47117440 System.EventHandler
000007feeffe7a90  1837702     58806464 System.WeakReference

I literally groaned when I saw that, based on my previous experience, it means that the memory leak is generated from inside WPF code, and that I am not likely to fix it. Then I set out to figure out where it is at. My suspicion fell on the statistics feature, it is the only thing that really updates frequently, after all.

So I setup to build a reproduction. Sadly, it is trivially easy to reproduce the issue. Given this XAML:

<StackPanel>
<ItemsControl ItemsSource="{Binding Path=Statistics}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Padding="2 0 0 0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Path=Key}"
ToolTip="{Binding Path=Key}"
TextTrimming="CharacterEllipsis" />
<GridSplitter Grid.Column="1"
Background="#3000"
Width="2"
Margin="2 0" />
<TextBlock Text="{Binding Path=Value}"
Grid.Column="2" />
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>

The following code will generate a memory leak:

public partial class Window1 : Window
{
readonly DispatcherTimer timer = new DispatcherTimer(DispatcherPriority.Normal)
{
Interval = TimeSpan.FromMilliseconds(1)
};

private readonly MyModel context;

public Window1()
{
InitializeComponent();
context = new MyModel();
context.UpdateData();
timer.Tick += (sender, args) => context.UpdateData();
DataContext = context;
timer.Start();
}

public class MyModel
{
public ObservableCollection<DictionaryEntry> Statistics { get; set; }

public MyModel()
{
Statistics = new ObservableCollection<DictionaryEntry>();
}

public void UpdateData()
{
Statistics.Clear();
Statistics.Add(new DictionaryEntry("Time", DateTime.Now.ToString()));
Statistics.Add(new DictionaryEntry("Mem", GC.GetTotalMemory(true).ToString("#,#")));
}
}
}

When you run the code, you can see that memory usage is going up all the time. As far as I can tell, I am not doing anything that is wrong or even slightly strange here. You can get the reproduction here: http://github.com/ayende/wpf-mem-leak

I tried several workarounds so far, but I didn’t like any of them. Any ideas?

Comments

Michael Sync
11/17/2009 09:12 AM by
Michael Sync

I think Clear (istics.Clear();) may take a while to clean

maybe. we can re-initialize Statistics

public void UpdateData() {

        Statistics = new ObservableCollection

<dictionaryentry();

        Statistics.Add(new DictionaryEntry("Time", DateTime.Now.ToString())); 

        Statistics.Add(new DictionaryEntry("Mem", GC.GetTotalMemory(true).ToString("#,#"))); 

    }
Szymon
11/17/2009 09:16 AM by
Szymon

Hi Ayende,

The problem here is that you use a TwoWay binding to an object that doesn't provide change notification. This forces WPF to use other methods to synchronize the values.

There are two solutions:

1) In your bindings use Mode=OneWay. Since you are recreating the items in each iteration you need to read the value only once.

2) Instead of DictionaryEntry use a custom class that implements INotifyPropertyChange interface. This way WPF is able to track changes to property values easily.

Michael Sync
11/17/2009 09:24 AM by
Michael Sync

Hi Szymon,

Please correct me if I

I tried changing like this below but it still keep increasing.

<itemscontrol
<itemscontrol.itemtemplate
<datatemplate
<border
<grid
<grid.columndefinitions
<columndefinition
<columndefinition
<columndefinition

<textblock
ToolTip="{Binding Path=Key,Mode=OneWay}"

                                           TextTrimming="CharacterEllipsis" />

<gridsplitter
Background="#3000"

                                              Width="2"

                                              Margin="2 0" />

<textblock
Grid.Column="2" />

public class MainViewModel : ViewModelBase

{

    public MainViewModel()

    {

        Statistics = new ObservableCollection

<model();

    } 


    public void UpdateData() {

        Statistics.Clear();

        Statistics.Add(new Model("Time", DateTime.Now.ToString()));

        Statistics.Add(new Model("Mem", GC.GetTotalMemory(true).ToString("#,#"))); 

    }


    public ObservableCollection

<model Statistics { get; set; }

}


public class Model : ViewModelBase

{

    public Model(string str1, string str2)

    {

        key = str1;

        val = str2;

    }


    private string key = string.Empty;

    public string Key

    {

        get { return key; }

        set { 

            key = value;

            this.OnPropertyChanged("Key");

        }

    }


    private string val = string.Empty;

    public string Value

    {

        get { return val; }

        set

        {

            val = value;

            this.OnPropertyChanged("Value");

        }

    }


}

Am I missing something?

Rafal
11/17/2009 10:52 AM by
Rafal

What the hell is MS.Utility.FrugalObjectList?

Paulo Quicoli
11/17/2009 11:02 AM by
Paulo Quicoli

Ayende, as you can see in my code i changed you DataContext object to:

public class MyModel: INotifyPropertyChanged

    {

        private string runningTime;

        public string RunningTime

        {

            get { return runningTime; }

            set

            {

                runningTime = value;

                OnPropertyChanged("RunningTime");

            }

        }


        private string memUsed;

        public string MemUsed

        {

            get { return memUsed; }

            set

            {

                memUsed = value;

                OnPropertyChanged(MemUsed);

            }

        }


        public void UpdateData()

        {

            RunningTime = DateTime.Now.ToString();

            MemUsed = GC.GetTotalMemory(true).ToString("#,#");

        }


        public event PropertyChangedEventHandler PropertyChanged;


        protected void OnPropertyChanged(string name)

        {

            PropertyChangedEventHandler handler = PropertyChanged;

            if (handler != null)

            {

                handler(this, new PropertyChangedEventArgs(name));

            }

        }

    }

And in WPF Window, i chaged to:

<stackpanel
<stackpanel
<textblock
<textblock

<stackpanel
<textblock
<textblock

If you run, you will see memory is not getting high by time pass... Memory leak is gone :)

(sorry for my bad english)

Eyston
11/17/2009 02:55 PM by
Eyston

Reading posts / comments like this make me glad I'm not developing desktop applications.

There are just some things you can appreciate people knowing, that you have no desire to know yourself. This kind of non-portable knowledge being one such case :)

Graham Nash
11/17/2009 05:10 PM by
Graham Nash

Does anyone have an easy way of doing such a clean memory dump in WinXP? I tried using Debugging Tools for Windows, but WinDbg keeps complaining I am missing symbols. (I followed the installation instructions on MSDN).

Andrew
11/17/2009 05:11 PM by
Andrew

I haven't looked at Paulo's solution, so if his works, that's great.

In the last blog posting about this issue I believe I posted a "hackish" solution to the problem that should solve your problem. It involves calling the GC and collapsing the process memory. I'm in no way saying it's an ideal solution, but works in a pinch if everything else fails.

J
11/17/2009 05:54 PM by
J

@Eyston

Between cross browser css hacks, javascript which behaves differently among different browsers and even browser versions, having to support every platform under the sun only to have to rebase your code every time a new version of a browser comes out...

boy I'm glad I develop desktop applications. :)

Also, BTW, WPF is the newest UI technology...Winforms (AFAIK) is much more stable since its approaching a decade in maturity

Justin Chase
11/17/2009 08:19 PM by
Justin Chase

Mode=OneTime

or

Mode=OneWay

On those bindings.

jdu
11/19/2009 10:18 AM by
jdu

This looks really bad to me. I think you should open a ticket about it at connect.microsoft.com. Because from my understanding of your case, it would mean almost any binding is leaking.

As someone wrote, there's a KB article describing a very similar leak here: http://support.microsoft.com/kb/938416. But in this KB there's an additional condition: the databinding source object X must hold a reference to the target of the binding. Which you don't have.

Your repro seems to imply that:

Binding to a plain CLR object (no DependencyProperty and no INotifyPropertyChanged) without specifying Mode=OneTime always creates a memory leak.

And there are tons of those in every WPF app...

Paulo Quicoli
11/19/2009 06:19 PM by
Paulo Quicoli

@jdu,

did you download my code? See, in my version, implementing INotifyPropertyChanged resolved memory leak...

Comments have been closed on this topic.