ИНЖЕНЕРНАЯ СТАТЬЯ

Как построить контроль регрессии прокрутки iOS с XCTMetric

Как построить контроль регрессии прокрутки iOS с XCTMetric

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

Сначала зафиксируйте переменные, влияющие на результат

Первый шаг при создании контроля производительности — не написание проверок, а снижение шума среды. Зафиксируйте версии Xcode и macOS, модель симулятора, язык системы, ориентацию экрана и тестовые данные. Тестовая учётная запись должна сразу открывать целевую страницу, чтобы запросы авторизации, загрузка изображений и время ответа сервера не попадали в показатели прокрутки.

Добавьте в приложение аргумент запуска, который используется только в UI-тестах, например -UITestSeed fixed. Он должен загружать локальные фикстуры, создавать заданное количество элементов списка в неизменном порядке и отключать случайные анимации. Для списка также нужны стабильные идентификаторы специальных возможностей, такие как feed.list и feed.reset; тестовый код не должен искать элементы по локализованному тексту.

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

Базовый уровень производительности описывает среду целиком, а не универсальное количество миллисекунд, применимое к любому Mac и любому симулятору.

Создайте воспроизводимый сценарий прокрутки

Одиночный вызов swipeUp() слишком сильно зависит от начальной позиции и длины списка. В более надёжном тесте следует сначала вернуться к началу списка, убедиться, что первый опорный элемент виден, а затем выполнить фиксированное количество жестов. После каждого цикла измерения нужно снова восстанавливать начальное состояние, чтобы следующий цикл не начинался в конце списка.

import XCTest

final class FeedScrollPerformanceTests: XCTestCase {
    func testFeedScrollPerformance() {
        let app = XCUIApplication()
        app.launchArguments += ["-UITestSeed", "fixed"]
        app.launch()

        let list = app.collectionViews["feed.list"]
        XCTAssertTrue(list.waitForExistence(timeout: 10))

        app.buttons["feed.reset"].tap()
        XCTAssertTrue(app.cells["feed.item.0"].waitForExistence(timeout: 5))

        let options = XCTMeasureOptions()
        options.iterationCount = 5

        measure(
            metrics: [XCTClockMetric(), XCTCPUMetric(), XCTMemoryMetric()],
            options: options
        ) {
            app.buttons["feed.reset"].tap()
            for _ in 0..<6 {
                list.swipeUp(velocity: .fast)
            }
        }
    }
}

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

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

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

set -euo pipefail

RESULT="$PWD/artifacts/FeedScroll.xcresult"
rm -rf "$RESULT"
mkdir -p "$PWD/artifacts"

xcodebuild test \
  -workspace App.xcworkspace \
  -scheme AppUITests \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest' \
  -only-testing:AppUITests/FeedScrollPerformanceTests \
  -resultBundlePath "$RESULT"

OS=latest подходит для конвейеров, которые всегда следуют за текущей версией набора инструментов. Для долгосрочных сравнений следует зафиксировать конкретную среду выполнения и заново построить базовый уровень после обновления Xcode или системы. Для заданий на узлах OVPS также нужно записывать конфигурацию хоста, идентификатор коммита, версию Xcode и среду выполнения симулятора, чтобы не принять обновление окружения за ухудшение кода.

Перед использованием xcresulttool из текущей версии Xcode просмотрите справку: набор подкоманд может меняться между версиями инструментов. Независимо от того, используется ли скрипт или анализатор отчётов о тестах, исходный файл xcresult необходимо сохранять как вложение при неудачном запуске.

Принимайте решение по медиане и относительному порогу

Не следует блокировать слияние из-за одного медленного замера. Для каждого коммита выполните от пяти до семи циклов, исключите прогревочный запуск и затем вычислите медиану. В качестве базового уровня можно использовать медиану нескольких последних стабильных запусков основной ветки, а не навсегда сохранять результат самого первого измерения.

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

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

При сбое сохраните доказательства и сузьте область поиска

Если контроль не пройден, не перезаписывайте базовый уровень сразу. Сначала повторно запустите тот же коммит. Если сбой повторится, сохраните xcresult, полный журнал, версию тестовых данных, версии системы и набора инструментов, а также сводные показатели CPU, памяти и времени. Затем сузьте диапазон коммитов и уделите особое внимание декодированию изображений, теням и размытию, ограничениям Auto Layout, синхронным операциям ввода-вывода в главном потоке, повторному использованию элементов списка и выводу журналов.

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

Обновление базового уровня следует оформлять отдельным коммитом с описанием обновления среды или допустимого изменения взаимодействия и прикладывать результаты измерений до и после обновления. В этом случае контроль производительности не будет постоянно срабатывать из-за случайного шума и не утратит смысл из-за произвольного обновления базового уровня.

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

Нужно ли задавать фиксированный порог в миллисекундах?

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

Почему первый запуск не включают в базу?

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

Что сохранять после провала теста?

Сохраните xcresult, журналы, идентификатор коммита, версии Xcode и macOS, модель симулятора, версию тестовых данных, а также показатели CPU и памяти.

OVPS CLOUD MAC

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

Выберите одну из трёх конфигураций Apple Silicon и шести доступных узлов. Фактический статус доступности отображается в консоли в реальном времени.

Выбрать конфигурацию и оформить заказ