こんにちは!スマートキャンプでインサイドセールスに特化したSaaSであるBALES CLOUDを開発しているエンジニアの井上です。
皆さんは、開発・調査などでChrome DevToolsはよく使われているかと思います。
私達の開発するプロダクトでメモリリーク問題が起きたことがあり、 その際に調査方法で知っていれば助けになった内容をまとめていければと思っています。
- JavaScriptのメモリ管理とは?
- GC(ガベージコレクション)とは?
- メモリリーク問題とGCで開放されないメモリ
- 計測に使用したToolについて
- Chrome DevTools Memory
- Allocation instrumentation timelineで見れるもの
- memory view
- 実行前の状態をタスクマネージャーで確認する
- 計測準備
- 計測結果を確認する
- まとめ
JavaScriptのメモリ管理とは?
JavaScriptはメモリ管理にGC(ガベージコレクション)を採用しており、これにより使用されなくなったメモリを自動的に開放します。
GC(ガベージコレクション)とは?
使用されていないメモリであるかどうかを見つけ自動でメモリを開放する仕組みのことです。
これは定期的に行われるわけではなく、ガベージコレクションの処理を実行するのが効果的である、と判断された場合に実行されます。
メモリリーク問題とGCで開放されないメモリ
GCの処理でメモリが開放されると言いましたが、上手いことGCがすべてのメモリを確認して開放してくれるわけではありません。 というのも、先述の通りメモリ開放の判断基準は「そのメモリが必要か不要か」なのですが、すべてのパターンにおいてGCが自動判定してくれるわけではなく、時にはその判定が開発者に委ねられることがあるからです。
普段意識しないですが、この委ねられた部分で開発者が問題のあるコードを書いてしまうことで GCで回収されないメモリが発生し内部処理を圧迫するほどメモリが肥大化した結果、メモリリークとなるパターンが多いかと思います。
よく言われるメモリリークの種類
開発者が意図的に破棄しない限り、GCにより開放されず残り続けるパターンとして代表的なものは下記です。
- タイマー、コールバック
- Dom参照
- クロージャ
- グローバル変数
こちらに関しては下記の記事が非常にわかりやすかったです。
GCのアルゴリズムやマークスイープアルゴリズムに関しても書かれておりとても参考になりました。
How JavaScript works: memory management + how to handle 4 common memory leaks
計測に使用したToolについて
タスクマネージャー
Shift + Escを押すか、Chromeのメインメニューに移動し、[その他のツール]>[タスクマネージャー]を選択して、タスクマネージャーを開きます。
タスクマネージャーではページが現在使用しているメモリの量をリアルタイムでみることが可能です。
Chromeタスクマネージャーで開くと下記のような画面が出てきます。
そこでヘッダー部分から表示する項目でJavaScriptメモリを選択すると、JavaScriptのリアルタイムのメモリの変化を追うことが可能です。
自分はこの画面では主に、「どのような操作でメモリの増加があるのか」をログを元に確認していました。 当時、初期調査においてメモリ上昇の原因作を特定するには、この機能はとても重宝しました。
Chrome DevTools Memory
Chrome DevToolsでは問題となっている箇所がどの部分かや、どのメモリが肥大化しているかを見ていました。 メモリを確認するMemoryパネルでは、メモリリークの追跡などのために以下のプロファイリングタイプを選択し確認できます。
Heap snapshot
ページのJavaScriptオブジェクトおよび関連するDOMのスナップショットを記録し、グラフやDOMツリーなどで確認できる機能です。
Allocation sampling
メモリ割当を記録します。パフォーマンス負荷がこれらのプロファイリングタイプの中でもっとも軽いです。
あまりこちらは使用したことがないのですが、大まかにメモリの分布を計測したいときに使うと良いようです。
Allocation instrumentation timeline
Allocation instrumentation timelineは、Heap snapshot情報と、Chome DevToolsのタイムラインパネルの増分更新と追跡を組み合わせたような機能です。
記録を開始するとリアルタイムにメモリの増減を関ししながら記録できます。
※これ使えばタスクマネージャーすらいらないのでは?とも思いました。
今回はこの3つの中でも一番使用していたmemoryのAllocation Instrumentation Timelineについてご紹介できればと思います。
Allocation instrumentation timelineで見れるもの
memory view
memoryパネルのなかでは4つの異なるViewでメモリの確認ができます。 パネルそれぞれで見れるものが異なり以下のような特徴があります。
Summary
オブジェクトをクラス名別のグループにして表示し、クラス別にメモリの使用量を表示します。
Containment
ヒープ領域のコンテンツを調査します。オブジェクトの構造に適したビューが提供されるため、グローバルの名前空間で参照されるオブジェクトを分析するのに役立ちます。 このビューでは、クロージャを分析して下位レベルのオブジェクトまで踏み込んで調査できます。
Statistics
オブジェクトごとのメモリの統計情報を円グラフで提供します。
Allocation
JavaScriptの機能別のメモリ割り当てを表示できます。
実際にメモリの増加を計測してみる
今回はPlayCodeを使用して下記のメモリー増加するようなコードとして クロージャを使用して長い文字列を返す関数を使用して計測していこうと思います。
※BootstrapとjQueryはPlayCodeで初期で設定されていました(そして久々にjQuery書きました)。
import 'bootstrap@4.6.0' import $ from 'jquery' $('button') .html('Click me') .on('click', () => new TestMemoryLeaker()) class TestMemoryLeaker { constructor() { this.createLargeClosure() } createLargeClosure() { var largeStr = new Array(1000000).join(','); var lC = function lC() { return largeStr; }; return lC; } }
これをPlaycodeで実行前のメモリと実行したあとでどう変わったか見ていければと思います。
実行前の状態をタスクマネージャーで確認する
実行前の状態だと182MBです。
ここからメモリー増加するようなコードを実行しChrome DevToolsで詳細を見ていきます。
計測準備
いよいよAllocation instrumentation timelineを使用して計測します。
Allocation instrumentation on timelineにチェックし、Startをクリックします。
StartするとTImelineが表示されメモリの増加をリアルタイムで確認できます。
この状態でPlayCodeでJavaScriptを実行します。
計測結果を確認する
計測を止めるとSnapshotが作成されます。
このSnapshotにメモリの増加の原因となるObjectなどが記録されているので、これを元に確認していきます。
タスクマネージャー
まずは、タスクマネージャーを確認してみると182MBだったメモリが311MBまで上がっているのがわかります。
182MBだったのが311MBまで上がっているのがわかります。
Statistics
StatisticsではどのObjectがメモリを使用している割合が大きいのかを確認していきます。
typed arraysの割合がとても大きくなっているのがわかります。
Allocation
Allocationでは関数ごとにどのくらいメモリを使用しているか確認していきます。
今回はテスト用に作成したfunction createLargeClosureのsizeが大きくなっているのがわかります。
また、Allocationの良いところは、どのjsファイルの関数かもわかるようになっているので、特定のライブラリを使用していて問題となっている場合も特定がしやすいことです。
まとめ
メモリの問題は対応も検知も難しいので頭を悩ませるところかと思います。 今後も、Chrome DevToolsはかなり高機能になってきているので、使いこなせるように更新を追っていこうと思います。