アクロクエストアドベントカレンダー 12月15日 の記事です。
こんにちは。最近の趣味がAtCoderのmaron8676です。パズルを解くみたいに楽しめて、勉強にもなるので毎週コンテストに参加しています。
さて、2022/09にMeta社からmemlabというメモリリーク検出のためのフレームワークがリリースされました。
気になって調べたところチュートリアルを試していた方はいましたが、他にもスナップショットファイルの解析などmemlabが提供している機能があるようだったため、この機会に試してみた結果を書いていきます。
memlabはJavaScriptのヒープ解析とメモリリーク検出を行うためのフレームワークです。
JavaScriptにはガベージコレクションの機能があり、基本的に使い終わったメモリ領域は自動的に解放されていきます。しかし、プログラムの書き方によってはメモリが自動的に解放されず、メモリ使用量が増えていってしまう「メモリリーク」と呼ばれる問題が発生します。メモリリークの問題があるサイトは、操作しているうちにだんだん動作が重くなってしまい、場合によってはダウンしてしまうこともあります。
このように避けたい問題であるメモリリークですが、memlabのドキュメントに書かれているように、原因を探すのは大変な作業です。そのため、フレームワークを活用して原因調査を効率化しつつ、メモリリークの検出も自動化できるとよいですね。
memlabのGitHubページでは以下の機能が書かれています。
検証のためのアプリとして、Angular公式のチュートリアルアプリをベースに、setIntervalを消し忘れる問題を追加したWebアプリを作成しました。問題を追加したのはheroes.component.tsで、以下の通りです。
exportclass HeroesComponentimplements OnInit, OnDestroy{ heroes: Hero[] =[]; leakObjects: number[] =[]; constructor(private heroService: HeroService){} ngOnInit(): void{window.setInterval(() =>{for (let i = 0; i < 1000; i++){this.leakObjects.push(Math.random());}}, 100);this.getHeroes();}...
このコードには「setIntervalで設定した定期実行スケジュールの解除を忘れているため、SPA内の別ページに切り替えてもfunctionオブジェクトが残ってしまう」という問題があります。
memlabを使ってメモリリークを検出してみましょう。まずは、memlabがメモリリークを検出する方式について説明します。memlabは以下の流れでメモリリークを検出します。
memlabがメモリリークと判断する条件は、以下の通りです。
次に実際に書いたテストコードを紹介します。今回は以下のようなテストコードtests/test-memory.js
を作成しました。
/** * シナリオとして最初に開くURLを書く */function url(){return'http://localhost:4200/dashboard';}/** * メモリリークを起こすきっかけとなる処理を書く * * @param page - Puppeteer's page object: * https://pptr.dev/api/puppeteer.page/ */asyncfunction action(page){ await page.click('a[href="/heroes"]');// 問題のコンポーネントを表示 await page.click('button.clear');// 画面表示しているメッセージを削除}/** * action関数の結果をリセットするための処理を書く * * @param page - Puppeteer's page object: * https://pptr.dev/api/puppeteer.page/ */asyncfunction back(page){ await page.click('a[href="/dashboard"]');// 初期状態のコンポーネントを表示 await page.click('button.clear');// 画面表示しているメッセージを削除}/** * リークしたオブジェクトかどうか判定する条件を書く * * @param node 判定対象オブジェクト * @param _snapshot スナップショット * @param _leakedNodeIds 判定対象オブジェクトのID */function leakFilter(node, _snapshot, _leakedNodeIds){return node.retainedSize > 1024 * 1024;}module.exports ={ action, back, leakFilter, url};
このテストコードは、url関数、action関数、back関数、leakFilter関数で構成されています。それぞれの役割は以下の通りです。url, action, back, leakFilterがそれぞれ、メモリリークを検出する方式の1, 2, 3, 4に対応しています。今回見つけたい「解放されないfunctionオブジェクト」はmemlabが設定している初期条件を満たさないため、1MiB以上のオブジェクトを追加条件としました。
関数名 | 役割 |
---|---|
url | (必須)テスト対象のURL文字列を指定する |
action | (必須)新たにメモリを確保するような画面操作について記載する |
back | (必須)ページを開いた初期状態に戻るための画面操作について記載する |
leakFilter | (任意)リークと判断する追加条件を記載する |
コンソールでnpx memlab run --scenario .\tests\test-memory.js --work-dir memlab_results
と実行すると、テスト結果が表示されます。workディレクトリとして指定したmemlab_resultsにはスナップショットなどの結果が保存されます。以下の図を見ると、メモリリークを検出することができました。
memlabを使って原因調査をしていきます。テスト結果からは、3MBのクロージャが問題になっているということまでしか分かりませんが、memlabのメモリ解析ツールを使うことでもう少し詳しい情報を得ることができます。今回は以下のようなコード(analyze.js)を作成しました。
const memlab = require('memlab');(asyncfunction (){const analysis =new memlab.ObjectSizeAnalysis();const result = await analysis.analyzeSnapshotFromFile("tests/s3.heapsnapshot");})()
memlab workディレクトリの構成にあるs3.heapsnapshotファイルをtestsにコピーしてから実行しています*1。初期状態に戻すback関数実行後である、s3.heapsnapshotの解析結果は以下のようになりました。
問題のクロージャ内で参照している、leakObjectsという名前のArray変数が3MBで検出されています。今回使ったObjectSizeAnalysisはスナップショットの中で大きいサイズのオブジェクトを表示してくれるため、問題の特定につなげることができました。他にもドキュメントを見ると、いろいろな解析パターンがあるため試してみるとよいと思います。例えばGlobalVariableAnalysisは、意図せずグローバル変数となってしまった問題を見つけるのに役立ちそうです。
原因が分かったため、以下のようにHeroesComponentのデストラクタでclearIntervalを実行するようにして、テストを再実行してみましょう。
exportclass HeroesComponentimplements OnInit, OnDestroy{ heroes: Hero[] =[]; leakObjects: number[] =[];private timer: number |undefined; constructor(private heroService: HeroService){} ngOnInit(): void{this.timer =window.setInterval(() =>{for (let i = 0; i < 1000; i++){this.leakObjects.push(Math.random());}}, 100);this.getHeroes();} ngOnDestroy(): void{ clearInterval(this.timer);}...
以下のように、メモリリークが解決していることを確認できます*2。
memlabを使って、Angularアプリのコンポーネント内に含まれたメモリリークを検出しました。また、memlabのスナップショット解析ツールを使って、メモリリークの原因を特定する作業の一例を紹介しました。変数名レベルまで出してくれるため、便利そうです。メモリリークはいったん起きてしまうと、解析が難しく、時間もかかる問題であるため、フレームワークで調査を助けてもらえるのはありがたいですね。
Acroquest Technologyでは、キャリア採用を行っています。少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。www.wantedly.com
- ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
- Elasticsearch等を使ったデータ収集/分析/可視化
- マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
- 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。