UnityEditor.TypeCache API for fast extraction of type attributes in the Editor tooling

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!

18 Likes

This is really nice! I guess if anything Iā€™d like to request a ā€˜GetFieldsWithAttributeā€™ as well? And I guess it might be worth asking why this is editor only? Could it easily be made to not be editor only? I donā€™t really have a use case in mind for either of these 2 points but I figureā€™d Iā€™d throw the question out there!

7 Likes

That would indeed be extremely useful.

Below I describe some use-cases in my project, which is attached to Case 1108597, where this feature would come in handy.

Iā€™ve implemented my own ā€œRuntimeInitializeOnLoadMethodAttributeā€, because I wanted to allow to specify in which order methods decorated with this attribute are called. It uses expensive and ugly Reflection code at startup. Allowing us to use TypeCache at runtime would hopefully get rid of this slow startup time. The code is located in Assets\Code\Plugins\Framework\Attributes\InvokeMethodAttribute.cs.

Iā€™ve implemented functionality where you can add an attribute to a static System.Random field:

[InitializeRandom(119)]
static System.Random _Random = new System.Random();

ā€¦ which causes it to get reset to its initial state whenever a new scene is loaded. This is for debugging, to make sure random values are ā€œalways the sameā€ when entering a scene.

Thatā€™s where the requested GetFieldsWithAttribute by @Prodigga would be useful. The code is located in Assets\Code\Plugins\Framework\Attributes\InitializeRandomAttribute.cs.

5 Likes

Hi guys and thanks for the feedback!

Weā€™ve considered caching fields as well, but it turned out to significantly increase the scan time (150ms ā†’ 800ms) and add extra memory overhead. Taking into account that the Editor itself doesnā€™t do fields lookup, we decided to not include fields into the cache. However, if this use case is common we can add it to the cache or alternatively have a lookup in native code to leverage from a fast native traversal.

This is a really good point. It has been also discussed internally and this use case requires a solution.
Similarly as above we did not want to add time to player startup and increase memory usage to the default scenario. While in the Editor we can guarantee that extra 150-250 ms of scanning will save 500-700ms due to the Editor always doing attributes scanning, we canā€™t guarantee the same for all games - some might use reflection and scan for attributes, interfaces, etc., and at the same times some games might not use reflection at all. So the runtime implementation should be smarter than just full traversal and caching and ideally bake the required data similarly to how [
RuntimeInitializeOnLoadMethod] works. That said the use case is important and we have the runtime support feature planned for 2020.1.

4 Likes

Have you considered to scan in a lazy fashion? That way, it should only cost time if someone actually uses the feature and then it makes sense that there is a cost associated with it. Basically initialize on the first time someone calls a particular TypeCache method.

If someone does not use that feature, it does not scan and has therefore no runtime penalty.

1 Like

Yes, this is one of a potential (and the easiest) implementations - have a lookup in native code to leverage from a fast traversal and store the result into a hash table for later reuse. Alternatively as mentioned above we have been discussing a solution where we scan assemblies for the required attributes at build time and bake the result into the code, so there is 0 time even for the first lookup.

7 Likes

That would be the ideal solution indeed!

1 Like

HI!
I donā€™t have a clear understanding about how we can get benefits of this API.
There will be a performance improvement if we upgrade to Unity 2019.2 by default or have we to help the Editor implementing something?

I got confused because in the post it is said that now the API is exposed but the example code appears to be a generic code that will work in all projects, so it has sense to be integrated in Unity engine by default.

Hi!

Yes to both questions :slight_smile:
We changed all usecases in the Editor code to benefit from a faster native cache, so Editor performance in cases like this should be improved.
But as Editor is quite extensible and a lot of its functionality is coming from AssetStore packages and custom game specific tooling, we are also asking to use this API there in order to make overall Editor performance better. If you have the Editor code in your game or are AssetStore package developer, then please consider using this api to reduce domain reload times in projects that use your tooling.

1 Like

Awesome news! Will definitely be using it for my editor extensions!
This could also be useful for runtime too, so Iā€™d definitely love to see something there too in the future

2 Likes

I made this attribute (docs) for fields to automatically check (on playmode) if field is assigned (have some value).
To do this Iā€™ve got to ā€œFindObjectsOfType()ā€ and get all fields with this attribute defined for every MB, so itā€™s pretty heavy and Iā€™d also would like to have a faster approach (like GetFieldsWithAttribute) but it seems like the old way will do for now :slight_smile:

2 Likes

I used to have my own type cache when I was developing editor tools and I ended up doing lazy initialization in editor mode, feeding a bake file of types of interest that was loaded at runtime, what you propose hereā€¦
If you are wondering, that was working really great !

1 Like

I still use mine and it works very well! I store any information that I need from types, like names and fqn. I only query the actual type once, then cache the info for future usages.
This massively increased performance of logging for us and we can now safely use type information when logging.
The result was so good that I now hot-patch Unityā€™s stack trace processing utility (in the Editor) using Harmony so that it uses my cached types instead of System.Type.

2 Likes

Hi Alex,

would it be possible to add functionality to extract fields marked with a specific attribute, as I described earlier (the _Random example)?

People start to write workarounds for the new ā€œenter playmode featureā€ and continue to rely on C# reflection, because TypeCache doesnā€™t offer field extraction yet. See the git link here .

2 Likes

Hi Peter,

Thank you for the highlighting the example! I think I can try to make an ondemand cache (to offload domain reload) and limit the scope to statics only - TypeCache.GetStaticFieldsWithAttribute<>. The first invocation though might be quite expensive - 100-300ms.

4 Likes

TypeCache.GetFieldsWithAttribute will be available in 2020.1a19.
On the first call it will scan loaded assemblies and cache the fields data. The scan performance is not that bad - on Empty Project in the Editor it is about 12-15ms.5348703--540168--2020-01-03_11-58-22.png

3 Likes

Hey Alex,

thatā€™s awesome news, thanks so much for considering our suggestions! On a related note, is TypeCache available in a build?

TypeCache has been very useful for us developers of Odin - as of Odinā€™s patch 2.1.8, itā€™s significantly helped our initialization time by locating relevant types far faster, so thank you for this excellent API! Unfortunately, Odin is still slower to initialize than weā€™d like, and this is still due to reflection. I wonder if Unity could also provide APIs to help with this?

The issue lies mostly in MemberInfo.GetCustomAttributes(), in which the majority (around 80-90%) of Odinā€™s static initialization time happens now. It is not that we need to find members or types decorated with said attributes (that is already done quickly with TypeCache), but that we need the data which the attributes themselves actually contain. IE, we need actual attribute instances to work with, for sorting data, deciding which drawers to use, and so on.

If, however, Unity has already processed this attribute metadata and generated acceleration data structures for it, would it be possible to get a way to access this data somehow?

5 Likes

This is great news, should help me with speeding up table generation for Quantum Console even further, but only in editor of course. Any plans or discussions for builds?

1 Like

If the TypeCache was extended to include the stuff @Tor_Vestergaard is talking about, it could also benefit Unity itself.
A huge part of the assembly reload times is used to get attributes.
For example, for the shortcuts, menu items, etc.
(another significant chunk is taken determining if a type is an editor type, so maybe that could get some love too)

Having that data on a fast path and actually using it for internal editor stuff would instantly give us a huge performance boost for assembly reloads. From my tests, it would save between 2-3 seconds on a large-ish project. Thatā€™s massive.

2 Likes