Object Graph Visualizer
C# makes it easy to create large graphs of objects connected by their fields. The larger this graph grows, the more complex it is to deal with objects in the graph. It’s hard to look at code or set a breakpoint in a debugger and get an intuitive sense of all these connections. So today we’ll write a small tool to visualize an object graph!
Planning
The goal here is to visualize a graph of managed objects such as string
and List<T>
that are connected by their fields. Properties, indexers, and events don’t count, as these are just functions that happen to be typically “backed” by fields. Access specifiers should be ignored too since there’s always at least an indirect way to have them influence the behavior of the code.
This tool executes at runtime on a particular object. This is different than other tools such as Visual Studio’s code maps which display the relationship between types and assemblies while the app isn’t running. Tools like this display the potential connections between objects while today’s tool displays the actual connections such as by omitting connections to null
objects. Both tools are useful, but for different purposes.
Now that we’ve defined the input—an object
—to the visualizer, let’s discuss the output. Unity has many ways to display graphics, but we don’t want to interfere with a running game so we won’t render anything to the screen. We could render to an editor window, but that would limit usage to just running in the editor. The amount of code required to render the graph is also non-trivial and we’d like to get a visualization up and running quickly.
So we’ll opt to render the object graph to the DOT language. DOT is designed to describe a graph and is consumed by programs like Graphviz that render the graph. This choice allows us to simply describe the graph and let Graphviz do all the work to render it. It’s a free, open source program available on all major operating systems so it should be easy to integrate into an analysis workflow.
Implementation
C# and .NET feature a powerful, easy-to-use reflection system that works well with IL2CPP. This allows us to inspect the types of variables at runtime and, crucially, enumerate their fields regardless of access specifier. We can use this to create the nodes of the graph consisting of an ID, the object’s type, and its links to other nodes. The links are a Dictionary<string, int>
mapping the field name to the linked node’s ID.
There are two tricky parts to the implementation. First is the handling of arrays. Since these don’t have fields, they don’t fall into the general case of enumerating fields. Instead, we must enumerate their elements. This is trivial for single-dimensional arrays, but more complex for arrays with arbitrary dimensions. See LinkArray
below for details. The second tricky part is handling inheritance. Type.GetFields
only returns the fields of the type in question, so we must traverse up the hierarchy of base types until reaching the root: object
. See EnumerateInstanceFieldInfos
for more.
The visualizer exists in a single static
class with a nested Node
class. This is all in one file that’s 400 lines long and largely made up of thorough xml-doc to help understand, maintain, and improve it. Here’s the code:
using System; using System.Collections.Generic; using System.Reflection; using System.Text; /// <summary> /// Utility functions to visualize a graph of <see cref="object"/> /// </summary> /// /// <author> /// Jackson Dunstan, https://JacksonDunstan.com/articles/5034 /// </author> /// /// <license> /// MIT /// </license> public static class ObjectGraphVisualizer { /// <summary> /// A node of the graph /// </summary> private sealed class Node { /// <summary> /// Type of object the node represents /// </summary> public readonly string TypeName; /// <summary> /// Links from the node to other nodes. Keys are field names. Values are /// node IDs. /// </summary> public readonly Dictionary<string, int> Links; /// <summary> /// ID of the node. Unique to its graph. /// </summary> public readonly int Id; /// <summary> /// Create a node /// </summary> /// /// <param name="typeName"> /// Type of object the node represents /// </param> /// /// <param name="id"> /// ID of the node. Must be unique to its graph. /// </param> public Node(string typeName, int id) { TypeName = typeName; Links = new Dictionary<string, int>(16); Id = id; } } /// <summary> /// Add a node to a graph to represent an object /// </summary> /// /// <returns> /// The added node or the existing node if one already exists for the object /// </returns> /// /// <param name="nodes"> /// Graph to add to /// </param> /// /// <param name="obj"> /// Object to add a node for /// </param> /// /// <param name="tempBuilder"> /// String builder to use only temporarily /// </param> /// /// <param name="nextNodeId"> /// ID to assign to the next node. Incremented after assignment. /// </param> private static Node AddObject( Dictionary<object, Node> nodes, object obj, StringBuilder tempBuilder, ref int nextNodeId) { // Check if there is already a node for the object Node node; if (nodes.TryGetValue(obj, out node)) { return node; } // Add a node for the object Type objType = obj.GetType(); node = new Node(objType.Name, nextNodeId); nextNodeId++; nodes.Add(obj, node); // Add linked nodes for all fields foreach (FieldInfo fieldInfo in EnumerateInstanceFieldInfos(objType)) { // Only add reference types Type fieldType = fieldInfo.FieldType; if (!fieldType.IsPointer && !IsUnmanagedType(fieldType)) { object field = fieldInfo.GetValue(obj); if (fieldType.IsArray) { LinkArray( nodes, node, (Array)field, fieldInfo.Name, tempBuilder, ref nextNodeId); } else { LinkNode( nodes, node, field, fieldInfo.Name, tempBuilder, ref nextNodeId); } } } return node; } /// <summary> /// Add new linked nodes for the elements of an array /// </summary> /// /// <param name="nodes"> /// Graph to add to /// </param> /// /// <param name="node"> /// Node to link from /// </param> /// /// <param name="array"> /// Array whose elements should be linked /// </param> /// /// <param name="arrayName"> /// Name of the array field /// </param> /// /// <param name="tempBuilder"> /// String builder to use only temporarily /// </param> /// /// <param name="nextNodeId"> /// ID to assign to the next node. Incremented after assignment. /// </param> private static void LinkArray( Dictionary<object, Node> nodes, Node node, Array array, string arrayName, StringBuilder tempBuilder, ref int nextNodeId) { // Don't link null arrays if (ReferenceEquals(array, null)) { return; } // Create an array of lengths of each rank int rank = array.Rank; int[] lengths = new int[rank]; for (int i = 0; i < lengths.Length; ++i) { lengths[i] = array.GetLength(i); } // Create an array of indices into each rank int[] indices = new int[rank]; indices[rank - 1] = -1; // Iterate over all elements of all ranks while (true) { // Increment the indices for (int i = rank - 1; i >= 0; --i) { indices[i]++; // No overflow, so we can link if (indices[i] < lengths[i]) { goto link; } // Overflow, so carry. indices[i] = 0; } break; link: // Build the field name: "name[1, 2, 3]" tempBuilder.Length = 0; tempBuilder.Append(arrayName); tempBuilder.Append('['); for (int i = 0; i < indices.Length; ++i) { tempBuilder.Append(indices[i]); if (i != indices.Length - 1) { tempBuilder.Append(", "); } } tempBuilder.Append(']'); // Link the element as a node object element = array.GetValue(indices); string elementName = tempBuilder.ToString(); LinkNode( nodes, node, element, elementName, tempBuilder, ref nextNodeId); } } /// <summary> /// Add a new linked node /// </summary> /// /// <param name="nodes"> /// Graph to add to /// </param> /// /// <param name="node"> /// Node to link from /// </param> /// /// <param name="obj"> /// Object to link a node for /// </param> /// /// <param name="name"> /// Name of the object /// </param> /// /// <param name="tempBuilder"> /// String builder to use only temporarily /// </param> /// /// <param name="nextNodeId"> /// ID to assign to the next node. Incremented after assignment. /// </param> private static void LinkNode( Dictionary<object, Node> nodes, Node node, object obj, string name, StringBuilder tempBuilder, ref int nextNodeId) { // Don't link null objects if (ReferenceEquals(obj, null)) { return; } // Add a node for the object Node linkedNode = AddObject(nodes, obj, tempBuilder, ref nextNodeId); node.Links[name] = linkedNode.Id; } /// <summary> /// Check if a type is unmanaged, i.e. isn't and contains no managed types /// at any level of nesting. /// </summary> /// /// <returns> /// Whether the given type is unmanaged or not /// </returns> /// /// <param name="type"> /// Type to check /// </param> private static bool IsUnmanagedType(Type type) { if (!type.IsValueType) { return false; } if (type.IsPrimitive || type.IsEnum) { return true; } foreach (FieldInfo field in EnumerateInstanceFieldInfos(type)) { if (!IsUnmanagedType(field.FieldType)) { return false; } } return true; } /// <summary> /// Enumerate the instance fields of a type and all its base types /// </summary> /// /// <returns> /// The fields of the given type and all its base types /// </returns> /// /// <param name="type"> /// Type to enumerate /// </param> private static IEnumerable<FieldInfo> EnumerateInstanceFieldInfos(Type type) { const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public; while (type != null) { foreach (FieldInfo fieldInfo in type.GetFields(bindingFlags)) { yield return fieldInfo; } type = type.BaseType; } } /// <summary> /// Visualize the given object by generating DOT which can be rendered with /// GraphViz. /// </summary> /// /// <example> /// // 0) Install Graphviz if not already installed /// /// // 1) Generate a DOT file for an object /// File.WriteAllText("object.dot", ObjectGraphVisualizer.Visualize(obj)); /// /// // 2) Generate a graph for the object /// dot -Tpng object.dot -o object.png /// /// // 3) View the graph by opening object.png /// </example> /// /// <returns> /// DOT, which can be rendered with GraphViz /// </returns> /// /// <param name="obj"> /// Object to visualize /// </param> public static string Visualize(object obj) { // Build the graph Dictionary<object, Node> nodes = new Dictionary<object, Node>(1024); int nextNodeId = 1; StringBuilder output = new StringBuilder(1024 * 64); AddObject(nodes, obj, output, ref nextNodeId); // Write the header output.Length = 0; output.Append("digraph\n"); output.Append("{\n"); // Write the mappings from ID to label foreach (Node node in nodes.Values) { output.Append(" "); output.Append(node.Id); output.Append(" [ label=\""); output.Append(node.TypeName); output.Append("\" ];\n"); } // Write the node connections foreach (Node node in nodes.Values) { foreach (KeyValuePair<string, int> pair in node.Links) { output.Append(" "); output.Append(node.Id); output.Append(" -> "); output.Append(pair.Value); output.Append(" [ label=\""); output.Append(pair.Key); output.Append("\" ];\n"); } } // Write the footer output.Append("}\n"); return output.ToString(); } }
Usage
To use the visualizer, follow these steps:
- Install Graphviz if it’s not already installed
- Paste the above code into a new C# file in a Unity project. It’s not an editor script, so don’t put it in an
Editor
directory. It works in older versions of Unity such as 2017.4. - Call
File.WriteAllText("object.dot", ObjectGraphVisualizer.Visualize(obj));
at some point during your game to visualizeobj
to the fileobject.dot
- Open a terminal and run
dot -Tpng object.dot -o object.png
to render the DOT file to a PNG image - View the graph by opening
object.png
in an image viewer
Because the output of ObjectGraphVisualizer.Visualize
is just a string
, it's very flexible and easy for more advanced usage. Here are some examples of other ways to use it:
- Call
EditorGUIUtility.systemCopyBuffer = ObjectGraphVisualizer.Visualize(obj);
to copy the DOT to the clipboard - Send the string over a network and have the receiver save it to a file
- Log the string with
Debug.Log
or to an analytics system like Splunk - Display the string in a runtime debugging GUI or editor window
- Run the visualizer outside of Unity. It's pure C# code with no dependency on Unity.
Example
To try out the visualizer, let's look at a little script with some complex types. These are designed to test various different cases such as the handling of arrays, generics, primitives, enums, pointers, managed and unmanaged structs, cycles, delegates, dynamic
variables, private
fields, and base types.
using System; using UnityEditor; using UnityEngine; class BaseClass { public string BaseField; } unsafe class TestClass : BaseClass { public string Str; private readonly OtherClass Other; public OtherClass[] Others; public OtherClass[,] OthersMulti; public GenericClass<TestClass> Generic; public dynamic Dynamic; public int Primitive; public TestStructUnmanaged StructUnmanaged; public TestEnum Enum; public TestStruct? Nullable; public TestStruct? NullableIsNull; public TestStructUnmanaged? NullableUnmanaged; public TestStructUnmanaged* Pointer; public Action SingleDelegate; public Action MultiDelegate; public Action<TestClass> GenericDelegate; public TestClass Self; public TestClass(OtherClass other) { Other = other; } } class OtherClass { public TestClass Test1; public TestClass Test2; public TestStruct Struct; } class GenericClass<T> { public T Value; } struct TestStruct { public TestClass Test; } struct TestStructUnmanaged { public int Primitive; } enum TestEnum { A, B, C } public class TestScript : MonoBehaviour { unsafe void Start() { OtherClass otherClass = new OtherClass(); TestClass testClass = new TestClass(otherClass); testClass.BaseField = "hello"; testClass.Str = "world"; testClass.Others = new OtherClass[] { otherClass, null, otherClass }; testClass.OthersMulti = new OtherClass[,] { { otherClass, null, otherClass }, { null, otherClass, null }}; GenericClass<TestClass> genericClass = new GenericClass<TestClass>(); genericClass.Value = testClass; testClass.Generic = genericClass; testClass.Dynamic = otherClass; testClass.Primitive = 123; TestStructUnmanaged testStructUnmanaged = new TestStructUnmanaged(); testStructUnmanaged.Primitive = 456; testClass.StructUnmanaged = testStructUnmanaged; testClass.Enum = TestEnum.B; testClass.Pointer = &testStructUnmanaged; otherClass.Test1 = null; otherClass.Test2 = testClass; TestStruct testStruct = new TestStruct(); testStruct.Test = testClass; otherClass.Struct = testStruct; testClass.Nullable = testStruct; testClass.NullableIsNull = default; testClass.NullableUnmanaged = testStructUnmanaged; testClass.SingleDelegate = () => print(testClass); testClass.MultiDelegate = () => print(testClass); testClass.MultiDelegate += () => print(testStruct); testClass.GenericDelegate = x => print(testStruct); testClass.Self = testClass; string report = ObjectGraphVisualizer.Visualize(testClass); print(report); EditorGUIUtility.systemCopyBuffer = report; EditorApplication.isPlaying = false; } }
To run this, just paste it into a new C# file in a Unity project and attach it to a game object in the scene. It'll run at startup and then exit play mode with the DOT being written to the log and copied to the clipboard. Here's the resulting DOT:
digraph { 1 [ label="TestClass" ]; 2 [ label="String" ]; 3 [ label="OtherClass" ]; 4 [ label="TestStruct" ]; 5 [ label="GenericClass`1" ]; 6 [ label="Action" ]; 7 [ label="<>c__DisplayClass0_0" ]; 8 [ label="MonoMethod" ]; 9 [ label="RuntimeType" ]; 10 [ label="Action" ]; 11 [ label="Action" ]; 12 [ label="MonoMethod" ]; 13 [ label="Action" ]; 14 [ label="MonoMethod" ]; 15 [ label="Action`1" ]; 16 [ label="MonoMethod" ]; 17 [ label="String" ]; 1 -> 2 [ label="Str" ]; 1 -> 3 [ label="Other" ]; 1 -> 3 [ label="Others[0]" ]; 1 -> 3 [ label="Others[2]" ]; 1 -> 3 [ label="OthersMulti[0, 0]" ]; 1 -> 3 [ label="OthersMulti[0, 2]" ]; 1 -> 3 [ label="OthersMulti[1, 1]" ]; 1 -> 5 [ label="Generic" ]; 1 -> 3 [ label="Dynamic" ]; 1 -> 4 [ label="Nullable" ]; 1 -> 6 [ label="SingleDelegate" ]; 1 -> 10 [ label="MultiDelegate" ]; 1 -> 15 [ label="GenericDelegate" ]; 1 -> 1 [ label="Self" ]; 1 -> 17 [ label="BaseField" ]; 3 -> 1 [ label="Test2" ]; 3 -> 4 [ label="Struct" ]; 4 -> 1 [ label="Test" ]; 5 -> 1 [ label="Value" ]; 6 -> 7 [ label="m_target" ]; 6 -> 8 [ label="method_info" ]; 7 -> 1 [ label="testClass" ]; 7 -> 4 [ label="testStruct" ]; 8 -> 9 [ label="reftype" ]; 10 -> 11 [ label="delegates[0]" ]; 10 -> 13 [ label="delegates[1]" ]; 11 -> 7 [ label="m_target" ]; 11 -> 12 [ label="method_info" ]; 12 -> 9 [ label="reftype" ]; 13 -> 7 [ label="m_target" ]; 13 -> 14 [ label="method_info" ]; 14 -> 9 [ label="reftype" ]; 15 -> 7 [ label="m_target" ]; 15 -> 16 [ label="method_info" ]; 16 -> 9 [ label="reftype" ]; }
After running dot
, we see the following graph: (click to view full size)
The object being visualized is at the top with type TestClass
. Its connections are directly typed into the class, so it's obvious what fields and array elements are connecting. The second-level connections, however, start to show more complex underpinnings. In this example, we see the internals of the Action
delegate type: its array of delegates
, its "targets" pointing to compiler-generated types like <>c__DisplayClass0_0
, its method_info
references to a MonoMethod
, and then the MonoMethod
references to a RuntimeType
. We get a lot of transparency into an otherwise opaque system!
Conclusion
This object graph visualizer can be very useful in analyzing the complexity and performance of runtime data. A large graph indicates increased complexity and traversing its nodes indicates a higher likelihood of CPU cache misses due to non-determinate memory layout. It's good to pair this tool with others such as Visual Studio's "code maps" feature that analyze offline at the type level rather than at runtime at the object level. Hopefully you'll find it useful!
#1 by Surely on January 28th, 2019 ·
A helper’s helper. THANK YOU!!!
#2 by Rackdoll on January 29th, 2019 ·
Love it
#3 by KonH on April 17th, 2019 ·
I think you have a typo in the example code:
output.Append(“digraphn”);
But later, in the test result it was:
digraph
#4 by jackson on April 17th, 2019 ·
Thanks for pointing this out. I’ve updated the article to fix the issue: it was supposed to be a
\n
newline.#5 by Shannon Rowe on June 8th, 2020 ·
This is great code. I spent a couple of days last week writing something similar, and after reading this post I can immediately see several improvements I can make to my implementation after what I’ve learned here.