TL;DR If you develop an Editor utility or a package that targets Unity 2019.2 or later, use UnityEditor.TypeCache API for type extraction to reduce tooling initialization time (as well as entering Play Mode and domain reload times).
Why performance problems arise
When looking into optimizing entering Play Mode, we discovered that types extraction from loaded assemblies takes noticeably long. The types extraction is used widely internally by Editor modules and externally by packages and user code to extend editing capabilities. Cumulative effects vary depending on the project and can contribute 300ā600 ms to the domain reload time (or more if the system has lazy initialization). In the new Mono runtime, the time increases significantly due to the Type.IsSubclassOf performance regression and can be up to 1300 ms.
The performance problem arises from the fact that code usually extracts all types from the current domain, and then iterates all of them doing expensive checks. The time scales linearly according to the amount of types the game has (typically 30ā60K).
Solution
Caching type information allows us to break the O(N) complexity arising from iterations over types in the domain. At the native level, we already had acceleration structures, which are populated after all assemblies are loaded and contain cached type data, such as method and class attributes and interface implementers. Internally, those structures were exposed through UnityEditor.EditorAssemblies API to leverage fast caching. Unfortunately, the API wasnāt available publicly and didnāt support the important SubclassesOf use case.
For 2019.2 we optimized and extended the native cache and exposed it as a public UnityEditor.TypeCache API. It can extract information very quickly, allowing iteration over the smaller number of types we are interested in (10ā100). That significantly reduces the time required to fetch types by Editor tooling.
public static class TypeCache
{
public static TypeCollection GetTypesDerivedFrom<T>();
public static TypeCollection GetTypesWithAttribute<T>() where T : Attribute;
public static MethodCollection GetMethodsWithAttribute<T>() where T : Attribute;
public struct MethodCollection : IList<MethodInfo> {...}
public struct TypeCollection : IList<Type> {...}
}
The underlying data we have on the native side is represented by an array, and it is immutable for the domain lifetime. Thus, we can have an API that returns IList interface which is implemented as a view over native dynamic_array data. This gives us:
- Flexibility and usability of IEnumerable (foreach, LINQ).
- Fast iteration with for (int i).
- Fast conversion to List and Array.
Itās quite simple to use.
Usage examples
Letās take a look at several examples.
Usually the code to find interface implementers does the following:
static List<Type> ScanInterfaceImplementors(Type interfaceType)
{
var types = new List<Type>();
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
Type[] allAssemblyTypes;
try
{
allAssemblyTypes = assembly.GetTypes();
}
catch (ReflectionTypeLoadException e)
{
allAssemblyTypes = e.Types;
}
var myTypes = allAssemblyTypes.Where(t =>!t.IsAbstract && interfaceType.IsAssignableFrom(t));
types.AddRange(myTypes);
}
return types;
}
With TypeCache you can use:
TypeCache.GetTypesDerivedFrom<MyInterface>().ToList()
Similarly finding types marked with attribute requires:
static List<Type> ScanTypesWithAttributes(Type attributeType)
{
var types = new List<Type>();
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
Type[] allAssemblyTypes;
try
{
allAssemblyTypes = assembly.GetTypes();
}
catch (ReflectionTypeLoadException e)
{
allAssemblyTypes = e.Types;
}
var myTypes = allAssemblyTypes.Where(t =>!t.IsAbstract && Attribute.IsDefined(t, attributeType, true));
types.AddRange(myTypes);
}
return types;
}
And only one line with TypeCache API:
TypeCache.GetTypesWithAttribute<MyAttribute>().ToList();
Performance
If we write a simple performance test using our Performance Testing Framework, we can clearly see the benefits of using TypeCache.
In an empty project we can save more than 100 ms after domain reload!
*In 2019.3 TypeCache.GetTypesDerivedFrom also gets support for generic classes and interfaces as a parameter.
Most of the Editor code is already converted to TypeCache API. We invite you to try using the API; your feedback can help make the Editor faster.
If you develop an Editor utility or a package that needs to scan all types in the domain for it to be customizable, consider using UnityEditor.TypeCache API. The cumulative effect of using it significantly reduces domain reload time.
Please use this thread for feedback and to discuss the TypeCache API.
Thanks!