В предыдущей статье мы узнали о причинах нестабильности unit-тестов и способах борьбы с этим. Теперь мы хотим рассмотреть один из новых инструментов Apple для отладки и профилирования кода. Речь о представленном на WWDC 2018 фреймворке для логирования os_log, который был расширен инструментом для анализа производительности — os_signpost.



В одном из спринтов нам поставили задачу реализовать генерацию pdf-документа на client-side. Мы выполнили задачу и успешно продемонстрировали результаты команде. Но нам захотелось убедиться в эффективности технических нюансов принятого решения. В этом нам помог signpost. С ним нам удалось ускорить отображение документа в несколько раз.

Чтобы узнать побольше о технологии применения os_signpost, посмотреть, где он может вам помочь и как уже помог нам, заходите под кат.

Глубже в проблематику


На телефоне пользователя есть много приложений, и все они используют общие ресурсы системы: CPU, RAM, network, battery и т.д. Если ваше приложение выполняет свои задачи и не падает, это еще не значит, что оно работает эффективно и корректно. Ниже опишем кейсы, с которыми потенциально вы можете столкнуться.

Неоптимальный алгоритм может привести к длительной загрузке CPU.

  • На старте приложения, после 20 секунд ожидания, система завершит работу приложения, а пользователь не увидит даже первый экран. При этом система выставит crash report, отличительной особенностью которого будет exception type — EXC_CRASH (SIGKILL) с типом 0x8badf00d.
  • Ресурсоемкие процессы в background-потоке могут отразиться на отзывчивости UI, повысить потребление батареи и принудительно завершить приложение системой (в случае длительного перегрева CPU).

RAM-кейсы:

В спецификациях к телефонам на сайте Apple не указана информация по RAM, но в других источниках приводится следующее распределение памяти по моделям телефонов:
Type
4S
5
5C
5S
6
6P
6S
6SP
RAM, ГБ
0.5
1
1
1
1
1
2
2

Type
SE
X
7
7P
8
8P
XS
XSM
XR
RAM, ГБ
2
3
2
3
2
3
4
4
3

Когда свободной оперативки остается слишком мало, iOS начинает искать память для освобождения, параллельно рассылая memory warning во все запущенные приложения. Этот процесс неявно влияет на CPU и батарею устройства. Если memory warning игнорируется, а выделение памяти продолжается, то система принудительно завершает процесс приложения. Для пользователя это выглядит как crash, при этом в crash report будут отсутствовать backtraces.

Чрезмерное использование сетевых запросов. Это также приводит к уменьшению времени жизни батареи. Дублирование запросов и/или отсутствие отмены лишних запросов дополнительно приводит к неэффективному использованию CPU.

Не стоит забывать и про CoreLocation. Чем чаще и точнее мы запрашиваем местоположение пользователя, тем больше тратится батарея девайса. Чтобы проверить корректность обработки описанных случаев, мы предлагаем воспользоваться os_signpost для профилирования процессов приложения и последующего анализа полученных данных.

Интеграция инструмента в проект


На верхнем уровне процесс формирования PDF состоит из трёх шагов:

  1. получение данных по сети;
  2. формирование документа;
  3. отображение на экране — мы решили раздробить и логировать этапы генерации документа, начиная с нажатия пользователем кнопки «Сформировать» и заканчивая отображением документа на экране.

Допустим, перед нами стоит задача проанализировать асинхронный сетевой запрос. Разметка в коде будет выглядеть следующим образом:

import os.signpost

let pointsOfInterestLog = OSLog(subsystem: "com.example.your-app", category: . pointsOfInterest)
let networkLog = OSLog(subsystem: "com.example.your-app", category: "NetworkOperations")

os_signpost(.event, log: pointsOfInterestLog, name: "Start work")
os_signpost(.begin, log: networkLog, name: "Overall work")

for element in elements {
    os_signpost(.begin, log: networkLog, name: "Element work")
    makeWork(for: element)
    os_signpost(.end, log: networkLog, name: "Element work")
}

os_signpost(.end, log: networkLog, name: "Overall work")

Шаги для использования signpost здесь следующие:

  • Импортировать фреймворк os.signpost.
  • Создать экземпляр OSLog. Стоит учитывать, что есть несколько типов событий: для интервальных событий (например, сетевой запрос) можно использовать произвольную категорию, а для одномоментных событий (например, нажатие по кнопке) предопределенную категорию pointsOfInterest / OS_LOG_CATEGORY_POINTS_OF_INTEREST.
  • Для интервальных событий вызвать функцию os_signpost c типом .begin и .end в начале и в конце исследуемого этапа. Для одномоментных событий использовать тип .event.
  • Если исследуемый код может выполняться асинхронно, то добавить Signpost ID, что позволит разделить интервалы однотипных операций с разными объектами.
  • Опционально можно добавить дополнительные данные (metadata) к отправляемым событиям. Например, размер загруженных по сети изображений или номер генерируемой PDF-страницы. Такая информация поможет понять, что конкретно происходит в исследуемой стадии исполнения кода.

Аналогично на obj-c:

@import os.signpost;

os_log_t pointsOfInterestLog = os_log_create("com.example.your-app",   OS_LOG_CATEGORY_POINTS_OF_INTEREST);
os_log_t networkLog = os_log_create("com.example.your-app",   "NetworkOperations");
os_signpost_id_t operationIdentifier = os_signpost_id_generate(networkLog);

