NodeJS 速習チュートリアル

Node.js Performance Hooks モジュール

1. Performance Hooks とは?

perf_hooks モジュールは、W3C Performance Timeline 仕様に基づいたパフォーマンス測定用 API を提供します。

これらのツールは以下の用途に不可欠です:

  • 特定の操作にかかる時間の測定
  • パフォーマンス上のボトルネックの特定
  • 異なる実装方法によるパフォーマンスの比較
  • アプリケーションのパフォーマンス推移の追跡

このモジュールには、高精度タイマー(High-resolution timers)、パフォーマンスマーク(Marks)、メジャー(Measures)、オブザーバー(Observers)、ヒストグラム(Histograms)など、多くの有用な機能が含まれています。

2. Performance Hooks モジュールの使用方法

Performance Hooks モジュールを使用するには、コード内で require する必要があります。

// モジュール全体をインポート
const { performance, PerformanceObserver } = require('perf_hooks');

// 特定のパーツのみを分割代入でインポート
const { performance } = require('perf_hooks');

3. 基本的な時間計測

performance API の最も基本的な使い方は、経過時間を高精度に測定することです。

const { performance } = require('perf_hooks');

// 現在の高精度タイムスタンプを取得
const startTime = performance.now();

// 何らかの操作を実行
let sum = 0;
for (let i = 0; i < 1000000; i++) {
  sum += i;
}

// 終了時間を取得
const endTime = performance.now();

// 経過時間をミリ秒単位で計算・表示
console.log(`操作にかかった時間: ${(endTime - startTime).toFixed(2)} ミリ秒`);

performance.now() メソッドは、現在の Node.js プロセスが起動した時を起点とした、高精度のタイムスタンプ(ミリ秒単位)を返します。

4. パフォーマンスマーク(Marks)とメジャー(Measures)

4.1 Marks (マーク)

パフォーマンスマークは、追跡したい特定の時点を記録するものです。

const { performance } = require('perf_hooks');

// コード内の特定のポイントにマークを作成
performance.mark('startProcess');

// 処理をシミュレート
let result = 0;
for (let i = 0; i < 1000000; i++) {
  result += Math.sqrt(i);
}

// 別のマークを作成
performance.mark('endProcess');

// すべてのマークを取得
console.log(performance.getEntriesByType('mark'));

4.2 Measures (メジャー)

パフォーマンスメジャーは、2つのマーク間の時間を計算します。

const { performance } = require('perf_hooks');

// 開始マークを作成
performance.mark('start');

// 処理をシミュレート
let result = 0;
for (let i = 0; i < 1000000; i++) {
  result += Math.sqrt(i);
}

// 終了マークを作成
performance.mark('end');

// 2つのマーク間のメジャー(計測)を作成
performance.measure('processTime', 'start', 'end');

// メジャーを取得
const measure = performance.getEntriesByName('processTime')[0];
console.log(`処理時間は ${measure.duration.toFixed(2)} ミリ秒でした`);

// マークとメジャーをクリア
performance.clearMarks();
performance.clearMeasures();

5. Performance Observer (パフォーマンス・オブザーバー)

PerformanceObserver を使用すると、パフォーマンスイベントを非同期に監視できます。

const { performance, PerformanceObserver } = require('perf_hooks');

// パフォーマンスオブザーバーを作成
const obs = new PerformanceObserver((items) => {
  // すべてのエントリを処理
  const entries = items.getEntries();
  entries.forEach((entry) => {
    console.log(`名前: ${entry.name}, タイプ: ${entry.entryType}, 所要時間: ${entry.duration.toFixed(2)}ms`);
  });
});

// 特定のエントリタイプ(今回は measure)を購読
obs.observe({ entryTypes: ['measure'] });

// タスク1
performance.mark('task1Start');
// 処理をシミュレート
setTimeout(() => {
  performance.mark('task1End');
  performance.measure('タスク 1', 'task1Start', 'task1End');
  
  // タスク2
  performance.mark('task2Start');
  setTimeout(() => {
    performance.mark('task2End');
    performance.measure('タスク 2', 'task2Start', 'task2End');
    
    // クリーンアップ
    performance.clearMarks();
    performance.clearMeasures();
    obs.disconnect();
  }, 1000);
}, 1000);

