Быстрое рисование множества прямоугольников по одному в 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, как и с формами - это просто лучше.
С некоторым кодом и дополнительной информацией в вашей системе будет проще ответить:)