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 からオブジェクトをキャプチャする、Worker を閉じない、配列にオブジェクトを蓄積するなどにより、イベント リスナーの登録解除を忘れた場合などが簡単に導入できます。ウェブページにメモリリークがあると、時間の経過とともにメモリ使用量が増え、ユーザーにとってはウェブページの動作が遅くなり、肥大化しているように見えます。

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

performance.measureUserAgentSpecificMemory() と以前の performance.memory API の違い

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

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

推奨されるユースケース

ウェブページのメモリ使用量は、イベント、ユーザー操作、ガベージ コレクションのタイミングによって異なります。そのため、Memory Measurement 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 は結果のプロミスをすぐに解決せず、次のガベージ コレクションを待機します。

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 でバグを報告します。できるだけ詳細な情報を含め、バグを再現するための簡単な手順を提示してください。また、[Components] は Blink>PerformanceAPIs に設定してください。 Glitch は、すばやく簡単に再現を共有するのに最適です。

サポートの気持ちを伝える

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

関連情報

謝辞

API 設計レビューに協力してくれた Domenic Denicola、Yoav Weiss、Mathias Bynens 氏、Chrome のコードレビューに協力してくれた Dominik Inführ、Hanes Payer、Kentaro Hara、Michael Lippautz に感謝します。また、API の大幅な改善に役立った貴重なユーザー フィードバックを提供してくれた Per Parker、Philipp Weis、Olga Belomestnykh、Matthew Bolohan、Neil Mckay にも感謝します。

ヒーロー画像Harrison BroadbentUnsplash より)