Search Unity

Inspecting memory with the new Memory Profiler package

February 28, 2023 in Engine & platform | 15 min. read
Inspecting memory with the new Memory Profiler package | Hero image
Inspecting memory with the new Memory Profiler package | Hero image
Share

Is this article helpful for you?

Thank you for your feedback!

In this blog, we will cover five key workflows in the new Memory Profiler package that you can use to diagnose and examine memory-related issues in your game. These are:

  • Monitoring your application’s memory pressure
  • Seeing the distribution of Unity Objects
  • Detecting poorly configured assets
  • Locating unintentional duplicate objects
  • Comparing memory captures to validate optimizations

For an introduction to the Memory Profiler, please see the recent blog, Everything you need to know about Memory Profiler 1.0.0.

Monitoring your application’s memory pressure

This first workflow monitors how demanding your application is on a device’s memory resources. This process is critical to determining whether or not your application is at risk of performance problems, or even being evicted and terminated by the operating system, due to consuming too much memory.

To begin, we have a build of an example game running on the target device. Naturally, it is essential that we take a memory capture of the game, running on the actual hardware, to see how it uses the devices' available memory resources. Furthermore, memory does not behave in the same way in the Unity Editor as it does in the Unity runtime, so taking a memory capture of the Editor in Play Mode is not a good representation of how a game’s memory will look on a device. (Taking a memory capture of the Editor is appropriate when developing tools for the Editor, such as custom Editor windows.)

After navigating to the stage in our game where we want to analyze the memory usage, we attach the Memory Profiler to our device using the dropdown in the Memory Profiler. We can then take a memory capture, as shown below.

Connecting to a build of our game and taking a memory capture.

After opening this capture, the Memory Profiler displays our application’s memory footprint at the top of the Summary page as “Memory Usage On Device”.

Our application’s memory footprint is displayed by the Memory Usage On Device indicator.
Our application’s memory footprint is displayed by the Memory Usage On Device indicator.

Here we can see that our application’s memory footprint is 492.5 MB, out of an available 3.50 GB. We need to use our best judgment next as to whether we believe that is a sensible proportion of the device’s physical memory (RAM) to be using at the time of capture. Remember that a device’s physical memory is shared by all running processes.

What exactly is meant by ‘memory footprint’?

You’ll notice that this visual indicator is showing you total resident memory. Total resident memory refers to how much of your application’s memory resides in the device’s physical memory hardware (RAM). This is the clearest indicator of how demanding your application’s current memory usage is on the target device for two reasons. First, this is because as your application’s total resident memory usage increases, so does the likelihood of incurring frequent page faults, where the operating system has to page virtual memory in and out of the device’s physical memory. Frequent page faults will cause significant performance degradation of your application. And second, this is because many operating systems use your application’s resident memory usage to determine its current memory footprint. If your application’s memory footprint gets too high, the operating system will evict your application and terminate it, causing a crash for your players.

Therefore, you can use the Memory Usage On Device visual indicator in the Memory Profiler to infer if an application might be at risk of performance issues or being terminated by the operating system, due to an overuse of memory at the time of capture.

This contrasts with Allocated Memory, sometimes referred to as Committed Memory, which you might notice is displayed in various graphics below this indicator, and is currently the default option shown by all other views, such as Unity Objects. Allocated Memory refers to all memory that your application currently has allocated, regardless of whether it has been made resident in physical memory or not, and therefore it matches your application’s view of memory more closely. As such, this can be useful for exploring all of your application’s currently allocated memory, whilst resident memory usage is key to understanding the memory pressure your application is placing on the hardware at any moment in time.

Seeing the distribution of Unity Objects

The Memory Profiler’s Unity Objects tab provides you with an overview of your application’s memory from the perspective of Unity Objects; that’s your application’s textures, shaders, meshes, materials, and so on. This is a great place to begin exploration in the Memory Profiler because Unity Objects will be inherently familiar to so many Unity users, as it is what the majority of us work with directly in the Unity Editor. Not only does this provide a familiar entry point to understanding our application’s memory, but it can also help to diagnose and fix a range of potential issues by providing this Unity-specific context.

The Unity Objects view.
The Unity Objects view.

To see the Unity Objects view, simply select the Unity Objects tab at the top of the Memory Profiler after opening a memory capture, as shown above.

You can see how the Unity Objects view quickly gives us an understanding of the distribution of Unity Object types in our application. This allows us to both gain a high-level understanding of what types were consuming the most memory at the time of capture, as well as to reason about this, such as whether it is expected that a particular scene is heavy on AudioClip objects, for example. Expanding each type also enables us to view every Unity Object that is currently allocated, individually, as shown below.

We can see every mesh object currently allocated by our application by expanding the Mesh type.
We can see every mesh object currently allocated by our application by expanding the Mesh type.

It’s important to remember that Unity Objects make up a proportion of our application’s total allocated memory. You can see exactly how much in the indicator above the table, highlighted below.

