Быстрое рисование множества прямоугольников по одному в WPF

Мое приложение получает данные с внешнего устройства. После каждой точки данных существует короткое электронное время ожидания (около 10 мкс), в течение которого никакая другая точка данных не может быть получена, и мое приложение должно использовать ее для обработки и отображения данных на экране в виде точечной диаграммы. Моя самая важная цель - не превышать это электронное время простоя. Как можно было бы подойти к этой проблеме в приложении на основе WPF, и как можно было бы сравнить различные методы?

Вот что я пробовал:

  • Создание Rectangle в Canvas за каждую прибывающую точку данных. Это слишком медленно в 10 раз.
  • Тот же подход, но рисунок DrawingVisuals в пользовательском контроле. Лучше, но все еще слишком медленно. Добавление визуальных / логических дочерних элементов к дереву может потребовать слишком много времени.
  • UserControl где все точки данных хранятся в массиве и отображаются в OnRender метод. Здесь я должен снова рисовать каждую точку при каждом вызове OnRender. Поэтому этот метод замедляется с течением времени, что нежелательно. Есть ли способ сказать OnRender не очищать экран на каждом проходе, чтобы я мог рисовать постепенно?
  • Отображение каждой точки в виде пикселя в WriteableBitmap, Кажется, это работает, но я не нашел способа определить, не делает ли недействительная часть растрового изображения случайным образом несколько очень длительных периодов ожидания (когда изображение фактически обновляется на экране). Любые идеи для измерения этого?

Редактировать:

В комментариях была поднята точка буферизации данных и их более медленного отображения. Проблема с этим подходом заключается в том, что в какой-то момент мне приходится обрабатывать буфер. Выполнение этого во время измерения вводит длительное время, в течение которого моя система занята, и новые события будут отбрасываться. Поэтому иметь дело с каждым пунктом индивидуально, но навсегда, было бы более желательным. Использование 10 мкс для запуска отображения для каждого события намного лучше, чем мгновенное сохранение его в буфере, и использование 100 мкс каждые 50 мс или около того для обработки накопленных событий.

В старые (то есть не WPF) дни вы могли, например, помещать необходимые данные в графическую память, и графическая карта справлялась с ними по своему усмотрению. Конечно, на самом деле он не будет отображаться с частотой выше 60 Гц, но вам не нужно было снова трогать эти данные.

Я надеюсь, что ясно дал понять, каковы мои требования. Черт мой английский =)

4 ответа

Решение

Использование WriteableBitmap будет самым быстрым подходом. Для тестирования вы можете предварительно выделить массив и использовать секундомер для выборки времени при рендеринге, затем вы можете проанализировать время, чтобы получить представление о производительности.

Одна из основных проблем, с которой вы столкнулись, связана со сборкой мусора. К сожалению, это создаст потенциальные возможности для проблем с производительностью, которые вы описываете, например, из-за случайного зависания во время проведения GC. Вы можете поэкспериментировать с GC с низкой задержкой, чтобы смягчить это.

Обновить

Вот пример использования GC с низкой задержкой:

http://blogs.microsoft.co.il/blogs/sasha/archive/2008/08/10/low-latency-gc-in-net-3-5.aspx

Вы можете использовать это, чтобы гарантировать, что во время вашего "мертвого времени", то есть времени рендеринга, не будет сборок мусора.

Обновление 2

Как я уже упоминал в своем комментарии некоторое время назад - вы пакетируете обновления для вашего WritableBitmap?

Частота обновления вашего устройства слишком высока, чтобы выдерживать запись в растровое изображение для каждого обновления устройства - я думаю, что в секунду производится 10–100 тыс. Обновлений. Попробуйте обновить свое растровое изображение на более ощутимой частоте (например, 60 или 25 раз в секунду), так как накладные расходы на принудительное отображение растрового изображения будут доминировать в производительности при 10–100 тыс. Обновлений в секунду. Выполняйте запись в буфер при получении обновлений устройства, а затем периодически переносите этот буфер в WritableBitmap. Вы можете использовать таймер для этого или делать это каждые n обновлений устройства. Таким образом, вы будете пакетировать свои обновления и значительно сократить накладные расходы на рендеринг WritableBitmap.

