本番環境でウェブページのメモリ使用量を測定して、回帰を検出する方法について説明します。
ブラウザはウェブページのメモリを自動的に管理します。ウェブページがオブジェクトを作成するたびに、ブラウザはオブジェクトを保存するために「内部で」メモリのチャンクを割り当てます。メモリは有限のリソースであるため、ブラウザはガベージ コレクションを実行して、オブジェクトが不要になったことを検出し、基盤となるメモリチャンクを解放します。
ただし、検出は完璧ではありません。アラン チューリングの停止問題では、完璧な検出は不可能なタスクであることが証明されています。そのため、ブラウザは「オブジェクトが必要」という概念を「オブジェクトに到達可能」という概念に近似します。ウェブページが変数や他の到達可能なオブジェクトのフィールドを介してオブジェクトに到達できない場合、ブラウザはオブジェクトを安全に再利用できます。この 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 テストを実施して、メモリへの影響を評価し、メモリリークを検出する。
- メモリ使用量とセッション継続時間を関連付けて、メモリリークの有無を確認する。
- メモリ使用量とユーザーに関する指標を関連付けて、メモリ使用量の全体的な影響を把握する。
ブラウザの互換性
現在、この 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 分ごとに)サンプリングすることです。ただし、サンプル間でメモリピークが発生する可能性があるため、データにバイアスが生じます。
次の例は、ポアソン過程を使用して、偏りのないメモリ測定を行う方法を示しています。これにより、サンプルが任意の時点で等しく発生することが保証されます。
まず、ランダムな間隔で 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 に複数のエントリが返され、これらのエントリのどれがメモリを所有しているかを区別できないことを示す場合があります。
フィードバック
Web Performance Community Group と Chrome チームは、performance.measureUserAgentSpecificMemory() に関するご意見やご感想をお待ちしております。
API 設計について教えてください
API が想定どおりに動作しない場合は、または、アイデアを実装するために必要なプロパティが不足していますか?performance.measureUserAgentSpecificMemory() GitHub リポジトリで仕様に関する問題を報告するか、既存の問題に意見を追加します。
実装に関する問題を報告する
Chrome の実装にバグが見つかった場合は、それとも、実装が仕様と異なるのでしょうか。new.crbug.com でバグを報告します。できるだけ詳細な情報を含め、バグを再現するための簡単な手順を記載し、[Components] を 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 にも感謝します。