UE4 Low Level Memory Tracker 使用
LLM(Low Level Memory Tracker) 是从 4.18 开始引入的新的内存统计工具,比 memreport 统计数据更加详细精确,但又不会像 MallocProfiler 那样有很大的本身开销
相关代码实现在 Engine\Source\Runtime\Core\Public\HAL\LowLevelMemTracker.h 和 Engine\Source\Runtime\Core\Private\HAL\LowLevelMemTracker.cpp 中
本文基于 UE4.23 分析使用 LLM
编译开关
在 Development 和 Debug 下,LLM 相关的代码是默认编译的,在 Test 模式下,需要在 YourGame.Target.cs 文件中增加宏定义 ALLOW_LOW_LEVEL_MEM_TRACKER_IN_TEST=1
才会编译相关代码,Shipping 模式下不会编译
运行开关
在编译时打开 LLM 后,要想在运行时使用 LLM 的功能,还要加上启动命令行 -LLM -LLMCSV
-LLM
:运行时打开 LLM 统计-LLMCSV
:将内存统计信息输出到 CSV 文件中,CSV 文件保存在 Saved\Profiling\LLM 目录下
如果想在运行时默认生效,可以在 Target.cs 文件中增加宏定义 LLM_AUTO_ENABLE=1
另外还有个命令行 -LLMTAGSETS
,用来指示资源统计分类是按类别来分,还是按具体资源来分,不过要使用这个功能需要在编译时在代码中打开 LLM_ALLOW_ASSETS_TAGS
宏(这个宏目前不能在 Target.cs 文件添加)
-LLMTAGSETS=Assets
:按资源分类统计-LLMTAGSETS=AssetClasses
:按资源类别分类统计
运行时开关 LLM 的处理见 FLowLevelMemTracker::ProcessCommandLine
函数
Android 命令行
在已经装好版本的手机上,如果想要启动 LLM,可以通过 push UE4CommandLine.txt 到手机上动态改变游戏启动参数
新建一个 UE4CommandLine.txt,内容如下
../../../YourProject/YourProject.uproject -LLM -LLMCSV
新建一个 PushCommandLine.bat 并执行,内容如下
%ANDROID_HOME%\platform-tools\adb.exe shell mkdir -p /sdcard/UE4Game/YourProject
%ANDROID_HOME%\platform-tools\adb.exe push UE4CommandLine.txt /sdcard/UE4Game/YourProject/UE4CommandLine.txt
IOS 命令行
IOS 需要将 ue4commandline.txt 通过文件共享复制到 Documents 目录下,注意这里的文件名是全小写
显示统计命令行
运行时,可以通过以下命令行来显示当前的统计信息
- stat LLM: 显示 Default Tracker 分组的简要内存统计信息
- stat LLMFULL: 显示 Default Tracker 分组的所有内存统计信息
- stat LLMOveread: 显示 LLM 本身使用的内存信息
- stat LLMPlatform: 显示 Platform Tracker 分组的内存统计信息
- LLM.LLMWriteInterval: 修改 LLM 的内存信息写入 csv 文件的频率,默认 5 秒写一行
LLMASSETS 命令行
编译时打开 LLM_ALLOW_ASSETS_TAGS
,运行时指定 -LLMTAGSETS=Assets
,执行 stat LLMAssets 命令行效果如下:
运行时指定 -LLMTAGSETS=AssetClasses
,执行 stat LLMAssets 命令行效果如下:
CSV 文件输出
指定 -LLMCSV
命令行后,会得到两个 csv 文件,一个是 *LLM_* 前缀,另一个是 *LLMPlatform_* 前缀,这两个文件分别由两个 Tracker 生成,这两个文件的关注点不同
- LLMPlatform 只关注系统内存的消费者,主要有几大类: ProgramSize, FMalloc, LLMOverhead。
- LLM 主要关注 FMalloc 内存的消费者,主要就是引擎内各个部分的内存开销
注意,当同时指定 -LLMTAGSETS
命令行后,CSV 文件会错乱,因为写 CSV 文件时预留了第一行标题栏内容的缓冲区,当打开 LLMTAGSETS 之后,因为每个资源或资源类很多,会导致标题栏内容溢出,覆盖了后面的数据区域,所以这里需要自己按需求修改一下
LLM 统计分类
- Total: 总内存
ELLMTag::PlatformTotal
: 在 Android 上为 RSS,在 iOS 上为 resident_size,在其它平台上为系统所有进程消耗的内存(不明白这里为什么是这样),所以这个值目前只有在移动端有参考意义ELLMTag::Total
: PlatformTotal - LLM Overhead,即为上面的内存减去 LLM 本身消耗的内存,这个内存值可以认为是在不打开 LLM 功能下的进程内存值,同上,这个值目前只在移动端上有参考意义
ELLMTag::PlatformOverhead
: 即 LLMOverhead,LLM 本身消耗的内存- TrackedTotal: 通过调用 LLM
OnLowLevelAlloc/OnLowLevelFree
统计到的所有内存ELLMTag::PlatformTrackedTotal
: Tracker 类型为ELLMTracker::Platform
ELLMTag::TrackedTotal
: Tracker 类型为ELLMTracker::Default
- Untracked: 未知的内存分配,即 LLM 无法追踪的内存,比如第三方库里的内存分配
ELLMTag::PlatformUntracked
: PlatformTotal - PlatformTrackedTotalELLMTag::Untracked
: Total - TrackedTotal
ELLMTag::Untagged
: 未分类的内存,即分配内存时候不在 LLM_SCOPE 宏范围内ELLMTag::FMalloc
: 通过 FMallocBinned/FMallocBinned2 内存分配器分配的内存,也就是一般意义上引擎分配的内存,比如各种UObject,纹理,动画等占用的内存ELLMTag::FMallocUnused
: 通过FMemory
分配的内存与ELLMTag::FMalloc
类别内存的差值- ProgramSize: 程序刚起来时占用的内存,即刚进 Main 函数时,在 Android 上为 RSS,在 iOS 上为 resident_size
ELLMTag::ProgramSize
ELLMTag::ProgramSizePlatform
- OOMBackupPool: Out of memory 备用内存池
ELLMTag::BackupOOMMemoryPoolPlatform
ELLMTag::BackupOOMMemoryPool
- GenericPlatformMallocCrash: 预分配的 crash 相关的内存,当 crash 发生时用来替换
GMalloc
ELLMTag::GenericPlatformMallocCrashPlatform
ELLMTag::GenericPlatformMallocCrash
ELLMTag::EngineMisc
: 在以下函数执行过程中分配的内存AActor::ProcessEvent
UPackageMapClient::SerializeNewActor
FEngineLoop::Tick
ELLMTag::TaskGraphTasksMisc
:FTaskThreadAnyThread::ProcessTasks
函数执行过程中分配的内存- Audio: 音频相关的内存,包括音频资源,音频线程中分配的内存,具体还包含以下子类别
ELLMTag::AudioMisc
ELLMTag::AudioSoundWaves
ELLMTag::AudioMixer
ELLMTag::AudioPrecache
ELLMTag::AudioDecompress
ELLMTag::AudioRealtimePrecache
ELLMTag::AudioFullDecompress
ELLMTag::AudioVoiceChat
ELLMTag::FName
: FName 资源占用的内存ELLMTag::Networking
: 网络相关的内存,包含网络包处理,连接对象内存等ELLMTag::Meshes
: Mesh 的顶点缓存,顶点索引缓存占用的内存,还有以下子类别ELLMTag::StaticMesh
: StaticMesh 相关的内存,包含 StaticMesh 资源,StaticMeshComponentELLMTag::SkeletalMesh
: SkeletalMeshComponent, SkinnedMeshComponent, SkeletalMesh 内存ELLMTag::InstancedMesh
: InstancedStaticMeshComponent, HierarchicalInstancedStaticMeshComponent 内存ELLMTag::Landscape
: LandscapeComponent, LandscapeProxy, LandscapeComponentSceneProxy 内存
ELLMTag::Stats
: stat 统计相关的内存占用ELLMTag::Shaders
: 各种类型 Shander 的内存占用ELLMTag::PSO
: Pipeline State Object 缓存的内存占用ELLMTag::Textures
: 纹理相关的内存占用ELLMTag::TextureMetaData
:UTexture2D::Serialize
函数执行过程中分配的内存ELLMTag::RenderTargets
: RT 相关的内存,包含在 Viewport 中渲染使用的内存ELLMTag::SceneRender
: 场景渲染相关的内存占用,包含 Slate RHI Render 的窗口渲染ELLMTag::RHIMisc
: 其它渲染相关的内存占用,如各种渲染状态,RHI Thread 运行过程中的内存ELLMTag::AsyncLoading
: 异步资源加载过程中的内存占用,包含 AsyncLoadingThread, Event Driven LoaderELLMTag::UObject
: UObject 占用的内存,包含 UClass 等ELLMTag::Animation
: 动画相关的内存,包含 AnimInstance, AnimSequence, AnimationAsset, AnimBlueprint, MorphTargetELLMTag::Materials
: 材质相关的内存,包含 MaterialInterface, MaterialFunctionELLMTag::Particles
: 特效相关的内存,包含 ParticleSystemComponentELLMTag::GC
: GC 过程中的内存,分别为以下两个函数CollectGarbageInternal
PerformReachabilityAnalysis
ELLMTag::UI
: Slate 相关的内存,包含字体,TextureAtlasELLMTag::PhysX
: PhysX 物理相关的内存ELLMTag::EnginePreInitMemory
:FEngineLoop::PreInit
函数执行过程中的内存ELLMTag::EngineInitMemory
:FEngineLoop::Init
函数执行过程中的内存ELLMTag::RenderingThreadMemory
: 渲染线程(RenderingThreadMain
)执行过程中的内存ELLMTag::LoadMapMisc
: 地图加载过程中的内存,分别为以下两个函数UEngine::LoadMap
UWorld::UpdateLevelStreaming
ELLMTag::StreamingManager
: StreamableManager 相关的内存,主要是资源 streaming 函数执行过程中的内存ELLMTag::GraphicsPlatform
: 显存,只在 D3D,Vulkan 上有ELLMTag::FileSystem
: 文件系统相关的内存,主要是文件读取时的缓冲区,比如 Pak 文件ELLMTag::Localization
: 本地化相关的内存ELLMTag::AssetRegistry
: AssetRegistryModule 内存ELLMTag::ConfigSystem
: 配置文件相关的内存ELLMTag::InitUObject
:InitUObject
函数执行过程中的内存ELLMTag::MaterialInstance
: MaterialInstance, MaterialInstanceDynamic 包括材质参数内存占用ELLMTag::Lua
: Lua 内存
Windows 标签
ELLMTag::WorkingSetSize
: 进程物理内存,可用这个值替代上面的 PlatformTotalELLMTag::PagefileUsed
: 进程虚拟内存- ThreadStack: Windows 线程栈内存
ELLMTag::ThreadStackPlatform
ELLMTag::ThreadStack
添加自定义标签
除了以上的分类标签外,还可以添加自定义的标签,主要有两种方式
第一种是直接修改 LowLevelMemTrakcer.h 文件,在 LLM_ENUM_GENERIC_TAGS
宏内按同样格式添加一个新标签定义
另外一种是通过调用 RegisterPlatformTag/RegisterProjectTag
来动态地注册新标签,主要参数如下:
Tag
: 标签枚举,不要和其它地方重复了Name
: 标签名,显示在 CSV 文件的标题栏中StatName
: 与这个标签相关的 Stat 统计,在游戏中通过 stat 命令来显示,使用DECLARE_LLM_MEMORY_STAT
宏进行定义,可以为空(NAME_None
)SummaryStatName
: 汇总 Stat 统计,可以将多个 LLM STAT 汇总在一起,可以为空(NAME_None
)ParentTag
: 父级标签枚举值,父级标签对应的内存值是所有子级标签的和,级数最多两级
DECLARE_LLM_MEMORY_STAT(TEXT("Test"), STAT_TestLLM, STATGROUP_LLMFULL);
enum class EMyLLMTag
{
TestTag = ELLMTag::ProjectTagStart + 20,
}
void Init()
{
LLM(FLowLevelMemTracker::Get().RegisterProjectTag((int32)EMyLLMTag::TestTag, TEXT("Test"), GET_STATFNAME(STAT_TestLLM), NAME_None));
}
void TestFunc()
{
LLM_SCOPE(EMyLLMTag::TestTag);
}