Обновление 3

Хорошо, похоже, что вы обновляете WritableBitmap 10–100 тыс. Раз в секунду - это невозможно. Пожалуйста, попробуйте механизм, основанный на кадрах и пакетах, как описано ранее. Также ваш дисплей будет обновляться только со скоростью 60 кадров в секунду.

Если вас беспокоит блокировка обновлений вашего устройства, рассмотрите возможность использования двух чередующихся обратных буферов и многопоточности. Таким образом, вы периодически переключаетесь в какой резервный буфер записывает ваше устройство, и используете второй поток для рендеринга замененного буфера в WritableBitmap. Пока вы можете поменять буфер в течение < 10 мкс, вы можете делать это в мертвое время, не блокируя обновления вашего устройства.

Обновление 4

В дополнение к ответу на мой вопрос, может показаться, что в настоящее время вызывается "блокировка \ разблокировка" для каждого из 100 000 обновлений в секунду. Это то, что, вероятно, убивает производительность. На моей (мощной) системе я измерил 100k "блокировка \ разблокировка" на ~275мс. Это довольно тяжело и будет намного хуже в системе с низким энергопотреблением.

Вот почему я думаю, что 100 000 обновлений в секунду не достижимо, т.е. блокировка -> обновление -> разблокировка. Блокировка слишком дорогая.

Вам нужно найти способ уменьшить количество блокирующих вызовов, либо вообще не блокируя, блокируя каждые n операций, или, возможно, пакетные запросы, а затем применяя пакетное обновление в блокировке. Здесь есть несколько вариантов.

Если вы выберете пакетное обновление, оно может составить всего 10 циклов, что снизит частоту обновления до 10 000 обновлений в секунду. Это уменьшит ваши накладные расходы в 10 раз.

Пример эталонного кода для блокировки накладных расходов на вызовах 100k:

lock/unlock - Interval:1 - :289.47ms
lock/unlock - Interval:1 - :287.43ms
lock/unlock - Interval:1 - :288.74ms
lock/unlock - Interval:1 - :286.48ms
lock/unlock - Interval:1 - :286.36ms
lock/unlock - Interval:10 - :29.12ms
lock/unlock - Interval:10 - :29.01ms
lock/unlock - Interval:10 - :28.80ms
lock/unlock - Interval:10 - :29.35ms
lock/unlock - Interval:10 - :29.00ms

Код:

public void MeasureLockUnlockOverhead()
{
    const int TestIterations = 5;

    Action<string, Func<double>> test = (name, action) =>
    {
        for (int i = 0; i < TestIterations; i++)
        {
            Console.WriteLine("{0}:{1:F2}ms", name, action());
        }
    };

    Action<int> lockUnlock = interval =>
    {
        WriteableBitmap bitmap =
           new WriteableBitmap(100, 100, 96d, 96d, PixelFormats.Bgr32, null);

        int counter = 0;

        Action t1 = () =>
        {
            if (++counter % interval == 0)
            {
                bitmap.Lock();
                bitmap.Unlock();
            }
        };

        string title = string.Format("lock/unlock - Interval:{0} -", interval);

        test(title, () => TimeTest(t1));
    };

    lockUnlock(1);
    lockUnlock(10);
}

[SuppressMessage("Microsoft.Reliability",
    "CA2001:AvoidCallingProblematicMethods", MessageId = "System.GC.Collect")]
