views:

1995

answers:

4

I have boiled down an issue I'm seeing in one of my applications to an incredibly simple reproduction sample. I need to know if there's something amiss or something I'm missing.

Anyway, below is the code. The behavior is that the code runs and steadily grows in memory until it crashes with an OutOfMemoryException. That takes a while, but the behavior is that objects are being allocated and are not being garbage collected.

I've taken memory dumps and ran !gcroot on some things as well as used ANTS to figure out what the problem is, but I've been at it for a while and need some new eyes.

This reproduction sample is a simple console application that creates a Canvas and adds a Line to it. It does this continually. This is all the code does. It sleeps every now and again to ensure that the CPU is not so taxed that your system is unresponsive (and to ensure there's no weirdness with the GC not being able to run).

Anyone have any thoughts? I've tried this with .NET 3.0 only, .NET 3.5 and also .NET 3.5 SP1 and the same behavior occurred in all three environments.

Also note that I've put this code in a WPF application project as well and triggered the code in a button click and it occurs there too.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Controls;
using System.Windows.Shapes;
using System.Windows;

namespace SimplestReproSample
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            long count = 0;
            while (true)
            {
                if (count++ % 100 == 0)
                {
                    // sleep for a while to ensure we aren't using up the whole CPU
                    System.Threading.Thread.Sleep(50);
                }
                BuildCanvas();
            }
        }

        [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        private static void BuildCanvas()
        {
            Canvas c = new Canvas();

            Line line = new Line();
            line.X1 = 1;
            line.Y1 = 1;
            line.X2 = 100;
            line.Y2 = 100;
            line.Width = 100;
            c.Children.Add(line);

            c.Measure(new Size(300, 300));
            c.Arrange(new Rect(0, 0, 300, 300));
        }
    }
}

NOTE: the first answer below is a bit off-base since I explicitly stated already that this same behavior occurs during a WPF application's button click event. I did not explicitly state, however, that in that app I only do a limited number of iterations (say 1000). Doing it that way would allow the GC to run as you click around the application. Also note that I explicitly said I've taken a memory dump and found my objects were rooted via !gcroot. I also disagree that the GC would not be able to run. The GC does not run on my console application's main thread, especially since I'm on a dual core machine which means the Concurrent Workstation GC is active. Message pump, however, yes.

To prove the point, here's a WPF application version that runs the test on a DispatcherTimer. It performs 1000 iterations during a 100ms timer interval. More than enough time to process any messages out of the pump and keep the CPU usage low.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Shapes;

namespace SimpleReproSampleWpfApp
{
    public partial class Window1 : Window
    {
        private System.Windows.Threading.DispatcherTimer _timer;

        public Window1()
        {
            InitializeComponent();

            _timer = new System.Windows.Threading.DispatcherTimer();
            _timer.Interval = TimeSpan.FromMilliseconds(100);
            _timer.Tick += new EventHandler(_timer_Tick);
            _timer.Start();
        }

        [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        void RunTest()
        {
            for (int i = 0; i < 1000; i++)
            {
                BuildCanvas();
            }
        }

        [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
        private static void BuildCanvas()
        {
            Canvas c = new Canvas();

            Line line = new Line();
            line.X1 = 1;
            line.Y1 = 1;
            line.X2 = 100;
            line.Y2 = 100;
            line.Width = 100;
            c.Children.Add(line);

            c.Measure(new Size(300, 300));
            c.Arrange(new Rect(0, 0, 300, 300));
        }

        void _timer_Tick(object sender, EventArgs e)
        {
            _timer.Stop();

            RunTest();

            _timer.Start();
        }
    }
}

NOTE2: I used the code from the first answer and my memory grew very slowly. Note that 1ms is much slower and less iterations than my example. You have to let it run for a couple minutes before you start to notice growth. After 5 minutes it's at 46MB from a starting point of 30MB.

NOTE3: Removing the call to .Arrange completely eliminates the growth. Unfortunately, that call is pretty vital to my use since in many cases I'm creating PNG files from the Canvas (via the RenderTargetBitmap class). Without the call to .Arrange it doesn't layout the canvas at all.

A: 

Edit 2: Obviously not the answer, but was part of the back-and-forth among answers and comments here, so I'm not deleting it.

The GC never gets a chance to collect those objects because your loop and its blocking calls never end, and therefore the message pump and events never get their turn. If you used a Timer of some sort so that messages and events actually have a chance to process, you probably wouldn't be able to eat up all your memory.

Edit: The following does not eat up my memory as long as the interval is greater than zero. Even if the interval is just 1 Tick, as long as it isn't 0. If it's 0, we're back to the infinite loop.

public partial class Window1 : Window {
    Class1 c;
    DispatcherTimer t;
    int count = 0;
    public Window1() {
        InitializeComponent();

        t = new DispatcherTimer();
        t.Interval = TimeSpan.FromMilliseconds( 1 );
        t.Tick += new EventHandler( t_Tick );
        t.Start();
    }

