[UE4] メモリーリークをどう追跡して修正するか

検証Ver:4.24.3

　メモリーリークが起きてるっぽいけど良く分からん時のための、原因特定から修正までの流れの一例です。
　(※:この手順ではMemProを利用します)

#1. Out Of Memoryでクラッシュする
　「アプリケーションを長時間動かした時に突然クラッシュした」といったように、メモリーリークは不意に発生します。クラッシュに心当たりがある場合は良いですが、心当たりがない場合はログを確認しましょう。"Out Of Memory"がログに掛かれていたらメモリーリークの可能性が考えられます。
![2020-06-05_01h27_23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101019/274c7646-4885-0267-b013-b68dc5854515.png)
　"Out Of Memory"はメモリーオーバーを示します。しかしながら、動作端末によってはアプリケーションが利用できるメモリ量の上限が決まっている事があるので、それがメモリーリークによって使用できるメモリが枯渇したのか、もしくは端末で決められた上限をオーバーした事によるものかは分かりません。もし後者であれば、アプリケーションが使用するメモリのバジェットを再考した方が良いでしょう。メモリーリークの疑いがある場合は次に進みましょう。

#2. 本当にリークしているか確認する
　もしメモリリークかな？と思ったら、その疑問を確定させるために本当にリークしているかどうか確認しましょう。[LLM](https://docs.unrealengine.com/ja/Programming/Development/Tools/LowLevelMemoryTracker/index.html)、もしくは[[UE4] LLM (Low Level Memory Tracker)を使用したメモリトラッキング](https://qiita.com/donbutsu17/items/dd410cd6ee53b0b348ca)を参考に、カテゴリ毎にメモリが上昇し続ける箇所がないか確認します。もし見つかった場合はリークしている事でほぼ確定的です。特定の手順で増加傾向がみられるとより問題点を絞れますが、手順が分からない場合は長い時間動かして傾向を見ましょう。

　以下のコマンド引数を付与してアプリケーションを起動しても確認することができます。

```:LLMのstatsコマンドで傾向を把握する
-llm -execcmds="stat llmfull"
```

![2018-12-18_03h32_20.png](https://qiita-image-store.s3.amazonaws.com/0/101019/527319e4-136f-3ec9-a1ca-c4fb8c275de6.png)

　以下のコマンド引数を付与してアプリケーションを起動するとcsv形式で出力もできます。長時間取りたい場合はこちらがおススメです。

```:LLMのトラッキング結果を60s毎にcsv出力する
-llm -llmcsv -execcmds="LLM.LLMWriteInterval 60"
```

　このcsvから作成したグラフですが、図はリークの一例でUIタグが増加傾向にあることが分かります。
![2020-06-05_01h49_52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101019/8ed4e346-ac60-7d8e-6a5c-54343cf0584c.png)

#3. キャプチャを取る
　リークが本格的に起きていそうであれば、改めてプロファイラーで解析するためのトレースログをキャプチャします。メモリトラッキングを解析できるプロファイラーツールは(コールツリーを見れるので)以下の2つがおススメですが、この記事ではMemproを利用します。Memproは特定のLLMタグだけのメモリをキャプチャできる点で良いです。

[[UE4] MemProを使用したメモリトラッキング](https://qiita.com/EGJ-Ken_Kuwano/items/288fe494cd6bf4f934c3)
[[UE4] Malloc Profiler/Memory Profiler2を使用したメモリトラッキング](https://qiita.com/donbutsu17/items/a72a282587390f43d12d)

　先程UIタグにリーク傾向がみられたので、以下のコマンドを利用して「アプリケーション起動時からMempro用のUIタグに限定したメモリ」を取得します。

```:MemproでUIタグを特定してキャプチャする場合
-llm -nothreadtimeout -execcmds="MemPro.LLMTag UI, MemPro.Enabled 1"
```

#4. ツールでキャプチャを解析する
　先程取得したトレースデータをMemproで読み込ませます。そうすると以下のようにUIタグに限定したメモリ使用量を表示し、それが増加傾向にあることが表示されます。

![2020-06-05_00h45_45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101019/8a3a5c92-ebfb-4ab2-9245-a26ac5a4a5d0.png)

　ここから更にどこでリークしているかを知るために、Memproにシンボルを食べさせてから差分のスナップショットを取得して(上記のMemProのリンク参照)CallTreeを表示させます。CallTreeはメモリ確保までの経路を階層的に表示しますが、右のメモリ占有率が高い個所を選択すると下の図のようにメモリ確保されたラインが表示されます。どうやらここに関連する箇所を修正する必要があるようです。

![2020-06-05_02h02_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101019/43790f58-5cd6-3b8f-848e-319b9e6f6bce.png)

#5. リークを修正する
　リークを修正するためにはそれが本来確保されるべきメモリか、どこで解放漏れがないか、などを吟味する必要があり、そのコードに対する理解と知識が多少必要になってきます。今回のケースでは `FT_Glyph_StrokeBorder`で確保されるメモリが解放されていない事が分かっていましたが、実際にフォントグリフの情報が自動的に開放されていないかったので、以下のような修正を行う事でリークが解消されます。

```cpp:SlateFontRenderer.cpp
		// If there is an outline, render it second after applying a border stroke to the font to produce an outline
		if(ScaledOutlineSize > 0)
		{
			FT_Stroker_New(FTLibrary->GetLibrary(), &Stroker);
			FT_Stroker_Set(Stroker, FMath::TruncToInt(FreeTypeUtils::ConvertPixelTo26Dot6<float>(ScaledOutlineSize)), FT_STROKER_LINECAP_ROUND, FT_STROKER_LINEJOIN_ROUND, 0);

			FT_Get_Glyph(Slot, &Glyph);

			FT_Bool bInner = false;
//			FT_Glyph_StrokeBorder(&Glyph, Stroker, bInner, 0);
			FT_Glyph_StrokeBorder(&Glyph, Stroker, bInner, 1);

			FT_Outline* Outline = &reinterpret_cast<FT_OutlineGlyph>(Glyph)->outline;

			RenderOutlineRows(FTLibrary->GetLibrary(), Outline, OutlineSpans);
		}
```

これで原因の特定と対策は完了です。

#6. 問題解決のためにやるべきこと
　問題解決のために重要なことは、問題点を出来るだけ絞ることです。主に以下の点を確認しましょう。

**・特定のプラットフォームに限定されるか？**
　問題がプラットフォームに限定されていない場合、MallocProfilerやMemproを使用してAllocされたポイントを特定することができます。プラットフォームに限定される場合、プラットフォームが提供する専用のツールを利用すると効率が良いです。まずは開発用PC(Windowsなど)でも再現することを確かめることをおすすめします。

**・特定のビルド構成に限定されるか？**
　ごく稀に問題が特定のビルド構成に限定して発生するケースがあります。例えばShippingやTestビルドでしか起きないようなケースは非常にやっかいですが、そのようなケースにおいてもやるべきことはロギングを有効にしたり、コールスタックを辿るといった、Development, Debug時と同じことをするのが近道です。例えTestビルドで発生したとしても、まずはDevelopmentなど、ログやLLMが出力できるビルド構成でも再現することを確認することをおすすめします。

**・100％再現する(リークする)ための手順は何か？**
　リークが発生するということは何らかの確固たる再現手順があるはずです。まずはどのような手順で再現するか、その手順がシンプルなプロジェクトやサンプルでも再現するかを確認することをおすすめします。サンプルでも再現する場合は問題点を共有できるので解析の時間がグッと短縮できます。もし自分のプロジェクトだけで発生する場合でも、ロジックやアセットを出来るだけ切り出して再現条件を小さく出来ることはとても大事なことです。

これらを切り分けた上で問題を報告すると解決までのスピードが速くなり、かつ自分で気づける点も出てくると思います。