Google の Leak Finder を使ってみました
先日、 Google から Leak Finder というツールがリリースされました。主に Closure Library を対象として、メモリリークの検出を行うツールです。 Closure Library は比較的規模の大きいアプリケーションに使われることが多く、その分メモリリークの危険も高いので、こうしたツールはとてもありがたい。さっそく試してみました。
インストール
Leak Finder は Python で構築されているので、 Python の処理系(バージョンが明記されていないのですが、たぶん 2.6 か 2.7?)が必要です。もっとも、 Closure Library で開発をしているならすでにインストール済みでしょうから、それを使ってください。
インストール先のディレクトリはどこでもいいのですが、私は ~/leak-finder ディレクトリを作成して、そこにすべてを入れるようにしました。
mkdir ~/leak-finder cd ~/leak-finder
このあたりは好みの問題なので、適当にアレンジしてください。
それでは、必要なツール等をインストールしていきます。以下は Mac OS (Mountain Lion) での例ですが、他の環境でも大きくは変わらないはずです。
depot_tools のインストール
まずは Google 内製のパッケージシステムである depot_tools です。 Chromium とかで使われているものですね。 Subversion か Git がインストールされていれば、以下のいずれかのコマンドでダウンロードできます。私はとりあえず Subversion で落としました。
svn co http://src.chromium.org/chrome/trunk/tools/depot_tools git clone https://git.chromium.org/chromium/tools/depot_tools.git
もし Windows 環境で svn も git もなければ、こちらの zip ファイルをダウンロードして展開するという手もあるようです。
ダウンロードができたら、実行パスを追加しておきます。パスの先頭ではなく、最後に追加しなければいけないようです。
export PATH="$PATH":~/lib/leak-finder/depot_tools
私は確認していませんが、 Windows 環境の場合はここで一度 gclient を実行する必要があるようです。それによって、 svn などのツールがインストールされるとのこと。
Leak Finder のインストール
depot_tools の gclient を使って Leak Finder をインストールするのですが、その前にリポジトリとなるディレクトリを準備します。私は ~/leak-finder/repos としました。
mkdir ~/leak-finder/repos
そして、以下の内容のファイルを ~/leak-finder/repos/.gclient として作成します。
solutions = [ { "name" : "leak-finder", "url" : "git+https://code.google.com/p/leak-finder-for-javascript", }, ]
あとは、リポジトリディレクトリで gclient sync を実行すれば、 Leak Finder と必要な依存ファイルがインストールされます。
cd ~/leak-finder/repos gclient sync
よくできてる・・・。
simplejson のインストール
Leak Finder は Python の simplejson ライブラリを使っているので、最後にそれをインストールします。 easy_install 等を使うのが手軽ですが、私はパッケージを ~/leak-finder 以下に直接展開して、 PYTHONPATH に含めるようにしました。
cd ~/leak-finder curl -O http://pypi.python.org/packages/source/s/simplejson/simplejson-2.6.1.tar.gz tar zxvf simplejson-2.6.1.tar.gz export PYTHONPATH=$PYTHONPATH:$HOME/leak-finder/simplejson-2.6.1
これでインストール作業は完了です。
使ってみる
Leak Finder を使う手順は、概ね以下になります。
- リモートデバッグを有効にして Chrome を起動する
- Chrome 上でデバッグ対象のアプリケーションのページを開く
- Leak Finder を実行する
(2) で実行するアプリケーションは、以下の条件を満たしていなければなりません。
- Closure Library の 2012/7/10 以降のバージョンを使っている
- Closure Compiler による最適化をかけていない
- goog.Disposable を require した直後に(goog.Disposable インスタンスをひとつも new していない時点で) 「goog.Disposable.ENABLE_MONITORING = true」を実行している
Chrome を起動する
ここでは、 Leak Finder のリポジトリにあるテスト用の HTMLで試すことにしましょう。
まずは Chrome をリモートデバッグ可能な状態で起動します。具体的には、「--remote-debugging-port=9222 --js-flags=--stack_trace_limit=-1 --user-data-dir=/tmp/jsleakcheck」というオプションを付けて起動すれば OK です。 Mac OS の場合はシェルで以下のコマンドを実行するのがよいでしょう。
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --remote-debugging-port=9222 --js-flags=--stack_trace_limit=-1 --user-data-dir=/tmp/jsleakcheck
ちなみに最後の --user-data-dir はプロファイルディレクトリの指定なので、とくに Windows では別の適当な場所を指定してください。このオプションはおそらく必須ではないでしょうが、公式サイトの例に倣って指定しています。
アプリケーションのページを開く
前述のとおり Leak Finder のテスト用 HTML を使います。 Chrome のアドレスバーに以下の URL を入力してページを開いてください。
http://leak-finder-for-javascript.googlecode.com/g...
複数のタブを開いたりせず、起動時の最初のタブを使う必要があるようです。ご注意ください。
Leak Finder を実行する
ページの読み込みが終わったら、 Leak Finder を実行します。シェルで以下のコマンドを実行すれば OK です。
cd ~/leak-finder/repos/leak-finder/src python jsleakcheck.py -d closure-disposable -v
実行が成功すれば、以下の様な感じで出力されるはずです。私の環境ではうまくいかないことが多いのですが、詳しくは後述。
$ python2.6 jsleakcheck.py -d closure-disposable -v INFO:root:Using leak definition closure-disposable INFO:root:Reading suppressions from "closure-disposable-suppressions.txt" INFO:root:Taking heap snapshot INFO:root:Analyzing heap snapshot INFO:root:Retrieving creating stack traces for leaking objects INFO:root:Scanning for new leaks. New memory leaks found: Leak: 1 MyObj allocated at: goog.Disposable Object.goog.base new MyObj MyObjCreator.Create http://leak-finder-for-javascript.googlecode.com/git/doc/test-page.html:23:23
単にリークが存在するというだけでなく、そのクラス名や確保時のコールスタックも表示されています。これだけ情報が表示されれば、リーク元を突き止めるのもかなり楽になりそうですね。
Leak Finder にできること・できないこと
テスト用 HTML ページの中を見ると、以下のようになっています。
<script src="http://closure-library.googlecode.com/svn/trunk/closure/goog/base.js"></script> <script> goog.require('goog.Disposable'); </script> <script> goog.Disposable.ENABLE_MONITORING = true MyObj = function() { goog.base(this); } goog.inherits(MyObj, goog.Disposable); MyObjCreator = function() {} MyObjCreator.prototype.Create = function() { return new MyObj(); } var creator = new MyObjCreator(); // Not a leak. var handle = creator.Create(); // Leak. var handle2 = creator.Create(); handle2 = null; </script>
最後のほうを見ると、 handle と handle2 の 2 つの MyObj インスタンスが生成されています。コードからすると両方共リークしているようにも読めますが、 Leak Finder がリークとして検出しているのは handle2 のほうだけです。これは、 Leak Finder がリークとして検出する条件が以下のようになっているからです。
- goog.Disposable クラス(もしくはその派生クラス)のインスタンスである
- 確保以降(goog.Disposable コンストラクタの呼び出し以降)、 dispose() メソッドが呼ばれていない
- ページ内のいずれのオブジェクトからも参照されていない
つまり、 dispose() される前にすべての参照が外されたオブジェクトのみがリークとして検出され、それ以外はリークとはみなされません。例えば、以下は実際にメモリリークしていても、 Leak Fider では検出されません。たぶん。
- goog.Dispose クラスを継承していないオブジェクト同士の相互参照
- dispose() の呼び出しと参照の削除の両方を忘れている
- disposeInternal() の実装がそもそも間違っている(DOM 要素への参照を null にしていないなど)
ということで、万能のツールというわけではないようです。
また、 Leak Finder のリリース時のブログ記事では Closure Library 以外のライブラリにも使える的な記述がありますが、それもそう簡単ではないと思います。そもそも disposable パターンのメモリ管理をしていないと意味がないでしょうし。事実上は Closure Library 専用のツールになるのではないかと思います。
まだ安定していない?
上記に加えて、少なくとも私の環境では、 Leak Finder の実行は 5 回に 1 回くらいしか成功しません。多くの場合は途中で動作が止まって、 Ctrl-C も効かず、 kill するしかない状態になります。少しだけ追ってみたのですが、 Chrome との通信、もしくはそのスレッド周りでデッドロックしているようです。必ず止まるわけではないので、たぶんタイミングなんでしょうね・・・。
これについては、なにかわかったらここに追記しようと思います。もし同様に試された方がおられましたら、情報を共有していただけるとありがたいです。
以上、本日は Google がリリースした Leak Finder をご紹介しました。あらゆるメモリリークを検出する、というわけではありませんが、有用なツールであることは確かだと思います。あとは安定して動くようになってくれれば・・・というところですね。今後の開発に期待したいです。
詳しくはこちらの記事をどうぞ!
この記事にコメントする