measureUserAgentSpecificMemory() を使用してウェブページのメモリの合計使用量をモニタリングする

本番環境でウェブページのメモリ使用量を測定して回帰を検出する方法について学びます。

Ulan Degenbaev
Ulan Degenbaev

ブラウザはウェブページのメモリを自動的に管理します。ウェブページがオブジェクトを作成するたびに、ブラウザは「裏側」でメモリのチャンクを割り振り、オブジェクトを保存します。メモリは有限のリソースであるため、ブラウザはガベージ コレクションを実行して、オブジェクトが不要になったタイミングを検出し、基盤となるメモリ チャンクを解放します。

ただし、検出は完璧ではありません。完璧な検出は不可能であることが証明されています。したがって、ブラウザは「オブジェクトが必要」という概念を「オブジェクトに到達可能」という概念に近似します。ウェブページが、その変数と他の到達可能なオブジェクトのフィールドを介してオブジェクトに到達できない場合、ブラウザはオブジェクトを安全に再利用できます。次の例に示すように、これらの概念の違いがメモリリークを引き起こします。

const object = {a: new Array(1000), b: new Array(2000)};
setInterval(() => console.log(object.a), 1000);

ここでは、大きな配列 b は不要になりましたが、コールバックの object.b から引き続きアクセスできるため、ブラウザはそれを再利用しません。そのため、大きな配列のメモリがリークします。

メモリリークはウェブでよく発生する問題です。イベント リスナーの登録解除を忘れたり、iframe からオブジェクトを誤ってキャプチャしたり、ワーカーを閉じなかったり、配列にオブジェクトを蓄積したりすると、簡単に発生します。ウェブページにメモリリークがあると、時間の経過とともにメモリ使用量が増加し、ウェブページが遅く、肥大化しているとユーザーに認識されます。

この問題を解決する最初のステップは、問題を測定することです。新しい performance.measureUserAgentSpecificMemory() API を使用すると、デベロッパーは本番環境でウェブページのメモリ使用量を測定し、ローカルテストで検出できなかったメモリリークを検出できます。

performance.measureUserAgentSpecificMemory() は以前の performance.memory API とどう違うのですか?

既存の非標準 performance.memory API に精通している場合は、新しい API とその違いについて疑問に思われるかもしれません。主な違いは、古い API が JavaScript ヒープサイズを返すのに対し、新しい API はウェブページで使用されるメモリを推定することである。この違いは、Chrome が複数のウェブページ(または同じウェブページの複数のインスタンス)と同じヒープを使用する場合に重要になります。このような場合、古い API の結果は任意にずれる可能性があります。古い API は「ヒープ」などの実装固有の用語で定義されているため、標準化は不可能です。

別の違いとして、新しい API はガベージ コレクション中にメモリ測定を行います。これにより、結果のノイズが低減されますが、結果が生成されるまでに時間がかかる場合があります。他のブラウザでは、ガベージ コレクションに依存せずに新しい API を実装する場合があります。

推奨されるユースケース

ウェブページのメモリ使用量は、イベント、ユーザー アクション、ガベージ コレクションのタイミングによって異なります。そのため、メモリ測定 API は、本番環境のメモリ使用量データを集約することを目的としています。個々の呼び出しの結果は有用性が低くなります。ユースケースの例:

  • 新しいバージョンのウェブページのロールアウト中にリグレッション検知を行い、新しいメモリリークを検出します。
  • 新しい機能の A/B テストで、メモリへの影響を評価し、メモリリークを検出します。
  • メモリ使用量とセッション継続時間を関連付けて、メモリリークの有無を確認します。
  • メモリ使用量とユーザー指標を関連付けて、メモリ使用量の全体的な影響を把握する。

ブラウザの互換性

対応ブラウザ

  • Chrome: 89。
  • Edge: 89.
  • Firefox: サポートされていません。
  • Safari: サポートされていません。

ソース

