Итак, я решил ответить на свой вопрос в эти выходные. Напомним, что это усилие превратилось во что-то довольно сложное, поэтому, как предложил Кендалл Хельмштеттер Глен, большинству людей, читающих этот вопрос, вероятно, следует просто разбираться с инструментами. Читайте дальше о мазохистах в толпе!
Начать проще всего с устранения проблемы. Вот что я придумал:
Я хочу получать уведомления о длительных периодах времени, проведенных в
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