Pull to refresh

Comments 29

Интересный вариант. Теперь хорошо бы придумать автоматический способ обнаруживать такой блокирующий код, чтобы можно было его зарефакторить.
Можно попробовать Debugger.Launch, но вопрос что будет в стеке что делать дальше.
Наверное, задачу останова будет попроще решить.
Ну что вы. Я же не прошу точно указать причину ошибки. Достаточно было бы обнаружить факт блокировки и получить callstack этого потока, чтобы отправить его в качестве багрепорта дальше уже в ручном режиме разобраться. Без этого приведенный в статье способ выглядит как заглушка — «мы знаем что проблема может быть, но локализовать и исправить не можем, так что пока хоть так».
Буквально только что решал проблему блокировки UI, которая была из-за проброса Exception в UI thread. И там нет пользовательского стектрейса.
Отличный пост, приму на заметку.
Небольшой комментарий не въедливости ради, а для того, чтобы у тех, кто будет использовать этот код, не произошёл «нежданчик»:
Скрытый текст
Эта строка:
Dispatcher.CurrentDispatcher.Invoke(() => UIBlocked());

и вторая для UIReleased — нет ни проверки на null, ни первичного переноса значения делегата в отдельную переменную (что настоятельно рекомендуется, тем более, что в этом месте гарантирована многопоточность).

Ну ещё можно посоветовать заменить
_lastForegroundTimerTickTime = DateTime.Now;
на
_stopwatch = Stopwatch.StartNew();

и
var totalMilliseconds = (DateTime.Now - _lastForegroundTimerTickTime).TotalMilliseconds;
на
var totalMilliseconds = _stopwatch.ElapsedMilliseconds;
Всё-таки мерить время через DateTime.Now как-то не очень прилично
Так это бенчмарки меряются Stopwatch'ем.
Не вижу проблемы измерять сотни миллисекунд через DateTime.
Слепые тоже не видят. Но обычно на форумах этим не гордятся.
Погрешность может быть очень большой. На моей памяти правда погрешность была в пределах 16мс, но я не писал под 95 винду. Там это было 55мс.

И самое главное «как долго заняла операция» и «сколько сейчас времени» — это разные задачи. Средства очевидно тоже разные.
Ну, очевидно, погрешность в 16мс поломает всю задачу определения, повис ли UI-поток.
Осталось выставить CPU Affinity, чтобы поток не скакал между ядрами, и прогреть кэш.
То есть человек меняющий константу FreezeTimeLimit должен помнить что если он ее уменьшит до некого N при котором погрешность будет составлять критические для тов. withkittens скажем 10% — ему нужно будет заменить семантически менее корректное решение на семантически более корректное, потому что когда писали изначально — погрешность всех устраивала. Так?
Отвечу сразу всем: спасибо за CodeReview, с большей частью комментариев полностью согласен. Узнал пару новых моментов.
А если пользователь изменит системное время?
У вас может перевод часов на зимнее время и обратно случиться. Или сработать синхронизация времени по NTP. Или пользователь что-то поменяет. DateTime.Now подходит для ответа на вопрос, какое сейчас системное время, но не подходит для ответа на вопрос, сколько прошло времени с заданного момента в прошлом. Для этого используется монотонный счётчик времени, до которого на винде можно достучаться через GetTickCount64 и QueryPerformanceCounter. Второй используется в Stopwatch и использует HPET, но затратнее. Первый даёт меньшую точность, менее затратен, но нужно делать P/Invoke.

См.
github.com/akkadotnet/akka.net/issues/846
github.com/akkadotnet/akka.net/tree/dev/src/core/Akka/Util/MonotonicClock.cs
Вот.
Спасибо за адекватный и исчерпывающий ответ.
Теперь я могу признать, что был не прав.
Dispatcher.CurrentDispatcher.Invoke(() => UIBlocked());

Я правильно понимаю, что BackgroundTimerTick вызывается на потоке ThreadPool-а, и вызов в нём Dispatcher.CurrentDispatcher порождает новый диспетчер, связанный с этим потоком? На мой взгляд, это довольно странно. Задачи, выполняемые на потоках пула, не должны влиять на состояние потоков, в которых они запускаются.
Я бы заранее создал отдельный UI-поток для таких случаев и передавал его в конструктор BlockDetector-а. Но вообще, прочитав Вашу статью, мне сначала показалось, что автор из моей команды — уж больно похоже на то, что мы сделали у себя)
Постоянно дергать DateTime.Now плохая идея. Это крайне медленная функция. Для таких задач лучше подходит DateTime.UtcNow
Кроме того, использование DateTime.Now может привести к неожиданным результатам при переходе между зимним и летним временами или при NTP-синхронизации системного времени.
Могу рассказать о настоящей серебряной пуле для детекта UI Freeze: есть в Windows Vista+ механизм ETW(Event Tracing for Windows), и готовый провайдер, который умеет кидать сообщения и коллстек, когда какое-нибудь приложение(не обязательно WPF и .NET) в системе не опрашивает очередь сообщений более 200ms. Не нужно лезть в код и инструментировать, создавать два диспетчера. Всё работает в режиме Attach.

С помощью этого механизма dotTrace в режиме Timeline показывает вам те самые UI Freeze на графике и можно поизучать хотспоты на этих участках.
Напишите пжл статью, очень интересно.
Обнаружить, что UI «отвис» можно подпиской на событе Dispatcher.Hooks.DispatcherInactive

Думаю обнаружить начало выполнения операций диспетчером можно с использованием других событий Dispatcher.Hooks.
WPF разрешает создавать несколько UI тредов. Делается это так:

Указанным способом создается несколько блокированных инстансов потока, которые работают синхронно.

Если кому-то понадобится создать несколько реальных UI потоков WPF, то можно подглядеть решение на MSDN.

Я использовал такое решение для создания на WPF индикатора длительных операций, способный крутить анимацию даже при замораживании основного потока UI. ОСТОРОЖНО!!! Корректная реализация IDisposable требуется.
Нет, указанный автором способ создает реальные потоки, ограничение тут в другом. Если делать как автор — то можно создать в отдельном потоке только отдельное окно. По вашей же ссылке предлагается создать контрол, лежащий где-то в дереве — но при этом работающий в своем потоке.
Тогда непонятно — зачем?
Инстансы окон и так, вроде бы, не должны блокировать друг друга.
Экземпляры окон UI по умолчанию работают в одном треде.
Скачайте демо-проект и поиграйтесь с исходниками.
Как дойдут руки до описанной проблемы, попробую предложенное решение скрестить с Dispatcher.Hooks. Есть ощущение, что «веселее вместе».
Sign up to leave a comment.

Articles