Простой пример WPF вызывает неконтролируемый рост памяти - PullRequest
13 голосов
/ 10 октября 2008

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

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

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

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

У кого-нибудь есть мысли? Я пробовал это только с .NET 3.0, .NET 3.5, а также .NET 3.5 с пакетом обновления 1 (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 не запускается в основном потоке моего консольного приложения, тем более что я работаю на двухъядерном компьютере, что означает, что Concurrent Workstation GC активен. Сообщение сообщения, однако, да.

Чтобы доказать это, вот версия приложения 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 он вообще не размещает холст.

Ответы [ 4 ]

10 голосов
/ 11 октября 2008

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

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

c.UpdateLayout()

до конца BuildCanvas.

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

2 голосов
/ 01 сентября 2010

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

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

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

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

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

1 голос
/ 10 октября 2008

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

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

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

0 голосов
/ 10 октября 2008

Редактировать 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 ) );
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...