A system to track memory allocations and deallocations with a visual interface.
Consists of a tree map that stores the allocation data. This storage allows for the association of pointers to their allocation size. This allows for tracking the memory usage by noting the size of allocations when they are allocated and freed. The storage also associates pointers with the type of allocator that was used to allocate them, the region of the engine it was allocated from, and the thread ID of where the allocation took place.
Maintains metrics that track the amount of memory allocations that take place and the size of those allocations. It is further broken down into number of and size of allocations by thread ID, region of engine, and allocator type in addition to the global metrics. It also tracks the number of size of allocations per frame.
Manages exporting the MemoryMetrics class to a file and writes the memory metrics state to a file after every allocation to allow for viewing the memory metrics of a period of time later.
Stores the state memory metrics over time. This contains the data of the memory metrics class after every allocation was made. This consists of arrays of memory metrics data.
Manages importing the MemoryMetrics class to a file and parsing the memory profile file into RAM and into a usable SceneMetricsStorage instance.
Provides the public interface of the memory tracking system and glues all the previous components together.
Manages the UI of the memory profiler. This allows the user to view all of the different memory metrics over time as well as letting the user select each memory allocation and seeing the region of the engine it was allocated in, the allocator type that made the allocation, the thread it came from, and the stacktrace that led to the allocation.
A macro is provided to the user to put inside each scope that is being profiled and it uses the constructor and destructor to measure the time that the scope took to run and also checks the memory tracker metrics at the beginning and end of the scope to determine how many allocations are made in the scope. This is then written to a file that can be viewed later.
Example Image of the Scope Profiler Trace Output (Using Perfetto)
![]()
Example of Integrating an Allocator into the MemoryTracker // TODO: Finish this
Core::MemoryTracker& memoryTracker = Core::MemoryTracker::Get();
memoryTracker.AddAllocation(pointer, size, region, allocatorType); // Adds the allocation to the memory tracking system
memoryTracker.RemoveAllocation(pointer); // Removes the allocation from the memory tracking system
This shows the visualizer tool that graphs the memory profiling scene data.
This shows the real time memory usage metrics of the engine.
###
Initially, I made a basic memory tracker that would display the memory usage and number of allocations. I noticed that I was doing more allocations than I thought I would. Additionally, I made the memory tracker and visualizer as a natural extension to my allocators I wrote. Because of those two things, I decided to make a more exhaustive memory tracker. I wanted a memory tracker that would help me profile systems I wrote and sections of the engine. Therefore, I decided to track memory allocations by the region of the engine and the thread ID to get a better idea of where the allocations are coming from. Additionally, I also tracked the memory allocations by the type of allocator to get an idea of the trends in my engine as well as seeing how many areas of the engine have used more performant allocators than the new operator/malloc. I think the most important aspect of the memory tracker is that it helps me locate and discover areas with excess amounts of allocations and their origins, so I can go to the debugger and find a simple solution to remove the allocations if there is one or come up with a solution to make the area more performant. This is why I also decided to add stacktraces to the memory metrics of each allocation. It allows me to see the exact functions and situation that led to these allocations. Combine this with the other metrics, and it enables me to discover and fix areas of my code that cause an unnecessary amount of allocations without the total number of allocations building up into a bigger problem that noticeable affects performance of the engine and requires much more time to fix in a scenario without the memory tracker. To extend this tool further, I also make a visualizer for the memory metrics data. It graphs the allocation data over time to allow the user to see trends in allocation over time. Additionally, the memory tracker also allows the user to mark their code with labels to show what is happening in the code, which will show up in the graph on the window to give the user context of what is happening when there are spikes in allocations in an area. The memory debug window also lets you choose a specific allocation and see all the associated data with that allocation. You can also view the memory metrics in a few different modes which allows the user to view and discover trends in the memory allocations. You can choose between to graph the memory usage, peak memory usage, active allocations, and total allocations. Furthermore, you can also choose to view the memory metrics by thread ID, region, or allocator type and the memory debug window will show a graph for each thread ID, region, or allocator type, and will also filter the list of allocations that you can select from to only the allocations in the selected graph.
As a part of the memory profiling tool, Astral Engine also provides a scope profiler that allows the user to see how many allocations are made in any given scope in addition to how long the scope took to execute. The output is written to a file and can be viewed by Chrome’s trace tool or Perfetto’s trace tool. An example image is shown earlier in the file under the following section: Brief Descriptions the Parts of the Memory Tracking System -> Scope Profiler.
The memory tracking system is thread safe by the nature of tracking memory allocations as allocations can happen on any thread but we need the metrics of the allocation to be stored in a central, single instance of MemoryTracker. It uses mutexes to ensure that there are no race conditions or other thread related errors.
The stacktraces are creating performance issues right now. I am going to try and wait to resolve the symbols of the stacktraces until the end of the scene recording. That way, I can record scenes during the runtime from the click of a button instead of having to hard code a limited scene recording due to bad performance.
The visualizer which loads and graphs the memory profiling data is treated as offline tool even though it is currently accessed through engine runtime. This means that the performance requirements are not as strict because it is not a part of the real time engine. However, there is a real time component to the memory tracking tool as the real time memory metrics can be found in the engine’s debug menu under the memory subsection.
During a profiling scene, the memory metrics, after each allocation is made, gets written to a file. The file extension is .ASTLMemProfile. Each write (called a snapshot) is in the following format: The memory metrics instance data, the time of allocation (microseconds since the start of the scene), the allocation data struct (data like region, allocation size, thread ID, etc.), and, lastly, the a string that holds the stacktrace of the allocation. At the footer of the file, there is size_t that contains the number of snapshots that the file holds. There is no header with data.
Another thing to note is that the library msgpack is used to serialize the data and handles the actual format of the data inside the file. The above was the order that the data will appear in.
The memory tracker does not throw and return any error codes. However, msgpack will throw exceptions when a cast fails or another error occurs, so the memory tracker catches anything that is thrown from msgpack.
###