Инженерные заметки

Как построить контроль регрессии времени XCTest через xcresult

Как построить контроль регрессии времени XCTest через xcresult

Как построить контроль регрессии времени XCTest через xcresult

Тест XCTest, который изначально выполнялся всего за 4 секунды, постепенно начинает занимать 11 секунд, но конвейер по-прежнему остаётся полностью зелёным. Обычно команда замечает проблему лишь тогда, когда очередь заданий уже заметно выросла. Общее время сборки плохо подходит для поиска таких изменений: в нём смешаны разрешение зависимостей, компиляция, запуск симулятора и запись результатов. Более надёжный подход — сохранять .xcresult для каждого тестового задания на облачном Mac от NUMACS, извлекать длительность отдельных тестов и сравнивать её с контролируемой базовой линией.

Сначала определите, что именно измеряет контроль

Контроль должен отвечать на вопрос «становится ли один и тот же тест стабильно медленнее», а не «заняло ли это задание на несколько секунд больше предыдущего». Для начала разделите метрики на три уровня:

Уровень Метрика Назначение
Тестовый случай Длительность одного выполнения Поиск конкретной регрессии
Набор тестов Медиана и высокие перцентили Наблюдение за общим смещением группы тестов
Задание CI Полная длительность от запуска до завершения Выявление проблем инфраструктуры или этапа компиляции

Не следует блокировать слияние по результату одного запуска. Короткие тесты чувствительны к колебаниям планировщика, а длительные могут замедляться из-за сети, анимаций или асинхронного ожидания. Практичный критерий — «двойной порог»: повторная проверка запускается, только если значение кандидата одновременно превышает допустимый относительный рост и абсолютный прирост относительно базовой линии. Например, при базовом времени 2 секунды рост на 20 %, составляющий всего 0.1 секунды, не должен приводить к сбою. При базовом времени 40 секунд увеличение на 8 секунд уже заслуживает проверки.

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

Зафиксируйте среду выполнения и создайте пакет результатов

Сначала зафиксируйте путь к Xcode, Scheme, целевое устройство и стратегию параллельного выполнения. Модель симулятора и версия системы должны быть указаны явно: нельзя полагаться на «устройство, запущенное в данный момент». Не следует также динамически менять уровень параллелизма внутри одного задания.

set -euo pipefail

RESULT_DIR="$PWD/artifacts"
RESULT_BUNDLE="$RESULT_DIR/RegressionTests.xcresult"

mkdir -p "$RESULT_DIR"
rm -rf "$RESULT_BUNDLE"

xcodebuild test \
  -workspace Example.xcworkspace \
  -scheme ExampleTests \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.0' \
  -resultBundlePath "$RESULT_BUNDLE" \
  -parallel-testing-enabled NO

Версия в примере — лишь одна из составляющих среды выполнения. В реальном проекте необходимо зафиксировать проверенные версии Xcode и среды выполнения симулятора. Сначала выполните один прогревочный запуск, который не учитывается в статистике: это позволит симулятору полностью запуститься, а необходимым тестовым ресурсам — сохраниться на диск. Для основных измерений требуется как минимум три запуска. Если сам тест отличается высокой дисперсией, увеличьте число запусков, а не ослабляйте пороги до полной потери их смысла.

Извлеките длительность тестов из xcresult

Новые версии Xcode позволяют получать дерево тестов через xcresulttool. Интерфейс команды меняется по мере развития Xcode, поэтому версию парсера необходимо фиксировать вместе с версией Xcode, используемой в CI. Перед обновлением совместимость следует проверять на сохранённых пакетах результатов.

xcrun xcresulttool get test-results tests \
  --path artifacts/RegressionTests.xcresult \
  --format json > artifacts/tests.json

jq -r '
  .. | objects
  | select(.nodeType? == "Test Case" and .duration? != null)
  | [.name, .duration] | @tsv
' artifacts/tests.json > artifacts/test-durations.tsv