6. Performance Timeline API

Performance Timeline API は、記録されたパフォーマンスエントリを取得するためのメソッドを提供します。

const { performance } = require('perf_hooks');

// パフォーマンスエントリをいくつか作成
performance.mark('mark1');
performance.mark('mark2');

let sum = 0;
for (let i = 0; i < 100000; i++) {
  sum += i;
}

performance.mark('mark3');
performance.measure('measure1', 'mark1', 'mark2');
performance.measure('measure2', 'mark2', 'mark3');

// すべてのパフォーマンスエントリを表示
console.log('すべてのエントリ:');
console.log(performance.getEntries());

// タイプ別にエントリを取得
console.log('\nマーク一覧:');
console.log(performance.getEntriesByType('mark'));

// 名前を指定してエントリを取得
console.log('\nメジャー 1:');
console.log(performance.getEntriesByName('measure1'));

7. パフォーマンス・タイミングのレベル

Node.js は、精度レベルの異なる複数のタイミング API を提供しています。

const { performance, monitorEventLoopDelay } = require('perf_hooks');

// 1. Date.now() - ミリ秒単位の精度
const dateStart = Date.now();
const dateEnd = Date.now();
console.log(`Date.now() の差分: ${dateEnd - dateStart}ms`);

// 2. process.hrtime() - ナノ秒単位の精度
const hrStart = process.hrtime();
const hrEnd = process.hrtime(hrStart);
console.log(`process.hrtime() の差分: ${hrEnd[0]}秒 ${hrEnd[1]}ナノ秒`);

// 3. performance.now() - マイクロ秒単位の精度
const perfStart = performance.now();
const perfEnd = performance.now();
console.log(`performance.now() の差分: ${(perfEnd - perfStart).toFixed(6)}ms`);

// 4. イベントループの遅延監視 (Node.js 12.0.0+ で利用可能)
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();

setTimeout(() => {
  histogram.disable();
  console.log('イベントループ遅延のメトリクス:');
  console.log(`  最小値: ${histogram.min}ns`);
  console.log(`  最大値: ${histogram.max}ns`);
  console.log(`  平均値: ${histogram.mean.toFixed(2)}ns`);
  console.log(`  標準偏差: ${histogram.stddev.toFixed(2)}ns`);
  console.log(`  パーセンタイル: 50=${histogram.percentile(50).toFixed(2)}ns, 99=${histogram.percentile(99).toFixed(2)}ns`);
}, 1000);

8. イベントループの監視(Event Loop Monitoring)

monitorEventLoopDelay 関数は、イベントループの遅延を監視する手段を提供します。

const { monitorEventLoopDelay } = require('perf_hooks');

// ヒストグラムを作成
const histogram = monitorEventLoopDelay({ resolution: 10 });

// 監視を有効化
histogram.enable();

// イベントループへの負荷をシミュレート
const operations = [];
for (let i = 0; i < 10; i++) {
  operations.push(new Promise((resolve) => {
    setTimeout(() => {
      // CPU 負荷の高い処理をシミュレート
      let sum = 0;
      for (let j = 0; j < 10000000; j++) {
        sum += j;
      }
      resolve(sum);
    }, 100);
  }));
}

// すべての操作が完了した後
Promise.all(operations).then(() => {
  // 監視を無効化
  histogram.disable();
  
  // 統計情報を出力
  console.log('イベントループ遅延統計:');
  console.log(`  最小値: ${histogram.min}ns`);
  console.log(`  最大値: ${histogram.max}ns`);
  console.log(`  平均値: ${histogram.mean.toFixed(2)}ns`);
  console.log(`  標準偏差: ${histogram.stddev.toFixed(2)}ns`);
  
  // パーセンタイル
  console.log('\nパーセンタイル:');
  [1, 10, 50, 90, 99, 99.9].forEach((p) => {
    console.log(`  p${p}: ${histogram.percentile(p).toFixed(2)}ns`);
  });
});

イベントループの監視は、長時間実行されるタスクがイベントループをブロックし、アプリケーションのレスポンス性が低下している可能性がある問題を検出するのに非常に役立ちます。

9. 非同期操作におけるパフォーマンス追跡

