Node.js Internals
Performance Hooks: Измерение производительности
Когда API внезапно начинает отвечать на 200мс дольше, интуиция говорит: "База данных тормозит!" Команда оптимизирует индексы, переписывает запросы, мигрирует на SSD. А проблема была в `JSON.parse()` огромного ответа - 150мс синхронной блокировки event loop, которую без профилировщика не увидеть. **Performance Hooks** - это рентген производительности: точное время каждой операции и мгновенный поиск узких мест.
- **E-commerce платформа:** Замер времени обработки каждого этапа checkout (валидация → резервация товара → платёж → отправка email). Обнаружили, что 80% времени уходит на синхронное создание PDF счёта - вынесли в очередь, время ответа упало с 1200мс до 300мс.
- **GraphQL API:** PerformanceObserver на каждом резолвере. Выяснили, что запрос "пользователь + его посты + комментарии" делает 1000+ запросов к БД (N+1 проблема). Добавили DataLoader, количество запросов упало до 3.
- **Real-time сервис:** Мониторинг Event Loop Delay показал p99 = 250мс каждые 5 минут. Оказалось, что cron задача синхронно парсила 100MB JSON файл. Заменили на streaming парсинг - p99 стабилен на уровне 10мс.
Зачем измерять производительность
API внезапно стал отвечать на 200мс дольше. Где проблема? В базе данных? В сериализации JSON? В сторонней библиотеке? Без точных измерений остаются только догадки. **Performance Hooks** - это встроенный профилировщик Node.js, который показывает, сколько времени занимает каждая операция с микросекундной точностью.
**Ключевое отличие от Date.now():** Performance Hooks используют монотонные часы (monotonic clock), которые не зависят от системного времени. Если пользователь изменит время на компьютере, `Date.now()` врёт, а `performance.now()` - нет.
Что происходит с измерениями Date.now(), если во время выполнения программы пользователь переведёт часы назад на час?
Основы perf_hooks API
Модуль `perf_hooks` предоставляет три основных способа измерения производительности: **точечный замер** через `performance.now()`, **маркеры и измерения** через `mark()`/`measure()`, и **автоматическое наблюдение** через `PerformanceObserver`. Каждый метод решает свою задачу: от простого "сколько заняла функция" до "построить timeline всех операций в приложении".
**Когда что использовать:** - `performance.now()` - для разового замера одной операции - `mark()`/`measure()` - когда нужно замерить несколько этапов процесса - `PerformanceObserver` - для автоматического сбора метрик в production
В чём главное преимущество PerformanceObserver перед обычным performance.measure()?
performance.now() и точность измерений
`performance.now()` возвращает время в миллисекундах с **микросекундной точностью** (десятые доли миллисекунд). Внутри Node.js использует системный вызов `clock_gettime(CLOCK_MONOTONIC)` на Linux или `QueryPerformanceCounter()` на Windows. Это означает, что даже операции быстрее 1мс можно измерить корректно.
**Разрешение времени:** `performance.now()` обычно имеет разрешение ~1 микросекунда (0.001мс). Для сравнения: `Date.now()` - 1 миллисекунда, `process.hrtime.bigint()` - 1 наносекунда (но сложнее в использовании).
Почему performance.now() лучше подходит для микробенчмарков (измерение очень быстрых операций) чем Date.now()?
Маркеры и измерения: performance.mark() / measure()
**Маркеры** (marks) - это именованные временные метки, **измерения** (measures) - это интервалы между маркерами. Аналогия с флажками на временной шкале: "началась загрузка", "загрузка завершена", "началась обработка", "обработка завершена". Затем вызов `measure()` даёт готовые интервалы: "загрузка заняла 150мс", "обработка заняла 50мс".
**Преимущество перед ручным вычитанием:** Маркеры можно ставить в разных частях кода (даже в разных модулях), а потом в одном месте собрать все измерения. Не нужно прокидывать переменные `startTime` через весь код.
Какое утверждение о performance.mark() и measure() верно?
PerformanceObserver: автоматический сбор метрик
`PerformanceObserver` - это подписка на события производительности. Вместо того чтобы после каждого `measure()` вызывать `getEntriesByName()`, observer создаётся один раз и автоматически получает **все** новые измерения. Это паттерн "наблюдатель": регистрируется callback, который срабатывает при любом новом entry.
**Типы entries:** `measure`, `mark`, `function`, `gc`, `http`, `dns`. С помощью observer можно не только следить за своими измерениями, но и получать автоматические метрики Node.js: время работы GC, DNS запросов, HTTP запросов.
В чём главное преимущество использования PerformanceObserver вместо ручного вызова performance.getEntriesByType('measure') после каждого измерения?
Мониторинг Event Loop: monitorEventLoopDelay
**Event Loop Delay** - это разница между тем, когда таймер должен был сработать, и когда он реально сработал. Если event loop занят (например, тяжёлые синхронные вычисления), коллбеки задерживаются. `monitorEventLoopDelay()` создаёт гистограмму задержек - распределение: сколько раз задержка была 0-1мс, 1-2мс, 2-5мс, и т.д. Это **главный индикатор здоровья** Node.js приложения.
**Здоровые значения:** p50 < 10мс, p99 < 50мс. Если p99 > 100мс - event loop заблокирован, приложение тормозит. Частые причины: синхронные crypto операции, большие JSON.parse(), регулярные выражения на огромных строках.
Если приложение быстро отвечает на запросы, значит event loop в порядке
Даже при нормальном времени ответа event loop может быть периодически заблокирован, что видно только через гистограмму задержек
Время ответа (response time) - это среднее или p50 значение. Event Loop Delay показывает **хвосты распределения** (p99, p999) - редкие, но критичные случаи блокировок. Одна синхронная операция на 500мс раз в минуту может быть незаметна в среднем времени ответа, но полностью заморозит приложение для всех пользователей на эти 500мс. Гистограмма Event Loop - единственный способ увидеть такие проблемы.
Что означает, если в гистограмме Event Loop Delay значение p99 = 150мс?
Ключевые идеи
- **performance.now()** - монотонные часы с микросекундной точностью. В отличие от Date.now(), не зависят от системного времени и могут измерять операции быстрее 1мс.
- **mark() и measure()** - именованные метки на временной шкале. Можно ставить маркеры в разных частях кода, потом в одном месте собрать все интервалы. Удобно для профилирования многоэтапных процессов.
- **PerformanceObserver** - паттерн Pub/Sub для метрик. Настраиваете сбор один раз (например, отправку в Prometheus), все measure() автоматически попадают туда. Поддерживает типы: measure, mark, function, gc, http, dns.
- **monitorEventLoopDelay()** - гистограмма задержек event loop. Главный индикатор здоровья приложения: если p99 > 100мс - где-то есть синхронные блокировки (crypto, JSON.parse, regex, fs.readFileSync).
- **Практический workflow:** 1) Добавить mark()/measure() в критичные места 2) PerformanceObserver для автосбора 3) Мониторинг Event Loop в production 4) Анализ гистограмм (p50, p95, p99) 5) Поиск аномалий и оптимизация.
Связанные темы
Performance Hooks тесно связаны с другими техниками оптимизации Node.js:
- Worker Threads — Если Event Loop Delay показывает постоянные блокировки из-за CPU-intensive операций, можно вынести их в отдельные потоки через Worker Threads, не блокируя главный event loop.
- Async Hooks — Async Hooks позволяют отслеживать жизненный цикл асинхронных операций. Комбинация с Performance Hooks даёт полную картину: какие async операции долго выполняются и почему.
- Child Processes — Если тяжёлая операция не может быть оптимизирована (например, запуск внешнего инструмента), Performance Hooks покажут это, и можно вынести в child process, чтобы не блокировать основное приложение.
Вопросы для размышления
- Какие части типичного приложения работают медленно? Добавление mark()/measure() помогает найти узкие места. Что чаще оказывается самым медленным - БД запросы, сериализация, или что-то неожиданное?
- При добавлении monitorEventLoopDelay() в production какое значение p99 ожидается? Есть ли в типичном коде синхронные операции, которые могут блокировать event loop?
- Как организовать централизованный сбор метрик производительности для микросервисной архитектуры? Где развернуть PerformanceObserver - в каждом сервисе или в shared библиотеке?