現在、この API は Chromium ベースのブラウザ(Chrome 89 以降)でのみサポートされています。ブラウザによってメモリ内のオブジェクトの表現方法やメモリ使用量の推定方法が異なるため、API の結果は実装に大きく依存します。適切なアカウンティングが高コストすぎる場合や不可能な場合は、ブラウザによって一部のメモリ領域がアカウンティングから除外されることがあります。したがって、ブラウザ間で結果を比較することはできません。比較できるのは、同じブラウザの結果のみです。

performance.measureUserAgentSpecificMemory() の使用

特徴検出

実行環境がクロスオリジン情報漏洩を防ぐためのセキュリティ要件を満たしていない場合、performance.measureUserAgentSpecificMemory 関数は使用できなくなります。また、SecurityError で失敗することもあります。これはクロスオリジン分離に依存しています。ウェブページは COOP+COEP ヘッダーを設定することで、クロスオリジン分離を有効にできます。

サポートは実行時に検出できます。

if (!window.crossOriginIsolated) {
  console.log('performance.measureUserAgentSpecificMemory() is only available in cross-origin-isolated pages');
} else if (!performance.measureUserAgentSpecificMemory) {
  console.log('performance.measureUserAgentSpecificMemory() is not available in this browser');
} else {
  let result;
  try {
    result = await performance.measureUserAgentSpecificMemory();
  } catch (error) {
    if (error instanceof DOMException && error.name === 'SecurityError') {
      console.log('The context is not secure.');
    } else {
      throw error;
    }
  }
  console.log(result);
}

ローカルテスト

Chrome はガベージ コレクション中にメモリ測定を実行します。つまり、API は結果の Promise をすぐに解決せず、次のガベージ コレクションを待機します。

API を呼び出すと、一定のタイムアウト後に強制的にガベージ コレクションが行われます。現在、タイムアウトは 20 秒に設定されていますが、それより早く行われる場合もあります。--enable-blink-features='ForceEagerMeasureMemory' コマンドライン フラグを使用して Chrome を起動すると、タイムアウトがゼロに短縮されます。これは、ローカルのデバッグとテストに役立ちます。

この API の推奨される使用方法は、ウェブページ全体のメモリ使用量をサンプリングし、結果をサーバーに送信して集計と分析を行うグローバル メモリ モニターを定義することです。最も簡単な方法は、定期的にサンプリングすることです(たとえば M 分ごとにサンプリングします)。ただし、サンプル間でメモリがピーク状態になる可能性があるため、データにバイアスが生じます。

次の例は、ポアソン過程を使用して偏見のないメモリ測定を行う方法を示しています。これにより、サンプルが任意の時点で発生する可能性を保証します(デモソース)。

まず、ランダムな間隔で setTimeout() を使用して次のメモリ測定をスケジュールする関数を定義します。

function scheduleMeasurement() {
  // Check measurement API is available.
  if (!window.crossOriginIsolated) {
    console.log('performance.measureUserAgentSpecificMemory() is only available in cross-origin-isolated pages');
    console.log('See https://web.dev/coop-coep/ to learn more')
    return;
  }
  if (!performance.measureUserAgentSpecificMemory) {
    console.log('performance.measureUserAgentSpecificMemory() is not available in this browser');
    return;
  }
  const interval = measurementInterval();
  console.log(`Running next memory measurement in ${Math.round(interval / 1000)} seconds`);
  setTimeout(performMeasurement, interval);
}

measurementInterval() 関数は、平均で 5 分ごとに 1 回の測定が行われるように、ランダムな間隔(ミリ秒単位)を計算します。この関数の背後にある数学に興味がある場合は、指数分布をご覧ください。

function measurementInterval() {
  const MEAN_INTERVAL_IN_MS = 5 * 60 * 1000;
  return -Math.log(Math.random()) * MEAN_INTERVAL_IN_MS;
}

最後に、非同期の performMeasurement() 関数が API を呼び出し、結果を記録して、次の測定をスケジュールします。

