import { monitorEventLoopDelay, IntervalHistogram } from 'perf_hooks'; import { DyFM_Log } from '@futdevpro/fsm-dynamo'; import { DyNTS_global_settings } from '../../_collections/global-settings.const'; /** Egy event-loop-lag mintavétel (a gördülő history eleme). */ export interface DyNTS_EventLoopSample { at: number; /** p50 lag ms-ben (a poll-ablakban). */ p50Ms: number; /** p95 lag ms-ben. */ p95Ms: number; /** max lag ms-ben — a „process él, HTTP néma" wedge fő jele. */ maxMs: number; /** Aktív handle-k száma (socket/timer/fd) — connection-leak jel. */ activeHandles: number; } /** * Event-loop-lag + connection diagnosztika (BFR-OVERSEER-007) — a „process él, de a HTTP néma" * jelenség (event-loop blokk / wedge) eddig LÁTHATATLAN volt: a MemoryGuard a heap-et figyeli, * a healthcheck kívülről néz — a loop-lag magát a tünetet méri, belülről. * * A `perf_hooks.monitorEventLoopDelay` NATÍV histogramjára épül (elhanyagolható overhead), * poll-onként mintát tesz egy ring-bufferbe; a `getSnapshot()` a `/server/diag` endpoint (és * bármely fogyasztó) számára adja vissza az aktuális + közelmúlt-képet. A MemoryGuard MELLÉ * illeszkedik (nem duplikálja): memória-nyomás = MemoryGuard.pressure(); loop-lag = ez. * * Install: a base App startup (a MemoryGuard-install mintájára), `eventLoopDiag.enabled` gate-tel * (default: true — a mérés olcsó; opt-out lehetséges). */ export class DyNTS_EventLoopDiag { private static instance: DyNTS_EventLoopDiag; static getInstance(): DyNTS_EventLoopDiag { if (!DyNTS_EventLoopDiag.instance) { DyNTS_EventLoopDiag.instance = new DyNTS_EventLoopDiag(); } return DyNTS_EventLoopDiag.instance; } private monitor: IntervalHistogram | null = null; private pollTimer: NodeJS.Timeout | null = null; private history: DyNTS_EventLoopSample[] = []; private installed: boolean = false; /** A diag-mérés telepítése (idempotens; a settings-gate itt érvényesül). */ install(): void { if (this.installed) { return; } const settings: { enabled?: boolean; pollIntervalMs?: number; maxHistoryCount?: number } = DyNTS_global_settings.eventLoopDiag ?? {}; if (settings.enabled === false) { return; } const pollIntervalMs: number = settings.pollIntervalMs ?? 5000; const maxHistoryCount: number = settings.maxHistoryCount ?? 120; // natív histogram — 10ms felbontás bőven elég a wedge-detektáláshoz (a jel: 100ms+ lag) this.monitor = monitorEventLoopDelay({ resolution: 10 }); this.monitor.enable(); this.pollTimer = setInterval((): void => { if (!this.monitor) { return; } // a histogram ns-ben mér → ms-re váltunk; poll után reset (ablakonkénti kép) this.history.push({ at: Date.now(), p50Ms: Math.round(this.monitor.percentile(50) / 1e6), p95Ms: Math.round(this.monitor.percentile(95) / 1e6), maxMs: Math.round(this.monitor.max / 1e6), activeHandles: this.getActiveHandleCount(), }); this.monitor.reset(); if (this.history.length > maxHistoryCount) { this.history = this.history.slice(-maxHistoryCount); } }, pollIntervalMs); // a poll-timer NE tartsa életben a processzt this.pollTimer.unref?.(); this.installed = true; DyFM_Log.log(`Event-loop diag installed (poll: ${pollIntervalMs}ms, history: ${maxHistoryCount})`); } /** A diag-pillanatkép — a `/server/diag` endpoint (és bármely fogyasztó) forrása. */ getSnapshot(): { installed: boolean; current: DyNTS_EventLoopSample | null; history: DyNTS_EventLoopSample[]; } { return { installed: this.installed, current: this.history.length ? this.history[this.history.length - 1] : null, history: this.history, }; } /** Aktív handle-k száma — best-effort (a nem-publikus API hiányát 0-val toleráljuk). */ private getActiveHandleCount(): number { // a _getActiveHandles nem publikus API — best-effort diag-jel (connection/timer-leak trend) const getHandles: (() => unknown[]) | undefined = (process as unknown as { _getActiveHandles?: () => unknown[] })._getActiveHandles; try { return getHandles ? getHandles().length : 0; } catch { return 0; } } }