Search Unity

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

Discussion in '2019.2 Beta' started by alexeyzakharov, May 31, 2019.

  1. alexeyzakharov

    alexeyzakharov

    Unity Technologies

    Joined:
    Jul 2, 2014
    Posts:
    279
    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.
    Code (CSharp):
    1.    public static class TypeCache
    2.    {
    3.        public static TypeCollection GetTypesDerivedFrom<T>();
    4.        public static TypeCollection GetTypesWithAttribute<T>() where T : Attribute;
    5.  
    6.        public static MethodCollection GetMethodsWithAttribute<T>() where T : Attribute;
    7.  
    8.        public struct MethodCollection : IList<MethodInfo> {...}
    9.  
    10.        public struct TypeCollection : IList<Type> {...}
    11.    }
    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<T> interface which is implemented as a view over native dynamic_array data. This gives us:
    1. Flexibility and usability of IEnumerable (foreach, LINQ).
    2. Fast iteration with for (int i).
    3. Fast conversion to List<T> 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:
    Code (CSharp):
    1. static List<Type> ScanInterfaceImplementors(Type interfaceType)
    2. {
    3.     var types = new List<Type>();
    4.     var assemblies = AppDomain.CurrentDomain.GetAssemblies();
    5.     foreach (var assembly in assemblies)
    6.     {
    7.         Type[] allAssemblyTypes;
    8.         try
    9.         {
    10.             allAssemblyTypes = assembly.GetTypes();
    11.         }
    12.         catch (ReflectionTypeLoadException e)
    13.         {
    14.             allAssemblyTypes = e.Types;
    15.         }
    16.  
    17.         var myTypes = allAssemblyTypes.Where(t =>!t.IsAbstract && interfaceType.IsAssignableFrom(t));
    18.         types.AddRange(myTypes);
    19.     }
    20.     return types;
    21. }
    With TypeCache you can use:
    Code (CSharp):
    1. TypeCache.GetTypesDerivedFrom<MyInterface>().ToList()
    Similarly finding types marked with attribute requires:
    Code (CSharp):
    1. static List<Type> ScanTypesWithAttributes(Type attributeType)
    2. {
    3.     var types = new List<Type>();
    4.     var assemblies = AppDomain.CurrentDomain.GetAssemblies();
    5.     foreach (var assembly in assemblies)
    6.     {
    7.         Type[] allAssemblyTypes;
    8.         try
    9.         {
    10.             allAssemblyTypes = assembly.GetTypes();
    11.         }
    12.         catch (ReflectionTypeLoadException e)
    13.         {
    14.             allAssemblyTypes = e.Types;
    15.         }
    16.         var myTypes = allAssemblyTypes.Where(t =>!t.IsAbstract && Attribute.IsDefined(t, attributeType, true));
    17.         types.AddRange(myTypes);
    18.     }
    19.     return types;
    20. }
    And only one line with TypeCache API:
    Code (CSharp):
    1. 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!
     
  2. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    780
    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!
     
    Remiel, Novack, SugoiDev and 2 others like this.
  3. Peter77

    Peter77

    Joined:
    Jun 12, 2013
    Posts:
    4,127
    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 some expensive any 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:
    Code (CSharp):
    1. [InitializeRandom(119)]
    2. 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
    .
     
    Last edited: Jun 1, 2019
    SugoiDev, alexeyzakharov and Prodigga like this.
  4. alexeyzakharov

    alexeyzakharov

    Unity Technologies

    Joined:
    Jul 2, 2014
    Posts:
    279
    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.
     
  5. Peter77

    Peter77

    Joined:
    Jun 12, 2013
    Posts:
    4,127
    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.
     
    alexeyzakharov likes this.
  6. alexeyzakharov

    alexeyzakharov

    Unity Technologies

    Joined:
    Jul 2, 2014
    Posts:
    279
    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.
     
    grizzly, SugoiDev, recursive and 2 others like this.
  7. Peter77

    Peter77

    Joined:
    Jun 12, 2013
    Posts:
    4,127
    That would be the ideal solution indeed!
     
    SugoiDev likes this.
  8. elhispano

    elhispano

    Joined:
    Jan 23, 2012
    Posts:
    52
    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.
     
  9. alexeyzakharov

    alexeyzakharov

    Unity Technologies

    Joined:
    Jul 2, 2014
    Posts:
    279
    Hi!
    Yes to both questions :)
    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.
     
    SugoiDev likes this.
  10. QFSW

    QFSW

    Joined:
    Mar 24, 2015
    Posts:
    2,437
    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
     
    alexeyzakharov and SugoiDev like this.
  11. Deadcow_

    Deadcow_

    Joined:
    Mar 13, 2014
    Posts:
    102
    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<MonoBehaviour>()" 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 :)
     
    SugoiDev and alexeyzakharov like this.
  12. unity_0IKPsUidqy8FlQ

    unity_0IKPsUidqy8FlQ

    Joined:
    Aug 27, 2019
    Posts:
    1
    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 !
     
    alexeyzakharov likes this.
  13. SugoiDev

    SugoiDev

    Joined:
    Mar 27, 2013
    Posts:
    245
    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.
     
    Prodigga likes this.