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

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

ブレンダン・ケニー(Brendan Kenny)氏
Brendan Kenny
ウラン・デゲンバエフ
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 は「ヒープ」などの実装固有の用語で定義されているため、標準化は望めません。

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

推奨されるユースケース

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

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

ブラウザの互換性

対応ブラウザ

  • 89
  • 89
  • x
  • x

ソース

現在、この API は Chrome 89 以降、Chromium ベースのブラウザでのみサポートされています。ブラウザによってメモリ内のオブジェクトの表示方法やメモリ使用量の推定方法が異なるため、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 分ごとに)サンプリングすることです。ただし、サンプル間でメモリピークが発生する可能性があるため、データにバイアスが生じます。

次の例は、ポアソン プロセスを使用してバイアスのないメモリ測定を行う方法を示しています。これにより、どの時点においてもサンプルが等しく発生する可能性が保証されます(demosource)。

まず、ランダムな間隔で 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 でバグを報告します。可能な限り詳細な情報を記載し、バグを再現するための簡単な手順を提供し、[Components] を 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