В долгом прогоне автотестов память процесса постепенно растёт: какой механизм поможет доказать утечку ресурсов в тестовой инфраструктуре?
Нужно провести профилирование потребления ресурсов на повторяющемся прогоне: зафиксировать базовый уровень, многократно выполнить один и тот же набор тестов и сравнить снимки памяти, открытых файлов, соединений и других ресурсов. Утечка подтверждается не единичным ростом, а устойчивым увеличением после эквивалентных итераций, которое не исчезает после сборки мусора или завершения теста.
Автотесты первоначально часто запускались короткими независимыми процессами, поэтому утечки ресурсов скрывались завершением процесса. По мере роста наборов тестов раннеры стали выполнять много сценариев в одном процессе, а CI — переиспользовать агентов и окружения.
Это повысило скорость, но сделало состояние процесса долгоживущим. Поэтому появились практики профилирования, контроль ресурсов и разделение тестов на процессы, когда локализовать причину утечки быстро невозможно.
Рост памяти не всегда означает утечку. Его могут вызывать кэш, отложенная сборка мусора, JIT-компиляция, одноразовая инициализация библиотек или корректное увеличение рабочего набора приложения.
Ошибочное решение — просто увеличить лимит памяти или перезапускать процесс после каждого теста. Это может скрыть дефект инфраструктуры, увеличить время CI и не показать, какой тест удерживает объекты или соединения.
Сначала выбирают стабильный сценарий воспроизведения и измеряют ресурсы в одинаковых точках: до прогона, после каждой итерации и после принудительной сборки мусора, если среда её поддерживает. Для памяти полезны снимки кучи и сравнение сохранённых объектов между итерациями; для внешних ресурсов — число открытых файлов, сокетов, подключений к базам и активных потоков.
Затем прогоняют один тест или небольшой набор много раз. Если после одинаковых итераций растёт число экземпляров объектов, которые должны быть временными, либо увеличивается число не закрытых ресурсов, это сильный признак утечки. Чтобы найти виновника, набор делят пополам и повторяют измерение, пока область поиска не сузится до конкретной фикстуры, теста или адаптера.
Проверять нужно не только код теста, но и жизненный цикл фикстур, браузеров, HTTP-клиентов, соединений с базой, подписок на события и временных файлов. Каждый ресурс должен иметь симметричное освобождение при успешном завершении и при исключении.
Полезно отделять диагностический прогон от обычного CI: профилирование может быть медленным и само менять характеристики процесса. В регулярном CI разумнее оставить ограниченный контроль — например, порог роста памяти или числа открытых дескрипторов — а подробные снимки запускать по расписанию или при подозрении на регрессию.
Перезапуск процесса после каждого теста эффективно ограничивает последствия утечки, но маскирует её и ухудшает производительность. Это допустимая временная изоляция для нестабильного стороннего инструмента, но не замена поиску причины.
После добавления сотен API-тестов агент CI стал завершаться из-за нехватки памяти только ближе к концу полного набора. Рассматривались три варианта: увеличить память агента, запускать каждый тест в отдельном процессе или найти удерживаемые ресурсы.
Увеличение памяти давало временный эффект и повышало стоимость CI. Отдельный процесс для каждого теста устранял накопление, но сделал прогон слишком долгим. Выбранным решением стало профилирование повторного запуска: сравнение снимков показало, что фикстура сохраняла ссылки на ответы запросов в общем реестре и не очищала их после теста.
После очистки реестра рост памяти исчез, а перезапуск процесса оставили только для группы тестов, использующей нестабильный сторонний драйвер. Это одновременно устранило первопричину и сохранило приемлемое время прогона.
Как отличить утечку памяти от обычной работы сборщика мусора?
Нужно сравнивать состояние после эквивалентных итераций и, если возможно, после сборки мусора. Если память временно растёт, а затем возвращается к близкому уровню, это может быть нормальным поведением. Если же после сборки мусора сохраняются временные объекты и базовый уровень последовательно повышается, вероятность утечки значительно выше.
Почему перезапуск процесса не является полноценным исправлением?
Перезапуск освобождает все ресурсы процесса операционной системой, поэтому симптом исчезает. Но причина остаётся: в другом режиме запуска, при увеличении набора или на более слабом агенте проблема может вернуться. Кроме того, частые перезапуски увеличивают время и стоимость тестов, а также могут скрыть ошибки закрытия ресурсов.
Как проверить, что утечка находится именно в тестовой инфраструктуре, а не в приложении?
Нужно разделить измерения: сравнить рост ресурсов самого тестового процесса, процесса приложения и внешних сервисов. Затем повторить прогон с минимальным тестом, который создаёт подозрительный объект, и отдельно — с реальным пользовательским сценарием. Если ресурс растёт в раннере даже при стабильно работающем приложении, искать следует в фикстурах, драйверах, клиентах или обработчиках событий тестовой инфраструктуры.