Memory leaks are an elusive category of bugs that often take quite a while to manifest. These bugs occur when we allocate memory for something and then lose track of it without ever freeing up the memory it’s using. Memory leaks contribute to overall instability, causing an application to get bogged down and eventually crash as it slowly consumes all of the available RAM.
While it’s hard to patch up every leaky allocation, there are a few techniques we can use to find the largest offenders and make sure they stop hogging memory that they don’t need. Recently, the Paragon team did just that in response to some memory leaks that would crash the game after a few hours. While Paragon matches rarely last long enough to eat up all of the system's memory, it’s difficult to predict how the match may play out. After all, nothing would be more frustrating than having your client crash right as your team engages in that pivotal team fight!
Following along with their experiences, here are some tips and tricks on how to track down the source of your memory leaks.
Finding Evidence of Leaks with MemReport
The first step to tracking down memory leaks is determining whether or not a leak is occurring. A simple way to do this is to take a snapshot of the current memory allocations at two different points in time, and comparing them to see what has changed. If no actions are taken in the game, we can expect similar memory usage regardless of the amount of time passed. However, if there is a steady rise in the amount of memory used as time passes, it may be time to start looking for a leak.
As an example, I created an Actor that allocated 1,000 integers per Tick, throwing away the reference immediately. I dropped my Actor in an empty scene and allowed it to run for ten minutes, and took a MemReport at the beginning and end of my session. A comparison of the two reports shows a noticeable increase in memory usage, despite the fact that the object count hasn’t changed at all.
More information about MemReport can be found in a previous blog post, Debugging and Optimizing Memory.
MemReport can help distinguish memory leaks from other types of leaks, such as object leaks. An object leak may be occurring if the number of objects present in the scene is consistently increasing. This typically indicates that spawned objects aren’t being cleaned up properly. For example, what if we create a projectile class that is destroyed when colliding with another object? Projectiles fired into the air will just travel skyward forever, holding on to valuable memory. The MemReport will show how many instances of each object exist in the world at that time, giving a clue as to which object is leaking so that we can investigate the spawning and despawning logic to see if there’s a problem.
Once we suspect some of our memory allocations are never being freed, we need to track down who is responsible for those allocations. To accomplish this, the Paragon team built a tool that can monitor any open memory allocations that haven’t yet been freed. The tool is still experimental and disabled by default, so you’ll need to make a quick change to MallocLeakDetection.h and recompile:
#define MALLOC_LEAKDETECTION 1
Once you’ve done that, make sure you have Visual Studio running and attached to your game instance. You can begin logging by typing “MallocLeak Start” in the console and stop by typing “MallocLeak Stop”. The tool will then dump all open allocations that were made during the logging period into Visual Studio’s output window. Optionally, you can type “MallocLeak Dump N” while the logger is running, where N is a size in bytes. This will immediately dump open allocations filtered to allocations of at least N bytes. It’s helpful to begin logging after the game has initialized, as many allocations may be made at initialization and not freed until the game is closed.
I modified my leaky Actor to make a large allocation 10 seconds after the game starts. I then ensured MallocLeak was running during the allocation, and used “MallocLeak Dump 1000000” to dump all open allocations larger than 1 megabyte. As expected, I found something suspicious in the results. Quite a bit of memory has been allocated by an Actor’s Tick function!
AllocSize: 12345678, Num: 1, FirstFrameEverAllocated: 1522 UE4Editor-Core.dll!FWindowsPlatformStackWalk::CaptureStackBackTrace() [d:\release-4.11\engine\source\runtime\core\private\windows\windowsplatformstackwalk.cpp:233] UE4Editor-Core.dll!FMallocLeakDetection::Malloc() [d:\release-4.11\engine\source\runtime\core\private\hal\mallocleakdetection.cpp:180] UE4Editor-Core.dll!FMallocLeakDetectionProxy::Malloc() [d:\release-4.11\engine\source\runtime\core\private\hal\mallocleakdetection.h:116] UE4Editor-MemoryLeak.dll UE4Editor-Engine.dll!AActor::TickActor() [d:\release-4.11\engine\source\runtime\engine\private\actor.cpp:807] UE4Editor-Engine.dll!FActorTickFunction::ExecuteTick() [d:\release-4.11\engine\source\runtime\engine\private\actor.cpp:111] UE4Editor-Engine.dll!FTickFunctionTask::DoTask() [d:\release-4.11\engine\source\runtime\engine\private\ticktaskmanager.cpp:262] UE4Editor-Engine.dll!TGraphTask<FTickFunctionTask>::ExecuteTask() [d:\release-4.11\engine\source\runtime\core\public\async\taskgraphinterfaces.h:999] UE4Editor-Core.dll!FNamedTaskThread::ProcessTasksNamedThread() [d:\release-4.11\engine\source\runtime\core\private\async\taskgraph.cpp:932] UE4Editor-Core.dll!FNamedTaskThread::ProcessTasksUntilQuit() [d:\release-4.11\engine\source\runtime\core\private\async\taskgraph.cpp:679] UE4Editor-Core.dll!FTaskGraphImplementation::WaitUntilTasksComplete() [d:\release-4.11\engine\source\runtime\core\private\async\taskgraph.cpp:1776] UE4Editor-Engine.dll!FTickTaskSequencer::ReleaseTickGroup() [d:\release-4.11\engine\source\runtime\engine\private\ticktaskmanager.cpp:530] UE4Editor-Engine.dll!FTickTaskManager::RunTickGroup() [d:\release-4.11\engine\source\runtime\engine\private\ticktaskmanager.cpp:1435] UE4Editor-Engine.dll!UWorld::RunTickGroup() [d:\release-4.11\engine\source\runtime\engine\private\leveltick.cpp:704] UE4Editor-Engine.dll!UWorld::Tick() [d:\release-4.11\engine\source\runtime\engine\private\leveltick.cpp:1197] UE4Editor-UnrealEd.dll!UEditorEngine::Tick() [d:\release-4.11\engine\source\editor\unrealed\private\editorengine.cpp:1346] UE4Editor-UnrealEd.dll!UUnrealEdEngine::Tick() [d:\release-4.11\engine\source\editor\unrealed\private\unrealedengine.cpp:368] UE4Editor.exe!FEngineLoop::Tick() [d:\release-4.11\engine\source\runtime\launch\private\launchengineloop.cpp:2772] UE4Editor.exe!GuardedMain() [d:\release-4.11\engine\source\runtime\launch\private\launch.cpp:148] UE4Editor.exe!WinMain() [d:\release-4.11\engine\source\runtime\launch\private\windows\launchwindows.cpp:189] UE4Editor.exe!__scrt_common_main_seh() [f:\dd\vctools\crt\vcstartup\src\startup\exe_common.inl:264] kernel32.dll ntdll.dll
As with most profiling techniques, it’s important to look at the results critically. Keep in mind that allocations made before you begin logging won’t show up in the result. Similarly, allocations freed after you stop logging will show up, even if they aren’t indicative of a leak. However, the MallocLeak tool is great for investigating where your memory is going and give some hints as to where to look for the offending code. Happy Hunting!