非同期操作のパフォーマンスを追跡するには、マークを配置する場所に注意が必要です。

const { performance, PerformanceObserver } = require('perf_hooks');
const fs = require('fs');

// メジャーを記録するためのオブザーバーを作成
const obs = new PerformanceObserver((items) => {
  items.getEntries().forEach((entry) => {
    console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
  });
});
obs.observe({ entryTypes: ['measure'] });

// 非同期のファイル読み込み操作を測定
performance.mark('readStart');

fs.readFile(__filename, (err, data) => {
  if (err) throw err;
  
  performance.mark('readEnd');
  performance.measure('ファイル読み込み', 'readStart', 'readEnd');
  
  // 非同期処理の時間を測定
  performance.mark('processStart');
  
  // ファイルデータの処理をシミュレート
  setTimeout(() => {
    const lines = data.toString().split('\n').length;
    
    performance.mark('processEnd');
    performance.measure('ファイル処理', 'processStart', 'processEnd');
    
    console.log(`ファイルは ${lines} 行あります`);
    
    // クリーンアップ
    performance.clearMarks();
    performance.clearMeasures();
  }, 100);
});

10. Promise の追跡

Promise のパフォーマンス測定も同様のテクニックで行います。

const { performance, PerformanceObserver } = require('perf_hooks');

// オブザーバーの設定
const obs = new PerformanceObserver((items) => {
  items.getEntries().forEach((entry) => {
    console.log(`${entry.name}: ${entry.duration.toFixed(2)}ms`);
  });
});
obs.observe({ entryTypes: ['measure'] });

// Promise を返す関数
function fetchData(delay) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ data: 'サンプルデータ' });
    }, delay);
  });
}

// データを処理する関数
function processData(data) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ processed: data.data.toUpperCase() });
    }, 200);
  });
}

// Promise チェーンを測定
async function run() {
  performance.mark('fetchStart');
  
  const data = await fetchData(300);
  
  performance.mark('fetchEnd');
  performance.mark('processStart');
  
  const processed = await processData(data);
  
  performance.mark('processEnd');
  
  // メジャーの作成
  performance.measure('データ取得', 'fetchStart', 'fetchEnd');
  performance.measure('データ処理', 'processStart', 'processEnd');
  performance.measure('全工程', 'fetchStart', 'processEnd');
  
  console.log('結果:', processed);
}

run().finally(() => {
  // 実行後にクリア
  performance.clearMarks();
  performance.clearMeasures();
});

11. パフォーマンス計測における注意点

パフォーマンス API を使用する際は、以下の点に注意してください。

  • タイミングの解像度はプラットフォームによって異なります。
  • 長時間稼働するプロセスでは、クロックドリフト(時間のズレ)が発生する可能性があります。
  • バックグラウンドアクティビティがタイミング計測に影響を与えることがあります。
  • JavaScript の JIT コンパイルにより、初回の実行時間が安定しない場合があります。
const { performance } = require('perf_hooks');

// 正確なベンチマークのために、複数回実行する
function benchmark(fn, iterations = 1000) {
  // ウォームアップ実行(JIT 最適化のため)
  fn();
  
  const times = [];
  
  for (let i = 0; i < iterations; i++) {
    const start = performance.now();
    fn();
    const end = performance.now();
    times.push(end - start);
  }
  
  // 統計の計算
  times.sort((a, b) => a - b);
  
  const sum = times.reduce((a, b) => a + b, 0);
  const avg = sum / times.length;
  const median = times[Math.floor(times.length / 2)];
  const min = times[0];
  const max = times[times.length - 1];
  
  return {
    平均値: avg,
    中央値: median,
    最小値: min,
    最大値: max,
    サンプル数: times.length
  };
}

// 使用例
function testFunction() {
  // ベンチマーク対象の関数
  let x = 0;
  for (let i = 0; i < 10000; i++) {
    x += i;
  }
  return x;
}

const results = benchmark(testFunction);
console.log('ベンチマーク結果:');
console.log(`  サンプル数: ${results.サンプル数}`);
console.log(`  平均値: ${results.平均値.toFixed(4)}ms`);
console.log(`  中央値: ${results.中央値.toFixed(4)}ms`);
console.log(`  最小値: ${results.最小値.toFixed(4)}ms`);
console.log(`  最大値: ${results.最大値.toFixed(4)}ms`);

