На этой странице

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 и оставить вас в контроле над каждым шагом.

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

Теперь приступим к делу.

  1. Установите perf (обычно доступен через пакет linux-tools-common, если ещё не установлен)
  2. Попробуйте запустить perf — он может пожаловаться на отсутствующие модули ядра, установите и их
  3. Запустите node с включённым perf (см. проблемы вывода perf для советов, специфичных для версий Node.js)
  1. Не обращайте внимания на предупреждения, если только они не говорят, что вы не можете запустить perf из-за отсутствующих пакетов; вы можете получить предупреждения о невозможности доступа к семплам модулей ядра, которые вам всё равно не нужны.
  2. Выполните perf script > perfs.out, чтобы сгенерировать файл данных, который вы сейчас визуализируете. Полезно применить некоторую очистку для более читаемого графа
  3. Просмотрите или сгенерируйте 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-процесс с трудновоспроизводимой проблемой.

Стоп, а зачем этот 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 встроены некоторые меры смягчения этого.

Подробнее см.:

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!