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

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

Brendan Kenny
Brendan Kenny
Ulan Degenbaev
Ulan Degenbaev

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

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

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