Как я могу получить точки останова / журналы / улучшенную видимость, когда блокирует основной поток? - PullRequest
10 голосов
/ 17 декабря 2011

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

Я ищу какой-то "режим отладки" или дополнительный код, или ловушку, или что-то еще, чтобы я мог установить точку останова / log / что-то, что получит удар и позволит мне проверить, что происходит, если мой основной поток «добровольно» блокирует ввод / вывод (или любую другую причину, на самом деле), кроме как для простоя в конце цикла выполнения.

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

Я понимаю, что есть операции, выполняемые UIKit / AppKit, которые предназначены только для основного потока, которые будут вызывать ввод-вывод и блокировать основной поток, так что, в определенной степени, это безнадежно (например, доступ к монтажному щиту представляется потенциально блокирующей операцией только для основного потока), но что-то будет лучше, чем ничего.

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

Ответы [ 2 ]

11 голосов
/ 19 декабря 2011

Итак, я решил ответить на свой вопрос в эти выходные. Напомним, что это усилие превратилось во что-то довольно сложное, поэтому, как предложил Кендалл Хельмштеттер Глен, большинству людей, читающих этот вопрос, вероятно, следует просто разбираться с инструментами. Читайте дальше о мазохистах в толпе!

Начать проще всего с устранения проблемы. Вот что я придумал:

Я хочу получать уведомления о длительных периодах времени, проведенных в syscalls / mach_msg_trap, которые не являются законными простоя. «Законные время простоя »определяется как время, проведенное в mach_msg_trap в ожидании следующее событие из ОС.

Также важно, что меня не заботил код пользователя, который занимает много времени. Эту проблему довольно легко диагностировать и понять с помощью инструмента Time Profiler от Instruments. Я хотел знать конкретно о заблокированном времени. Хотя верно и то, что вы также можете диагностировать заблокированное время с помощью Time Profiler, я обнаружил, что использовать его для этой цели значительно сложнее. Аналогично, инструмент System Trace также полезен для подобных исследований, но он чрезвычайно мелкозернистый и сложный. Я хотел что-то попроще - более нацеленное на эту конкретную задачу.

С самого начала казалось очевидным, что предпочтительным инструментом здесь будет Dtrace. Я начал с использования наблюдателя CFRunLoop, который выстрелил по kCFRunLoopAfterWaiting и kCFRunLoopBeforeWaiting. Вызов моему обработчику kCFRunLoopBeforeWaiting будет указывать на начало "законного простоя", а обработчик kCFRunLoopAfterWaiting будет сигналом для меня, что законное ожидание закончилось. Я бы использовал провайдера ptra Dtrace для перехвата вызовов этих функций в качестве способа сортировки допустимого простоя от блокирования простоя.

Этот подход заставил меня начать, но в итоге оказался ошибочным. Самая большая проблема заключается в том, что многие операции AppKit являются синхронными , поскольку они блокируют обработку событий в пользовательском интерфейсе, но фактически вращают RunLoop ниже в стеке вызовов. Эти спины RunLoop не являются «законными» простоями (для моих целей), потому что пользователь не может взаимодействовать с пользовательским интерфейсом в течение этого времени. Они ценны, чтобы быть уверенным - представьте runloop в фоновом потоке, который наблюдает за кучей операций ввода-вывода, ориентированных на RunLoop, - но пользовательский интерфейс все еще блокируется, когда это происходит в основном потоке. Например, я поместил следующий код в IBAction и вызвал его с помощью кнопки:

NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL: [NSURL URLWithString: @"http://www.google.com/"] 
                                                   cachePolicy: NSURLRequestReloadIgnoringCacheData
                                               timeoutInterval: 60.0];    
NSURLResponse* response = nil;
NSError* err = nil;
[NSURLConnection sendSynchronousRequest: req returningResponse: &response error: &err];

Этот код не препятствует запуску RunLoop - AppKit раскручивает его для вас во время вызова sendSynchronousRequest:... - но он не позволяет пользователю взаимодействовать с пользовательским интерфейсом до его возврата. На мой взгляд, это не «законный холостой ход», поэтому мне нужен был способ выяснить, какие именно были. (Подход CFRunLoopObserver был также несовершенен, так как требовал внесения изменений в код, чего нет в моем окончательном решении.)

Я решил, что я буду моделировать свой UI / основной поток как конечный автомат. Он всегда находился в одном из трех состояний: LEGIT_IDLE, RUNNING или BLOCKED и переходил назад и вперед между этими состояниями в процессе выполнения программы. Мне нужно было найти датчики Dtrace, которые позволили бы мне уловить (и, следовательно, измерить) эти переходы. Конечный конечный автомат, который я реализовал, был немного сложнее, чем просто эти три состояния, но это вид с высоты 20 000 футов.

