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 は「ヒープ」などの実装固有の用語で定義されているため、標準化は不可能です。

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

おすすめのユースケース

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

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

ブラウザの互換性

Browser Support

  • Chrome: 89.
  • Edge: 89.
  • Firefox: not supported.
  • Safari: not supported.

Source

現在、この 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 に設定してください。

サポートを表示

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 にも感謝いたします。