├── Assets └── UnityHeapDump │ ├── Editor │ └── UnityHeapDump.cs │ └── UnityHeapDump.cs ├── LICENSE └── README.md /Assets/UnityHeapDump/Editor/UnityHeapDump.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | 3 | class HeapDumping 4 | { 5 | [MenuItem("Tools/Memory/Dump Heap")] 6 | public static void DumpIt () 7 | { 8 | UnityHeapDump.Create(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Assets/UnityHeapDump/UnityHeapDump.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Reflection; 5 | using System.Runtime.InteropServices; 6 | using System.Linq; 7 | using UnityEngine; 8 | using UObject = UnityEngine.Object; 9 | 10 | public class UnityHeapDump 11 | { 12 | const int TYPE_MIN_SIZE_TO_PRINT = 1; 13 | const int ROOT_MIN_SIZE = 1; 14 | const int CHILD_MIN_SIZE = 1; 15 | 16 | static int ThreadJobsPrinting = 0; 17 | static int ThreadJobsDone = 0; 18 | 19 | static HashSet genericTypes = new HashSet(); 20 | 21 | public static void Create () 22 | { 23 | TypeData.Start(); 24 | ThreadJobsPrinting = 0; 25 | ThreadJobsDone = 0; 26 | 27 | if (Directory.Exists("dump")) 28 | { 29 | Directory.Delete("dump", true); 30 | } 31 | Directory.CreateDirectory("dump"); 32 | Directory.CreateDirectory("dump/sobjects"); 33 | Directory.CreateDirectory("dump/uobjects"); 34 | Directory.CreateDirectory("dump/statics"); 35 | 36 | using (var logger = new StreamWriter("dump/log.txt")) 37 | { 38 | Dictionary> assemblyResults = new Dictionary>(); 39 | Dictionary assemblySizes = new Dictionary(); 40 | List> parseErrors = new List>(); 41 | foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) 42 | { 43 | string assemblyFolder; 44 | if (assembly.FullName.Contains("Assembly-CSharp")) 45 | { 46 | assemblyFolder = string.Format("dump/statics/{0}/", assembly.FullName.Replace("<", "(").Replace(">", ")").Replace(".", "_")); 47 | } else 48 | { 49 | assemblyFolder = "dump/statics/misc/"; 50 | } 51 | Directory.CreateDirectory(assemblyFolder); 52 | List types = new List(); 53 | foreach (var type in assembly.GetTypes()) 54 | { 55 | if (type.IsEnum || type.IsGenericType) 56 | { 57 | continue; 58 | } 59 | try 60 | { 61 | types.Add(new StructOrClass(type, assemblyFolder)); 62 | } 63 | catch (Exception e) 64 | { 65 | parseErrors.Add (new KeyValuePair(type, e)); 66 | } 67 | } 68 | assemblyResults[assembly] = types; 69 | } 70 | 71 | List unityComponents = new List(); 72 | 73 | foreach (var go in Resources.FindObjectsOfTypeAll()) 74 | { 75 | foreach (var component in go.GetComponents()) 76 | { 77 | if (component == null) 78 | { 79 | continue; 80 | } 81 | try 82 | { 83 | unityComponents.Add(new StructOrClass(component)); 84 | } 85 | catch (Exception e) 86 | { 87 | parseErrors.Add(new KeyValuePair(component.GetType(), e)); 88 | } 89 | } 90 | } 91 | 92 | List unityScriptableObjects = new List(); 93 | 94 | foreach (ScriptableObject scriptableObject in Resources.FindObjectsOfTypeAll()) 95 | { 96 | if (scriptableObject == null) 97 | { 98 | continue; 99 | } 100 | try 101 | { 102 | unityScriptableObjects.Add(new StructOrClass(scriptableObject)); 103 | } 104 | catch (Exception e) 105 | { 106 | parseErrors.Add(new KeyValuePair(scriptableObject.GetType(), e)); 107 | } 108 | } 109 | 110 | foreach (var genericType in genericTypes.ToList()) 111 | { 112 | try 113 | { 114 | assemblyResults[genericType.Assembly].Add(new StructOrClass(genericType, "dump/statics/misc/")); 115 | } 116 | catch (Exception e) 117 | { 118 | parseErrors.Add(new KeyValuePair(genericType, e)); 119 | } 120 | } 121 | 122 | foreach (var pair in assemblyResults) 123 | { 124 | assemblySizes[pair.Key] = pair.Value.Sum(a => a.Size); 125 | pair.Value.Sort((a, b) => b.Size - a.Size); 126 | } 127 | 128 | TypeData.Clear(); 129 | 130 | var assemblySizesList = assemblySizes.ToList(); 131 | assemblySizesList.Sort((a, b) => (b.Value - a.Value)); 132 | 133 | unityComponents.Sort((a, b) => (b.Size - a.Size)); 134 | int unityComponentsSize = unityComponents.Sum(a => a.Size); 135 | bool printedUnityComponents = false; 136 | 137 | unityScriptableObjects.Sort((a, b) => (b.Size - a.Size)); 138 | int unityScriptableObjectsSize = unityScriptableObjects.Sum(a => a.Size); 139 | bool printedUnityScriptableObjects = false; 140 | 141 | logger.WriteLine("Total tracked memory (including duplicates, so too high) = {0}", assemblySizesList.Sum(a => a.Value) + unityComponentsSize + unityScriptableObjectsSize); 142 | 143 | 144 | foreach (var pair in assemblySizesList) 145 | { 146 | var assembly = pair.Key; 147 | var size = pair.Value; 148 | 149 | if (!printedUnityComponents && size < unityComponentsSize) 150 | { 151 | printedUnityComponents = true; 152 | logger.WriteLine("Unity components of total size: {0}", unityComponentsSize); 153 | foreach (var instance in unityComponents) 154 | { 155 | if (instance.Size >= TYPE_MIN_SIZE_TO_PRINT) 156 | { 157 | logger.WriteLine(" Type {0} (ID: {1}) of size {2}", instance.ParsedType.FullName, instance.InstanceID, instance.Size); 158 | } 159 | } 160 | } 161 | 162 | if (!printedUnityScriptableObjects && size < unityScriptableObjectsSize) 163 | { 164 | printedUnityScriptableObjects = true; 165 | logger.WriteLine("Unity scriptableobjects of total size: {0}", unityScriptableObjectsSize); 166 | foreach (var instance in unityScriptableObjects) 167 | { 168 | if (instance.Size >= TYPE_MIN_SIZE_TO_PRINT) 169 | { 170 | logger.WriteLine(" Type {0} (ID: {1}) of size {2}", instance.ParsedType.FullName, instance.InstanceID, instance.Size); 171 | } 172 | } 173 | } 174 | 175 | logger.WriteLine("Assembly: {0} of total size: {1}", assembly, size); 176 | foreach (var type in assemblyResults[assembly]) 177 | { 178 | if (type.Size >= TYPE_MIN_SIZE_TO_PRINT) 179 | { 180 | logger.WriteLine(" Type: {0} of size {1}", type.ParsedType.FullName, type.Size); 181 | } 182 | } 183 | } 184 | foreach (var error in parseErrors) 185 | { 186 | logger.WriteLine(error); 187 | } 188 | } 189 | 190 | while (ThreadJobsDone < ThreadJobsPrinting) 191 | { 192 | System.Threading.Thread.Sleep(1); 193 | } 194 | } 195 | 196 | class StructOrClass 197 | { 198 | public int Size { get; private set; } 199 | public Type ParsedType { get; private set; } 200 | public int InstanceID { get; private set; } 201 | int ArraySize { get; set; } 202 | string Identifier { get; set; } 203 | 204 | List Children = new List(); 205 | 206 | /// 207 | /// Parse static types 208 | /// 209 | public StructOrClass (Type type, string assemblyFolder) 210 | { 211 | ParsedType = type; 212 | HashSet seenObjects = new HashSet(); 213 | Identifier = type.FullName; 214 | foreach (var fieldInfo in type.GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)) 215 | { 216 | ParseField(fieldInfo, null, seenObjects); 217 | } 218 | if (Size < ROOT_MIN_SIZE) 219 | { 220 | return; 221 | } 222 | 223 | System.Threading.Interlocked.Increment(ref ThreadJobsPrinting); 224 | System.Threading.ThreadPool.QueueUserWorkItem (obj => { 225 | try 226 | { 227 | string fileName = Identifier.Replace("<", "(").Replace(">", ")").Replace(".", "_"); 228 | using (var writer = new StreamWriter(string.Format("{0}{1}-{2}", assemblyFolder, Size, fileName))) 229 | { 230 | writer.WriteLine("Static ({0}): {1} bytes", ParsedType, Size); 231 | Children.Sort((a, b) => b.Size - a.Size); 232 | string childIndent = " "; 233 | foreach (var child in Children) 234 | { 235 | if (child.Size >= CHILD_MIN_SIZE) 236 | { 237 | child.Write(writer, childIndent); 238 | } else 239 | { 240 | break; 241 | } 242 | } 243 | } 244 | } 245 | finally 246 | { 247 | System.Threading.Interlocked.Increment(ref ThreadJobsDone); 248 | } 249 | }); 250 | } 251 | 252 | /// 253 | /// Parse monobehaviour and scriptableobject instances 254 | /// 255 | public StructOrClass (UObject uObject) 256 | { 257 | InstanceID = uObject.GetInstanceID(); 258 | ParsedType = uObject.GetType(); 259 | Identifier = uObject.name + uObject.GetInstanceID(); 260 | HashSet seenObjects = new HashSet(); 261 | 262 | foreach (var field in ParsedType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) 263 | { 264 | ParseField(field, uObject, seenObjects); 265 | } 266 | 267 | if (Size < ROOT_MIN_SIZE) 268 | { 269 | return; 270 | } 271 | 272 | string fileName = string.Format("dump/{0}/{1}-{2}", 273 | uObject is ScriptableObject ? "sobjects" : "uobjects", 274 | Size, 275 | Identifier.Replace("<", "(").Replace(">", ")").Replace(".", "_") 276 | ); 277 | 278 | using (var writer = new StreamWriter(fileName)) 279 | { 280 | writer.WriteLine("{0} ({1}): {2} bytes", Identifier, ParsedType, Size); 281 | Children.Sort((a, b) => b.Size - a.Size); 282 | foreach (var child in Children) 283 | { 284 | if (child.Size >= CHILD_MIN_SIZE) 285 | { 286 | child.Write(writer, " "); 287 | } 288 | } 289 | } 290 | } 291 | 292 | /// 293 | /// Parse field objects; only called for arrays, references and structs with references in them 294 | /// 295 | StructOrClass(string name, object root, TypeData rootTypeData, HashSet seenObjects) 296 | { 297 | Identifier = name; 298 | ParsedType = root.GetType(); 299 | Size = rootTypeData.Size; 300 | if (ParsedType.IsArray) 301 | { 302 | int i = 0; 303 | ArraySize = GetTotalLength((Array)root); 304 | Type elementType = ParsedType.GetElementType(); 305 | TypeData elementTypeData = TypeData.Get(elementType); 306 | if (elementType.IsValueType || elementType.IsPrimitive || elementType.IsEnum) 307 | { 308 | if (elementTypeData.DynamicSizedFields == null) 309 | { 310 | Size += elementTypeData.Size * ArraySize; 311 | return; 312 | } 313 | 314 | foreach (var item in (Array)root) 315 | { 316 | StructOrClass child = new StructOrClass((i++).ToString(), item, elementTypeData, seenObjects); 317 | Size += child.Size; 318 | Children.Add(child); 319 | } 320 | } 321 | else 322 | { 323 | Size += IntPtr.Size * ArraySize; 324 | foreach (var item in (Array)root) 325 | { 326 | ParseItem(item, (i++).ToString(), seenObjects); 327 | } 328 | } 329 | } else 330 | { 331 | if (rootTypeData.DynamicSizedFields != null) 332 | { 333 | foreach (var fieldInfo in rootTypeData.DynamicSizedFields) 334 | { 335 | ParseField(fieldInfo, root, seenObjects); 336 | } 337 | } 338 | } 339 | } 340 | 341 | /// 342 | /// Parse the field of the object, ignoring any seenObjects. If root is null, it is considered a static field. 343 | /// 344 | void ParseField(FieldInfo fieldInfo, object root, HashSet seenObjects) 345 | { 346 | if (!fieldInfo.FieldType.IsPointer) 347 | { 348 | ParseItem(fieldInfo.GetValue(root), fieldInfo.Name, seenObjects); 349 | } 350 | } 351 | 352 | void ParseItem (object obj, string objName, HashSet seenObjects) 353 | { 354 | if (obj == null) 355 | { 356 | return; 357 | } 358 | Type type = obj.GetType(); 359 | if (type.IsPointer) 360 | { 361 | return; // again, a pointer cast to whatever the fieldtype is, shoo. 362 | } 363 | if (type == typeof(string)) 364 | { 365 | // string needs special handling 366 | int strSize = 3 * IntPtr.Size + 2; 367 | strSize += ((string)(obj)).Length * sizeof(char); 368 | int pad = strSize % IntPtr.Size; 369 | if (pad != 0) 370 | { 371 | strSize += IntPtr.Size - pad; 372 | } 373 | Size += strSize; 374 | return; 375 | } 376 | // obj is not null, and a primitive/enum/array/struct/class 377 | TypeData fieldTypeData = TypeData.Get(type); 378 | if (type.IsClass || type.IsArray || fieldTypeData.DynamicSizedFields != null) 379 | { 380 | // class, array, or struct with pointers 381 | if (!(type.IsPrimitive || type.IsValueType || type.IsEnum)) 382 | { 383 | if (!seenObjects.Add(obj)) 384 | { 385 | return; 386 | } 387 | } 388 | 389 | StructOrClass child = new StructOrClass(objName, obj, fieldTypeData, seenObjects); 390 | Size += child.Size; 391 | Children.Add(child); 392 | return; 393 | } 394 | else 395 | { 396 | // primitive, enum, or a struct without pointers, embed it in parent 397 | Size += fieldTypeData.Size; 398 | } 399 | } 400 | 401 | void Write(StreamWriter writer, string indent) 402 | { 403 | if (ParsedType.IsArray) 404 | { 405 | writer.WriteLine("{0}{1} ({2}:{3}) : {4}", indent, Identifier, ParsedType, ArraySize, Size); 406 | } else 407 | { 408 | writer.WriteLine("{0}{1} ({2}) : {3}", indent, Identifier, ParsedType, Size); 409 | } 410 | Children.Sort((a, b) => b.Size - a.Size); 411 | string childIndent = indent + " "; 412 | foreach (var child in Children) 413 | { 414 | if (child.Size >= CHILD_MIN_SIZE) 415 | { 416 | child.Write(writer, childIndent); 417 | } else 418 | { 419 | return; 420 | } 421 | } 422 | } 423 | 424 | static int GetTotalLength(Array val) 425 | { 426 | int sum = val.GetLength(0); 427 | for (int i = 1; i < val.Rank; i++) 428 | { 429 | sum *= val.GetLength(i); 430 | } 431 | return sum; 432 | } 433 | } 434 | 435 | public class TypeData 436 | { 437 | public int Size { get; private set; } 438 | public List DynamicSizedFields { get; private set; } 439 | 440 | static Dictionary seenTypeData; 441 | static Dictionary seenTypeDataNested; 442 | 443 | public static void Clear() 444 | { 445 | seenTypeData = null; 446 | } 447 | 448 | public static void Start() 449 | { 450 | seenTypeData = new Dictionary(); 451 | seenTypeDataNested = new Dictionary(); 452 | } 453 | 454 | public static TypeData Get(Type type) 455 | { 456 | TypeData data; 457 | if (!seenTypeData.TryGetValue(type, out data)) 458 | { 459 | data = new TypeData(type); 460 | seenTypeData[type] = data; 461 | } 462 | return data; 463 | } 464 | 465 | public static TypeData GetNested(Type type) 466 | { 467 | TypeData data; 468 | if (!seenTypeDataNested.TryGetValue(type, out data)) 469 | { 470 | data = new TypeData(type, true); 471 | seenTypeDataNested[type] = data; 472 | } 473 | return data; 474 | } 475 | 476 | public TypeData(Type type, bool nested = false) 477 | { 478 | if (type.IsGenericType) 479 | { 480 | genericTypes.Add(type); 481 | } 482 | Type baseType = type.BaseType; 483 | if (baseType != null 484 | && baseType != typeof(object) 485 | && baseType != typeof(ValueType) 486 | && baseType != typeof(Array) 487 | && baseType != typeof(Enum)) 488 | { 489 | TypeData baseTypeData = GetNested(baseType); 490 | Size += baseTypeData.Size; 491 | 492 | if (baseTypeData.DynamicSizedFields != null) 493 | { 494 | DynamicSizedFields = new List(baseTypeData.DynamicSizedFields); 495 | } 496 | } 497 | if (type.IsPointer) 498 | { 499 | Size = IntPtr.Size; 500 | } 501 | else if (type.IsArray) 502 | { 503 | Type elementType = type.GetElementType(); 504 | Size = ((elementType.IsValueType || elementType.IsPrimitive || elementType.IsEnum) ? 3 : 4) * IntPtr.Size; 505 | } 506 | else if (type.IsPrimitive) 507 | { 508 | Size = Marshal.SizeOf(type); 509 | } 510 | else if (type.IsEnum) 511 | { 512 | Size = Marshal.SizeOf(Enum.GetUnderlyingType(type)); 513 | } 514 | else // struct, class 515 | { 516 | if (!nested && type.IsClass) 517 | { 518 | Size = 2 * IntPtr.Size; 519 | } 520 | foreach (var field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) 521 | { 522 | ProcessField(field, field.FieldType); 523 | } 524 | if (!nested && type.IsClass) 525 | { 526 | Size = Math.Max(3 * IntPtr.Size, Size); 527 | int pad = Size % IntPtr.Size; 528 | if (pad != 0) 529 | { 530 | Size += IntPtr.Size - pad; 531 | } 532 | } 533 | } 534 | } 535 | 536 | void ProcessField(FieldInfo field, Type fieldType) 537 | { 538 | if (IsStaticallySized(fieldType)) 539 | { 540 | Size += GetStaticSize(fieldType); 541 | } 542 | else 543 | { 544 | if (!(fieldType.IsValueType || fieldType.IsPrimitive || fieldType.IsEnum)) 545 | { 546 | Size += IntPtr.Size; 547 | } 548 | if (fieldType.IsPointer) 549 | { 550 | return; 551 | } 552 | if (DynamicSizedFields == null) 553 | { 554 | DynamicSizedFields = new List(); 555 | } 556 | DynamicSizedFields.Add(field); 557 | } 558 | } 559 | 560 | static bool IsStaticallySized(Type type) 561 | { 562 | 563 | if (type.IsPointer || type.IsArray || type.IsClass || type.IsInterface) 564 | { 565 | return false; 566 | } 567 | if (type.IsPrimitive || type.IsEnum) 568 | { 569 | return true; 570 | } 571 | foreach (var nestedField in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) 572 | { 573 | if (!IsStaticallySized(nestedField.FieldType)) 574 | { 575 | return false; 576 | } 577 | } 578 | return true; 579 | } 580 | 581 | /// 582 | /// Gets size of type. Assumes IsStaticallySized (type) is true. (primitive, enum, {struct or class with no references in it}) 583 | /// 584 | static int GetStaticSize(Type type) 585 | { 586 | if (type.IsPrimitive) 587 | { 588 | return Marshal.SizeOf(type); 589 | } 590 | if (type.IsEnum) 591 | { 592 | return Marshal.SizeOf(Enum.GetUnderlyingType(type)); 593 | } 594 | int size = 0; 595 | foreach (var nestedField in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) 596 | { 597 | size += GetStaticSize(nestedField.FieldType); 598 | } 599 | return size; 600 | } 601 | } 602 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Zuntatos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnityHeapDump 2 | Tool to dump memory to text files for inspection 3 | 4 | Primary use is to find which static fields hold how much memory in order to find possible object leaks. For leaking of unity objects, please use the detailed memory profiler included in Unity3D. 5 | Total tracked memory found by this script can be much higher than the heap size, as an object with shared ownership will be counted towards every owner. 6 | 7 | Use the menu item "Tools/Memory/Dump Heap" or call UnityHeapDump.Create() somewhere from your code to create a memory dump at %projectroot%/dump/. The process may take several seconds (or a lot more), depending on heap size and complexity. 8 | 9 | Tool was developed for lack of a method to inspect the heap or create a dump using Unity3D, as: 10 | 11 | * The builtin profiler only tracks objects inheriting from UnityEngine.Object 12 | * Can't seem to use the 3rd party memory dump tools from visual studio or monodevelop with unity 13 | * Can't seem to use the profiling functionality of mono, as one can't pass the required arguments (would require mono rebuild/hack) 14 | * The experimental Memory Profiler from Unity3D only works with IL2CPP (and standalone IL2CPP is not available yet) 15 | 16 | # Output format 17 | 18 | Example of a small class with 3 static fields: 19 | ``` 20 | Static (Players): 720 bytes 21 | loadedPlayers (System.Collections.Generic.Dictionary`2[NetworkID,Players+Player]) : 588 22 | valueSlots (Players+Player[]:12) : 184 23 | 0 (Players+Player) : 56 24 | keySlots (NetworkID[]:12) : 132 25 | linkSlots (System.Collections.Generic.Link[]:12) : 120 26 | table (System.Int32[]:12) : 72 27 | connectedPlayers (System.Collections.Generic.List 1[Players+Player]) : 104 28 | _items (Players+Player[]:4) : 72 29 | PlayerHasID (System.Func 3[Players+Player,NetworkID,System.Boolean]) : 24 30 | ``` 31 | 32 | Example of a small assembly summary (this example is not a sensible usecase I guess, but it's small so:) 33 | ``` 34 | Assembly: System.Core, Version=2.0.5.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e of total size: 13684 35 | Type: System.Security.Cryptography.AesTransform of size 9092 36 | Type: Consts of size 4528 37 | Type: System.Linq.Expressions.ExpressionPrinter of size 32 38 | Type: System.Linq.Expressions.Expression of size 24 39 | Type: System.TimeZoneInfo of size 4 40 | Type: System.Threading.ReaderWriterLockSlim of size 4 41 | ``` 42 | 43 | A summary of the results is available at %projectroot%/dump/log.txt. This contains all Types and Components parsed and Errors at the end. The summary lists Types in order of size, by order of total Assembly size. 44 | 45 | * Unity3D code Assemblies are printed to a file in the form of %projectroot%/dump/statics/%assembly%/%size%-%type%. 46 | * Other Assemblies are printed to a file at %projectroot%/dump/statics/misc/%size%-%type%. 47 | * Unity object results are printed to %projectroot%/dump/uobjects/%size%-%type% 48 | * Unity scriptableObject results to %projectroot%/dump/sobjects/%size%-%type% 49 | 50 | Every file starts with the Types total summed size or all static references (or instance in case of unity objects). 51 | 52 | * A 'normal' reference is printed in the following format: %fieldName% (%type%) : %totalSize%. 53 | * An Array is printed like: %fieldName% (%type%:%arraySize%) : %totalSize%. An array item has its index as %fieldName%. 54 | 55 | Indented afterwards are all object fields associated with the instance. Value type fields are not listed individually, but their size is included in the parent. Unless the value type is a struct with references to objects; those are listed. 56 | 57 | # Settings 58 | 59 | Not much, but there are 3 constants in the main file; 60 | TYPE_MIN_SIZE_TO_PRINT, minimum byte count required before the type is printed to log.txt 61 | ROOT_MIN_SIZE, minimum byte count to create a file for the type 62 | CHILD_MIN_SIZE, minimum byte count to print the reference itself inside of the file (this is the main variable to tweak for scaling output size) 63 | 64 | # Issues 65 | 66 | Not all memory is properly traversed. Known missing regions: 67 | 68 | * Static fields of generic class instances, only static fields on found instances will be found 69 | * Proper traversing of [System.ThreadStatic] fields. 70 | * References on the stack 71 | 72 | Memory use will be biased because ALL the static constructors will be run, whether or not they were referenced before. 73 | 74 | Various Types throw errors due to having bad implementations for GetHashCode() or Equals (Object). Parsing of the root Type causing the error is currently stopped, and parsing of the next Type is started. (This may actually be the cause for the missing size) 75 | 76 | # Credits 77 | 78 | Idea/code was vaguely inspired by https://github.com/Cotoff/UnityHeapEx , but that code took multiple minutes to dump and created a 500 MiB .xml file for my use case, which was not really usable. 79 | --------------------------------------------------------------------------------