async function performMeasurement() {
  // 1. Invoke performance.measureUserAgentSpecificMemory().
  let result;
  try {
    result = await performance.measureUserAgentSpecificMemory();
  } catch (error) {
    if (error instanceof DOMException && error.name === 'SecurityError') {
      console.log('The context is not secure.');
      return;
    }
    // Rethrow other errors.
    throw error;
  }
  // 2. Record the result.
  console.log('Memory usage:', result);
  // 3. Schedule the next measurement.
  scheduleMeasurement();
}

最後に、測定を開始します。

// Start measurements.
scheduleMeasurement();

結果は次のようになります。

// Console output:
{
  bytes: 60_100_000,
  breakdown: [
    {
      bytes: 40_000_000,
      attribution: [{
        url: 'https://example.com/',
        scope: 'Window',
      }],
      types: ['JavaScript']
    },

    {
      bytes: 20_000_000,
      attribution: [{
          url: 'https://example.com/iframe',
          container: {
            id: 'iframe-id-attribute',
            src: '/iframe',
          },
          scope: 'Window',
      }],
      types: ['JavaScript']
    },

    {
      bytes: 100_000,
      attribution: [],
      types: ['DOM']
    },
  ],
}

合計メモリ使用量の推定値が bytes フィールドに返されます。この値は実装に大きく依存するため、ブラウザ間で比較することはできません。同じブラウザの異なるバージョンでも異なる場合があります。この値には、現在のプロセス内のすべての iframe、関連するウィンドウ、ウェブワーカーの JavaScript メモリと DOM メモリが含まれます。

breakdown リストには、使用済みメモリに関する詳細情報が表示されます。各エントリはメモリの一部を記述し、URL で識別される一連のウィンドウ、iframe、ワーカーに関連付けます。types フィールドには、メモリに関連付けられている実装固有のメモリタイプが一覧表示されます。

すべてのリストを汎用的な方法で扱い、特定のブラウザに基づく前提条件をハードコードしないことが重要です。たとえば、一部のブラウザでは、空の breakdown または空の attribution が返される場合があります。他のブラウザでは、attribution に複数のエントリが返されることがあります。これは、どのエントリがメモリを所有しているかを区別できなかったことを示します。

フィードバック

ウェブ パフォーマンス コミュニティ グループと Chrome チームは、performance.measureUserAgentSpecificMemory() に関するご意見やご感想をお待ちしております。

API 設計について

API が想定どおりに機能していない点はありますか?または、アイデアを実装するために必要なプロパティが不足していますか?performance.measureUserAgentSpecificMemory() GitHub リポジトリで仕様に関する問題を報告するか、既存の問題にコメントを追加してください。

実装に関する問題を報告する

Chrome の実装にバグが見つかりましたか?それとも、実装が仕様と異なるのでしょうか?new.crbug.com でバグを報告します。できるだけ詳細な情報を含め、バグの再現手順を簡単に説明してください。[コンポーネント] は Blink>PerformanceAPIs に設定します。Glitch は、簡単な再現手順をすばやく共有するのに適しています。

クリエイターを応援する

performance.measureUserAgentSpecificMemory() を使用する予定はありますか?公開サポートは、Chrome チームが機能を優先付けするうえで役立ち、他のブラウザ ベンダーにその機能のサポートが重要であることを示します。@ChromiumDev にツイートして、どこでどのように使用しているかをお知らせください。

関連情報

謝辞

API 設計のレビューを担当した Domenic Denicola 氏、Yoav Weiss 氏、Mathias Bynens 氏、Chrome のコードレビューを担当した Dominik Inführ 氏、Hannes Payer 氏、Kentaro Hara 氏、Michael Lippautz 氏に感謝します。また、API を大幅に改善する貴重なユーザー フィードバックを送信していただいた Per Parker、Philipp Weis、Olga Belomestnykh、Matthew Bolohan、Neil Mckay にも感謝いたします。

ヒーロー画像: Harrison BroadbentUnsplash