CPU 프로파일링과 플레임 차트 — 병목 함수 찾기

Chrome DevTools Performance 탭으로 CPU 프로파일을 녹화하는 방법, 플레임 차트·Bottom-Up·Call Tree 뷰로 병목 함수를 찾는 방법, performance.mark/measure로 코드 구간을 마킹하는 실전 기법을 정리합니다.

· 8 min read · PALDYN Team

지난 글에서 힙 스냅샷으로 메모리 누수를 탐지하는 방법을 살펴봤습니다. 이번에는 성능 최적화 섹션의 마지막 주제인 CPU 프로파일링입니다. “어떤 함수가 가장 많은 CPU 시간을 쓰는가?”를 찾아내는 플레임 차트와 Bottom-Up 뷰, 그리고 performance.mark로 코드 구간을 직접 마킹하는 방법을 다룹니다.


CPU 프로파일링 시작

Chrome DevTools → Performance 탭에서 녹화 버튼을 클릭하면 CPU 프로파일 수집이 시작됩니다.

1. DevTools → Performance 탭
2. ⚙ 설정 → CPU throttling: 4x slowdown (모바일 환경 시뮬레이션)
3. 🔴 Record 버튼 클릭
4. 측정하고 싶은 동작 수행 (예: 버튼 클릭, 리스트 렌더링)
5. ⏹ Stop
6. 하단 Panel → "Bottom-Up" 또는 "Flame Chart" 탭 선택

CPU throttling 4x는 고사양 데스크탑에서 중저가 안드로이드 환경을 시뮬레이션합니다. 실제 사용자 환경에 가까운 프로파일을 얻을 수 있습니다.


플레임 차트 읽기

플레임 차트는 콜스택을 시간 축으로 펼쳐서 어떤 함수가 얼마나 오래 실행되었는지 한눈에 보여줍니다.

플레임 차트 읽기

읽는 법

  • 가로폭 = 실행 시간. 넓은 막대가 오래 걸린 함수
  • 세로 위치 = 콜스택 깊이. 아래로 갈수록 더 내부 호출
  • 가장 아래 가장 넓은 막대 = 실제 CPU 시간을 소비하는 함수 (리프 함수)

병목을 찾는 핵심은 **“탑(탑처럼 생긴 넓고 깊은 구조)“**을 찾는 것입니다. 상위 함수가 넓고 하위 함수도 비슷하게 넓다면, 그 탑의 바닥 함수가 실제 시간을 소비하는 곳입니다.


Bottom-Up 뷰 — 셀프 시간 기준 정렬

DevTools Performance → Bottom-Up 탭
"Self Time" 컬럼을 클릭해 내림차순 정렬
→ 가장 위 항목이 실제 CPU 시간을 가장 많이 쓴 함수

Self Time은 해당 함수 자체(자식 함수 제외)에 사용된 시간입니다. Total Time이 크더라도 자식 함수 때문일 수 있으므로, Self Time으로 실제 병목을 찾습니다.

// Bottom-Up에서 높은 Self Time을 가진 함수 예시

// ❌ 정렬 시 매번 Date 파싱 → new Date()의 Self Time이 높게 나옴
const sorted = items.sort((a, b) =>
  new Date(a.dateStr) - new Date(b.dateStr) // 비교마다 파싱
);

// ✅ 미리 변환 후 정렬 — compareFunc에서 Date 파싱 제거
const sorted = items
  .map(item => ({ ...item, timestamp: Date.parse(item.dateStr) }))
  .sort((a, b) => a.timestamp - b.timestamp);

performance.mark / measure — 코드 구간 마킹

DevTools 타임라인에서 어떤 코드 구간이 어디에 해당하는지 찾기 어려울 때, performance.mark로 마킹하면 타임라인에서 직접 확인할 수 있습니다.

// 특정 함수의 성능 측정
function measuredSort(items) {
  performance.mark('sort-start');

  const result = items
    .map(item => ({ ...item, ts: Date.parse(item.dateStr) }))
    .sort((a, b) => a.ts - b.ts);

  performance.mark('sort-end');
  performance.measure('정렬 작업', 'sort-start', 'sort-end');

  return result;
}