12. Node.js Performance Hooks vs ブラウザ Performance API

Node.js の Performance Hooks API は W3C Performance Timeline 仕様に基づいていますが、ブラウザの Performance API とはいくつか違いがあります。

機能ブラウザ Performance APINode.js Performance Hooks
時刻の起点 (Time Origin)ページ遷移の開始時プロセスの開始時
リソースタイミング利用可能該当なし
ナビゲーションタイミング利用可能該当なし
ユーザータイミング (mark/measure)利用可能利用可能
高精度時刻利用可能利用可能
イベントループ監視制限あり利用可能

13. 実践的な例:API パフォーマンス・モニタリング

API エンドポイントを監視するためにパフォーマンスフックを使用する実践的な例です。

const { performance, PerformanceObserver } = require('perf_hooks');
const express = require('express');
const app = express();
const port = 8080;

// ログ出力用のパフォーマンスオブザーバーを設定
const obs = new PerformanceObserver((items) => {
  items.getEntries().forEach((entry) => {
    console.log(`[${new Date().toISOString()}] ${entry.name}: ${entry.duration.toFixed(2)}ms`);
  });
});
obs.observe({ entryTypes: ['measure'] });

// リクエスト処理時間を追跡するミドルウェア
app.use((req, res, next) => {
  const requestId = `${req.method} ${req.url} ${Date.now()}`;
  
  // リクエスト処理の開始をマーク
  performance.mark(`${requestId}-start`);
  
  // レスポンス送信時を捉えるために end メソッドをオーバーライド
  const originalEnd = res.end;
  res.end = function(...args) {
    performance.mark(`${requestId}-end`);
    performance.measure(
      `リクエスト ${req.method} ${req.url}`,
      `${requestId}-start`,
      `${requestId}-end`
    );
    
    // マークのクリーンアップ
    performance.clearMarks(`${requestId}-start`);
    performance.clearMarks(`${requestId}-end`);
    
    return originalEnd.apply(this, args);
  };
  
  next();
});

// API ルート
app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.get('/fast', (req, res) => {
  res.send('高速レスポンス!');
});

app.get('/slow', (req, res) => {
  // 遅い API エンドポイントをシミュレート
  setTimeout(() => {
    res.send('ディレイ後の遅いレスポンス');
  }, 500);
});

app.get('/process', (req, res) => {
  // CPU 負荷の高い処理をシミュレート
  const requestId = `process-${Date.now()}`;
  performance.mark(`${requestId}-process-start`);
  
  let result = 0;
  for (let i = 0; i < 1000000; i++) {
    result += Math.sqrt(i);
  }
  
  performance.mark(`${requestId}-process-end`);
  performance.measure(
    'CPU 処理',
    `${requestId}-process-start`,
    `${requestId}-process-end`
  );
  
  res.send(`処理結果: ${result}`);
});

// サーバー起動
app.listen(port, () => {
  console.log(`パフォーマンス監視の例が http://localhost:${port} で実行中です`);
});

14. 高度なパフォーマンス監視

本番環境レベルのアプリケーションでは、以下の高度な監視手法を検討してください。

14.1 メモリリークの検出

パフォーマンスフックとメモリ監視を使用した、メモリリークの検出と分析です。

const { performance, PerformanceObserver } = require('perf_hooks');

class MemoryMonitor {
  constructor() {
    this.leakThreshold = 10 * 1024 * 1024; // 10MB
    this.checkInterval = 10000; // 10秒
    this.interval = null;
    this.lastMemoryUsage = process.memoryUsage();
    this.leakDetected = false;
    
    // GC(ガベージコレクション)イベント用のパフォーマンスオブザーバー
    const obs = new PerformanceObserver((items) => {
      items.getEntries().forEach((entry) => {
        if (entry.name === 'gc') {
          this.checkMemoryLeak();
        }
      });
    });
    obs.observe({ entryTypes: ['gc'] });
  }
  
  start() {
    console.log('メモリ監視を開始しました');
    this.interval = setInterval(() => this.checkMemoryLeak(), this.checkInterval);
  }
  
