Flame graph («флейм-граф», пламенный график) — это способ визуализировать процессорное время, потраченное в функциях. Он помогает точно определить, где вы тратите слишком много времени на синхронные операции.
Вы могли слышать, что создать flame graph для Node.js сложно, но это (больше) не так. Solaris-виртуалки для flame graph больше не нужны!
Flame graph генерируются из вывода perf, который не является node-специфичным инструментом. Хотя это самый мощный способ визуализировать потраченное процессорное время, у него могут быть проблемы с тем, как JavaScript-код оптимизируется в Node.js 8 и выше. См. раздел проблемы вывода perf ниже.
Если вы хотите единственный шаг, который построит flame graph локально, попробуйте 0x
Для диагностики production-развёртываний прочитайте эти заметки: 0x на production-серверах.
Цель этого руководства — показать шаги создания flame graph и оставить вас в контроле над каждым шагом.
Если хотите лучше понять каждый шаг, посмотрите следующие разделы, где мы разбираем детали подробнее.
Теперь приступим к делу.
- Установите
perf(обычно доступен через пакет linux-tools-common, если ещё не установлен) - Попробуйте запустить
perf— он может пожаловаться на отсутствующие модули ядра, установите и их - Запустите node с включённым perf (см. проблемы вывода perf для советов, специфичных для версий Node.js)
perf record -e cycles:u -g -- node --perf-basic-prof --interpreted-frames-native-stack app.js- Не обращайте внимания на предупреждения, если только они не говорят, что вы не можете запустить perf из-за отсутствующих пакетов; вы можете получить предупреждения о невозможности доступа к семплам модулей ядра, которые вам всё равно не нужны.
- Выполните
perf script > perfs.out, чтобы сгенерировать файл данных, который вы сейчас визуализируете. Полезно применить некоторую очистку для более читаемого графа - Просмотрите или сгенерируйте flame graph:
-
Просмотр в браузере (локальная настройка не требуется):
- Загрузите сгенерированный файл
perfs.outна https://flamegraph.com, чтобы визуализировать flame graph.
- Загрузите сгенерированный файл
-
Склонируйте инструменты FlameGraph от Brendan Gregg: https://github.com/brendangregg/FlameGraph
- Выполните
cat perfs.out | ./FlameGraph/stackcollapse-perf.pl | ./FlameGraph/flamegraph.pl --colors=js > profile.svg, а теперь откройте файл flame graph в любимом браузере и смотрите, как он «горит»
- Выполните
-
Как только flame graph отрендерен, сначала осматривайте самые насыщенные оранжевые полосы. Они, скорее всего, представляют функции, тяжёлые для CPU.
Стоит упомянуть: если кликнуть по элементу flame graph, он приблизит (zoom-in) секцию, по которой вы кликнули.
Это отлично подходит для записи данных flame graph с уже работающего процесса, который вы не хотите прерывать. Представьте production-процесс с трудновоспроизводимой проблемой.
perf record -F99 -p `pgrep -n node` -g -- sleep 3Стоп, а зачем этот sleep 3? Он нужен, чтобы perf продолжал работать — несмотря на то что опция -p указывает на другой pid, команда должна выполняться на процессе и завершаться вместе с ним.
perf работает столько, сколько живёт переданная ему команда, независимо от того, профилируете ли вы на самом деле эту команду. sleep 3 гарантирует, что perf проработает 3 секунды.
Почему -F (частота профилирования) установлена в 99? Это разумное значение по умолчанию. Можно скорректировать при желании.
-F99 говорит perf брать 99 семплов в секунду; для большей точности увеличьте значение. Меньшие значения дадут меньше вывода с менее точными результатами. Нужная точность зависит от того, как долго на самом деле работают ваши интенсивные по CPU функции. Если вы ищете причину заметного замедления, 99 кадров в секунду должно быть более чем достаточно.
После того как получите ту 3-секундную запись perf, продолжайте с генерацией flame graph по двум последним шагам выше.
Обычно вы хотите смотреть только на производительность своих вызовов, поэтому отфильтровывание внутренних функций Node.js и V8 может сделать граф гораздо легче для чтения. Очистить ваш perf-файл можно так:
sed -i -r \
-e "/( __libc_start| LazyCompile | v8::internal::| Builtin:| Stub:| LoadIC:|\[unknown\]| LoadPolymorphicIC:)/d" \
-e 's/ LazyCompile:[*~]?/ /' \
perfs.outЕсли вы читаете свой flame graph и он выглядит странно, будто чего-то не хватает в ключевой функции, занимающей больше всего времени, попробуйте сгенерировать flame graph без фильтров — возможно, вам попался редкий случай проблемы с самим Node.js.
--perf-basic-prof-only-functions и --perf-basic-prof — две опции, полезные для отладки вашего JavaScript-кода. Другие опции используются для профилирования самого Node.js, что за рамками этого руководства.
--perf-basic-prof-only-functions производит меньше вывода, поэтому это опция с наименьшими накладными расходами.
Ну, без этих опций вы всё равно получите flame graph, но большинство полос будут помечены v8::Function::Call.
Node.js 8.x и выше поставляется с новыми оптимизациями конвейера компиляции JavaScript в движке V8, из-за которых имена/ссылки функций иногда недостижимы для perf. (Это называется Turbofan)
В результате имена ваших функций в flame graph могут отображаться неправильно.
Вы заметите ByteCodeHandler: там, где ожидали бы имена функций.
У 0x встроены некоторые меры смягчения этого.
Подробнее см.:
- https://github.com/nodejs/benchmarking/issues/168
- https://github.com/nodejs/diagnostics/issues/148#issuecomment-369348961
Node.js 10.x решает проблему с Turbofan с помощью флага --interpreted-frames-native-stack.
Запустите node --interpreted-frames-native-stack --perf-basic-prof-only-functions, чтобы получить имена функций в flame graph независимо от того, какой конвейер V8 использовал для компиляции вашего JavaScript.
Если вы видите метки вроде такой
node`_ZN2v88internal11interpreter17BytecodeGenerator15VisitStatementsEPNS0_8ZoneListIPNS0_9StatementEEE
это значит, что используемый вами Linux perf был скомпилирован без поддержки demangle; см., например, https://bugs.launchpad.net/ubuntu/+source/linux/+bug/1396654
Попрактикуйтесь в захвате flame graph сами с упражнением по flame graph!