Heap Snapshotを使った時のsetIntervalの罠

Chrome DevtoolsにはHeap Snapshopという機能があり、この機能を使うと任意のタイミングでJavaScriptのメモリの詳細を見ることができます。インスタンスがどれだけ存在するかやどの変数から参照されているかなどがかなり細かくわかります。

このHeap Snapshotはメモリリークを見つけるのに役立ちます。そこで、setIntervalを使ったコードでわざとメモリリークさせてみたところ、Heap Snapshot上でそのメモリリークを確認することができませんでした(あたかもリークしてないように見えた)。

<div id="count">show count</div>
<div id="end">end</div>
<script>
  function Sample() {
    this.count = 0;
    var self = this;
    this.id = setInterval(function selfClosureFunction(){
      self.onInterval();
    }, 3000);
  }

  Sample.prototype.onInterval = function () {
    this.count++;
  };

  Sample.prototype.destroy = function() {
    console.log('destroy');
    clearInterval(this.id);
  };

  function init() {
    var obj = new Sample();
    var onCount = function() {
      console.log(obj.count);
    };
    var onEnd = function() {
      document.getElementById('count').removeEventListener('click', onCount);
      document.getElementById('end').removeEventListener('click', onEnd);
      // 本来ならdestroyメソッドを呼ばなければならないがわざと呼ばずにメモリリークさせてみる.
      // obj.destroy();
    };
    document.getElementById('count').addEventListener('click', onCount);
    document.getElementById('end').addEventListener('click', onEnd);
  }

  init();
</script>
  • Sampleクラスは3秒間隔でSample#countを1ずつ増やしていきます
  • show countをクリックすると現在のcountをログに表示します
  • endをクリックするとイベントリスナを解除して処理を終了します

このendをクリックした時に本来ならSample#destroyを実行する必要がありますが、これを呼ばずにわざとリークさせてみました。(setIntervalにしかけた関数が変数selfを参照し続けるのでSampleインスタンスが残り続ける)

まずページを読み込んだ直後のHeap Snapshot

f:id:h13i32maru:20140104010730p:plain

  • Sampleのインスタンスが2つ存在します(Object Count 2)
  • Sample@76077はSampleコンストラクタそのものです
  • Sample@73791がSampleコンストラクタから生成されたインスタンスです(変数self、変数objから参照されている)

ここまでは予想通り。


次にendをクリックした直後のHeap Snapshot

f:id:h13i32maru:20140104010743p:plain

  • Sample@76077(コンストラクタ)は特に変化なし
  • Sample@73791がなくなってObject Count 1となっています

Sample@73791はGCに回収されてしまったことになっています。コード上はわざとメモリリークをさせたはずなのでSample@73791が変わらず表示されて「お、メモリリークしている。destoryメソッド呼ぶの忘れてるな」みたいな感じでメモリリークを発見できないと困ります。

そもそもsetInterval関数から変数selfが参照されているので本当にGCで回収されていたらsetInterval中でエラーになるはずです。が、そんなことはなく正常に動いています。ではSample@73791はどこに行ってしまったんでしょうか?


ここでSnapshot 2の表示方法をSummaryからContainmentに変更してみます。そうするとルートインスタンスから参照をたどっていけるようになります。

f:id:h13i32maru:20140104010757p:plain

ここからGC roots → Global handlersと探していくと「function selfClosureFunction」というのが見つかります。これはsetIntervalに登録した関数です!ということで中を覗いてみるとSample@73791がありました! つまりインスタンス自体は存在するけど、SummaryからClass filterでは見ることができないようです。


ちなみにどうやって見つけたかというとSnapshot 1のSample@73791のObject retaining treeの変数selfを覗いてみるとGlobal handlersのselfClosureFunctionから参照されていることがわかったので、Snapshot 2でContainmentから探して行ったという感じです。

この現象はsetTimeout, requestAnimationFrameでも発生します。タイマー系のイベントを扱う時は特に気をつけたほうが良さそうです。DOMのイベント(clickやtouch)でリークしている場合はClass filterでインスタンスを正しく見ることができました。