forked from Cotoff/UnityHeapEx
-
Notifications
You must be signed in to change notification settings - Fork 0
/
HeapDump.cs
362 lines (338 loc) · 14.6 KB
/
HeapDump.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;
using System.Linq;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace UnityHeapEx
{
public class HeapDump
{
[MenuItem( "Tools/Memory/HeapDump" )]
public static void DoStuff()
{
var h = new HeapDump();
h.DumpToXml();
}
private readonly HashSet<object> seenObjects = new HashSet<object>();
private StreamWriter writer;
// when true, types w/o any static field (and skipped types) are removed from output
public static bool SkipEmptyTypes = true;
/* TODO
* ignore cached delegates for lambdas(?)
* better type names
* deal with arrays of arrays
* maybe special code for delegates? coroutines?
*/
/// <summary>
/// Collect all roots, i.e. static fields in all classes and all scripts; then dump all object
/// hierarchy reachable from these roots to file
/// </summary>
public void DumpToXml()
{
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
// assembles contain all referenced system assemblies, as well as UnityEngine, UnityEditor and editor
// code. To make root list smaller, we only select main game assembly from the list, although it might
// concievably miss some statics
var gameAssembly = assemblies.Single( a => a.FullName.Contains( "Assembly-CSharp," ) );
var allTypes = gameAssembly.GetTypes();
var allScripts = UnityEngine.Object.FindObjectsOfType( typeof( MonoBehaviour ) );
seenObjects.Clear(); // used to prevent going through same object twice
using( writer = new StreamWriter( "heapdump.xml" ) )
{
writer.WriteLine( "<?xml version=\"1.0\" encoding=\"utf-8\"?>" );
int totalSize = 0;
writer.WriteLine( "<statics>" );
// enumerate all static fields
foreach( var type in allTypes )
{
bool tagWritten = false;
if(!SkipEmptyTypes)
writer.WriteLine( " <type name=\"{0}\">", SecurityElement.Escape( type.GetFormattedName() ) );
if(type.IsEnum)
{
// enums don't hold anything but their constants
if( !SkipEmptyTypes )
writer.WriteLine( "<ignored reason=\"IsEnum\"/>" );
}
else if(type.IsGenericType)
{
// generic types are ignored, because we can't access static fields unless we
// know actual type parameters of the class containing generics, and we have no way
// of knowing these - they depend on what concrete type were ever instantiated.
// This may miss a lot of stuff if generics are heavily used.
if( !SkipEmptyTypes )
writer.WriteLine( "<ignored reason=\"IsGenericType\"/>" );
}
else
{
foreach( var fieldInfo in type.GetFields( BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic ) )
{
try
{
if(SkipEmptyTypes && !tagWritten)
{
writer.WriteLine( " <type name=\"{0}\">", SecurityElement.Escape( type.GetFormattedName() ) );
tagWritten = true;
}
int size = ReportField( null, " ", fieldInfo );
totalSize += size;
}
catch( Exception ex )
{
writer.WriteLine( "Exception: " + ex.Message + " on " + fieldInfo.FieldType.GetFormattedName() + " " +
fieldInfo.Name );
}
}
}
if( !SkipEmptyTypes || tagWritten )
writer.WriteLine( " </type>" );
}
writer.WriteLine( "</statics>" );
// enumerate all MonoBehaviours - that is, all user scripts on all existing objects.
// TODO this maybe misses objects with active==false.
writer.WriteLine( "<scripts>" );
foreach( MonoBehaviour mb in allScripts )
{
writer.WriteLine( " <object type=\"{0}\" name=\"{1}\">", SecurityElement.Escape( mb.GetType().GetFormattedName() ), SecurityElement.Escape( mb.name ) );
var type = mb.GetType();
foreach( var fieldInfo in type.EnumerateAllFields() )
{
try
{
int size = ReportField( mb, " ", fieldInfo );
totalSize += size;
}
catch( Exception ex )
{
writer.WriteLine( "Exception: " + ex.Message + " on " + fieldInfo.FieldType.GetFormattedName() + " " +
fieldInfo.Name );
}
}
writer.WriteLine( " </object>" );
}
writer.WriteLine( "</scripts>" );
// writer.WriteLine( "Total size: " + totalSize );
}
Debug.Log( "OK" );
}
/// <summary>
/// Works through all fields of an object, dumpoing them into xml
/// </summary>
public int GatherFromRootRecursively(object root, string depth)
{
var seen = seenObjects.Contains( root );
if( root is Object )
{
var uo = root as Object;
WriteUnityObjectData( depth, uo, seen );
}
if( seen )
{
if(!(root is Object))
{
// XXX maybe add some object index so that this is traceable to original object dump
// earlier in xml?
writer.WriteLine( depth + "<seen/>" );
}
return 0;
}
seenObjects.Add( root );
var type = root.GetType();
var fields = type.EnumerateAllFields();
var res = 0;
foreach( var fieldInfo in fields )
{
try
{
res += ReportField( root, depth, fieldInfo );
}
catch( Exception ex )
{
writer.WriteLine( "Exception: " + ex.Message + " on " + fieldInfo.FieldType.GetFormattedName() + " " + fieldInfo.Name );
}
}
return res;
}
private void WriteUnityObjectData( string depth, Object uo, bool seen )
{
// shows some additional info on UnityObjects
writer.WriteLine( depth + "<unityObject type=\"{0}\" name=\"{1}\" seen=\"{2}\" hash=\"{3}\"/>",
SecurityElement.Escape( uo.GetType().GetFormattedName() ),
SecurityElement.Escape( uo ? uo.name : "--missing reference--" ),
seen,
uo ? uo.GetHashCode().ToString() : "0");
// todo we can show referenced assets for renderers, materials, audiosources etc
}
/// <summary>
/// Dumps info on the field in xml. Provides some rough estimate on size taken by its contents,
/// and recursively enumerates fields if this one contains an object reference.
/// </summary>
/// <returns>
/// Rough estimate of memory taken by field and its contents
/// </returns>
private int ReportField(object root, string depth, FieldInfo fieldInfo)
{
var v = fieldInfo.GetValue( root );
int res = 0;
var ftype = v==null?null:v.GetType();
writer.WriteLine( depth + "<field type=\"{0}\" name=\"{1}\" runtimetype=\"{2}\" hash=\"{3}\">",
SecurityElement.Escape( fieldInfo.FieldType.GetFormattedName() ),
SecurityElement.Escape( fieldInfo.Name ),
SecurityElement.Escape( v==null?"-null-":ftype.GetFormattedName()),
v==null?"0":v.GetHashCode().ToString());
if(v==null)
{
res += IntPtr.Size;
}
else if( ftype.IsArray )
{
// arrays have special treatment b/c we have to work on every array element
// just like a single value.
// TODO refactor this so that arry item and non-array field value share code
var val = v as Array;
res += IntPtr.Size; // reference size
if( val != null && !seenObjects.Contains( val ))
{
seenObjects.Add( val );
var length = GetTotalLength( val );
writer.WriteLine( depth + " <array length=\"{0}\"/>", length );
var eltype = ftype.GetElementType();
if( eltype.IsValueType )
{
if( eltype.IsEnum )
eltype = Enum.GetUnderlyingType( eltype );
try
{
res += Marshal.SizeOf( eltype ) * length;
}
catch( Exception )
{
writer.WriteLine( depth + " <error msg=\"Marshal.SizeOf() failed\"/>" );
}
}
else if( eltype == typeof( string ) )
{
// special case
res += IntPtr.Size * length; // array itself
foreach( string item in val )
{
if( item != null )
{
writer.WriteLine( depth + " <string length=\"{0}\"/>", item.Length );
if(!seenObjects.Contains( val ))
{
seenObjects.Add( val );
res += sizeof( char ) * item.Length + sizeof( int );
}
}
}
}
else
{
res += IntPtr.Size * length; // array itself
foreach( var item in val )
{
if( item != null )
{
writer.WriteLine( depth + " <item type=\"{0}\" hash=\"{1}\">",
SecurityElement.Escape( item.GetType().GetFormattedName() ),
item.GetHashCode().ToString());
res += GatherFromRootRecursively( item, depth + " " );
writer.WriteLine( depth + " </item>");
}
}
}
}
else
{
writer.WriteLine( depth + " <null/>" );
}
}
else if( ftype.IsValueType )
{
if( ftype.IsPrimitive )
{
var val = fieldInfo.GetValue( root );
res += Marshal.SizeOf( ftype );
writer.WriteLine( depth + " <value value=\"{0}\"/>", val );
}
else if( ftype.IsEnum )
{
var val = fieldInfo.GetValue( root );
res += Marshal.SizeOf( Enum.GetUnderlyingType( ftype ) );
writer.WriteLine( depth + " <value value=\"{0}\"/>", val );
}
else
{
// this is a struct. This code assumes that all structs contain only primitive types,
// which is very strong. Structs that contain references will break, and fail to traverse these
// references properly
int s = 0;
try
{
s = Marshal.SizeOf( ftype );
}
catch( Exception )
{
// this breaks if struct has a reference member. We should probably never have such structs, but we'll see...
writer.WriteLine( depth + " <error msg=\"Marshal.SizeOf() failed\"/>" );
}
writer.WriteLine( depth + " <struct size=\"{0}\"/>", s );
res += s;
}
}
else if( ftype == typeof( string ) )
{
// special case
res += IntPtr.Size; // reference size
var val = fieldInfo.GetValue( root ) as string;
if( val != null )
{
writer.WriteLine( depth + " <string length=\"{0}\"/>", val.Length );
if(!seenObjects.Contains( val ))
{
seenObjects.Add( val );
res += sizeof( char ) * val.Length + sizeof( int );
}
}
else
writer.WriteLine( depth + " <null/>" );
}
else
{
// this is a reference
var classVal = fieldInfo.GetValue( root );
res += IntPtr.Size; // reference size
if( classVal != null )
{
res += GatherFromRootRecursively( classVal, depth + " " );
}
else
{
writer.WriteLine( depth + " <null/>" );
}
}
writer.WriteLine( depth + " <total size=\"{0}\"/>", res );
writer.WriteLine( depth + "</field>" );
return res;
}
private int GetTotalLength(Array val)
{
var rank = val.Rank;
if( rank == 1 )
return val.Length;
var l = 1;
while( rank > 0 )
{
l *= val.GetLength( rank - 1 );
rank--;
}
return l;
}
}
}