os_signpost_event_emit(pointsOfInterestLog, operationIdentifier, "Start work");
os_signpost_interval_begin(networkLog, operationIdentifier, "Overall work");

for element in elements {	
    os_signpost_id_t elementIdentifier = os_signpost_id_make_with_pointer(networkLog, element);
    os_signpost_interval_begin(networkLog, elementIdentifier, "Element work");
    [element makeWork];
    os_signpost_interval_end(networkLog, elementIdentifier, "Element work");
}

os_signpost_interval_end(networkLog, operationIdentifier, "Overall work");

На заметку. Если проект должен запускаться на iOS до 12.0 версии, Xcode предложит обернуть вызовы os_signpost в конструкции if #available. Чтобы не загромождать код, можно вынести эту логику в отдельный класс.

Стоит учесть, что os_signpost требует статический строковый литерал в качестве параметра имени события. Чтобы добавить более строгую типизацию, можно создать enum с типами событий, а в реализации класса сопоставлять их в строковые литералы. Вынесение OSLog в отдельный класс позволит добавить логику его отключения для релизной схемы (для этого есть отдельная команда OSLog).

import os.signpost

let networkLog: OSLog

if ProcessInfo.processInfo.environment.keys.contains("SIGNPOSTS_FOR_NETWORK") {
    networkLog = OSLog(subsystem: "com.example.your-app", category: "NetworkOperations"
} else {	
    networkLog = .disabled
}

К маркировке событий можно добавлять значения из любых свойств со следующими type decoders для удобного форматирования:

Value type
Custom specifier
Example output
time_t
%{time_t}d
2016-01-12 19:41:37
timeval
%{timeval}.*P
2016-01-12 19:41:37.774236
timespec
%{timespec}.*P
2016-01-12 19:41:37.2382382823
errno
%{errno}d
Broken pipe
iec-bytes
%{iec-bytes}d
2.64 MiB
bitrate
%{bitrate}d
123 kbps
iec-bitrate
%{iec-bitrate}d
118 Kibps
uuid_t
%{uuid_t}.*16P
%{uuid_t}.*P
10742E39-0657-41F8-AB99-878C5EC2DCAA

Теперь при профилировании приложения события от os_signpost будут отправляться в Instruments в виде табличных данных. Для того чтобы перейти в инструменты, воспользуйтесь сочетанием клавиш Cmd+I, затем выберите необходимый для профилирования инструмент. Чтобы увидеть размеченные данные, достаточно включить инструменты os_signpost и Point of Interest в правой части интерфейса инструмента.



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

Примеры использования


Кейс №1. PDFKit vs WKWebView


За счет использования os_signpost мы увидели, что для небольших документов (на пару страниц) самым длительным этапом стал последний шаг — отображение документа — а не работа с сетью или графикой. Это привело нас к решению заменить WKWebView на PDFView, что ускорило отображение документа с 1,5 секунды до 30 миллисекунд. На графиках это выглядит следующим образом:


Отображение PDF документа (WKWebView) в Time Profiler


Отображение PDF документа (PDFView) в Time Profiler

Полученные данные можно имплементировать в другие инструменты, которые предоставляет XCode. Как показал инструмент Allocations, выигрыш в скорости загрузки был достигнут за счет увеличения использования RAM.

Кейс №2. LowMemory Warning


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

Как известно, при использовании NSOperationQueue метод cancelAllOperation освобождает существующую очередь, но не ос��анавливает уже запущенные операции. Отсюда делаем вывод, что в реализации операции нужно периодически определять её состояние и прекращать работу. Тем самым освобождая ресурсы, если ей выставляется статус Cancelled.

На следующем шаге перед нами асинхронная операция, которую надо проверять на отмену. Но при этом нет ясности, с какой частотой делать данную проверку. У нас было два варианта — построковая проверка и постраничная. os_signpost помог и тут. Как оказалось, добавив проверку на отмену в построчный цикл отрисовки таблицы в документе, мы увеличили время формирования документа (на 150 страниц) в 2 раза. Второй вариант оказался более оптимальным по показателям и фактически не увеличил время создания документа. В итоге при получении события memory warning мы отменяем операцию программно и выводим пользователю экран ошибки.

Чтобы убедиться, что память действительно освобождается, мы можем также использовать os_signpost. В этот раз за счет добавления маркера о начале события в методе didRecieveMemoryWarning и маркера об окончании во viewDidLoad экрана ошибки. Кстати, эмулировать событие недостаточной памяти можно в симуляторе (shift+command+m).

Кейс №3. Update Constraints


Signpost может оказаться полезным в процессе верстки. Для создания констрейнтов мы используем фреймворк masonry. В документации к фреймворку сказано, что для создания констрейтов рекомендуется использовать метод updateConstraints(). Но Apple категорически не рекомендует так делать, и в этом можно убедиться с помощью разметки signpost.



Согласно документации Apple, updateConstraints стоит использовать только для изменения constraints, если мы не можем сделать это в месте возникновения изменения.



Проанализировав полученные результаты, мы сделали вывод, что в нашем приложении вызов updateConstraints происходит не так часто — примерно при каждом появлении view на экране.
Несмотря на это, во избежание потенциальных дефектов с performance, мы советуем следовать советам компании Apple по данному вопросу.

Выводы


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

В следующей статье мы рассмотрим, как можно использовать полученную от signpost информацию более эффективно за счет написания собственной экспертной системы (Custom Instruments).



Полезные ссылки


Статью писали вместе с @victoriaqb — Викторией Кашлиной, iOS-разработчиком.