Skip to content

Commit 495e42c

Browse files
committed
Implement leaked managed shell detection
1 parent 9620e9e commit 495e42c

File tree

3 files changed

+62
-14
lines changed

3 files changed

+62
-14
lines changed

UMS.Analysis/SnapshotFile.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ public class SnapshotFile : LowLevelSnapshotFile
2525
private Dictionary<ulong, RawManagedObjectInfo> _managedObjectInfoCache = new();
2626

2727
private Dictionary<ulong, ManagedClassInstance> _managedClassInstanceCache = new();
28+
29+
public IEnumerable<ManagedClassInstance> AllManagedClassInstances => _managedClassInstanceCache.Values;
2830

2931
public SnapshotFile(string path) : base(path)
3032
{
@@ -73,7 +75,7 @@ public RawManagedObjectInfo ParseManagedObjectInfo(ulong address)
7375
return info;
7476
}
7577

76-
public ManagedClassInstance? GetManagedClassInstance(ulong address, ManagedClassInstance parent, int depth)
78+
public ManagedClassInstance? GetManagedClassInstance(ulong address, ManagedClassInstance? parent = null, int depth = 0)
7779
{
7880
if (_managedClassInstanceCache.TryGetValue(address, out var ret))
7981
return ret;

UMS.Analysis/Structures/Objects/ManagedClassInstance.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,21 @@ private bool CheckIfRecursiveReference()
132132

133133
return false;
134134
}
135+
136+
public bool InheritsFromUnityEngineObject(SnapshotFile file)
137+
{
138+
if((TypeDescriptionFlags & TypeFlags.Array) == TypeFlags.Array)
139+
return false;
140+
141+
var parent = TypeInfo.BaseTypeIndex;
142+
while (parent != -1)
143+
{
144+
if (parent == file.WellKnownTypes.UnityEngineObject)
145+
return true;
146+
147+
parent = file.GetTypeInfo(parent).BaseTypeIndex;
148+
}
149+
150+
return false;
151+
}
135152
}

UnityMemorySnapshotThing/Program.cs

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ public static void Main(string[] args)
5858
// Console.WriteLine($"Querying large dynamic arrays took {(DateTime.Now - start).TotalMilliseconds} ms\n");
5959

6060
CrawlManagedObjects(file);
61+
62+
FindLeakedUnityObjects(file);
6163
}
6264

6365
private static void CrawlManagedObjects(SnapshotFile file)
@@ -78,36 +80,63 @@ private static void CrawlManagedObjects(SnapshotFile file)
7880
// GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;
7981
foreach (var gcHandle in gcHandles)
8082
{
81-
var info = file.ParseManagedObjectInfo(gcHandle);
82-
83-
if(!info.IsKnownType)
84-
continue;
85-
86-
// var type = file.ReadSingleStringFromChapter(EntryType.TypeDescriptions_Name, info.TypeDescriptionIndex);
87-
88-
// Console.WriteLine($"Size {info.Size}, type {type} ({info.TypeDescriptionIndex})");
89-
90-
rootObjects.Add(new(file, info));
83+
rootObjects.Add(file.GetManagedClassInstance(gcHandle)!.Value);
9184

9285
validCount++;
9386

94-
if(validCount % 1000 == 0)
95-
Console.WriteLine($"Processed {validCount} GC roots in {(DateTime.Now - start).TotalMilliseconds} ms");
87+
// if(validCount % 1000 == 0)
88+
// Console.WriteLine($"Processed {validCount} GC roots in {(DateTime.Now - start).TotalMilliseconds} ms");
9689
}
9790

9891
GCSettings.LatencyMode = GCLatencyMode.Interactive;
9992

10093
Console.WriteLine($"Found {validCount} valid GC roots out of {gcHandles.Length} total in {(DateTime.Now - start).TotalMilliseconds} ms");
10194
}
10295

103-
private static void FindLeakedUnityObjects(LowLevelSnapshotFile file)
96+
private static void FindLeakedUnityObjects(SnapshotFile file)
10497
{
10598
var start = DateTime.Now;
10699
Console.WriteLine("Finding leaked Unity objects...");
107100

108101
//Find all the managed objects, filter to those which have a m_CachedObjectPtr field
109102
//Then filter to those for which that field is 0 (i.e. not pointing to a native object)
110103
//That gives the leaked managed shells.
104+
Console.WriteLine($"Snapshot contains {file.AllManagedClassInstances.Count()} managed objects");
105+
106+
var filterStart = DateTime.Now;
107+
108+
var unityEngineObjects = file.AllManagedClassInstances.Where(i => i.InheritsFromUnityEngineObject(file)).ToArray();
109+
110+
Console.WriteLine($"Of those, {unityEngineObjects.Length} inherit from UnityEngine.Object (filtered in {(DateTime.Now - filterStart).TotalMilliseconds} ms)");
111+
112+
var detectStart = DateTime.Now;
113+
114+
int numLeaked = 0;
115+
foreach (var managedClassInstance in unityEngineObjects)
116+
{
117+
var fields = file.GetFieldInfoForTypeIndex(managedClassInstance.TypeInfo.TypeIndex);
118+
for (var fieldNumber = 0; fieldNumber < fields.Length; fieldNumber++)
119+
{
120+
var basicFieldInfoCache = fields[fieldNumber];
121+
var name = file.ReadSingleStringFromChapter(EntryType.FieldDescriptions_Name, basicFieldInfoCache.FieldIndex);
122+
123+
if (name == "m_CachedPtr")
124+
{
125+
var value = managedClassInstance.Fields[fieldNumber];
126+
127+
if(value is not IntegerFieldValue integerFieldValue)
128+
throw new Exception("Expected integer field value");
129+
130+
if (integerFieldValue.Value == 0)
131+
{
132+
var typeName = file.ReadSingleStringFromChapter(EntryType.TypeDescriptions_Name, managedClassInstance.TypeInfo.TypeIndex);
133+
Console.WriteLine($"Found leaked managed object of type: {typeName}");
134+
numLeaked++;
135+
}
136+
}
137+
}
138+
}
111139

140+
Console.WriteLine($"Finished detection in {(DateTime.Now - detectStart).TotalMilliseconds} ms. {numLeaked} of those are leaked managed shells");
112141
}
113142
}

0 commit comments

Comments
 (0)