We released a big update to the Memory Profiler package today that marks a major milestone on it’s route to the 1.0 release.
The broad strokes of the release is that we’ve drastically simplified the analysis of what references a given object in memory, as well as the workflow of diving into the details of that object. We have more on both of these points that we will add and improve upon going forward, but the new UI and workflow affordances should already alleviate a lot of issues that you have rightfully pointed out to us over the years.
For now, we hope that this release helps speed up your workflows for memory analysis and that we can refine these further with your feedback.
So let’s dive into the release:
(Click here for the full changelog for 0.5.0-preview.1 or here for a forum thread you can subscribe to in order to get notified of new releases of this package.)
Details Panel
When you select an item, such as a row in a table, a rectangle in the Tree Map or a bar or it’s row in the Memory Usage Summary, the newly added right side panel will provide further information regarding that selection.
The side panel has two sections:
- References
- Selection Details
The References section shows data related to the selection within the main view panel, while the Selection Details section will provide information about the selection within the main view panel or within the references section.
(Raw) References at a glance
In previous versions of the Memory Profiler, what other objects referenced an object, and thereby possibly kept it in memory, was only visible as a blue link in the Referenced By column. To see the entire trail of references, one had to click through several screens, possibly going in circles.
This information is now displayed in the References section as a Tree View. Clicking any of the tree’s expand arrows while holding down the alt/option modifier key will expand the entire subtree (like in the rest of the editor), providing the full reference chain at a glance, pruned for cyclic dependency chains.
Worth noting is that the references here might, but don’t have to be the ones that keep an element in memory.
For example: a managed object referencing the managed shell for a scene object in a different scene would keep the managed object in memory, even if the other scene is unloaded. The native object referenced by the managed shell would however be unloaded (and the managed shell “leaked” as a result).
The current snapshot data is missing some information such as which scene a scene object is rooted to, which we are about to add to the data on the Unity Editor & Player side. A future update to the package and that extra data from the snapshots will allow us to make this view show significantly fewer items by filtering out references that may be informative for the purpose of seeing what is connected to what, but ultimately don’t matter much in terms of what is keeping what in memory. These filtered views will be called “Path To Roots” or “Path From Roots” as we are considering a bottom-up and top-down variation for different workflows.
As such, we are thinking of keeping this “Raw” version of the references even when we have the more helpful Path To/From Roots, A) as a fallback for snapshots from older Players/Editor’s and B) for the curious that want to dive deeper than “What is holding this in memory” via the references.
A last thing to note is that this view lists Managed Shell Objects and their Native Objects separately, while when you select either of them the Selection Details will show these as one item.
Diving Deeper into References - a quick showcase
So let’s examine the references to that InventorySlotBG object from the previous screenshot. As you can see, we’ve selected the Native Object part of this object, which is shown as the very first line in this view.
Each Object inheriting from UnityEngine.Object (or it’s Native code equivalent) can have exactly one Managed Shell object that is used by Managed C# Code to access and handle this resource. You can see the Managed Shell of this Texture2D object directly in the second line. The Managed Shell object has a pointer back to the Native Object, so these 2 reference each other. Thus, it makes not much of a difference for the References view, which one of the two you selected, the first two lines might just be flipped.
Next, this Texture is referenced by a Sprite of the same name, indicating that this Texture2D is imported as a Sprite. The Sprite is referenced by the Managed code of the Image component that derives from MonoBehaviour, which sits on a GameObject named “Background Image”.
Now, technically our examination could stop here. We’ve hit the first GameObject reference. This GameObject will be referenced by other GameObjects, or rather, their Transform Components, within the Scene Hierarchy and ultimately roots this GameObject and the Texture to the Scene. Except, we don’t yet have the data in this snapshot, to identify which Transform is at the root of that hierarchy and which Scene it is placed in. This is where the additional Scene Root data and filtering is going to come in later.
For the time being, the remaining references in this example can illuminate what other components are on that GameObject, and what is referencing those, which may help you identify it further if you just had its name.
Also Note that the Texture2D Asset itself is marked as “Persistent”, which means that it is associated with a file and that the Persistent Manager is handling its live time and will unload it during Resources.UnloadUnusedAssets if it is no longer referenced, so just unloading the scene alone may not get rid of the memory just yet. That it is indeed marked as Persistent can be seen in the Selection Details.
Selection Details
The Selection Details has different sections based on the current selection. This example shows all of them.
Name, Icons, Select and Search Buttons
The selected Item is the Texture2D we’ve examined in the References view before. The view shows both its managed and native data, as indicated by the “#++” icon. The Asset is named “InventorySlotBG”, i.e. this is what you would get if you queried the .name property on its Managed Shell object.
Every UnityEngine.Object has a name associated with its Native Object, but those created at run time (i.e. not loaded from a file) might not have one set. If you find an Object that doesn’t have a name, it may just be the leaked Managed Shell (the Status field will tell you as much) or you might want to find where in your project’s code that object is created or instantiated and make sure to set the .name property to something helpful.
The Type Name in this case is the shortened form. You can toggle between seeing the full Managed Type Name (and the Native Type Name) by right-clicking the name or the table headers of the References or Managed Fields table. With this option turned off, this text displays as "<Native Object Name>" <Managed Type Name> : <Native Type Name>
When you selected the Object, the Memory Profiler went ahead and searched the project for something that matches this object’s name or type. If there was exactly one match, the “Select In Editor” button is activated and, if applicable, it’s preview is loaded.
Please note that this is NOT GUARANTEED to be the same object, and that the Preview is not based on e.g. what the Texture looked like in the build, because the Texture pixel data is not captured within the snapshot. Instead this preview comes from within the current Editor Project, which may have different import settings or an entirely different or changed texture with the same name.
That said, the “Select In Editor” will ping and select the object in the Editor, so you can use any Inspector to examine its details.
If you just captured the current Editor instance, it may also just use the Instance ID directly to find the Object still in memory.
If no exact match is found, you can use the Search buttons to look for it instead. This will filter the Scene View and Scene Hierarchy View (if it is a Scene Object) or the Project Browser Window if it is an Asset. In Editor versions 2021.1 or newer you can also directly open the search in the Quick Search window.
Basic Info and Help
The Basic info shows the total size as well as how that is split across the Managed and Native side of the Object. Similarly, the References field shows how many other objects are referencing this object, and that it is referencing “it-self”, i.e. the Native Object references the Managed Shell and vice versa.
The Status field gives a brief, at a glance insight into what this object is, why it may have been loaded and why it may not have been unloaded yet. The Tooltip of that field, as well as the Help section below go into more details regarding that Status assessment, as well as tips for possible next steps.
Advanced Info
The information this section provides may be interesting for some very specific workflows. But mostly, what can be derived from this info has already been boiled down into the Status and Help section, but made easier to read.
Preview Image
As noted in the context of the Select and Search Buttons above, please note that the Preview is NOT based on e.g. what the Texture looked like in the build, because the Texture pixel data is not captured within the snapshot. Instead this preview comes from within the current Editor Project, which may have different import settings or an entirely different or changed texture with the same name.
Managed Fields inspector
The Managed Shell object for a Texture2D doesn’t have a whole lot of managed field data associated with it.
There are the 2 static fields and the private field m_CachedPtr, which is how the Managed Shell references the Native Object underneath it. That Native object in turn, references the Managed one again via a GCHandle. To avoid going around in circles, this circular reference is cut short as it reaches around to the Selected Managed Object, as the Notes column indicates as well.
Another relatively simple object that no less showcases an interesting use case for the Managed Fields inspector is this MonoBehaviour I wrote:
It too has the static field (which is used internally for finding the instance ID on the Native Object) and the m_CachedPtr. Because this is a capture from the Editor, it also has a managed field for the Instance ID and an error string. These are used for the Missing component error messages in the Editor.
The other fields however show that you can now see how much memory your scripts are referencing to in native memory, via IntPtr or in this example, Persistent NativeArrays.
Details for Memory Usage Overview Categories
We haven’t yet implemented Selection Details for all items you can select in the Memory Profiler, but we do have a basic implementation for the Memory Usage Overview Categories, which are now selectable. The Selection Details for those are currently just text that explains what they are in more detail than a tooltip could. We are looking at other possibilities for what information we could surface here and would love to hear your thoughts on this.
Simplified Tables
While we are already working on a new table UI to deliver more intuitive workflows than the existing table UI can provide, we have taken this release as an opportunity to reassess what needs to be displayed in the ones we already have.
We’ve removed all links. They were cumbersome, prone to getting clicked on accidentally and made some key workflows hard to discover.
- Given that References are now way easier to analyze in the References section, the Referenced By column no longer needed to link to a table of it’s direct Referrers.
- Similarly, the Instance ID was no longer needed to ping an object in the Editor, and it only worked if the capture was made in the current Editor session anyways
- Seeing details for just one Object, or examining the Native Object under a Managed Shell, can now be done right in the Selection Details, so Native Object Name and the Value column no longer needed a link
Additionally, the Address moved to the Advanced section in the Selection Details and the Index column was entirely useless outside of debugging purposes for us. The Addresses column was also responsible for unfolding Managed Objects to inspect their fields and data layout, which is now moved to the Managed Fields section in the Selection Details. With that move, the Static, Length, Managed Size and Target Field Size columns became obsolete too, and since the tables show Managed Objects and Native Objects separately anyways, only one Size column remained.
The Value column is still the easiest, at a glance, way of looking at a brief glimpse of the fields of a Managed Object, so it remains for now.
So with all that in mind, we decided to hide all these now (mostly) obsolete columns) and provide you with the ability to show them again if absolutely required. (We are aware that this choice is not saved. Please let us know if you have any workflows that you require these columns for so that we can consider that for the new table UI.)
Lastly, we re-arranged the order of the columns in a way that felt more intuitive and better prioritized to us. This leaves us with this set of columns in the extreme case (Diff All Objects):
As you can see, we also added a much requested Count and Total Size summary above the table, tha updates as you filter the table. Given that we now hide the expandable bit of Managed Objects and only have one Size column, this was now straightforward, but may have some odd edge cases if you add the now hidden columns again.
Feedback
As mentioned in several places above, this update does not deliver the final workflows we are envisioning, but are a major step towards them, that we would love to get your feedback on. So even though we believe to have a good understanding and vision for what is needed, your feedback to us along this road is going to be critical in order for us to be able to deliver you the Memory Profiler workflows you deserve.
-
Do these new views help you in analyzing your apps memory usage more quickly and clearly?
-
Is there anything that you are missing or that is confusing in these views?
-
Did we break your workflow by hiding some columns by default?
Or just whatever else you have in mind regarding this update and the status of the tool in general.
You can send us your feedback by replying to this thread and/or via this quick survey questionnaire, or by opening new threads within the Profiler Previews subforum.