ChromeでJavaScriptのメモリリークを見つける


163

バックボーンビューを作成し、イベントにハンドラーをアタッチし、ユーザー定義クラスをインスタンス化する非常に単純なテストケースを作成しました。このサンプルの[削除]ボタンをクリックすると、すべてがクリーンアップされ、メモリリークは発生しないはずです。

コードのjsfiddleは次のとおりです:http ://jsfiddle.net/4QhR2/

// scope everything to a function
function main() {

    function MyWrapper() {
        this.element = null;
    }
    MyWrapper.prototype.set = function(elem) {
        this.element = elem;
    }
    MyWrapper.prototype.get = function() {
        return this.element;
    }

    var MyView = Backbone.View.extend({
        tagName : "div",
        id : "view",
        events : {
            "click #button" : "onButton",
        },    
        initialize : function(options) {        
            // done for demo purposes only, should be using templates
            this.html_text = "<input type='text' id='textbox' /><button id='button'>Remove</button>";        
            this.listenTo(this,"all",function(){console.log("Event: "+arguments[0]);});
        },
        render : function() {        
            this.$el.html(this.html_text);

            this.wrapper = new MyWrapper();
            this.wrapper.set(this.$("#textbox"));
            this.wrapper.get().val("placeholder");

            return this;
        },
        onButton : function() {
            // assume this gets .remove() called on subviews (if they existed)
            this.trigger("cleanup");
            this.remove();
        }
    });

    var view = new MyView();
    $("#content").append(view.render().el);
}

main();

ただし、Google Chromeのプロファイラーを使用して、これが実際に事実であることを確認する方法は不明です。ヒーププロファイラーのスナップショットには膨大な数のものが表示されますが、何が良い/悪いかをデコードする方法がわかりません。これまでに見てきたチュートリアルでは、「スナップショットプロファイラを使用する」か、プロファイラ全体の動作に関する非常に詳細なマニフェストを教えてください。プロファイラーをツールとして使用することは可能ですか、それとも全体がどのように設計されたかを本当に理解する必要がありますか?

編集:次のようなチュートリアル:

Gmailのメモリリークの修正

DevToolsの使用

私が見たものから、そこにあるより強い素材のいくつかを代表しています。ただし、3スナップショットテクニックの概念を紹介する以外に、実用的な知識に関しては(私のような初心者にとって)ほとんど提供していません。'DevToolsの使用'チュートリアルは実際の例では機能しないため、曖昧で一般的な概念の説明はあまり役に立ちません。「Gmail」の例については:

リークを発見しました。それで?

  • [プロファイル]パネルの下半分で、リークしたオブジェクトの保持パスを調べます

  • 割り当てサイトを簡単に推測できない場合(イベントリスナーなど):

  • JSコンソールを介して保持オブジェクトのコンストラクターをインスツルメントし、割り当てのスタックトレースを保存します

  • クロージャーを使用していますか?適切な既存のフラグ(つまり、goog.events.Listener.ENABLE_MONITORING)を有効にして、構築中にcreationStackプロパティを設定します。

それを読んだ後、私はもっと混乱していると思います。そして、繰り返しますが、それは私に物事を行うように指示するだけであり、方法を指示することではありません。私の見解では、そこにあるすべての情報は曖昧すぎるか、すでにプロセスを理解している人にしか意味がありません。

これらのより具体的な問題のいくつかは、以下の@Jonathan Naguinの回答で提起されています。


2
ブラウザーでのメモリ使用量のテストについては何も知りませんが、それを見たことがない場合は、Chromeウェブインスペクターに関するAddy Osmaniの記事が参考になるかもしれません。
ポールD.ウェイト2013年