// 측정 결과 조회
const measures = performance.getEntriesByName('정렬 작업');
console.log(`정렬 시간: ${measures[0].duration.toFixed(2)}ms`);

// 모든 측정 정리
performance.clearMarks();
performance.clearMeasures();

performance.measure로 마킹된 구간은 DevTools Performance 타임라인의 Timings 행에 녹색 마커로 표시됩니다. 긴 녹화 중에서도 원하는 구간을 즉시 찾을 수 있습니다.


프로파일링 워크플로

CPU 프로파일링 워크플로


롱 태스크 식별 — PerformanceObserver

50ms 이상 걸리는 태스크는 Long Task로 분류되며 INP를 악화시킵니다. PerformanceObserver로 프로덕션에서도 자동 감지할 수 있습니다.

// 롱 태스크 자동 감지 — 프로덕션 모니터링
const observer = new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (entry.duration > 50) {
      // 실제 사용자 환경에서 발생한 롱 태스크 수집
      sendToMonitoring({
        type: 'longtask',
        duration: entry.duration,
        startTime: entry.startTime,
        url: location.href,
      });
    }
  }
});

observer.observe({ type: 'longtask', buffered: true });

롱 태스크 분할

// ❌ 단일 롱 태스크 — 메인 스레드 독점
function processAll(items) {
  return items.map(item => expensiveTransform(item)); // ~500ms
}

// ✅ 청크로 분할 — 브라우저에 제어권 반환
async function processChunked(items, chunkSize = 50) {
  const results = [];
  for (let i = 0; i < items.length; i += chunkSize) {
    const chunk = items.slice(i, i + chunkSize);
    results.push(...chunk.map(item => expensiveTransform(item)));
    // 각 청크 후 다음 프레임 양보
    await new Promise(resolve => setTimeout(resolve, 0));
  }
  return results;
}

Node.js CPU 프로파일링

# Node.js 내장 프로파일러
node --cpu-prof app.js
# isolate-*.cpuprofile 파일 생성 → Chrome DevTools에서 열기

# clinic.js — Node.js 전용 프로파일링 도구
npm install -g clinic
clinic flame -- node app.js
# 터미널에서 자동으로 플레임 차트 생성

최적화 전후 비교

// 최적화 전후를 정량적으로 비교
async function benchmark(fn, label, runs = 100) {
  const times = [];
  for (let i = 0; i < runs; i++) {
    const start = performance.now();
    await fn();
    times.push(performance.now() - start);
  }
  const avg = times.reduce((a, b) => a + b) / runs;
  const p95 = times.sort((a, b) => a - b)[Math.floor(runs * 0.95)];
  console.log(`[${label}] avg: ${avg.toFixed(2)}ms, p95: ${p95.toFixed(2)}ms`);
}

await benchmark(() => sortWithDateParsing(data), '최적화 전');
await benchmark(() => sortWithTimestamp(data), '최적화 후');
// [최적화 전] avg: 124.3ms, p95: 156.7ms
// [최적화 후] avg: 8.2ms, p95: 11.1ms

정리

CPU 프로파일링은 “추측이 아닌 측정”의 원칙이 가장 잘 드러나는 영역입니다.

  • Performance 탭 → CPU throttling 4x → 모바일 환경 기준으로 측정합니다.
  • 플레임 차트에서 가장 넓고 깊은 탑을 찾아 바닥의 리프 함수를 확인합니다.
  • Bottom-Up 뷰에서 Self Time 정렬로 실제 CPU를 소비하는 함수를 빠르게 찾습니다.
  • performance.mark로 관심 구간을 마킹하면 긴 녹화에서도 빠르게 탐색할 수 있습니다.
  • 최적화 전후를 benchmark 함수로 정량 비교해 효과를 검증합니다.

지난 글: 메모리 프로파일링 — 누수 탐지와 힙 스냅샷

다음 글: XSS — 크로스 사이트 스크립팅 완전 방어 가이드


읽어주셔서 감사합니다. 😊