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

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

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

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

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

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

Ответ 1

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

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

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

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

С самого начала казалось очевидным, что инструментом выбора здесь будет Dtrace. Я начал с использования наблюдателя CFRunLoop, который запускал на kCFRunLoopAfterWaiting и kCFRunLoopBeforeWaiting. Вызов моего обработчика kCFRunLoopBeforeWaiting указывает на начало "законного времени простоя", а обработчик kCFRunLoopAfterWaiting будет сигналом для меня, что законное ожидание закончилось. Я бы использовал провайдер Pid 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 также был испорчен тем, что он требовал внесения изменений в код, который не имеет моего окончательного решения.)

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

Как описано выше, сортировка законного простоя с плохого простоя не была простой, поскольку оба случая заканчиваются на 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 pid, по-видимому, не может повсеместно исследовать как запись, так и возврат всех произвольных символов. В частности, я не мог получить пробники на pid000:*:__CFRunLoopServiceMachPort:return или на pid000:*:_DPSNextEvent:return для работы. Детали не важны, но, наблюдая за различными другими событиями и отслеживая определенное состояние, я смог установить (опять же, казалось бы, надежно), когда я был введен и оставил законное состояние бездействия.

Затем мне пришлось определить зонды, чтобы сообщить разницу между RUNNING и BLOCKED. Это было немного легче. В конце концов, я решил рассмотреть системные вызовы BSD (используя Sansall-зонд Dtrace) и вызывает mach_msg_trap() (используя pid-зонд), не возникающий в периоды законного простоя BLOCKED. (Я посмотрел на Dtrace mach_trap, но он, похоже, не делал то, что я хотел, поэтому я вернулся к использованию pid-зонда.)

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

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

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

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

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

Ответ 2

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