1
提案をありがとう、ポール。ただし、[削除]をクリックする前に1つのスナップショットを取得し、それをクリックした後に別のスナップショットを取得し、[スナップショット1と2の間に割り当てられたオブジェクト]を選択すると(彼の記事で提案)、まだ2000個を超えるオブジェクトが存在します。たとえば、4つの「HTMLButtonElement」エントリがありますが、これは私には意味がありません。本当に、私は何が起こっているのか分かりません。
EleventyOne 2013年

3
ええ、それは特に役に立ちそうに聞こえません。JavaScriptのようなガベージコレクションされた言語では、テストと同じくらい細かいレベルでメモリを使用して何を実行しているのかを検証することを意図しているのではない場合があります。メモリリークをチェックするより良い方法は、main1回ではなく10,000回呼び出すことであり、最後に使用中のメモリが多くなるかどうかを確認することです。
ポールD.ウェイト2013年

3
@ PaulD.Waiteそうだね。しかし、問題を正確に特定するには、「ここではどこかにメモリの問題があります」と言うだけではなく、詳細なレベルの分析が必要なようです。そして私は、私は...私は、このような細かいレベルで彼らのプロファイラを使用することができるはずという印象を得るかだけではないことを確認する方法:(
EleventyOne

回答:


205

メモリリークを見つけるための適切なワークフローは、3つのスナップショット手法です。これは、Loreena LeeとGmailチームがメモリの問題のいくつかを解決するために最初に使用しました。一般的な手順は次のとおりです。

  • ヒープのスナップショットを作成します。
  • 物事を行います。
  • 別のヒープスナップショットを取得します。
  • 同じことを繰り返します。
  • 別のヒープスナップショットを取得します。
  • スナップショット3の「概要」ビューで、スナップショット1と2の間に割り当てられたオブジェクトをフィルタリングします。

あなたの例として、私はこのプロセスを表示するようにコードを調整しました(ここで見つけることができます)、[スタート]ボタンのクリックイベントまでバックボーンビューの作成を遅らせます。今:

  • HTMLを実行し(ローカルにこのアドレスを使用して保存)、スナップショットを作成します。
  • [開始]をクリックしてビューを作成します。
  • 別のスナップショットを撮ります。
  • [削除]をクリックします。
  • 別のスナップショットを撮ります。
  • スナップショット3の「概要」ビューで、スナップショット1と2の間に割り当てられたオブジェクトをフィルタリングします。

これで、メモリリークを見つける準備ができました!

いくつかの異なる色のノードに気づくでしょう。赤いノードはJavascriptからそれらへの直接参照はありませんが、分離されたDOMツリーの一部であるため、生きています。Javascriptから参照されたツリー内にノードがある可能性があります(おそらくクロージャーまたは変数として)が、DOMツリー全体がガベージコレクションされるのを偶然に妨げています。

ここに画像の説明を入力してください

ただし、黄色のノードにはJavascriptからの直接参照があります。同じ分離されたDOMツリーで黄色のノードを探し、Javascriptからの参照を見つけます。DOMウィンドウから要素に至る一連のプロパティがあるはずです。

特に、HTMLのDiv要素が赤くマークされているのがわかります。要素を展開すると、「キャッシュ」関数によって参照されていることがわかります。

ここに画像の説明を入力してください

行を選択し、コンソールに$ 0と入力すると、実際の機能と場所が表示されます。

>$0
function cache( key, value ) {
        // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
        if ( keys.push( key += " " ) > Expr.cacheLength ) {
            // Only keep the most recent entries
            delete cache[ keys.shift() ];
        }
        return (cache[ key ] = value);
    }                                                     jquery-2.0.2.js:1166

これは、要素が参照されている場所です。残念ながら、できることは多くありません。jQueryの内部メカニズムです。ただし、テスト目的でのみ、関数に移動してメソッドを次のように変更します。

function cache( key, value ) {
    return value;
}

このプロセスを繰り返すと、赤いノードが表示されなくなります:)

ドキュメンテーション:


