Подтвердить что ты не робот

Простой образец WPF вызывает неконтролируемый рост памяти

У меня возникла проблема, которую я вижу в одном из моих приложений, в невероятно простой образец воспроизведения. Мне нужно знать, есть ли что-то неладное или что-то мне не хватает.

В любом случае, ниже приведен код. Поведение в том, что код работает и неуклонно растет в памяти, пока он не завершится с OutOfMemoryException. Это занимает некоторое время, но поведение заключается в том, что объекты выделяются и не собираются мусором.

Я взял свалки памяти и побежал! gcroot на некоторые вещи, а также использовал ANTS, чтобы выяснить, в чем проблема, но я был на это какое-то время и нуждался в новых глазах.

Этот образец воспроизведения представляет собой простое консольное приложение, которое создает холст и добавляет к нему строку. Он делает это постоянно. Это все, что делает код. Он все время спит, чтобы гарантировать, что процессор не облагается налогом, чтобы ваша система не реагировала (и чтобы там не было странности, когда GC не мог работать).

У кого-нибудь есть мысли? Я пробовал это только с .NET 3.0,.NET 3.5, а также с .NET 3.5 SP1, и такое же поведение произошло во всех трех средах.

Также обратите внимание, что я поместил этот код в проект приложения WPF, а также вызвал код нажатием кнопки, и он также встречается там.

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));
        }
    }
}

ПРИМЕЧАНИЕ. Первый ответ ниже - это бит вне базы, так как я прямо заявил, что такое же поведение происходит во время события нажатия кнопки приложения WPF. Однако я не указал, что в этом приложении я занимаюсь ограниченным числом итераций (скажем, 1000). Выполнение этого способа позволит GC работать, когда вы нажимаете на приложение. Также обратите внимание, что я явно сказал, что я взял дамп памяти и обнаружил, что мои объекты были внедрены через! Gcroot. Я также не согласен с тем, что GC не сможет работать. GC не запускается в основном потоке моего консольного приложения, тем более, что я на двухъядерной машине, что означает, что GC Concurrent Workstation активен. Насос сообщений, однако, да.

Чтобы доказать эту точку, вот версия приложения WPF, которая запускает тест на DispatcherTimer. Он выполняет 1000 итераций во время интервала таймера 100 мс. Более чем достаточно времени для обработки любых сообщений из насоса и низкого уровня использования ЦП.

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();
        }
    }
}

ПРИМЕЧАНИЕ 2. Я использовал код из первого ответа, и моя память росла очень медленно. Обратите внимание, что 1 мс намного медленнее и меньше итераций, чем мой пример. Вы должны позволить ему работать в течение пары минут, прежде чем вы начнете замечать рост. Через 5 минут оно составляет 46 МБ от начальной точки 30 МБ.

ПРИМЕЧАНИЕ 3: Удаление вызова .Arrange полностью исключает рост. К сожалению, этот вызов очень важен для моего использования, поскольку во многих случаях я создаю PNG файлы с Canvas (через класс RenderTargetBitmap). Без вызова .Arrange это вовсе не макет холста.

4b9b3361

Ответ 1

Я смог воспроизвести вашу проблему, используя предоставленный вами код. Память продолжает расти, потому что объекты Canvas никогда не выпускаются; профилировщик памяти указывает, что диспетчер ContextLayoutManager поддерживает их все (чтобы он мог при необходимости активировать OnRenderSizeChanged).

Кажется, что простым обходным путем является добавление

c.UpdateLayout()

до конца BuildCanvas.

Тем не менее, обратите внимание, что Canvas является UIElement; он должен использоваться в пользовательском интерфейсе. Он не предназначен для использования в качестве произвольной поверхности рисования. Как уже отмечали другие комментаторы, создание тысяч объектов Canvas может указывать на дефект дизайна. Я понимаю, что ваш производственный код может быть более сложным, но если он просто рисует простые фигуры на холсте, более подходящим может быть код на основе GDI + (то есть классы System.Drawing).

Ответ 2

WPF в .NET 3 и 3.5 имеет утечку внутренней памяти. Это срабатывает только при определенных ситуациях. Мы никогда не смогли точно определить, что это заставляет, но у нас это было в нашем приложении. По-видимому, он исправлен в .NET 4.

Я думаю, что это то же самое, что упоминалось в этом сообщении в блоге

В любом случае, поместив следующий код в конструктор App.xaml.cs, он решил это для нас

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

Если ничего другого не решает, попробуйте это и посмотрите

Ответ 3

Обычно в .NET GC запускается при распределении объектов при пересечении определенного порога, это не зависит от сообщений насосов (я не могу представить, что это отличается от WPF).

Я подозреваю, что объекты Canvas каким-то образом внедрены глубоко внутри или что-то в этом роде. Если вы выполняете c.Children.Clear() до завершения метода BuildCanvas, рост памяти резко замедляется.

В любом случае, как отметил комментатор, такое использование элементов структуры довольно необычно. Зачем вам так много холстов?

Ответ 4

Изменить 2: Очевидно, что это не ответ, но он был частью ответов и комментариев здесь, поэтому я не удаляю его.

GC никогда не получает возможность собирать эти объекты, потому что ваш цикл и его блокирующие вызовы никогда не заканчиваются, и поэтому насос сообщений и события никогда не получат свою очередь. Если вы использовали Timer, чтобы сообщения и события действительно имели возможность обрабатывать, вы, вероятно, не смогли бы съесть всю свою память.

Изменить: Ниже не хватает моей памяти, пока интервал больше нуля. Даже если интервал составляет всего 1 тик, если он не равен 0. Если он равен 0, мы вернемся к бесконечному циклу.

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 ) );
    }
}