Как описано выше, сортировка законного простоя от плохого простоя была непростой, поскольку оба случая заканчиваются на mach_msg_trap() и __CFRunLoopRun. Я не мог найти один простой артефакт в стеке вызовов, который я мог бы использовать, чтобы надежно определить разницу; Похоже, что простое исследование одной функции мне не поможет. В итоге я использовал отладчик для просмотра состояния стека в различных случаях законного простоя или плохого простоя. Я решил, что во время законного простоя я бы (на первый взгляд надежно) увидел стек вызовов следующим образом:

#0  in mach_msg
#1  in __CFRunLoopServiceMachPort
#2  in __CFRunLoopRun
#3  in CFRunLoopRunSpecific
#4  in RunCurrentEventLoopInMode
#5  in ReceiveNextEventCommon
#6  in BlockUntilNextEventMatchingListInMode
#7  in _DPSNextEvent
#8  in -[NSApplication nextEventMatchingMask:untilDate:inMode:dequeue:]
#9  in -[NSApplication run]
#10 in NSApplicationMain
#11 in main

Поэтому я постарался установить группу вложенных / цепочечных зондов, которые установят, когда я достиг этого состояния, а затем покинул его. К сожалению, по какой-либо причине pid-провайдер Dtrace, похоже, не может универсально проверять как вход, так и возврат всех произвольных символов. В частности, я не смог заставить работать зонды на pid000:*:__CFRunLoopServiceMachPort:return или pid000:*:_DPSNextEvent:return. Детали не важны, но, наблюдая за другими событиями и отслеживая определенное состояние, я смог установить (опять же, на первый взгляд, надежно), когда я вошел и вышел из законного незанятого состояния.

Затем я должен был определить зонды для определения разницы между RUNNING и BLOCKED. Это было немного проще. В конце концов, я решил рассмотреть системные вызовы BSD (с использованием проверки системного вызова Dtrace) и вызовы mach_msg_trap() (с использованием проверки pid), которые не происходят в периоды законного простоя, чтобы они были БЛОКИРОВАНЫ. (Я посмотрел на датчик Dtrace mach_trap, но, похоже, он не делал то, что хотел, поэтому я вернулся к использованию датчика pid.)

Изначально я проделал дополнительную работу с провайдером Dtrace Sched, чтобы измерить реальное заблокированное время (т. Е. Время, когда мой поток был приостановлен планировщиком), но это добавило значительную сложность, и я в итоге подумал про себя: «Если я в ядре, какое мне дело, спит ли поток на самом деле или нет? Для пользователя все равно: он заблокирован». Таким образом, последний подход просто измеряет все время в (syscalls || mach_msg_trap()) && !legit_idle и называет это заблокированным временем.

На этом этапе перехват одноядерных вызовов большой длительности (например, вызова sleep(5)) представляется тривиальным. Однако чаще всего задержка потока пользовательского интерфейса возникает из-за множества небольших задержек, накапливающихся в ядре при нескольких вызовах (вспомним сотни вызовов read () или select ()), поэтому я подумал, что было бы также желательно сбросить стек вызовов SOME, когда общее количество системных вызовов или mach_msg_trap времени за один проход цикла событий превысило определенный порог. Я закончил настройку различных таймеров и регистрировал накопленное время, проведенное в каждом состоянии, ограничиваясь различными состояниями конечного автомата, и выдавая оповещения, когда нам приходилось выходить из состояния BLOCKED, и превысил некоторый порог. Этот метод, очевидно, будет генерировать данные, которые могут быть неверно истолкованы или могут представлять собой общую красную сельдь (т. Е. Какой-то случайный, относительно быстрый системный вызов, который просто приводит нас к порогу оповещения), но я чувствую, что это лучше, чем ничего.

В конце сценарий Dtrace заканчивает тем, что сохраняет конечный автомат в D-переменных, и использует описанные пробники для отслеживания переходов между состояниями и дает мне возможность что-то делать (например, предупреждения печати), когда конечный автомат находится в процессе перехода состояние, основанное на определенных условиях. Я немного поиграл с надуманным примером приложения, которое выполняет кучу дискового ввода-вывода, сетевого ввода-вывода и вызовов sleep (), и смог перехватить все три из этих случаев, не отвлекаясь от данных, относящихся к законным ожиданиям. , Это было именно то, что я искал.

Это решение, очевидно, довольно хрупкое и совершенно ужасное почти во всех отношениях. :) Это может быть или не быть полезным для меня или кого-либо еще, но это было забавное упражнение, поэтому я решил поделиться историей и полученным сценарием Dtrace. Может быть, кто-то найдет это полезным. Я также должен признаться, что был относительным n00b по отношению к написанию сценариев Dtrace, так что я уверен, что сделал миллион вещей неправильно. Наслаждайтесь!

Он был слишком большим для поста в строке, поэтому любезно размещается здесь @Catfish_Man: MainThreadBlocking.d

1 голос
/ 17 декабря 2011

Действительно, это такая работа для инструмента Time Profiler.Я полагаю, что вы можете видеть, сколько времени тратится на код в потоке, поэтому вы можете посмотреть, какой код занимал некоторое время для выполнения, и получить ответ о том, что потенциально блокировало пользовательский интерфейс.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...