Перед сравнением убедитесь, что файл TSV не пуст и количество тестов соответствует ожиданиям. Пустой файл нельзя считать признаком «отсутствия регрессий». Обычно он означает, что изменился интерфейс команды, тесты не были запущены или условие парсинга больше не соответствует данным. Идентификатор теста должен включать модуль, класс и метод. Для параметризованных тестов необходимо также сохранять имя параметра, чтобы разные наборы данных не были ошибочно объединены.

Не теряйте исходные доказательства при нормализации

Приведите всю длительность к секундам и добавьте к каждой записи commit, версию Xcode, целевое устройство и номер задания. Агрегированный файл удобен для сравнения, но исходный .xcresult всё равно необходимо сохранять как артефакт задания. При обнаружении аномалии он также предоставит сведения об ошибках, журналы активности и контекст вложений.

Используйте устойчивую базовую линию вместо последнего результата

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

Для каждого теста рекомендуется сохранять следующие поля:

{
  "ExampleTests.testParsing": {
    "median_seconds": 3.84,
    "absolute_limit_seconds": 1.5,
    "relative_limit": 0.25,
    "sample_count": 9
  }
}

После первого выхода кандидатной ветки за пределы порогов повторно запускайте только затронутые тесты или набор, к которому они относятся. Задание следует помечать как неуспешное, только если медиана повторных запусков по-прежнему одновременно превышает median_seconds + absolute_limit_seconds и median_seconds × (1 + relative_limit). Новые тесты сначала переводятся в режим наблюдения: пока образцов недостаточно, результаты только отображаются в отчёте и не блокируют изменения.

Исключите самые распространённые ложные регрессии

Холодный запуск симулятора — главный источник шума. Инициализация тестовых данных, первая загрузка шрифтов и создание таблиц базы данных также могут замедлять первый проход. Эти операции следует отделить с помощью прогрева или явно выделенного setUp. Затем проверьте, не обращается ли тест к сети, не ожидает ли реального времени, не использует ли общие пользовательские настройки и не переиспользует ли файлы, оставленные предыдущим тестом.

Параллельное выполнение может изменить конкуренцию за CPU, память и диск. Если задача состоит в построении базовой линии для отдельных тестов, параллельность следует отключить. Если же требуется оценить реальную пропускную способность конвейера, зафиксируйте количество worker-процессов и сохраните его как отдельное измерение базовой линии. Данные, полученные с разными версиями Xcode, средами выполнения системы или аппаратными конфигурациями, нельзя смешивать напрямую.

При внезапном замедлении проверяйте по порядку:

  1. Изменились ли количество тестов или цель выполнения?
  2. Возникло ли ожидание на этапах компиляции, установки или запуска симулятора?
  3. Связан ли тайм-аут с опросом состояния или фиксированными задержками?
  4. Увеличились ли тестовые фикстуры или остались ли они неочищенными после завершения?
  5. Используют ли несколько заданий одновременно один и тот же рабочий каталог?

Сделайте изменения базовой линии проверяемыми

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

Итоговый отчёт следует разбить на группы «новые регрессии», «восстановлено» и «под наблюдением», указав абсолютный прирост, относительный рост и результаты повторной проверки. Такой контроль блокирует скрытую деградацию производительности, но не задерживает разработку из-за единичного колебания симулятора. В итоге команда получает не хрупкий секундомер, а воспроизводимую, объяснимую и проверяемую базовую линию длительности XCTest.

Часто задаваемые вопросы

Почему нельзя проверять только общее время xcodebuild?

Оно включает разрешение зависимостей, компиляцию, запуск симулятора и запись результатов. Для точной диагностики нужно извлекать длительность каждого теста из xcresult.

Какой порог лучше: секунды или проценты?

Надёжнее использовать оба порога одновременно. Замедление считается регрессией, только если после повторного запуска превышены абсолютное и относительное ограничения.

Что сохранять при обновлении базовой линии?

Нужно сохранить файл базовой линии, commit, версии Xcode и целевой среды, исходный xcresult и объяснение изменения.

Numacs облачный Mac

Перенесите задачи сборки на выделенную физическую рабочую станцию

Две конфигурации Apple Silicon доступны на выделенных физических машинах, а не на виртуальных, с арендой на день, неделю, месяц или квартал в Сингапуре, Токио, Сеуле и Гонконге.

Выбрать устройство и оформить заказ