8
あなたの努力に感謝します。実際、3つのスナップショット手法は、チュートリアルで定期的に言及されています。残念ながら、詳細はしばしば省略されています。たとえば$0、コンソールでの機能の導入に感謝します。これは私にとって新しいものでした。もちろん、それが何をしているか、どのようにそれを使用することを知っていたのかわかりません(同じことをして$1いる$2ように見えても役に立たないようです)。次に、行を強調表示し#button in function cache()、他の何十行も強調表示しないことをどのようにして知りましたか?最後に、内部にも赤いノードがNodeListありHTMLInputElementますが、わかりません。
EleventyOne 2013年

7
cache行に情報が含まれているのに他の行には含まれていないことをどのようにして知りましたか?枝よりも距離が短い枝がたくさんありますcache。そして、あなたHTMLInputElementががの子であることをどのように知っていたかはわかりませんHTMLDivElement。私はそれが内部で参照されているのを見ます( "HTMLDivElementのネイティブ")が、それ自体と2つHTMLButtonElementのも参照しているので、私には意味がありません。この例の答えを特定していただきありがとうございますが、これを他の問題に一般化する方法を私は本当に知りません。
EleventyOne 2013年

2
それは奇妙です、私はあなたの例を使用していて、あなたとは異なる結果を得ました(スクリーンショットから)。それにもかかわらず、私はあなたのすべての助けに大いに感謝しています。今のところ十分だと思います。具体的な支援が必要な実際の例がある場合は、ここで新しい質問を作成します。再度、感謝します。
EleventyOne 2013年

2
$ 0の説明はこちらにあります: developer.chrome.com/devtools/docs/commandline-api#0-4
Sukrit Gupta

4
どういうFilter objects allocated between Snapshots 1 and 2 in Snapshot 3's "Summary" view.意味ですか?
K-SOの毒性が高まっています。

8

次に、jsfiddleのメモリプロファイリングに関するヒントを示します。次のURLを使用してjsfiddleの結果を分離します。これにより、jsfiddleフレームワークがすべて削除され、結果のみが読み込まれます。

http://jsfiddle.net/4QhR2/show/

次のドキュメントを読むまで、タイムラインとプロファイラーを使用してメモリリークを追跡する方法を理解することはできませんでした。「オブジェクト割り当てトラッカー」というタイトルのセクションを読んだ後、「ヒープ割り当ての記録」ツールを使用して、いくつかの切り離されたDOMノードを追跡できました。

jQueryイベントバインディングからバックボーンイベント委任を使用するように切り替えることで問題を修正しました。新しいバージョンのBackboneでは、に電話をかけると自動的にイベントのバインドが解除されることを理解していますView.remove()。デモのいくつかを自分で実行します。それらは、特定できるようにメモリリークが設定されています。このドキュメントを読んでも問題が解決しない場合は、こちらからお気軽に質問してください。

https://developers.google.com/chrome-developer-tools/docs/javascript-memory-profiling


6

基本的に、ヒープスナップショット内のオブジェクトの数を確認する必要があります。2つのスナップショット間でオブジェクトの数が増え、オブジェクトを破棄した場合は、メモリリークが発生しています。私のアドバイスは、分離されないコード内のイベントハンドラーを探すことです。


3
たとえば、jsfiddleのヒープスナップショットを見ると、[削除]をクリックする前に、100,000を超えるオブジェクトが存在しています。jsfiddleのコードが実際に作成したオブジェクトはどこにありますか?Window/http://jsfiddle.net/4QhR2/show便利かもしれないと思ったのですが、無限の機能です。何が起こっているのか分かりません。
EleventyOne 2013年

@EleventyOne:私はjsFiddleを使用しません。テスト用に自分のコンピュータにファイルを作成するだけではどうですか。
青い空

1
@BlueSkiesここの人々が同じコードベースから作業できるように、jsfiddleを作成しました。それでも、テスト用に自分のコンピューターでファイルを作成した場合、ヒープスナップショットには50,000以上のオブジェクトが残っています。
EleventyOne 2013年

