Chart leak found, solution available

Jun 19, 2010 at 2:12 PM

I've heard others have had a memory leak with the WPF Toolkit chart objects.  I have the same issue.  My application is pretty simple.  I have a class called View which I derive from ContentControl.  On regular intervals I create a new Chart object and set my View's Content property to that new chart.  The previous chart which was the content should no longer be referenced and therefore collected during the next GC.  However, this was not the case.  All chart objects I had created were still being referenced and therefore memory consumption kept rising.

I took a couple process dumps using adplus.vbs (part of the debugging tools for windows) and viewed the managed heap.  The managed heap showed all the chart objects which were created were still being referenced, along with all the LineSeries I had created.  I still haven't figured out the root cause but I was able to modify the my code and the LineSeries code such that the majority of the leak is resolved.  Before I get into the change I made I want to comment on what might be at the root of the problem.  What I noticed while inspecting the process dumps was that each chart object's ResourceDictionaryDispenser for the Palette property contain the SAME ResourceDictionary collection.  Since the leak was "somewhat" caused by the line series not pulling itself out of the SeriesHost dictionary and since each chart was holding onto the same resource dictionary collection, all charts were holding onto all line series.  In my case I thought I was letting go of one chart when I created the new one, yet the new one was holding onto the same resources I thought I was letting go of and therefore everything was sticking around.  I probably didn't do a good job of explaining that.

There are two related fixes.  The "inner" one which is dependent on the "outer" is that the line series was not removing itself from the dictionary when its host was going away.  Here is the before code:

From DataPointSingleSeriesWithAxes.cs:

protected override void OnSeriesHostPropertyChanged(ISeriesHost oldValue, ISeriesHost newValue)
{
    base.OnSeriesHostPropertyChanged(oldValue, newValue);

    if (oldValue != null)
    {
        oldValue.ResourceDictionariesChanged -= new EventHandler(SeriesHostResourceDictionariesChanged);
    }

    if (newValue != null)
    {
        newValue.ResourceDictionariesChanged += new EventHandler(SeriesHostResourceDictionariesChanged);

        DispensedResourcesChanging();
    }
}

Note that when the newValue is null, e.g. series host going away, it doesn't call DispensedResourcesChanging().  I changed this method to:

protected override void OnSeriesHostPropertyChanged(ISeriesHost oldValue, ISeriesHost newValue)
{
    base.OnSeriesHostPropertyChanged(oldValue, newValue);

    if (oldValue != null)
    {
        oldValue.ResourceDictionariesChanged -= new EventHandler(SeriesHostResourceDictionariesChanged);
    }

    if (newValue != null)
    {
        newValue.ResourceDictionariesChanged += new EventHandler(SeriesHostResourceDictionariesChanged);
    }
    DispensedResourcesChanging();
}

Now we have to modify DispensedResourcesChanging() since it was assuming its SeriesHost was always non-null.  Below is the original code.  I highlighted the significant line which wasn't getting executed because DispensedResourcesChanging() wasn't getting called when the series host was being set to null.

private void DispensedResourcesChanging()
{
    if (null != PaletteResources)
    {
        Resources.MergedDictionaries.Remove(PaletteResources);
        PaletteResources = null;
    }
    using (IEnumerator<ResourceDictionary> enumerator = GetResourceDictionaryEnumeratorFromHost())
    {
        if (enumerator.MoveNext())
        {
            PaletteResources =
#if SILVERLIGHT
                enumerator.Current.ShallowCopy();
#else
                enumerator.Current;
#endif
            Resources.MergedDictionaries.Add(PaletteResources);
        }
    }
    CreateLegendItemDataPoint();
    ActualDataPointStyle = DataPointStyle ?? (Resources[DataPointStyleName] as Style);
    ActualLegendItemStyle = LegendItemStyle ?? (Resources[LegendItemStyleName] as Style);
    Refresh();
}

I just added the 'if (SeriesHost != null)':

