После добавления градиента, декодирования изображений или сложных теней в список товаров функциональные тесты по-прежнему проходят, однако при прокрутке начинают выпадать кадры. Несколько ручных свайпов не позволяют надёжно понять, вызваны ли рывки случайным отклонением или регрессией в коде. Более устойчивый подход — зафиксировать симулятор, набор данных и жесты на облачном 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 и памяти.
Перенесите следующую сборку на выделенный физический узел
Выберите одну из трёх конфигураций Apple Silicon и шести доступных узлов. Фактический статус доступности отображается в консоли в реальном времени.