[UE4] LLM (Low Level Memory Tracker)を使用したメモリトラッキング

#1. はじめに
　Low Level Memory Tracker(LLM)はメモリの使用状況を追跡するツールです。LLMは4.18から実装されるツールの1つで、UE4のアプリケーションとOSによって割り当てられるすべてのメモリを記録します。
　本記事では4.21.1にて確認しています。

#2. 注意点
　事前に認識しておくべきいくつかの注意点があります。

**・Editor起動時には不要な情報を含むためパッケージやクック済のゲームで計測する必要があります**
　　**(各プラットフォームで正確にメモリをトラッキングするためにも端末上で計測できるのが望ましい)**
**・カテゴリ単位でメモリをトラッキングすることができますが、詳細なリーク箇所を特定することはできません**
**・カテゴリ単位でメモリの占有率を計測できますが、Miscなど精度や定義が曖昧になる部分が含まれます**

#3. 使い方
　まずは単純にLLMを有効にして、Statsコマンドでメモリ使用量を確認する手順を記載します。
##3.1. 起動と確認
**・EditorからStandaloneで利用する場合**
引数に*"-LLM"*を付与してStandalone起動するだけです。
![2018-12-18_03h22_33.png](https://qiita-image-store.s3.amazonaws.com/0/101019/81c085c9-3b03-7d52-1c9f-007a1b88b5f3.png)
**・Package Gameで利用する場合**
パッケージを作成した後、引数に*"-LLM"*を付与して起動するだけです。
![2018-12-18_03h26_00.png](https://qiita-image-store.s3.amazonaws.com/0/101019/634fba6c-3c18-432a-0530-0c0be841889a.png)
**・Cooked Gameで利用する場合**
引数に*"-LLM"*を付与して起動するだけです。以下はProjectのプロパティから指定するケースです。
![2018-12-18_03h28_22.png](https://qiita-image-store.s3.amazonaws.com/0/101019/61800ea2-8326-26dc-b97c-1ed2dcf72e98.png)
もし[UnrealVS](http://api.unrealengine.com/JPN/Programming/Development/VisualStudioSetup/UnrealVS/)をインストールしている場合は、画面上部から指定することができます。
![2018-12-18_03h27_42.png](https://qiita-image-store.s3.amazonaws.com/0/101019/8fff9eb7-7efa-fb68-45fb-fa70aeedbac3.png)

　うまくいけば*"stat llm"*や*"stat llmfull"*コマンドを入力するとリストを表示することができます。LLMの起動時の引数が渡せていない場合では統計情報が全て0MBで表示されることがあります。
![2018-12-18_03h32_20.png](https://qiita-image-store.s3.amazonaws.com/0/101019/527319e4-136f-3ec9-a1ca-c4fb8c275de6.png)
　起動引数に*"-LLM"*、statsの表示に*"stat llm"*を使用しましたが、次の項では他に利用可能な引数やコマンドを説明します。

##3.2. Option
　LLMで利用可能な引数・コマンドは以下の通りです。

| 引数		|機能					|
|:--------------|:--------------------------------	|
| -LLM		|LLMを有効にして起動します							|
| -NOLLM	|LLMを無効にして起動します <br> (Defaultでは無効で起動します)			|
| -LLMCSV	|Tag付けされた情報を.csvに記録します。<br>	saved/profiling/llm/ 以下に出力されます	|
| -LLMTAGSETS	|Tag情報を記録/表示することを許可します<br> ※実験的機能				|

| コマンド		|機能							|
|:--------------	|:--------------------------------			|
| stat LLM		|Tag付けされた使用中のメモリを表示します			|
| stat LLMFull		|Tag付けの有無に関わらず全てのメモリを表示します		|
| stat LLMOverhead	|LLM自身で使用しているメモリ情報を表示します		|
| stat LLMPlatform	|OSの情報を含むメモリ情報を表示します			|
| LLM.LLMWriteInterval	|.csvに出力する周期を指定して変更できます (Default:5秒)	|

　LLMTAGSETSは実験的な機能ですが、以下のマクロを有効にすることでAsset単位での情報を出力できます。
(ただしcsvの出力が正しく実施できない問題があります)

```cpp:\Engine\Source\Runtime\Core\Public\HAL\LowLevelMemTracker.h
	// using asset tagging requires a significantly higher number of per-thread tags, so make it optional
	// even if this is on, we still need to run with -llmtagset=assets because of the shear number of stat ids it makes
	#define LLM_ALLOW_ASSETS_TAGS 1
```
上記の定義を追加した後、Assetsを以下のように追加すると有効になります。

```
-llmtagsets="Assets"
```
![2018-12-18_12h30_51.png](https://qiita-image-store.s3.amazonaws.com/0/101019/a8253b61-2be8-7971-f2e3-e31bd771a342.png)

Class単位で表示する場合は、AssetClassesを引数とすることで以下のようなClass単位での出力が得られます。

```
-llmtagsets="AssetClasses"
```
![2018-12-18_12h53_32.png](https://qiita-image-store.s3.amazonaws.com/0/101019/fc00ab3e-075f-89cf-3f2e-7b6a270248e4.png)

#4. トラッキングメモリ
　LLMでトラッキングする際に割り当てられるタグや、トラッキングしたメモリからの解析に関する情報について説明します。
##4.1. タグ
　以下にLLMでトラッキングするタグの一覧を示します。ここで表示する内容はLLMのstatsで表示する内容や.csvファイルとして出力する内容に含まれます。また、プラットフォームでは固有のタグを持つものも存在しますが、それらの内容については割愛しています。

| タグ				|概要		|
|:----------------------------	|:--------------|
| Untagged			|タグ付けされていないメモリ|
| Total				|Applicationが使用している総メモリ|
| Untracked			|未トラッキング総メモリ<br>OS込みで利用可能なメモリを示します|
| Tracked Total			|LLMでトラッキングしている総メモリ<br>全てのタグ付けされたメモリの総量を示します|
| FMalloc			|仮想メモリとしてトラッキングしているメモリ<br>MallocBinned2/ページングキャッシュの総量を示します|
| FMalloc Unused		|仮想メモリで未トラッキングのメモリ|
| ThreadStack			|WinThreadで確保されたメモリ<br>Windowsのみトラッキングします|
| Program Size			|プログラムのみで確保されたメモリ|
| OOM Backup Pool		|OOMのバックアッププールで確保するメモリ<br>特定のプラットフォームのみトラッキングします|
| GenericPlatformMallocCrash	|事前割り当てメモリとして確保するメモリ|
| Engine Misc			|他のカテゴリでトラッキングされないメモリ<br>ActorのEventやEngineLoopで確保されるメモリなど定義が曖昧なものが含まれます|
| TaskGraph Misc Tasks		|他のカテゴリでトラッキングされないTaskGraphで確保されるメモリ|
| Audio				|Audio関連のメモリ<br>SoundWave/StreamingAudio/AudioThreadなどのリソースを含みます|
| FName				|FNameで確保されているメモリ|
| Networking			|Network関連のメモリ<br>ReplocationLayoutやNetDriverなどを含みます	|
| Meshes			|Mesh関連のメモリ<br>IndexBuffer/VertexBufferで確保するものを含みます|
| Stats				|Stats関連のメモリ<br>デバッグ時の計測や描画によるStatsで確保するものを含みます|
| Shaders			|Shader関連のメモリ<br>GeometryShaderやPixelShaderなど全てのShaderStageで確保するものを含みます|
| Textures			|Texture関連のメモリ<br>Texture2D/Texture3D/Mipmapで確保するメモリを含みます|
| Render Targets		|RenderTarget関連のメモリ<br>RenderTarget/Viewportで確保するメモリを含みます|
| RHI Misc			|分類されないRHI関連のメモリ<br>RenderThread/RHIThreadで確保するメモリを含みます|
| PhysX TriMesh			|PhysXで確保するTriangleMesh関連のメモリ|
| PhysX ConvexMesh		|PhysXで確保するConvexMesh関連のメモリ|
| AsyncLoading			|非同期ロード時に確保するメモリ|
| UObject			|UObject関連のメモリ<br>UObjectを継承したクラスおよびプロパティなどシリアライズされるものを含みます|
| Animation			|Animation関連のメモリ<br>AnimationAsset/AnimBP/MorphTargetのアセットやシリアライズデータを含みます|
| StaticMesh			|StaticMesh関連のメモリ<br>StaticMeshと関連するプロパティを含みますがMesh自体のデータは含みません|
| Materials			|Material関連のメモリ<br>Material/MaterialInstanceのアセットやシリアライズデータを含みます|
| Particles			|Particle関連のメモリ<br>ParticleComponentで利用中のアセットやシリアライズデータを含みます|
| GC				|GC関連のメモリ<br>GC実行時(到達可能チェック)処理中に確保されるメモリを含みます|
| UI				|UI関連のメモリ<br>Widget/Slate/TextureAtlasを含みます|
| PhysX				|PhysXに投入されるメモリ<br>PhysXが確保するEngine側の内部的なメモリを含みます|
| EnginePreInit			|EngineがPreInitフェーズで確保するメモリ<br>FEngineLoop::Init()のみトラッキングします|
| EngineInit			|EngineがInitフェーズで確保するメモリ<br>FEngineLoop::Init()のみトラッキングします|
| Rendering Thread		|RenderingThreadのメインループで確保するメモリ|
| LoadMap Misc			|Mapロード時に確保するメモリ<br>UEngine::LoadMap()をトラッキングします|
| StreamingManager		|StreamingManager関連のメモリ<br>リソースロード時に確保するメモリをトラッキングします|
| Graphics			|プラットフォームのGraphics関連のメモリ<br>D3D11/D3D12で確保するTexture/Statsなどメモリをトラッキングします|
| FileSystem			|FileSystem関連のメモリ<br>ファイルアクセス時のメモリをトラッキングします|



##4.2. メモリの推移とリークチェック
　ゲームを長時間稼働させた場合メモリーリークが発生していると特定のタグが増加し続けている傾向がみられるかもしれません。以下は意図的にCharacterを解放させない状態をキャプチャした図ですが、Characterは複数のタグに含まれる要素を持っているため、複数のタグで増加していることが確認できます。このように、長時間でのタグの変化をチェックしてもしリークの傾向がみられる場合は、増加しているタグの箇所を中心に、特定のタグを追加したり、調整することでリークの箇所を特定することができます。
![2018-12-18_15h42_11.png](https://qiita-image-store.s3.amazonaws.com/0/101019/8d582e19-eaef-2e82-077d-abf6f4e72715.png)

　しかしながら、注意点で記載したようにLLMだけではどこでリークが発生しているかを特定するのは難しいかもしれません。LLMではある程度の傾向を把握し、どのオブジェクトでリークが発生しているかはmemreportを使用したり、どのモジュールでリークしているかはプロファイラーを使用することで、より効率的にリークのポイントを探しだせることに繋がります。

#5. その他
**強制的にLLMを有効にするためのマクロ**
以下の定義を有効にすることで、-LLMを付与せずともLLMの実行を強制します。

```cpp:\Engine\Source\Runtime\Core\Public\HAL\LowLevelMemTracker.h
	// when set to 1, forces LLM to be enabled without having to parse the command line.
#ifndef LLM_AUTO_ENABLE
	#define LLM_AUTO_ENABLE 1
#endif
```
**Test構成でLLMを有効にするためのマクロ**
以下の定義を有効にすることで、Test構成においてもLLMの実行を有効にすることができます。

```cpp:\Engine\Source\Runtime\Core\Public\HAL\LowLevelMemTracker.h
	#define ALLOW_LOW_LEVEL_MEM_TRACKER_IN_TEST 1
```

#6. まとめ
　メモリリークやメモリに関する検証はアセットやプログラムが揃った終盤に実施することが多いかと思います。しかしながら開発の終盤でもし問題が発覚した場合、大きな変更やアセットの修正を行う時間も限られるため、メモリに関する問題も出来るだけ早めに把握して対応できるのが望ましいです。LLMは容易利用することができるため、開発の中盤などから自動テスト等で検証することをワークフローとして組み込むことで、早期に問題を発見することができます。また、アセットに関してもメモリバジェットを考慮するようになり、以降の作業において手戻りが少なくなるため、開発の全体を俯瞰して見ても非常に便利なツールの1つです。