Custom instruments: когда signpost недостаточно
Instruments для Xcode компании Apple — это инструменты для анализа производительности iOS-приложения. Их используют для сбора и отображения данных, которые необходимы в отладке кода. В прошлом году Apple презентовала Custom Instruments. Это возможность расширить стандартный набор инструментов для профилирования приложений. Когда существующих инструментов недостаточно, вы сможете самостоятельно создать новые — они соберут, проанализируют и отобразят данные так, как вам потребуется.
Прошел год, а новых публичных инструментов и информации по их созданию в сети почти нет. Так что мы решили исправить ситуацию и поделиться тем, как создавали собственный Custom Instrument, который определяет причину слабой изоляции unit-тестов. Он базируется на технологии signpost (мы писали о ней в предыдущей статье) и позволяет быстро и точно определять место возникновения мигания теста.
Теоретический минимум
Чтобы создать новый инструмент для Xcode, потребуется понимание двух теоретических блоков. Тем, кто хочет разобраться самостоятельно, сразу дадим нужные ссылки:
Для остальных — ниже краткий конспект по необходимым темам.
Сперва выберите File → New → Project → категория macOS → Instruments package. Созданный проект включает в себя файл с расширением .instrpkg, в котором декларативно в формате xml объявлен новый инструмент. Ознакомимся с элементами разметки:
Что | Атрибуты | Описание |
Схемы данных |
interval-schema, point-schema и т.д. |
Описывает структуру данных в виде таблицы подобно sql-схемам. Схемы используются в других элементах разметки, чтобы определить тип данных на входе и выходе модели, например, при описании отображения (UI). |
Импорт схем данных |
import-schema |
Импорт готовых схем. Он позволяет использовать структуры данных, которые определены Apple. |
Модель инструмента |
modeler |
Связывает инструмент с файлом .clp, в котором определена логика инструмента, и объявляет ожидаемую схему данных на входе и выходе модели. |
Описание инструмента |
instrument |
Описывает модель данных и определяет, как события будут отображаться в UI. Модель данных описывается с помощью атрибутов create-table, create-parameter и тд. Графики инструмента определяются атрибутами graph, а таблица деталей — list, narrative и т.д. |
Если хотим дополнить логику нового инструмента, то создаем файл .clp с кодом на языке CLIPS. Базовые сущности языка:
- «Fact» — это некое событие, зарегистрированное в системе с помощью команды assert;
- «Rule» — это if-блок со специфичным синтаксисом, содержащий условие, при котором выполняется набор действий.
Какие правила и в какой последовательности будут активированы, определяется самим CLIPS на основе входящих фактов, приоритетов правил и механизма разрешения конфликтов.
Язык поддерживает создание типов данных на основе примитивов, использование арифметических, логических операций и функций. А также полноценное объектно-ориентированное программирование (ООП) с определением классов, посылкой сообщений, множественным наследованием.
Рассмотрим базовый синтаксис языка, который позволит создавать логику для кастомных инструментов.
1. Чтобы создать fact
, используем конструкцию assert
:
CLIPS> (assert (duck))
Таким образом, мы получим запись duck
в таблице фактов, которую можно посмотреть с помощью команды facts
:
CLIPS> (facts)
Для удаления факта используем команду retract
: (retract duck)
2. Чтобы создать rule
, используем конструкцию defrule
:
CLIPS> (defrule duck) — создание правила с названием duck
(animal-is duck) — если animal-is duck присутствует в таблице фактов
=>
(assert (sound-is quack))) — то создается новый факт sound-is quack
3. Для создания и использования переменных применяется следующий синтаксис (перед именем переменной идет обязательный знак »?»):
?
4. Можно создавать новые типы данных с помощью:
CLIPS>
(deftemplate prospect
(slot name (type STRING) (default ?DERIVE))
(slot assets (type SYMBOL) (default rich))
(slot age (type NUMBER) (default 80)))
Так, мы определили структуру с названием prospect и тремя атрибутами name, assets и age соответствующего типа и значением по умолчанию.
5. Арифметические и логические операции имеют префиксный синтаксис. То есть чтобы сложить 2 и 3, необходимо использовать следующую конструкцию:
CLIPS> (+ 2 3)
Либо чтобы сравнить две переменные x и y:
CLIPS> (> ?x ?y)
Практический пример
В своем проекте мы используем библиотеку OCMock для создания объектов-заглушек. Однако возникают ситуации, когда мок живет дольше теста, для которого создавался, и влияет на изоляцию других тестов. В итоге это приводит к «миганию» (нестабильности) unit-тестов. Чтобы отследить время жизни тестов и моков, создадим собственный инструмент. Ниже приведен алгоритм действий.
Шаг 1. Делаем разметку событий signpost
Для обнаружения проблемных моков нужны две категории интервальных событий — время создания и уничтожения мока, время старта и завершения теста. Чтобы получить эти события, переходим в библиотеку OCMock
и размечаем их с помощью signpost
в методах init
и stopMocking
класса OCClassMockObject
.
Далее переходим в исследуемый проект, делаем разметку в unit-тестах, методах setUp
и tearDown
:
Шаг 2. Создаем новый инструмент из шаблона Instrument Package
Сначала определяем тип данных на входе. Для этого в файле .instrpkg
импортируем схему signpost
. Теперь события, созданные signpost
, будут попадать в инструмент:
Далее определяем тип данных на выходе. В этом примере будем выводить одномоментные события. У каждого события будет время и описание. Для этого объявляем схему:
Шаг 3. Описываем логику инструмента
Создаем отдельный файл с расширением .clp
, в котором задаем правила с помощью языка CLIPS. Чтобы новый инструмент знал, в каком файле определена логика, добавляем блок modeler
:
В этом блоке с помощью атрибута production-system
указываем относительный путь к файлу с логикой. В атрибутах output
и required-input
определяем схемы данных на входе и выходе соответственно.
Шаг 4. Описываем специфику представления инструмента (UI)
В файле .instrpkg
остается описать сам инструмент, то есть отображение результатов. Создаем таблицу для данных в атрибуте create-table
, используя ранее объявленную схему detected-mocks-narrative
в атрибуте schema-ref
. И настраиваем тип вывода информации — narrative (описательный):
Шаг 5. Пишем код логики
Перейдем к файлу .clp
, в котором определена логика экспертной системы. Логика будет следующая: если время старта теста пересекается с интервалом жизни мока, то считаем, что этот мок «пришел» из другого теста — что нарушает изоляцию текущего unit-теста. Для того, чтобы в итоге создать событие с интересующей информацией, нужно проделать следующие шаги:
1. Определяем структуры mock и unitTest с полями — время события, идентификатор события, название теста и класс мока.
2. Определяем правила, которые создадут факты с типами mock
и unitTest
на основе входящих событий signpost
:
Читать эти правила можно следующим образом: если на входе мы получаем факт типа os- signpost с искомыми subsystem
, category
, name
и event-type
, то создаем новый факт с типом, что был определен выше (unitTest или mock), и наполняем значениями. Здесь важно помнить — CLIPS это регистрозависимый язык и значения subsystem, category, name и event- type должны совпадать с тем, что использовалось в коде исследуемого проекта.
Значения переменных от событий signpost передаются следующим образом:
3. Определяем правила, которые освобождают завершенные события (являются лишними, так как не влияют на результат).
Шаг 6. Определяем правило, которое будет генерировать результаты
Прочитать правило можно так.
Если
1) существует unitTest и mock;
2) при этом начало теста наступает позже существующего мока;
3) существует таблица для хранения результатов со схемой detected-mocks-narrative;
то
4) создаем новую запись;
5) заполняем временем;
6)… и описанием.
В результате видим следующую картину при использовании нового инструмента:
Исходный код custom instrument и пример проекта для использования инструмента можно посмотреть на GitHub.
Отладка инструментов
Для отладки кастомных инструментов используется debugger.
Он позволяет
1. Увидеть компилируемый код на основе описания в instrpkg.
2. Увидеть подробную информацию о том, что происходит с инструментом во время выполнения.
3. Вывести полный список и описание системных схем данных, которые можно использовать в качестве входных данных в новых инструментах.
4. Выполнить произвольные команды в консоли. Например, вывести список правил командой «list-defrules» или фактов командой «facts»
Настройка на CI сервере
Можно запускать инструменты из командной строки — профилировать приложение во время выполнения unit- или UI-тестов на CI-сервере. Это позволит, к примеру, ловить memory leak как можно раньше. Для профилирования тестов в pipeline используем следующие команды:
1. Запуск инструментов с атрибутами:
xcrun instruments -t -l -w
- где
template_name
— путь до шаблона с инструментами или название шаблона. Можно получить командойxcrun instruments -s
; average_duration_ms
— время записи в миллисекундах, должно быть больше или равно времени выполнения тестов;device_udid
— идентификатор симулятора. Можно получить командой xcrun instruments -s. Должен совпадать с идентификатором симулятора, на котором будут выполняться тесты.
2. Запуск тестов на этом же симуляторе командой:
xcodebuild -workspace -scheme -destination
test-without-building
- где
path_to_workspace
— путь к рабочему пространству Xcode; scheme_with_tests
— схема с тестами;device
— идентификатор симулятора.
В результате в рабочей директории будет создан отчет с расширением .trace, который можно открыть приложением Instruments или нажав правой кнопкой по файлу и выбрав Show Package Contents.
Выводы
Мы рассмотрели пример модернизации signpost до полноценного инструмента и рассказали, как автоматически применять его на «прогонах» CI-сервера, использовать в решении проблемы «мигающих» (нестабильных) тестов.
По мере погружения в возможности custom instruments вы будете лучше понимать, в каких еще случаях можно применять инструменты. Например, нам они также помогают разобраться в проблемах многопоточности — где и когда использовать потокобезопасный доступ к данным.
Создать новый инструмент оказалось достаточно просто. Но главное — потратив несколько дней на изучение механики и документации для его создания сегодня, вам удастся избежать нескольких бессонных ночей в попытках исправить баги.
Источники
Статью писали вместе с @regno — Антоном Власовым, iOS-разработчиком.