private void DispensedResourcesChanging()
{
    if (null != PaletteResources)
    {
        Resources.MergedDictionaries.Remove(PaletteResources);
        PaletteResources = null;
    }
    if (SeriesHost != null)
    {
        using (IEnumerator<ResourceDictionary> enumerator = GetResourceDictionaryEnumeratorFromHost())
        {
            if (enumerator.MoveNext())
            {
                PaletteResources =
#if SILVERLIGHT
                    enumerator.Current.ShallowCopy();
#else
                    enumerator.Current;
#endif
                Resources.MergedDictionaries.Add(PaletteResources);
            }
        }
    }
    CreateLegendItemDataPoint();
    ActualDataPointStyle = DataPointStyle ?? (Resources[DataPointStyleName] as Style);
    ActualLegendItemStyle = LegendItemStyle ?? (Resources[LegendItemStyleName] as Style);
    Refresh();
}

In order for any of this to get called I needed to change my code so that instead of just updating my content control's Content property to the new chart, I now need to clear the series collection of the old chart.  So it now does something like this:

Chart oldchart = this.Content as Chart;
Chart chart = new Chart();
.
.
.
this.Content = chart;
if (oldchart != null)
    oldChart.Series.Clear();
.
.
.

The only reason I'm holding off on clearing the old chart series until after I set the new chart as the content is just in case WPF decided to update the screen right away.  I didn't want any flashing to a blank screen followed by the new chart displaying.  I assume this won't happen, but just in case I update the new content before clearing the series of the old chart.

These changes seem to clear up MOST of the leak.  There might still be a small leak, but I can't say for sure since I have yet to go back and test thoroughly.  I did go back and verify that the charts and the line series are no longer being referenced by running my app and capturing a process dump and viewing the managed heap.

While these changes fix the leak I was experiencing I don't think I should have to clear the old chart's series.  All I should need to do if update the content to the new chart.  The old chart should no longer be referenced and everything should work.  As I mentioned at the beginning it appears all charts hold onto the same resource dictionary collection and that's why the leak is occurring.  I think if each chart had its own resource dictionary collection the leak would not have existed.

Nick

Jun 30, 2010 at 9:54 PM

Thanks for this! I was trying to figure out what was keeping the chart around in memory and couldn't figure it out. I've been using this fix for a week or so now and it definitely works.

Jul 1, 2010 at 9:28 PM

Today I decided to see what, if any, leak still exists.  It appears there is still a leak.  I seem to be leaking System.WeakReference and System.EventHandler objects.  At this point I'm not sure what's holding onto them.

Thanks,

Nick

Nov 12, 2010 at 3:14 PM

I thought I would leave this here because nickdu's post put me on the right direction. This is a fix that will allow you to clear you memory when you close the charts. From generic. xaml in the source remove all the items in <datavis:ResourceDictionaryCollection that begins on line 282. I think this will have the effect of you not having any default points styles. Most importantly it has the effect of memory being freed after close.

This is the end result - 

 <Style TargetType="charting:Chart">
        <Setter Property="BorderBrush" Value="Black" />
        <Setter Property="BorderThickness" Value="1" />
        <Setter Property="IsTabStop" Value="False" />
        <Setter Property="Padding" Value="10" />
        <Setter Property="SnapsToDevicePixels" Value="True" />
        <Setter Property="Palette">
            <Setter.Value>
                <datavis:ResourceDictionaryCollection>
                </datavis:ResourceDictionaryCollection>
            </Setter.Value>
        </Setter>

Dec 5, 2013 at 5:09 PM
One question....

If you have to recompile the dll?
I tried to rebuild the project but did not succeed

you can link them
Dec 6, 2013 at 9:59 PM
It was a long time ago. Since I made a change to the WPF Toolkit code I would say that, yes, you do need to recompile the toolkit. Not sure why it didn't build for you. Does the toolkit rebuild without making the change? I haven't done any WPF Toolkit work in a while. Got kind of frustrated with it. Seemed to be buggy and missing core features I would expect in a charting toolkit.

Nick