--- id: perf-node-profiling title: Node Profiling — CPU / Memory / Async hooks category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [performance, node, profiling, vibe-coding] tech_stack: { language: "Node / TS", applicable_to: ["Backend"] } applied_in: [] aliases: [clinic, 0x, flamegraph, heapdump, async hooks, --inspect] --- # Node Profiling > "느림" 감으로 X — **profile**. **CPU = clinic flame / 0x, Memory = heapdump, Event loop = clinic doctor**. Production = `--inspect` + Chrome DevTools. ## 📖 핵심 개념 - CPU profile: 어느 함수가 시간 소모. - Heap snapshot: 메모리 누가 보유. - Event loop lag: 비동기 정체. - Async hooks: async chain 추적. ## 💻 코드 패턴 ### Clinic.js (가장 단순) ```bash yarn add -D clinic clinic doctor -- node app.js # 실행 → 부하 (autocannon) → Ctrl+C # HTML report: doctor 가 진단 (CPU bound? I/O? memory?) clinic flame -- node app.js # Flame graph — 시간 hot spot clinic bubbleprof -- node app.js # Async operation 시각화 clinic heapprofiler -- node app.js # Memory allocation ``` ### 0x (flame graph) ```bash npx 0x app.js # 또는 0x -- node app.js ``` ### --inspect (Chrome DevTools) ```bash node --inspect app.js # 또는 prod (조심) node --inspect=0.0.0.0:9229 app.js # Chrome → chrome://inspect → DevTools # CPU profile, heap snapshot, allocation timeline ``` → Production attach 가능 (보안 주의). ### CPU profile (programmatic) ```ts import { Session } from 'node:inspector'; const session = new Session(); session.connect(); session.post('Profiler.enable', () => { session.post('Profiler.start', () => { setTimeout(() => { session.post('Profiler.stop', (err, { profile }) => { require('fs').writeFileSync('profile.cpuprofile', JSON.stringify(profile)); // Chrome DevTools → Performance → load profile }); }, 30_000); }); }); ``` ### Heap snapshot ```ts import { writeHeapSnapshot } from 'node:v8'; // Trigger: signal 또는 API process.on('SIGUSR2', () => { const path = `heap-${Date.now()}.heapsnapshot`; writeHeapSnapshot(path); console.log('snapshot:', path); }); // 분석: Chrome DevTools → Memory → Load snapshot ``` ```bash kill -USR2 $PID ``` → "Comparison" view 로 두 snapshot 비교 → leak. ### Memory leak 패턴 ```ts // ❌ Global Map 만 쌓임 const cache = new Map(); app.get('/x', (req, res) => { cache.set(req.id, req.data); // 영원 — leak }); // ✅ TTL / LRU import LRU from 'lru-cache'; const cache = new LRU({ max: 1000, ttl: 60_000 }); ``` ### Event loop lag ```ts import { monitorEventLoopDelay } from 'node:perf_hooks'; const h = monitorEventLoopDelay({ resolution: 20 }); h.enable(); setInterval(() => { console.log('p99 lag:', h.percentile(99) / 1e6, 'ms'); h.reset(); }, 5000); ``` → 50ms+ = problem. CPU bound 또는 sync I/O 의심. ### Async hooks (실험적, 비싸) ```ts import { createHook, executionAsyncId } from 'node:async_hooks'; const stack = new Map(); const hook = createHook({ init: (asyncId, type, triggerAsyncId) => { stack.set(asyncId, { type, parent: triggerAsyncId }); }, destroy: (asyncId) => stack.delete(asyncId), }); hook.enable(); ``` → AsyncLocalStorage 의 internal. Trace 추적. ### AsyncLocalStorage (context) ```ts import { AsyncLocalStorage } from 'node:async_hooks'; const als = new AsyncLocalStorage<{ requestId: string; userId?: string }>(); app.use((req, res, next) => { als.run({ requestId: uuid() }, next); }); // 어디서나 log.info({ ...als.getStore(), msg: 'event' }); ``` ### Production tracing (low overhead) ```ts // OpenTelemetry auto-instrumentation (위 OTel 문서 참조) // Sentry profiling import * as Sentry from '@sentry/node'; import { ProfilingIntegration } from '@sentry/profiling-node'; Sentry.init({ dsn, profilesSampleRate: 0.1, integrations: [new ProfilingIntegration()] }); ``` → Continuous profiling — 항상 켜져있음. ### perf_hooks 측정 ```ts import { performance } from 'node:perf_hooks'; const t = performance.now(); await heavyWork(); console.log('took', performance.now() - t, 'ms'); // PerformanceObserver import { PerformanceObserver, performance } from 'node:perf_hooks'; const obs = new PerformanceObserver((items) => { for (const e of items.getEntries()) console.log(e.name, e.duration); }); obs.observe({ type: 'measure' }); performance.mark('start'); await ...; performance.mark('end'); performance.measure('myWork', 'start', 'end'); ``` ### V8 flags (debugging) ```bash node --max-old-space-size=4096 app.js # heap 4GB node --trace-gc app.js # GC log node --prof app.js # CPU profile (raw) node --prof-process isolate-*.log # process ``` ### Memory limit ```bash # Default V8 = ~1.5 GB on 64-bit NODE_OPTIONS="--max-old-space-size=4096" node app.js ``` ### Hot path 측정 (JIT) ```ts // 반복 측정 — JIT warmup for (let i = 0; i < 10000; i++) myHotFn(); // warm const t = performance.now(); for (let i = 0; i < 100000; i++) myHotFn(); console.log((performance.now() - t) / 100000, 'us per call'); ``` ## 🤔 의사결정 기준 | 증상 | 도구 | |---|---| | CPU 100% | Flame graph (0x / clinic) | | Memory 자라남 | Heap snapshot diff | | 응답 느려짐 점진 | Event loop lag | | 가끔 spike | Continuous profile (Sentry) | | Async tracing | OTel + auto-instrumentation | | Production attach | --inspect=0.0.0.0:9229 (보안) | ## ❌ 안티패턴 - **`console.time` 만 + 추측**: 정확 X. profiler. - **Production --inspect 공개**: 누구나 attach. SSH tunnel. - **Async hook prod 항상**: 큰 overhead. 디버깅 시만. - **Heap snapshot prod 큰 process**: GC pause + freeze. - **JIT warmup 무시**: 첫 call 측정. - **Memory limit 안 설정 prod**: 무한 자라남. - **Sample rate 100% prod**: 비용. 1-10%. ## 🤖 LLM 활용 힌트 - Clinic doctor 가 진단 시작. - Heap snapshot diff 가 leak. - Event loop lag 가 핵심 SLI. - Production = Sentry / OTel sampled. ## 🔗 관련 문서 - [[Native_Memory_Profiling]] - [[DevOps_Observability_Stack]] - [[Backpressure_Patterns]]