Custom instruments: когда signpost недостаточно

Instruments для Xcode компании Apple — это инструменты для анализа производительности iOS-приложения. Их используют для сбора и отображения данных, которые необходимы в отладке кода. В прошлом году Apple презентовала Custom Instruments. Это возможность расширить стандартный набор инструментов для профилирования приложений. Когда существующих инструментов недостаточно, вы сможете самостоятельно создать новые — они соберут, проанализируют и отобразят данные так, как вам потребуется.

Прошел год, а новых публичных инструментов и информации по их созданию в сети почти нет. Так что мы решили исправить ситуацию и поделиться тем, как создавали собственный Custom Instrument, который определяет причину слабой изоляции unit-тестов. Он базируется на технологии signpost (мы писали о ней в предыдущей статье) и позволяет быстро и точно определять место возникновения мигания теста.

09b0364507aac3a8e15ca618f772c329.png

Теоретический минимум


Чтобы создать новый инструмент для 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.

1b65bf777d1e5df8e3f172e628e982c3.jpg

c534c29a6bf6e72066a3e25a71db2bf5.jpg

Далее переходим в исследуемый проект, делаем разметку в unit-тестах, методах setUp и tearDown:

eb17350e0ef68bb0892382064ad10440.jpg

Шаг 2. Создаем новый инструмент из шаблона Instrument Package


7648bccecdbae756b7d5bf6bf116f9c6.jpg

Сначала определяем тип данных на входе. Для этого в файле .instrpkg импортируем схему signpost. Теперь события, созданные signpost, будут попадать в инструмент:

58622780b3b064241766cc8035a39ec3.jpg

Далее определяем тип данных на выходе. В этом примере будем выводить одномоментные события. У каждого события будет время и описание. Для этого объявляем схему:

d99ed990423be50e65a2a9abba4e4ae6.jpg

Шаг 3. Описываем логику инструмента


Создаем отдельный файл с расширением .clp, в котором задаем правила с помощью языка CLIPS. Чтобы новый инструмент знал, в каком файле определена логика, добавляем блок modeler:

d571e86ee091947afc51d90afb796b6a.jpg

В этом блоке с помощью атрибута production-system указываем относительный путь к файлу с логикой. В атрибутах output и required-input определяем схемы данных на входе и выходе соответственно.

878e14c8a5354ee00f80fd646991e8e6.jpg

Шаг 4. Описываем специфику представления инструмента (UI)


В файле .instrpkg остается описать сам инструмент, то есть отображение результатов. Создаем таблицу для данных в атрибуте create-table, используя ранее объявленную схему detected-mocks-narrative в атрибуте schema-ref. И настраиваем тип вывода информации — narrative (описательный):

934fae927ead3e65219a2385373b1470.jpg

Шаг 5. Пишем код логики


Перейдем к файлу .clp, в котором определена логика экспертной системы. Логика будет следующая: если время старта теста пересекается с интервалом жизни мока, то считаем, что этот мок «пришел» из другого теста — что нарушает изоляцию текущего unit-теста. Для того, чтобы в итоге создать событие с интересующей информацией, нужно проделать следующие шаги:

1. Определяем структуры mock и unitTest с полями — время события, идентификатор события, название теста и класс мока.

1606fe36993cbd7592ae56c81b963682.jpg

2. Определяем правила, которые создадут факты с типами mock и unitTest на основе входящих событий signpost:

5af470a70c9ee1c387ccedc116c71ffa.jpg

Читать эти правила можно следующим образом: если на входе мы получаем факт типа os- signpost с искомыми subsystem, category, name и event-type, то создаем новый факт с типом, что был определен выше (unitTest или mock), и наполняем значениями. Здесь важно помнить — CLIPS это регистрозависимый язык и значения subsystem, category, name и event- type должны совпадать с тем, что использовалось в коде исследуемого проекта.

4b4cd572322313efde53e0fa65d502cd.jpg

Значения переменных от событий signpost передаются следующим образом:

bc39521dd8ec5000f3c98653971173d2.jpg

3. Определяем правила, которые освобождают завершенные события (являются лишними, так как не влияют на результат).

5b96cd33eb906e4acce8072a8f652786.jpg

Шаг 6. Определяем правило, которое будет генерировать результаты


Прочитать правило можно так.

Если

1) существует unitTest и mock;

2) при этом начало теста наступает позже существующего мока;

3) существует таблица для хранения результатов со схемой detected-mocks-narrative;

то

4) создаем новую запись;

5) заполняем временем;

6)… и описанием.

dcfc30682ea50a0d391ae1702bcde9c1.jpg

В результате видим следующую картину при использовании нового инструмента:

bc0b3b4cf1d0d99f2359814c7881d22c.jpg

Исходный код custom instrument и пример проекта для использования инструмента можно посмотреть на GitHub.

Отладка инструментов


Для отладки кастомных инструментов используется debugger.

966265db7bf5d7eeebbe69359b63cd06.jpg

Он позволяет

1. Увидеть компилируемый код на основе описания в instrpkg.
2. Увидеть подробную информацию о том, что происходит с инструментом во время выполнения.

7d4c2e6bfb531a31b4b69a3f4eb9974d.jpg

3. Вывести полный список и описание системных схем данных, которые можно использовать в качестве входных данных в новых инструментах.

a138954daa535c02dba79f79291338bf.jpg

4. Выполнить произвольные команды в консоли. Например, вывести список правил командой «list-defrules» или фактов командой «facts»

4858d00333fb89957892560f37d5c17f.jpg

Настройка на 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-разработчиком.

© Habrahabr.ru