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



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)</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 вы будете лучше понимать, в каких еще случаях можно применять инструменты. Например, нам они также помогают разобраться в проблемах многопоточности — где и когда использовать потокобезопасный доступ к данным.
Создать новый инструмент оказалось достаточно просто. Но главное — потратив несколько дней на изучение механики и документации для его создания сегодня, вам удастся избежать нескольких бессонных ночей в попытках исправить баги.

Источники



Статью писали вместе с @regno — Антоном Власовым, iOS-разработчиком.

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (0):