JS↔DOMをトレース、またはそこに戻って

Chrome 66でのメモリリークの検索は、はるかに便利になりました。 DevToolsは、C ++からDOMオブジェクトのトレース、スナップショットの作成、JavaScriptから利用可能なすべてのDOMオブジェクト、およびそれらへのリンクを表示できるようになりました。 これらの機能の外観は、V8ガベージコレクターの新しいC ++トレースメカニズムの結果でした。







Chromeの安定版(2018年3月20日)にはバージョン65が搭載されていることを思い出させてください。そのため、この機能に驚嘆するには、不安定なアセンブリの1つをインストールする必要があります(たとえば、ベータ版にはバージョン66、DevおよびCanary-67)。







基本



ガベージコレクションでのメモリリークは、不要になったオブジェクトが他のオブジェクトから意図せずに追加されたために収集されない場合に発生します。 Webページでのメモリリークは、JSオブジェクトとDOM要素が相互作用するときによく発生します。







プログラマーがイベントハンドラーの削除を忘れたときに発生するリークを示すおもちゃの例を見てみましょう。 ハンドラーはオブジェクトを参照し、削除できなくなります。 特に、iframeがリークしています。







// Main window: const iframe = document.createElement('iframe'); iframe.src = 'iframe.html'; document.body.appendChild(iframe); iframe.addEventListener('load', function() { const local_variable = iframe.contentWindow; function leakingListener() { // Do something with `local_variable`. if (local_variable) {} } document.body.addEventListener('my-debug-event', leakingListener); document.body.removeChild(iframe); // BUG: forgot to unregister `leakingListener`. });
      
      





さらに悪いことに、リークされたiframeは、すべてのJSオブジェクトを存続させます。







 // iframe.html: class Leak {}; window.global_variable = new Leak();
      
      





リークの原因を見つけるには、保持パスの概念を理解することが重要です。 中断されたパスは、漏れているオブジェクトのアセンブリを妨げるオブジェクトのチェーンです。 チェーンは、メインウィンドウのグローバルオブジェクトのような、ある種のルートオブジェクトから始まります。 チェーンは、漏れているオブジェクトで終了します。 すべての中間オブジェクトには、チェーン内の次のオブジェクトへの直接リンクがあります。 たとえば、 Leak



オブジェクトとこのiframeの一時停止パスは次のようになります。



















中断されたパスは、JavaScriptとDOM(それぞれ緑と赤でマークされている)の境界を2回まで通過することに注意してください。 JSオブジェクトはV8ヒープ上に存在し、DOMオブジェクトはChromeのC ++オブジェクトです。







DevToolsヒープスナップショット



これで、ヒープスナップショットを使用して、選択したオブジェクトの一時停止パスを調べることができます。 この場合、V8ヒープにあるすべてのオブジェクトが正確に保存されます。 最近では、C ++ DOMオブジェクトに関する非常に大まかなデータのみがそこに保存されていました。 たとえば、Chrome 65は、前のおもちゃの例のLeak



オブジェクトの不完全な一時停止パスを示しています。



















最初の行だけが十分に正確です: Leak



オブジェクトは、iframeウィンドウのglobal_variable



に実際に格納されます。 他のすべての行は実際のパスを近似しようとするため、メモリリークのデバッグが非常に困難になります。







Chrome 66以降、DevToolsはC ++ DOMオブジェクトをトレースし、オブジェクトとそれらの間のリンクを正確にキャプチャします。 この機能は、クロスコンポーネントガベージコレクション用に作成されたC ++オブジェクトをトレースするための新しい強力なメカニズムに基づいています。 その結果、DevToolsのパスが正しくなりました!















アクションガイド





実験用ファイル: https://ulan.github.io/misc/leak.html







内部:クロスコンポーネントトレース



DOMオブジェクトは、Chromeで使用されるレンダリングエンジンであるBlinkを使用して管理されます。Blinkは、DOMを画面上の実際のテキストと画像に変換します。 Blinkとその内部DOM実装はC ++で記述されています-これは、DOMをJavaScriptに直接反映できないことを意味します。 代わりに、DOM内のオブジェクトは2つの部分に分かれているようです。JSからアクセス可能なラッパーと、C ++-DOMからのノードの表現であるオブジェクトです。 これらのオブジェクトには、相互への直接リンクが含まれています。 いくつかのシステムの境界を越えるコンポーネント(この場合はBlinkとV8)の存続期間と所有権を決定するのはかなり難しいタスクです。これは、どのコンポーネントがまだ生きており、どのコンポーネントを廃棄すべきかを事前に合意する必要がある関係者が関与するためです。







Chrome 56以前(たとえば、2017年3月まで)では、Chromeはオブジェクトグループ化と呼ばれるメカニズムを使用していました。 オブジェクトは、同じドキュメントに属している場合、1つのグループにリンクされます。 グループとそのすべてのオブジェクトは、別の中断されたパスの終わりに少なくとも1つの生きているオブジェクトがある限り、生き続けます。 これは、常にそれらを含むドキュメントに関連付けられ、いわゆるDOMツリーを形成するDOMノードのコンテキストで意味があります。 しかし、この抽象化により、中断されていた実際のすべてのパスが失われ、デバッグが非常に困難になりました。 オブジェクトが上記のシナリオに適合しなくなるとすぐに(たとえば、イベントハンドラーとして使用されるJavaScriptクロージャー)、このアプローチの実装は困難になり、JSラッパーが事前に組み立てられるバグが発生し、空のラッパーに置き換えられましたすべてのプロパティが完全に失われたJSラッパー。







Chrome 57から、このアプローチは「クロスコンポーネントトレース」に置き換えられました。これは、オブジェクトの活性を判断し、JavaScriptからDOMのC ++実装までトレースし、同じパスに沿って追跡するメカニズムです。 C ++側では、インクリメンタルトレースが実装されます。これは、ストップオブザワールドに陥らないように書き込みバリアを作成します。 クロスコンポーネントテストは、レイテンシを改善するだけでなく、コンポーネントの境界上のオブジェクトの活性度をより良く近似し、以前にリークを引き起こしたいくつかの頻繁に発生するシナリオを修復します。 さらに、これのおかげで、DevToolsはDOMの状態を実際に反映するスナップショットを作成する機会を得ました。







広告の分。 おそらくご存知のように、会議を行っています。 次の JavaScript会議はHolyJS 2018 Piterで、2018年5月19〜20日にサンクトペテルブルクで開催されます。 そこに来て、レポートを聞いて(レポートの内容- 会議プログラムで説明されています )、JavaScriptやフロントエンドのさまざまな最先端技術の開発者と一緒に実際にチャットすることができます。 要するに、私たちはあなたを待っています!



All Articles