Skip to content

Commit 7bfbc91

Browse files
committed
An empty shell is an Unity object that's kept alive due to C# holding a reference to it, but the native side has been destroyed already.
1 parent d5d3bc5 commit 7bfbc91

File tree

8 files changed

+226
-0
lines changed

8 files changed

+226
-0
lines changed
94.4 KB
Loading

Editor/Scripts/ManagedEmptyShellObjectsView.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//
2+
// Heap Explorer for Unity. Copyright (c) 2019-2020 Peter Schraut (www.console-dev.de). See LICENSE.md
3+
// https://github.com/pschraut/UnityHeapExplorer/
4+
//
5+
using System.Collections;
6+
using System.Collections.Generic;
7+
using Unity.Collections.LowLevel.Unsafe;
8+
using UnityEditor.IMGUI.Controls;
9+
using UnityEngine;
10+
11+
namespace HeapExplorer
12+
{
13+
public class ManagedEmptyShellObjectsControl : AbstractManagedObjectsControl
14+
{
15+
public Progress progress = new Progress();
16+
17+
public ManagedEmptyShellObjectsControl(HeapExplorerWindow window, string editorPrefsKey, TreeViewState state)
18+
: base(window, editorPrefsKey, state)
19+
{
20+
}
21+
22+
protected override void OnBuildTree(TreeViewItem root)
23+
{
24+
progress.value = 0;
25+
26+
var lookup = new Dictionary<string, AbstractItem>();
27+
var memoryReader = new MemoryReader(m_Snapshot);
28+
29+
for (int n = 0, nend = m_Snapshot.managedObjects.Length; n < nend; ++n)
30+
{
31+
progress.value = (n + 1.0f) / nend;
32+
33+
var obj = m_Snapshot.managedObjects[n];
34+
if (obj.address == 0)
35+
continue; // points to null
36+
37+
if (obj.nativeObjectsArrayIndex != -1)
38+
continue; // has a native object, thus can't be an empty shell object
39+
40+
var type = m_Snapshot.managedTypes[obj.managedTypesArrayIndex];
41+
42+
// Only UnityEngine.Object objects can have a m_CachedPtr connection to a native object.
43+
if (!type.isUnityEngineObject)
44+
continue;
45+
46+
// Could be an array of an UnityEngine.Object, such as Texture[]
47+
if (type.isArray)
48+
continue;
49+
50+
// Get type as a "higher level" representation that is easier to work with
51+
var richType = new RichManagedType(m_Snapshot, obj.managedTypesArrayIndex);
52+
53+
// Try to get the m_InstanceID field
54+
PackedManagedField packedField;
55+
if (!richType.FindField("m_InstanceID", out packedField))
56+
continue;
57+
58+
// The editor contains various empty shell objects whose instanceID all contain 0.
59+
// I guess it's some kind of special object? In this case we just ignore them.
60+
var instanceID = memoryReader.ReadInt32(obj.address + (ulong)packedField.offset);
61+
if (instanceID == 0)
62+
continue;
63+
64+
// Check if we already have a grouping node for that type.
65+
// Create a new node if we don't have it.
66+
AbstractItem parent;
67+
if (!lookup.TryGetValue(type.name, out parent))
68+
{
69+
var group = new GroupItem()
70+
{
71+
id = m_UniqueId++,
72+
depth = root.depth + 1,
73+
displayName = ""
74+
};
75+
group.Initialize(m_Snapshot, type);
76+
77+
lookup[type.name] = parent = group;
78+
root.AddChild(group);
79+
}
80+
81+
// Create and add the managed object item
82+
var item = new ManagedObjectItem
83+
{
84+
id = m_UniqueId++,
85+
depth = parent.depth + 1,
86+
displayName = ""
87+
};
88+
item.Initialize(this, m_Snapshot, obj);
89+
parent.AddChild(item);
90+
91+
m_ManagedObjectCount++;
92+
m_ManagedObjectSize += item.size;
93+
}
94+
95+
progress.value = 1;
96+
}
97+
}
98+
}