private static double TimeTest(Action action)
{
    const int Iterations = 100 * 1000;

    Action gc = () =>
    {
        GC.Collect();
        GC.WaitForFullGCComplete();
    };

    Action empty = () => { };

    Stopwatch stopwatch1 = Stopwatch.StartNew();

    for (int j = 0; j < Iterations; j++)
    {
        empty();
    }

    double loopElapsed = stopwatch1.Elapsed.TotalMilliseconds;

    gc();

    action(); //JIT
    action(); //Optimize

    Stopwatch stopwatch2 = Stopwatch.StartNew();

    for (int j = 0; j < Iterations; j++)
    {
        action();
    }

    gc();

    double testElapsed = stopwatch2.Elapsed.TotalMilliseconds;

    return (testElapsed - loopElapsed);
}

Полное раскрытие: я участвовал в проекте с открытым исходным кодом WriteableBitmapEx, однако это не моя библиотека и я не связан с ее владельцем

Чтобы добавить к отличному ответу по Tim Lloyd, я бы предложил посмотреть библиотеку WriteableBitmapEx. Это отличная библиотека WPF, Silverlight и Windows Phone, которая добавляет GDI-подобные методы расширения рисования (блики, линии, формы, преобразования, а также пакетные операции) к WriteableBitmap учебный класс.

Последняя версия WBEx содержит рефакторинг, который я выполнил, чтобы разрешить пакетные операции. WriteableBitmapEx библиотека теперь имеет метод расширения под названием GetBitmapContext(), чтобы вернуть IDisposable структура, которая обертывает один блок блокировки / разблокировки / аннулирования. С помощью следующего синтаксиса вы можете легко группировать вызовы для рисования и выполнять только одну блокировку / разблокировку / отмену в конце

// Constructor of BitmapContext locks the bmp and gets a pointer to bitmap
using (var bitmapContext = writeableBitmap.GetBitmapContext())
{
     // Perform multiple drawing calls (pseudocode)
     writebleBitmap.DrawLine(...)
     writebleBitmap.DrawRectangle(...) 
     // etc ...
} // On dispose of bitmapcontext, it unlocks and invalidates the bmp

WPF опирается на сохраненный движок композиции, который клевый, но, похоже, вам нужно больше после "простого" и необработанного отображения растрового изображения.

Я думаю, у вас есть хороший пример того, что вы хотите сделать здесь: https://web.archive.org/web/20140519134127/http://khason.net/blog/how-to-high-performance-graphics-in-wpf/

Если я правильно понял, у вас есть сценарий, в котором вы хотите получить данные от вашего датчика в течение нескольких секунд - и показать это. У вас есть требование в реальном времени - или вы храните данные со своей специальной "камеры" в виде изображения, а в режиме реального времени - только для шоу?

Если это так, вы могли бы подождать несколько секунд, а затем показать результат?

Похоже, WritableBitmap может быть способом решения вашей проблемы. Я бы предположил, что каждый раз, когда у вас есть блокировка / разблокировка, возникают накладные расходы, так как это связано с тем, что происходит, - поэтому я не думаю, что это хорошая идея для каждой точки. Чтобы рассчитать время, вы можете использовать профилировщик для тестового проекта / тестовых данных - dotTrace из jetbrains в порядке - я думаю, что у них есть пробная версия. Вы также можете использовать счетчик производительности - это может быть полезно и для других вещей.

Я хотел бы сделать его многопоточным и иметь высокоприоритетный поток для обработки входящих точек - или вы получаете прерывания от вашего устройства? Как я понимаю, более важно получить всю точку, чем нарисовать все точки сразу.

Вы пишете, что WritableBitmap едва ли достаточно быстр - поэтому в вашем текущем решении я бы попытался сохранить вызовы в AddDirtyRect, так что это происходит только при каждом n точках / миллисекундах - передача в передний буфер должна быть быстрой, даже если это большой блок. Вы должны быть в состоянии получить его так же быстро с wpf, как и с формами - это просто лучше.

С некоторым кодом и дополнительной информацией в вашей системе будет проще ответить:)

Другие вопросы по тегам