C# – Simple WPF sample causes uncontrolled memory growth

cmemory-leakswpf

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.

Best Answer

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.

Related Topic