Editor/Scripts/ManagedEmptyShellObjectsView/ManagedEmptyShellObjectsControl.cs.meta

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//
2+
// Heap Explorer for Unity. Copyright (c) 2019-2020 Peter Schraut (www.console-dev.de). See LICENSE.md
3+
// https://github.com/pschraut/UnityHeapExplorer/
4+
//
5+
using System.Collections;
6+
using System.Collections.Generic;
7+
using UnityEngine;
8+
using UnityEditor;
9+
using UnityEditor.IMGUI.Controls;
10+
using System;
11+
12+
namespace HeapExplorer
13+
{
14+
public class ManagedEmptyShellObjectsView : AbstractManagedObjectsView
15+
{
16+
[InitializeOnLoadMethod]
17+
static void Register()
18+
{
19+
HeapExplorerWindow.Register<ManagedEmptyShellObjectsView>();
20+
}
21+
22+
public override void Awake()
23+
{
24+
base.Awake();
25+
26+
titleContent = new GUIContent("C# Empty Shell Objects", "");
27+
viewMenuOrder = 258;
28+
}
29+
30+
protected override void OnCreate()
31+
{
32+
base.OnCreate();
33+
34+
var job = new Job();
35+
job.snapshot = snapshot;
36+
job.control = (ManagedEmptyShellObjectsControl)m_ObjectsControl;
37+
ScheduleJob(job);
38+
}
39+
40+
protected override AbstractManagedObjectsControl CreateObjectsTreeView(string editorPrefsKey, TreeViewState state)
41+
{
42+
return new ManagedEmptyShellObjectsControl(window, editorPrefsKey, state);
43+
}
44+
45+
protected override void OnDrawHeader()
46+
{
47+
var text = string.Format("{0} empty shell object(s)", m_ObjectsControl.managedObjectsCount);
48+
window.SetStatusbarString(text);
49+
EditorGUILayout.LabelField(titleContent, EditorStyles.boldLabel);
50+
}
51+
52+
public override void OnGUI()
53+
{
54+
base.OnGUI();
55+
56+
var control = (ManagedEmptyShellObjectsControl)m_ObjectsControl;
57+
if (control.progress.value < 1)
58+
{
59+
window.SetBusy(string.Format("Analyzing Managed Objects, {0:F0}% done", control.progress.value * 100));
60+
}
61+
}
62+
63+
class Job : AbstractThreadJob
64+
{
65+
public ManagedEmptyShellObjectsControl control;
66+
public PackedMemorySnapshot snapshot;
67+
68+
// Output
69+
TreeViewItem tree;
70+
71+
public override void ThreadFunc()
72+
{
73+
tree = control.BuildTree(snapshot);
74+
}
75+
76+
public override void IntegrateFunc()
77+
{
78+
control.SetTree(tree);
79+
}
80+
}
81+
}
82+
}

Editor/Scripts/ManagedEmptyShellObjectsView/ManagedEmptyShellObjectsView.cs.meta

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Editor/Scripts/ManagedObjectsView/ManagedObjectsControl.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,10 @@ public TreeViewItem BuildTree(PackedMemorySnapshot snapshot)
149149
}
150150
}
151151

152+
// For the TreeView to work, make this at least one item exists
153+
if (!root.hasChildren)
154+
root.AddChild(new TreeViewItem { id = 1, depth = -1, displayName = "" });
155+
152156
return root;
153157
}
154158

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,14 @@ For example, if you have ten "Banana" strings and ten "Apple" strings, these wou
200200

201201
The view can be sorted by various columns. The most interesting ones likely being "Size" and "Count". Sorting the view by "Size" allows to quickly see where most memory is wasted due duplicated objects.
202202

203+
# C# Empty Shell Objects
204+
205+
The C# Empty Shell Objects View analyzes managed objects of type UnityEngine.Object if their native object counter-part has been destroyed, which can be a memory leak.
206+
207+
If the m_CachedPtr value of a managed unity engine object points to null, it means its native object underneath has already been destroyed, but it leaked the managed shell. Please see [this post](https://forum.unity.com/threads/help-understanding-diff-data-from-memory-profiler.862813/#post-5684956) for more details.
208+
209+
![alt text](Documentation~/images/cs_empty_shell_objects_01.png "Empty Shell Objects")
210+
203211

204212
# C# Delegates
205213

0 commit comments

Comments
 (0)