  stop() {
    if (this.interval) {
      clearInterval(this.interval);
      console.log('メモリ監視を停止しました');
    }
  }
  
  checkMemoryLeak() {
    const current = process.memoryUsage();
    const heapDiff = current.heapUsed - this.lastMemoryUsage.heapUsed;
    
    if (heapDiff > this.leakThreshold) {
      this.leakDetected = true;
      console.warn(`⚠️  メモリリークの可能性を検出: ヒープが ${(heapDiff / 1024 / 1024).toFixed(2)}MB 増加しました`);
      console.log('メモリ・スナップショット:', {
        rss: this.formatMemory(current.rss),
        heapTotal: this.formatMemory(current.heapTotal),
        heapUsed: this.formatMemory(current.heapUsed),
        external: this.formatMemory(current.external)
      });
      
      // 必要に応じてヒープダンプを出力
      if (process.env.NODE_ENV === 'development') {
        this.takeHeapSnapshot();
      }
    }
    
    this.lastMemoryUsage = current;
  }
  
  formatMemory(bytes) {
    return `${(bytes / 1024 / 1024).toFixed(2)}MB`;
  }
  
  takeHeapSnapshot() {
    // 実際の実装には heapdump パッケージなどが必要です
    console.log('ヒープスナップショットを記録中...');
  }
}

// 使用例
const monitor = new MemoryMonitor();
monitor.start();

// メモリリークをシミュレート
const leaks = [];
setInterval(() => {
  for (let i = 0; i < 1000; i++) {
    leaks.push(new Array(1000).fill('*'.repeat(100)));
  }
}, 1000);

// 1分後に監視を停止
setTimeout(() => {
  monitor.stop();
  console.log('メモリ監視を終了しました');
}, 60000);

       注意: メモリリーク検出の例でヒープスナップショットを出力するには、npm install heapdump でパッケージをインストールする必要があります。

14.2 カスタム・パフォーマンス・メトリクス

詳細なタイミング情報を含むカスタムメトリクスの追跡です。

const { performance, PerformanceObserver } = require('perf_hooks');

class PerformanceTracker {
  constructor() {
    this.metrics = new Map();
    this.setupDefaultObserver();
  }
  
  setupDefaultObserver() {
    const obs = new PerformanceObserver((items) => {
      items.getEntries().forEach((entry) => {
        if (!this.metrics.has(entry.name)) {
          this.metrics.set(entry.name, []);
        }
        this.metrics.get(entry.name).push(entry);
        this.logMetric(entry);
      });
    });
    
    obs.observe({ entryTypes: ['measure'] });
  }
  
  startTimer(name) {
    performance.mark(`${name}-start`);
  }
  
  endTimer(name, attributes = {}) {
    performance.mark(`${name}-end`);
    performance.measure(name, {
      start: `${name}-start`,
      end: `${name}-end`,
      ...attributes
    });
    
    performance.clearMarks(`${name}-start`);
    performance.clearMarks(`${name}-end`);
  }
  
  logMetric(entry) {
    const { name, duration, detail } = entry;
    console.log(`📊 [${new Date().toISOString()}] ${name}: ${duration.toFixed(2)}ms`);
    if (detail) {
      console.log('   詳細:', JSON.stringify(detail, null, 2));
    }
  }
  
  getStats(name) {
    const metrics = this.metrics.get(name) || [];
    if (metrics.length === 0) return null;
    
    const durations = metrics.map(m => m.duration);
    const sum = durations.reduce((a, b) => a + b, 0);
    
    return {
      件数: durations.length,
      合計: sum,
      平均: sum / durations.length,
      最小: Math.min(...durations),
      最大: Math.max(...durations)
    };
  }
}

// 使用例
const tracker = new PerformanceTracker();

tracker.startTimer('db-query');
setTimeout(() => {
  tracker.endTimer('db-query', {
    detail: { query: 'SELECT * FROM users', success: true }
  });
  console.log('統計情報:', tracker.getStats('db-query'));
}, 200);

15. パフォーマンスフックによる分散トレーシング

マイクロサービス間で分散トレーシング(Distributed Tracing)を実装する例です。

const { performance } = require('perf_hooks');
const crypto = require('crypto');

