Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Недавно наши интеграционные тесты Jest упали из-за недостатка памяти (ошибка V8 "heap out of memory"). Как оказалось, это не было аномалией, и тесты постоянно аккумулировали на себе столько памяти, что единственный процесс Node.js достиг стандартного предела в 4 ГБ, установленного в V8 для размера кучи (мы явно запускаем их в последовательном порядке, поэтому существует только единственный процесс). У нас около 450 тестов, объединенных в 50 сьютов, которые в основном являются интеграционными: имитированные HTTP-запросы обрабатываются на сервере, который взаимодействует с базой данных Postgres, запущенной в Docker. По этой причине мы используем последовательный запуск, поскольку у нас нет изолированного хранилища для каждого сьюта. После того, как мы обнаружили несколько проблем с Jest, соответствующих нашим диагнозам (выберите понравившийся: ts-jest#1967, jest#10550, jest#11956, jest#12142, jest#7311, jest#7874, jest#11956), одним из действий, которые мы предприняли, было выяснить, сможет ли миграция на альтернативный фреймворк решить наши проблемы.
Давайте вместе выясним, как обстоят дела с требованиями к памяти и производительностью Jest в сравнении с его конкурентами: AVA, Mocha и Tap.
Методы измерения
Сэмплирование памяти в Node.js
Если вы запускаете однопоточное приложение Node.js в рамках одного процесса, то можно выполнить семплирование памяти в каждом тике цикла событий с помощью следующего сниппета:
let memStats = {}; // глобальный аккумулятор
process.nextTick(() => { // регистрируем функцию для каждого "тика"
const current = process.memoryUsage(); // получаем информацию об использовании памяти
for (const key in current) {
// для каждой пары ключ-значение сохраняем максимальное значение
memStats[key] = Math.max(memStats[key] || 0, current[key]);
}
});
process.on("exit", () => {
// когда процесс завершится, выведите в лог максимальные значения
console.log(memStats);
});
Этот подход становится проблематичным, если один из ваших тестируемых субъектов (обращаю внимание на тебя, AVA) не может быть принудительно запущен в одном процессе и одном потоке. Даже если вы можете сконфигурировать размер пула потоков, у вас не будет глобальной настройки для процесса, а только для отдельных воркеров, что приведет к искажению результатов.
Само собой разумеется, что это влияет на производительность и не должно использоваться в продакшн-коде.
Time util
GNU-имплементация команды time (обычно ее следует вызывать с указанием полного пути, например, /usr/bin/time, чтобы избежать использования времени вашей оболочкой) имеет опцию verbose, которая дает вам подробную информацию не только о времени, но и о ресурсах, используемых процессом.
$ /usr/bin/time -v npm run test
Время пользователя (секунды): 4.39
Системное время (секунды): 0.45
Процент использования CPU, который получило это задание: 7%
Истекшее ( wall clock) время (ч:мм:сс или м:сс): 1:06.7
Средний размер общего текста (кбайт): 0
Средний размер нераспределенных данных (кбайт): 0
Средний размер стека (кбайт): 0
Средний общий размер (кбайт): 0
Максимальный размер резидентного набора (кбайт): 95012 # это то, что нам нужно, то же самое, что и "rss" из "process.memoryUsage()".
Средний размер резидентного сета (кбайт): 0
Основные (требующие операций ввода-вывода) ошибки страницы : 0
Незначительные (восстановление фрейма) ошибки страницы: 60030
Добровольные контекстные переключения: 6206
Вынужденные контекстные переключения: 499
Свопы: 0
Файловая система вводы: 0
Файловая система выводы: 8
Отправлено сокетных сообщений: 0
Получено сокетных сообщений: 0
Передано сигналов: 0
Размер страницы (байты): 4096
Статус выхода: 0
Пока мы можем ограничить применение фреймворков в рамках одного процесса, время, взятое за основу, дает возможность унифицировать подход к измерению скорости и потреблению памяти. Давайте проведем тестирование!
Результаты: Jest сравнительно с Mocha, сравнительно с AVA, сравнительно с Tap
Прежде чем мы перейдем к основным моментам, если вас интересуют более "заумные" подробности, посмотрите этот репозиторий. Вы даже можете клонировать его и провести свои собственные тесты. Более подробную информацию вы найдете в readme.
Множество (орда) тестов!
Самым простым для измерения был сценарий с большим количеством тривиальных тестов: 100K тестов (в 1K сьютах). Вот результаты:
Как видите, в результате реализации данного сценария возникает несколько выводов
Jest требует более чем в 3 раза больше памяти, чем среднестатистически.
Tap и AVA работают на удивление медленно.
Tap имеет самые низкие требования к памяти (в 10 раз меньше, чем Jest!)
Mocha очень быстрая
Асинхронные тесты
Первый тест был очень неудачным для любого из вариантов по распределению рабочих заданий, так как не было никакого пендинга ввода-вывода. Неудивительно, что Mocha превзошла всех остальных. На этот раз мы попробуем сценарий, более близкий к нашим интеграционным тестам: Представим, что каждый тест совершает операцию ввода-вывода длительностью 500 мс, а также сократим количество тестов до 100 (в 10 сьютах).
(Объем памяти при выполнении нескольких процессов сложно измерить, поэтому значения отсутствуют).
AVA имеет худшие показатели производительности используемой памяти, но достаточные для непревзойденной скорости (отдельные тесты выполняются параллельно).
Mocha по-прежнему демонстрирует лучшие нормативы потребления памяти и немного превосходит Jest как в последовательных, так и в параллельных запусках
Tap использует память, сопоставимую с Jest. Он не выполняет тесты параллельно, как AVA, но ему удается превзойти Jest и Mocha, скорее всего, за счет того что главный (master) процесс также используется в качестве рабочего (воркер. worker) (на моих 4 ядрах и Jest, и Mocha обрабатывали параллельно 3 + 3 + 3 + 3 + 1 сьютов, тогда как Tap делал 4 + 4 + 1, сократив общее время на 5 с).
Leaking bucket (алгоритм "дырявое ведро")
Если быть откровенным, мы точно не занимаемся тестированием, используя метод "черного ящика". Мы знаем, для чего мы здесь, и мы знаем, что с Jest утекает немного памяти в промежутках между сьютами, и после осмотра дампов кучи мы уже знали, что искать: кэш модулей. Поэтому в последнем тесте мы внедрили некоторые импорты. Для данного теста (все варианты), это отсутствие ожидания и 1K сьютов, каждый с одним тестом.
Простой запуск:
Импортируем стандартные модули Node: path, crypto, os, fs в каждый тест и вызываем по одной функции из каждого:
Вдобавок к этому оставляем в сьюте "зависший" setInterval (потенциально имитирующий открытое соединение):
В последнем запуске таймаут Tap был установлен на 1 мс, потому что иначе он не стал бы запускать новый сьют, ожидая завершения текущего.
Импортирование модулей не оказывает существенного влияния на память для AVA, Tap или Mocha, в то время как у Jest потребление памяти увеличивается более чем в два раза.
Mocha сохраняет самый низкий уровень использования памяти и лучшую производительность.
Mocha и AVA остаются стабильными независимо от открытых хендлов, в то время как Jest выходит сам из себя с памятью.
Tap хуже справляется с открытыми хендлами с точки зрения памяти, но не так плохо, как Jest.
Резюме: Самый эффективный фреймворк для тестирования Node
Если производительность является для вас ключевым требованием, выбирайте Mocha. Во всех тестах он занимает меньше всего памяти и превосходит остальных конкурентов как в последовательном, так и в параллельном режиме.
AVA может быть интересным выбором, если вы не планируете использовать большое количество тестов. Его распараллеливание тестов по умолчанию внутри сьютов уникально. Во избежание спешки это потребует большой дисциплины при написании и делает сложной их интеграцию на общем ресурсе, но результат того стоит.
Tap имеет низкую производительность при очень большом количестве тестов (то же самое, что и AVA, но для AVA вполне резонно наличие более низкого быстродействия из-за одновременного выполнения тестов, тогда как для Tap не совсем очевидно, почему он настолько хуже, чем, например, Jest). Удивительно, но это единственный раннер, который использует все доступные ядра во время выполнения тестов.
И, наконец, всеми любимый Jest превосходит своих конкурентов почти во всех протестированных сценариях по потреблению памяти и, как правило, по времени выполнения.
Приглашаем всех желающих на открытое занятие «Обзор новых возможностей Node.js», на котором обсудим нововведения в последних версиях Node.js, а также разберем подробнее каждое из них и то, как они могут быть полезны. Регистрация по ссылке.