One of the most annoying bug reports that we got for NH Prof is that it crash with out of memory exception after long use. We did the usual checkups, and the reason for the memory leak was obvious, something kept a lot of objects internal to WPF in memory. In fact, here are the heavy hitters, as extracted from the dump:
Count | Size (kliobytes) | Type |
5,844 | 3,337 | System.Byte[] |
69,346 | 5,474 | System.String |
49,400 | 37,198 | System.Object[] |
1,524,355 | 47,636 | MS.Utility.SingleItemList`1[[System.WeakReference,mscorlib]] |
3,047,755 | 71,432 | MS.Internal.Data.ValueChangedEventArgs |
1,523,918 | 71,434 | MS.Utility.ThreeItemList`1[[System.WeakReference,mscorlib]] |
3,048,292 | 71,444 | MS.Utility.FrugalObjectList`1[[System.WeakReference,mscorlib]] |
3,048,292 | 95,259 | System.Windows.WeakEventManager+ListenerList |
3,047,755 | 166,674 | MS.Internal.Data.ValueChangedEventManager+ValueChangedRecord |
3,056,462 | 191,029 | System.EventHandler |
7,644,217 | 238,882 | System.WeakReference |
As you can see, this just says that we are doing something that cause WPF to keep a lot of data in memory. In fact, this looks like a classic case of “memory leak” in .NET, where we aren’t releasing references to something. This usually happen with events, and it was the first thing that I checked.
It took a while, but I convinced myself that this wasn’t that. The next step was to try to figure out what is causing this. I’ll skip the sordid tale for now, I’ll queue it up for posting at a later date. What we ended up with is a single line of code that we could prove caused the issue. If we removed it, there was no leak, if it was there, the leak appeared.
That was the point where I threw up my hands and asked Christopher to look at this, I couldn’t think of something bad that we were doing wrong, but Christopher and Rob are the experts in all things WPF.
Christopher managed to reproduce this in an isolated fashion. here is how it goes:
public class TestModel : INotifyPropertyChanged
{
private readonly DispatcherTimer timer;
private int count;
public event PropertyChangedEventHandler PropertyChanged = delegate { };
public TestModel()
{
timer = new DispatcherTimer(DispatcherPriority.Normal)
{
Interval = TimeSpan.FromMilliseconds(50)
};
timer.Tick += Timer_Tick;
timer.Start();
}
public IEnumerable Data
{
get
{
return new[]
{
new {Name = "Ayende"},
};
}
}
private void Timer_Tick(object sender, EventArgs e)
{
if (count++ % 100 == 0)
{
GC.Collect(2, GCCollectionMode.Forced);
Console.WriteLine("{0:#,#}", Process.GetCurrentProcess().WorkingSet64);
}
PropertyChanged(this, new PropertyChangedEventArgs("Data"));
}
}
This is a pretty standard model, showing data that updates frequently. (The short time on the time is to show the problem in a short amount of time.) Note that we are being explicit about forcing a GC release here, to make sure it isn’t just waste memory that haven’t been reclaimed yet.
And the XAML:
<Grid>
<ItemsControl ItemsSource="{Binding Data}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Executing this will result in the following memory pattern:
Looking at the code, I really don’t see anything that is done wrong there.
I uploaded a sample project that demonstrate the issue here.
Am I going crazy? Am I being stupid? Or is select really broken?