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

Диагностика давления памяти в CI на облачном Mac

Диагностика давления памяти в CI на облачном Mac

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

Сначала задайте воспроизводимое окно наблюдения

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

Сначала определите исходные значения физической памяти и пространства подкачки:

sysctl -n hw.memsize
sysctl vm.swapusage
memory_pressure -Q
vm_stat

hw.memsize показывает объём физической памяти в байтах. vm.swapusage сообщает текущее использование пространства подкачки, а размер страниц, количество сжатых страниц и счётчики ввода-вывода страниц из vm_stat позволяют оценить динамику. Важны изменения между началом и окончанием задания, а не отдельное мгновенное значение, принятое за порог.

Малый объём свободной памяти сам по себе не означает её нехватку. macOS активно использует память для кеширования файловых страниц. Обращать внимание нужно на устойчивый рост давления памяти и пространства подкачки, особенно если одновременно увеличивается длительность сборки или завершаются процессы.

Непрерывно собирайте метрики во время сборки

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

#!/bin/zsh
set -eu

out="${1:-memory-samples.log}"

while true; do
  printf '
=== %s ===
' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')" >> "$out"
  memory_pressure -Q >> "$out" 2>&1
  sysctl vm.swapusage >> "$out" 2>&1
  ps -axo pid,ppid,rss,etime,command | sort -nrk3 | head -n 16 >> "$out"
  sleep 10
done

Сначала запустите zsh sample-memory.zsh в отдельном терминале, а затем запустите конвейер. После завершения задания остановите сбор метрик. RSS обычно измеряется в КБ и не соответствует полному виртуальному адресному пространству процесса. Однако этого показателя достаточно, чтобы выявить компилятор, симулятор или тестовый процесс, потребление памяти которого непрерывно растёт.

Одновременно сохраняйте отметки этапов

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

Разделяйте источники давления памяти

Типичные сценарии можно предварительно классифицировать по следующей таблице:

Наблюдение Более вероятная причина Следующий шаг
На этапе компиляции RSS нескольких процессов растёт одновременно Параллелизм сборки превышает бюджет памяти Уменьшить число заданий сборки и повторить проверку
После запуска симуляторов давление резко возрастает Слишком много параллельных тестовых целей Ограничить число одновременно работающих симуляторов
RSS одного тестового процесса непрерывно растёт Утечка памяти в тесте или тестируемом коде Разделить тесты и собрать сэмплы процесса
Пространство подкачки постоянно растёт, а разброс времени выполнения увеличивается Система часто перемещает страницы между памятью и диском Снизить параллелизм и сократить число постоянно работающих фоновых задач
Давление памяти остаётся нормальным, но задание завершается принудительно Проблема не обязательно связана с памятью Проверить код завершения и унифицированный журнал

Сразу после аварийного завершения можно найти связанные системные события:

log show --last 30m --style compact \
  | grep -Ei 'memory pressure|memorystatus|killed process' \
  > memory-events.log

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

Преобразуйте параллелизм в явный бюджет

Верхнюю границу параллелизма следует определять по измеренному пиковому потреблению. Сначала зарезервируйте около 20% физической памяти для системы, удалённых сеансов и процессов журналирования, а оставшуюся память распределите между компиляторами и симуляторами. Если компиляция и тестирование не выполняются одновременно, для них можно задать отдельные бюджеты. Если этапы конвейера перекрываются, потребление необходимо рассчитывать совместно.

Для сборки Xcode сначала можно провести консервативную проверку с параметром -jobs:

set -o pipefail

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -jobs 4 \
  build

Для тестов в симуляторах ограничьте число параллельных целей:

xcodebuild \
  -workspace App.xcworkspace \
  -scheme AppTests \
  -parallel-testing-enabled YES \
  -maximum-concurrent-test-simulator-destinations 2 \
  test

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

Завершите настройку по приёмочному списку

В каждом цикле изменяйте только один параметр и фиксируйте следующие результаты:

  1. Завершается ли один и тот же коммит успешно три раза подряд.
  2. Пиковое использование пространства подкачки и его изменение между началом и окончанием задания.
  3. На каком этапе конвейера возрастает давление памяти.
  4. Продолжают ли процессы аварийно завершаться.
  5. Общее время, медианное время и разброс результатов трёх запусков.
  6. Увеличилось ли после снижения параллелизма число полезных заданий, выполняемых за единицу времени.

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

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

Достаточно ли смотреть только на свободную память облачного Mac?

Нет. Нужно сопоставлять давление памяти, объём сжатой памяти, рост swap и RSS процессов. Малый объём свободной памяти сам по себе нормален, если система освобождает кэш без постоянной подкачки.

Как проверить эффект от снижения параллельности Xcode?

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

Выделенный физический облачный Mac

Разверните воспроизводимую среду разработки на выделенном физическом узле

Выберите конфигурацию, срок аренды и один из пяти узлов для сборки в Xcode, автоматизированного тестирования, удалённой разработки или инференса моделей.

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