    void t_Tick( object sender, EventArgs e ) {
        count++;
        BuildCanvas();
    }

    private static void BuildCanvas() {
        Canvas c = new Canvas();

        Line line = new Line();
        line.X1 = 1;
        line.Y1 = 1;
        line.X2 = 100;
        line.Y2 = 100;
        line.Width = 100;
        c.Children.Add( line );

        c.Measure( new Size( 300, 300 ) );
        c.Arrange( new Rect( 0, 0, 300, 300 ) );
    }
}
Joel B Fant
Not to mention the continual recreation of FrameworkElement's is EXTREMELY expensive.
sixlettervariables
See the comment on the next answer.
Adam Sills
Very interesting.
Joel B Fant
+1 Maybe not _the_ answer, but looks like a valid attempt. Hurts me to see it at -1.
J.Hendrix
+1  A: 

Normally in .NET GC gets triggered on object allocation upon crossing a certain threshold, it does not depend on message pumps (I can't imagine it's different with WPF).

I suspect that Canvas objects are somehow rooted deep inside or something. If you do c.Children.Clear() right before the BuildCanvas method finishes, the memory growth slows down dramatically.

Anyway, as a commenter noted here, such usage of framework elements is pretty unusual. Why do you need so many Canvases?

liggett78
This is a large WPF application which is experiencing very slow memory growth over time. After a few days of use the memory usage is incredibly high and after examining memory dumps, I've been able to recreate the scenario that causes the growth.
Adam Sills
This is a drawing application; file-new and whatnot over a long time keeps the memory growing and it keeps trending upwards. The act of creating new drawn elements, making a new document, new drawn elements, and so on causes a leak. This sample code does what the users see much quicker.
Adam Sills
Doing a .Children.Clear has no effect. You can actually see the behavior without adding any children at all to the canvas.
Adam Sills
We've got a WPF application with over 10k bindings on ~3000 framework elements in a scene, without any excessive growth over time, etc. However, we reuse framework elements rather than removing/adding new elements.
sixlettervariables
Any chance you're calling the .Arrange method ever?
Adam Sills
We do almost nothing in code behind, so the Arrange method is called automatically by the framework when required.
sixlettervariables
+5  A: 

I was able to reproduce your problem using the code you provided. Memory keeps growing because the Canvas objects are never released; a memory profiler indicates that the Dispatcher's ContextLayoutManager is holding on to them all (so that it can invoke OnRenderSizeChanged when necessary).

It seems that a simple workaround is to add

c.UpdateLayout()

to the end of BuildCanvas.

That said, note that Canvas is a UIElement; it's supposed to be used in UI. It's not designed to be used as an arbitrary drawing surface. As other commenters have already noted, the creation of thousands of Canvas objects may indicate a design flaw. I realise that your production code may be more complicated, but if it's just drawing simple shapes on a canvas, GDI+-based code (i.e., the System.Drawing classes) may be more appropriate.

Bradley Grainger
and there endeth the lesson on why Garbage Collection is a poor memory management system, especially in languages who purport to be easy to use.
gbjbaanb
I 100% totally agree with you and your answer does seem to solve the problem. I'm coming into this code 2 years in and unfortunately there's no simple way to change it at this point.
Adam Sills
I had seen my Canvas being connected to ContextLayoutManager via some SizeChangeInfo objects, but I'm way too green on the WPF classes to have figured it out (apparently).Thanks.
Adam Sills
+1  A: 

WPF in .NET 3 and 3.5 has an internal memory leak. It only triggers under certain situations. We could never figure out exactly what triggers it, but we had it in our app. Apparently it's fixed in .NET 4.

I think it's the same as the one mentioned in this blog post

At any rate, putting the following code in the App.xaml.cs constructor solved it for us

public partial class App : Application
{
   public App() 
   { 
       new HwndSource(new HwndSourceParameters()); 
   } 
}

If nothing else solves it, try that and see

Orion Edwards
That's certainly interesting, but this was on Vista and this source blog post indicates it's XP only. http://blogs.msdn.com/b/jgoldb/archive/2008/02/04/finding-memory-leaks-in-wpf-based-applications.aspx
Adam Sills
And for me anyway, the UpdateLayout solved my issue.
Adam Sills