class Tracer {
  constructor(serviceName) {
    this.serviceName = serviceName;
    this.spans = new Map();
  }
  
  startSpan(name, parentSpanId = null) {
    const spanId = crypto.randomBytes(8).toString('hex');
    const traceId = parentSpanId ? this.spans.get(parentSpanId)?.traceId : crypto.randomBytes(16).toString('hex');
    
    const span = {
      id: spanId,
      traceId,
      parentSpanId,
      name,
      service: this.serviceName,
      startTime: performance.now(),
      tags: {}
    };
    
    this.spans.set(spanId, span);
    return spanId;
  }
  
  endSpan(spanId, status = 'OK') {
    const span = this.spans.get(spanId);
    if (!span) return;
    
    span.endTime = performance.now();
    span.duration = span.endTime - span.startTime;
    span.status = status;
    
    console.log('スパンをエクスポート中:', JSON.stringify(span, null, 2));
    this.spans.delete(spanId);
  }
  
  addTag(spanId, key, value) {
    const span = this.spans.get(spanId);
    if (span) span.tags[key] = value;
  }
}

// 使用例
const tracer = new Tracer('user-service');

function handleRequest(req) {
  const spanId = tracer.startSpan('handle-request');
  tracer.addTag(spanId, 'http.method', req.method);
  
  setTimeout(() => {
    const childSpanId = tracer.startSpan('auth-check', spanId);
    setTimeout(() => {
      tracer.endSpan(childSpanId);
      tracer.endSpan(spanId);
    }, 100);
  }, 50);
}

handleRequest({ method: 'GET', url: '/api/data' });

16. パフォーマンス最適化テクニック

Node.js アプリケーションを最適化するための高度なテクニックです。

16.1 CPU 負荷の高いタスクに Worker Threads を使用

イベントループのブロッキングを防ぐため、重い処理をワーカースレッドにオフロードします。

const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const { performance } = require('perf_hooks');

if (isMainThread) {
  function runWorker(data) {
    return new Promise((resolve, reject) => {
      const start = performance.now();
      const worker = new Worker(__filename, { workerData: data });
      
      worker.on('message', (result) => {
        const duration = performance.now() - start;
        resolve({ ...result, duration: `${duration.toFixed(2)}ms` });
      });
      worker.on('error', reject);
    });
  }
  
  runWorker({ data: [1, 2, 3] }).then(console.log);
} else {
  // ワーカースレッド側の処理
  const result = workerData.data.map(x => x * 2);
  parentPort.postMessage({ result });
}

16.2 効率的なデータ処理

大量のデータを処理する際は、ストリームとバッファを適切に使用します。

const { Transform } = require('stream');
const { performance } = require('perf_hooks');

class ProcessingPipeline {
  constructor() {
    this.startTime = performance.now();
    this.processedItems = 0;
  }
  
  getStats() {
    const duration = performance.now() - this.startTime;
    return {
      処理件数: this.processedItems,
      所要時間: `${duration.toFixed(2)}ms`,
      スループット: (this.processedItems / (duration / 1000)).toFixed(2) + ' 件/秒'
    };
  }
}

17. パフォーマンス・テストのベストプラクティス

テストを実施する際は、以下のベストプラクティスに従ってください。

  • 本番に近い環境でテストする
    • 本番と同様のハードウェア、データ量、トラフィックパターンを使用します。
  • 統計的有意性を確保する
    • テストを複数回繰り返し、中央値や 95 パーセンタイルを算出します。外れ値は適切に除外します。
  • システムリソースを監視する
    • CPU、メモリ、ガベージコレクション、メモリリークを同時に追跡します。
  • 最適化の前にプロファイリングを行う
    • 実際のボトルネックを特定し、影響の大きい箇所から修正します。

18. まとめ

WebAssembly や Worker Threads と並び、Performance Hooks は Node.js を高性能なプラットフォームにするための鍵となります。

  • perf_hooks を使用して高精度の計測を行う。
  • PerformanceObserver で非同期にメトリクスを収集する。
  • monitorEventLoopDelay でイベントループの健全性を監視する。
  • 計測結果に基づいて、Worker Threads やストリームによる最適化を適用する。

これらを体系的に実装することで、推測ではなくデータに基づいたパフォーマンス改善が可能になります。