The indicator above the table shows us the proportion of our total allocated memory that we are seeing in the table.
The indicator above the table shows us the proportion of our total allocated memory that we are seeing in the table.

Here, you can see that our total allocated memory size, “Total Memory In Snapshot”, is 4.64GB and that our Unity Objects account for 2.37GB of that. Furthermore, if we filter the table – for example, by using the search feature – you’ll notice that this bar updates to reflect our search results. In other words, it displays the size of all the memory currently shown in the table. This helps you to maintain perspective of exactly how much memory you are inspecting as a proportion of the whole capture and can help to inform where to invest optimization efforts.

The indicator above the table updates as we filter to reflect the size of the memory currently shown in the table, as a proportion of the total allocated memory.
The indicator above the table updates as we filter to reflect the size of the memory currently shown in the table, as a proportion of the total allocated memory.

In version 1.0 of Memory Profiler, the Unity Objects table shows you Allocated Memory, or, put another way, it shows you all Unity Objects that are alive in your application. We are exploring adding Resident Memory visibility to these views in an upcoming release, which would enable you to see exactly which of your Unity Objects are currently resident in physical memory, and therefore see exactly which are directly contributing to your application’s current memory footprint.

You can use the All Of Memory tab to inspect the remainder of your application’s memory at the time of capture, which will include memory outside of Unity Objects, such as various Unity subsystems, managed-only (C#) memory, and DLLs and executables.

Detecting poorly configured assets

The Unity Objects view can help us to diagnose a range of potential issues. One such issue is detecting assets that have been badly configured, causing them to consume more memory than is necessary.

In the capture below, you can see that a substantial portion of our Unity Objects are textures. The capture is from a project with high graphical fidelity that uses the High Definition Render Pipeline and makes heavy use of visual effects. So, with this context in mind, we expect to see heavy use of textures, which we do.

The amount of memory associated with Unity Objects in this capture is dominated by RenderTexture and Texture2D types.
The amount of memory associated with Unity Objects in this capture is dominated by RenderTexture and Texture2D types.

However, upon expanding our second large category, Texture2D, we can notice that two textures appear much bigger than the others. Using our understanding of our project, we are surprised that these textures are bigger than comparable textures, like HoloTable_Normal or HoloTable_Mask, as we expected them to be similar in size.

Two textures in our capture appear to be double the size of comparable textures.
Two textures in our capture appear to be double the size of comparable textures.

So, we select one of these textures in the table to learn more details about it, and to begin investigating what might be the cause for this. Here, in the Details view we find our explanation – our texture is writable, or “Read/Write Enabled.”

The Details view displays additional information about the selected object, and reveals that our texture is “read/write enabled.”
The Details view displays additional information about the selected object, and reveals that our texture is “read/write enabled.”

This is a common problem that we see across many user projects: accidentally making a texture writable when it’s not needed by checking the “Read/Write” setting on the texture’s import settings. When a texture has this flag enabled, it will double its size in memory. This is because a second copy of the texture data is required so that it can be accessed on the CPU. A tell-tale sign of this is that the Total Size of a texture is twice the size of what you expected, or twice the size of similar textures.

After disabling the “Read/Write” flag on both of these textures and taking a second capture, we can see both of these textures have halved in size.

The offending two textures in our capture are now the same size as comparable textures after the change.
The offending two textures in our capture are now the same size as comparable textures after the change.

We are exploring adding a column for graphics (GPU) memory to the Unity Objects table in a future release to make locating cases where a Unity Object has allocated graphics memory, such as in this example, easier.

Locating unintentional duplicate objects

A common mistake that we see in Unity projects is unintentionally creating duplicate Unity Objects. For example, it is very easy to accidentally create a duplicate Material by accessing a MeshRenderer’s material property. Not only does this add up quickly in this case – if, for example, it is done on every instance of a particular MeshRenderer – but, furthermore, these dynamically created materials must be explicitly destroyed.

To help with locating this type of issue, the Unity Objects table provides a quick filter to show you potential duplicate Unity Objects only. This view will filter the table to show only Unity Objects that have multiple instances with both an identical name and size. It is important to note that many potential duplicates will be expected and not a cause for concern at all. For example, multiple instances of a prefab might have identically named and sized Transform components, and these would be expected duplicates. It is only discovering unintentional duplicates that we are interested in, which we will illustrate in this workflow following example.

The capture below was taken in a simple scene with two instances of a Door prefab, and we have enabled the Show Potential Duplicates Only filter located underneath the Unity Objects table. This has filtered the table to show us only Unity Objects that have multiple instances with the same name and size.

The “Show Potential Duplicates Only” quick filter shows us only Unity Objects that have multiple instances with the same name and size.
The “Show Potential Duplicates Only” quick filter shows us only Unity Objects that have multiple instances with the same name and size.

Because we have two instances of a Door prefab in our scene, we also have, as expected, two instances of all the relevant objects: MeshRenderer, Transform, GameObject, and so on. However, we also have two instances of the “Door” Material in our capture above. These Door instances look the same in our scene, so it is expected that they would share a Material. This is, therefore, an unintentional duplicate, and in this particular example was caused by accessing the MeshRenderer’s material property in the prefab. Removing this property access and taking a second capture shows the duplicate material is no longer present in the Unity Objects table.

The unintentional duplicate material is no longer present in the Unity Objects table.
The unintentional duplicate material is no longer present in the Unity Objects table.

It’s important to remember that this filter is simply showing you all Unity Objects that have multiple instances with the same name and size. It requires your knowledge of your project to interpret whether the potential duplicates you see are expected, or are, in fact, unintentional and cause for investigation. We recommend paying attention to the Total Memory In Table bar at the top, which gives you a visual indication of what proportion of your application’s allocated memory you are seeing in the table. This can help you to maintain perspective of where to invest your optimization efforts.

Comparing memory captures to validate optimizations

The Memory Profiler also provides functionality to compare two memory captures. This allows us to make changes to our project, for example to address an issue we might have found, and subsequently test if our changes have indeed had the desired outcome. It is important to always test that your hypothesis is correct and your changes have had the desired outcome on the actual hardware. Here, let’s explore an example of this comparison workflow.

Below is a capture of our mobile game taken during the first level. We can see that the biggest category of Unity Objects is Texture2D. After opening this category to check what our biggest textures are, we can see there are a few UI textures that are quite large in relation to the rest of our game – megabytes each. This raises a suspicion for us: Why are these textures so much larger than the others and do they need to be? To discover why, we can first locate the source texture asset in our project by selecting the texture in the Memory Profiler and using the “Select In Editor” button, which will highlight the source texture asset in our Project window.

Use the “Select In Editor” button in the Details view to select the source asset in the Project window.
Use the “Select In Editor” button in the Details view to select the source asset in the Project window.

Using the Inspector window, we can see that all of our offending large UI textures are not being compressed due to their dimensions not being a power-of-two, as shown by the “NPOT” (non-power-of-two) text.

The six large textures are all not being compressed due to their dimensions not being a power-of-two.
The six large textures are all not being compressed due to their dimensions not being a power-of-two.

This explains these large texture sizes. We can now use our knowledge of our project to reduce this memory usage. We know that three of these textures (the help controls) are always displayed together in the UI, as well as the other three textures (the creatures). Therefore, we can hypothesize with high confidence that creating two Sprite Atlases for each set of three textures will reduce our allocated memory usage, because it will enable them to be compressed without increasing the number of textures in memory.

Creating two sprite atlases for each set of three textures.
Creating two sprite atlases for each set of three textures.

To compare two snapshots, begin by opening the first snapshot. This is the “base” against which we want to compare. Now above the open snapshot, select the “Compare Snapshots” tab and choose the second snapshot. The Memory Profiler will now present a summary comparing the two snapshots, as shown below.

The Memory Profiler’s Summary page presents a summary of the differences between two memory captures when two snapshots are being compared.
The Memory Profiler’s Summary page presents a summary of the differences between two memory captures when two snapshots are being compared.

To see the effect of our change and verify that it did, in fact, reduce the size of our application’s allocated memory for the Texture2D category, we can select the Unity Objects tab. Here, we are presented with a comparison table that shows the Unity Object types that have changed, as well as how they have changed between the captures (shown below).

The Unity Object types that changed between the two memory captures.
The Unity Object types that changed between the two memory captures.

We can see our Texture2D type as a whole has reduced in size by 3.6MB and has four less textures than before. Expanding this category, we can see the removal of our individual, uncompressed Sprite textures, and the addition of our two Sprite Atlas textures, resulting in a net reduction of 3.6MB and 4 Texture2D objects.

The Texture2D Unity Objects that have changed between the two captures.
The Texture2D Unity Objects that have changed between the two captures.

So this was a success – we have confirmed that our hypothesis was correct using the comparison functionality, and we have reduced the size of these textures in allocated memory.

Conclusion

From reading this blog, you should now have a better understanding of five key workflows in the new Memory Profiler package. These workflows are designed for diagnosing and examining memory-related issues in ayour game. We hope the Memory Profiler package released in Unity 2022.2 helps you to better monitor, examine, and understand your game’s memory footprint. Please feel free to reach out to the team to share your feedback on how we can improve performance profiling tools via our forum page –, or share your suggestions through our roadmap page, where you can also see some of the features that are being worked on.

If you’re interested in more details on this topic, we will be publishing another blog in the coming weeks that will dive deeper into how an application’s memory footprint is calculated, covering topics such as resident and allocated memory in more detail.

February 28, 2023 in Engine & platform | 15 min. read

Is this article helpful for you?

Thank you for your feedback!

Related Posts