@EleventyOne 1つのヒープスナップショットでは、メモリリークがあるかどうかはわかりません。少なくとも2つ必要です。
Konstantin Dinev 2013年

2
確かに。何千ものオブジェクトが存在する場合、何を探すべきかを知るのがいかに難しいかを強調しました。
EleventyOne 2013年


3

開発者ツールの[タイムライン]タブを確認することもできます。アプリの使用状況を記録し、DOMノードとイベントリスナーの数を監視します。

メモリグラフが実際にメモリリークを示している場合は、プロファイラを使用して、リークしているものを特定できます。



2

ヒープのスナップショットを取ることをお勧めします。これらはメモリリークの検出に優れており、Chromeはスナップショットの優れた機能を果たします。

私の学位の研究プロジェクトでは、「レイヤー」で構築された大量のデータを生成する必要があるインタラクティブなWebアプリケーションを構築していました。これらのレイヤーの多くはUIで「削除」されましたが、何らかの理由でメモリが不足していました割り当てが解除されると、スナップショットツールを使用して、JQueryがオブジェクトへの参照を保持していることを確認できました(ソースは.load()、範囲外になっても参照を保持するイベントをトリガーしようとしたときです)。この情報を手元に置いて自分のプロジェクトを保存すると、他の人のライブラリを使用しているときに非常に便利なツールとなり、参照を長引かせることでGCの機能が停止するという問題が発生します。

編集:スナップショットに費やす時間を最小限に抑え、問題の原因を推測し、各シナリオをテストして前後にスナップショットを作成するために、実行するアクションを事前に計画することも役立ちます。


0

Chromeデベロッパーツールを使用したメモリリークの特定に関する重要な注意事項:

1)Chrome自体には、パスワードや数値フィールドなどの特定の要素のメモリリークがあります。https://bugs.chromium.org/p/chromium/issues/detail?id=967438。分離された要素を検索するときにヒープスナップショットを汚染するため、デバッグ中にそれらを使用しないでください。

2)ブラウザーコンソールに何も記録しないでください。Chromeはコンソールに書き込まれたオブジェクトをガベージコレクションしないため、結果に影響します。スクリプト/ページの先頭に次のコードを配置することで、出力を抑制できます。

console.log = function() {};
console.warn = console.log;
console.error = console.log;

3)ヒープスナップショットを使用して「detach」を検索し、切り離されたDOM要素を特定します。オブジェクトをホバーすると、各要素の識別に役立つ可能性のあるidouterHTMLを含むすべてのプロパティにアクセスできます。 デタッチされたDOM要素の詳細を含むJSヒープスナップショットのスクリーンショット 分離された要素がまだ汎用的すぎて認識できない場合は、テストを実行する前に、ブラウザーコンソールを使用してそれらに一意のIDを割り当てます。

var divs = document.querySelectorAll("div");
for (var i = 0 ; i < divs.length ; i++)
{
    divs[i].id = divs[i].id || "AutoId_" + i;
}
divs = null; // Free memory

ここで、切り離された要素を特定したら、id = "AutoId_49"と言い、ページをリロードし、上記のスニペットを再度実行し、DOMインスペクターまたはdocument.querySelector(..)を使用してid = "AutoId_49"の要素を見つけます。 。当然、これはページのコンテンツが予測可能な場合にのみ機能します。

テストを実行してメモリリークを特定する方法

1)ページの読み込み(コンソール出力は抑制されています!)

2)メモリリークを引き起こす可能性のあるページでの操作

3)デベロッパーツールを使用してヒープのスナップショットを作成し、「detach」を検索します

4)idまたはouterHTMLプロパティからそれらを識別するために要素をホバーします


また、ブラウザーでのデバッグがより困難になるため、縮小化/醜化を無効にすることは常に良い考えです。
ジミートムセン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.