Instruments для Xcode компании Apple — это инструменты для анализа производительности iOS-приложения. Их используют для сбора и отображения данных, которые необходимы в отладке кода. В прошлом году Apple презентовала Custom Instruments. Это возможность расширить стандартный набор инструментов для профилирования приложений. Когда существующих инструментов недостаточно, вы сможете самостоятельно создать новые — они соберут, проанализируют и отобразят данные так, как вам потребуется.
Прошел год, а новых публичных инструментов и информации по их созданию в сети почти нет. Так что мы решили исправить ситуацию и поделиться тем, как создавали собственный Custom Instrument, который определяет причину слабой изоляции unit-тестов. Он базируется на технологии signpost (мы писали о ней в предыдущей статье) и позволяет быстро и точно определять место возникновения мигания теста.
Чтобы создать новый инструмент для Xcode, потребуется понимание двух теоретических блоков. Тем, кто хочет разобраться самостоятельно, сразу дадим нужные ссылки:
Для остальных — ниже краткий конспект по необходимым темам.
Сперва выберите File -> New -> Project -> категория macOS -> Instruments package. Созданный проект включает в себя файл с расширением .instrpkg, в котором декларативно в формате xml объявлен новый инструмент. Ознакомимся с элементами разметки:
Если хотим дополнить логику нового инструмента, то создаем файл .clp с кодом на языке CLIPS. Базовые сущности языка:
Какие правила и в какой последовательности будут активированы, определяется самим CLIPS на основе входящих фактов, приоритетов правил и механизма разрешения конфликтов.
Язык поддерживает создание типов данных на основе примитивов, использование арифметических, логических операций и функций. А также полноценное объектно-ориентированное программирование (ООП) с определением классов, посылкой сообщений, множественным наследованием.
Рассмотрим базовый синтаксис языка, который позволит создавать логику для кастомных инструментов.
1. Чтобы создать
Таким образом, мы получим запись
Для удаления факта используем команду
2. Чтобы создать
3. Для создания и использования переменных применяется следующий синтаксис (перед именем переменной идет обязательный знак "?"):
4. Можно создавать новые типы данных с помощью:
Так, мы определили структуру с названием prospect и тремя атрибутами name, assets и age соответствующего типа и значением по умолчанию.
5. Арифметические и логические операции имеют префиксный синтаксис. То есть чтобы сложить 2 и 3, необходимо использовать следующую конструкцию:
Либо чтобы сравнить две переменные x и y:
В своем проекте мы используем библиотеку OCMock для создания объектов-заглушек. Однако возникают ситуации, когда мок живет дольше теста, для которого создавался, и влияет на изоляцию других тестов. В итоге это приводит к «миганию» (нестабильности) unit-тестов. Чтобы отследить время жизни тестов и моков, создадим собственный инструмент. Ниже приведен алгоритм действий.
Для обнаружения проблемных моков нужны две категории интервальных событий — время создания и уничтожения мока, время старта и завершения теста. Чтобы получить эти события, переходим в библиотеку
Далее переходим в исследуемый проект, делаем разметку в unit-тестах, методах
Сначала определяем тип данных на входе. Для этого в файле
Далее определяем тип данных на выходе. В этом примере будем выводить одномоментные события. У каждого события будет время и описание. Для этого объявляем схему:
Создаем отдельный файл с расширением
В этом блоке с помощью атрибута
В файле
Перейдем к файлу
1. Определяем структуры mock и unitTest с полями — время события, идентификатор события, название теста и класс мока.
2. Определяем правила, которые создадут факты с типами
Читать эти правила можно следующим образом: если на входе мы получаем факт типа os- signpost с искомыми
Значения переменных от событий signpost передаются следующим образом:
3. Определяем правила, которые освобождают завершенные события (являются лишними, так как не влияют на результат).
Прочитать правило можно так.
Если
1) существует unitTest и mock;
2) при этом начало теста наступает позже существующего мока;
3) существует таблица для хранения результатов со схемой detected-mocks-narrative;
то
4) создаем новую запись;
5) заполняем временем;
6)… и описанием.
В результате видим следующую картину при использовании нового инструмента:
Исходный код custom instrument и пример проекта для использования инструмента можно посмотреть на GitHub.
Для отладки кастомных инструментов используется debugger.
Он позволяет
1. Увидеть компилируемый код на основе описания в instrpkg.
2. Увидеть подробную информацию о том, что происходит с инструментом во время выполнения.
3. Вывести полный список и описание системных схем данных, которые можно использовать в качестве входных данных в новых инструментах.
4. Выполнить произвольные команды в консоли. Например, вывести список правил командой «list-defrules» или фактов командой «facts»
Можно запускать инструменты из командной строки — профилировать приложение во время выполнения unit- или UI-тестов на CI-сервере. Это позволит, к примеру, ловить memory leak как можно раньше. Для профилирования тестов в pipeline используем следующие команды:
1. Запуск инструментов с атрибутами:
2. Запуск тестов на этом же симуляторе командой:
В результате в рабочей директории будет создан отчет с расширением .trace, который можно открыть приложением Instruments или нажав правой кнопкой по файлу и выбрав Show Package Contents.
Мы рассмотрели пример модернизации signpost до полноценного инструмента и рассказали, как автоматически применять его на «прогонах» CI-сервера, использовать в решении проблемы «мигающих» (нестабильных) тестов.
По мере погружения в возможности custom instruments вы будете лучше понимать, в каких еще случаях можно применять инструменты. Например, нам они также помогают разобраться в проблемах многопоточности — где и когда использовать потокобезопасный доступ к данным.
Создать новый инструмент оказалось достаточно просто. Но главное — потратив несколько дней на изучение механики и документации для его создания сегодня, вам удастся избежать нескольких бессонных ночей в попытках исправить баги.
Статью писали вместе с @regno — Антоном Власовым, iOS-разработчиком.
Прошел год, а новых публичных инструментов и информации по их созданию в сети почти нет. Так что мы решили исправить ситуацию и поделиться тем, как создавали собственный Custom Instrument, который определяет причину слабой изоляции unit-тестов. Он базируется на технологии signpost (мы писали о ней в предыдущей статье) и позволяет быстро и точно определять место возникновения мигания теста.
Теоретический минимум
Чтобы создать новый инструмент для Xcode, потребуется понимание двух теоретических блоков. Тем, кто хочет разобраться самостоятельно, сразу дадим нужные ссылки:
- о структуре файла разметки инструмента .instrpkg
- о языке CLIPS для программирования логики инструмента
Для остальных — ниже краткий конспект по необходимым темам.
Сперва выберите 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)</i> — если animal-is duck присутствует в таблице фактов
=>
(assert (sound-is quack))) — то создается новый факт sound-is quack
3. Для создания и использования переменных применяется следующий синтаксис (перед именем переменной идет обязательный знак "?"):
?<variable-name>
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 <template_name> -l <average_duration_ms> -w <device_udid>
- где
template_name
— путь до шаблона с инструментами или название шаблона. Можно получить командойxcrun instruments -s
; average_duration_ms
— время записи в миллисекундах, должно быть больше или равно времени выполнения тестов;device_udid
— идентификатор симулятора. Можно получить командой xcrun instruments -s. Должен совпадать с идентификатором симулятора, на котором будут выполняться тесты.
2. Запуск тестов на этом же симуляторе командой:
xcodebuild -workspace <path_to_workspace>-scheme <scheme_with_tests> -destination
<device> test-without-building
- где
path_to_workspace
— путь к рабочему пространству Xcode; scheme_with_tests
— схема с тестами;device
— идентификатор симулятора.
В результате в рабочей директории будет создан отчет с расширением .trace, который можно открыть приложением Instruments или нажав правой кнопкой по файлу и выбрав Show Package Contents.
Выводы
Мы рассмотрели пример модернизации signpost до полноценного инструмента и рассказали, как автоматически применять его на «прогонах» CI-сервера, использовать в решении проблемы «мигающих» (нестабильных) тестов.
По мере погружения в возможности custom instruments вы будете лучше понимать, в каких еще случаях можно применять инструменты. Например, нам они также помогают разобраться в проблемах многопоточности — где и когда использовать потокобезопасный доступ к данным.
Создать новый инструмент оказалось достаточно просто. Но главное — потратив несколько дней на изучение механики и документации для его создания сегодня, вам удастся избежать нескольких бессонных ночей в попытках исправить баги.
Источники
- https://developer.apple.com/videos/play/wwdc2018/410/
- https://developer.apple.com/videos/play/wwdc2018/405/
- https://help.apple.com/instruments/developer/mac/current/
- https://help.apple.com/instruments/mac/current/#/devb14?aa5
- http://www.clipsrules.net/Documentation.html
- https://medium.com/appspector/building-custom-instruments-package-9d84fd9339b6
- http://desappstre.com/how-to-custom-instruments-xcode/
Статью писали вместе с @regno — Антоном Власовым, iOS-разработчиком.