Search Unity

  1. Unity Asset Manager is now available in public beta. Try it out now and join the conversation here in the forums.
    Dismiss Notice
  2. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  3. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

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

    Joined:
    Jul 2, 2014
    Posts:
    507
    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!
     
    NotaNaN, a436t4ataf, Grizmu and 15 others like this.
  2. Prodigga

    Prodigga

    Joined:
    Apr 13, 2011
    Posts:
    1,123
    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!
     
    NotaNaN, Xarbrough, Remiel and 4 others like this.
  3. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,589
    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:
    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: Jan 8, 2020
  4. alexeyzakharov

    alexeyzakharov

    Joined:
    Jul 2, 2014
    Posts:
    507
    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

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,589
    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

    Joined:
    Jul 2, 2014
    Posts:
    507
    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.
     
    NotaNaN, esg_evan, grizzly and 4 others like this.
  7. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,589
    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

    Joined:
    Jul 2, 2014
    Posts:
    507
    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,906
    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:
    133
    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:
    395
    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.
     
    alexeyzakharov and Prodigga like this.
  14. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,589
    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.
     
    Last edited: Dec 8, 2019
    alexeyzakharov and SugoiDev like this.
  15. alexeyzakharov

    alexeyzakharov

    Joined:
    Jul 2, 2014
    Posts:
    507
    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.
     
  16. alexeyzakharov

    alexeyzakharov

    Joined:
    Jul 2, 2014
    Posts:
    507
    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. 2020-01-03_11-58-22.png
     
    Xarbrough, SugoiDev and Peter77 like this.
  17. Peter77

    Peter77

    QA Jesus

    Joined:
    Jun 12, 2013
    Posts:
    6,589
    Hey Alex,

    that's awesome news, thanks so much for considering our suggestions! On a related note, is TypeCache available in a build?
     
  18. Tor-Vestergaard

    Tor-Vestergaard

    Joined:
    Mar 20, 2013
    Posts:
    186
    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?
     
    Last edited: Jan 8, 2020
  19. QFSW

    QFSW

    Joined:
    Mar 24, 2015
    Posts:
    2,906
    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?
     
    alexeyzakharov likes this.
  20. SugoiDev

    SugoiDev

    Joined:
    Mar 27, 2013
    Posts:
    395
    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.
     
    alexeyzakharov and sirxeno like this.
  21. alexeyzakharov

    alexeyzakharov

    Joined:
    Jul 2, 2014
    Posts:
    507
    No, it is Editor-only API atm - UnityEditor.TypeCache.
    And related to that
    We had a discussion about it and it is on our backlog afaik, but we haven't yet planned the implementation.
     
  22. alexeyzakharov

    alexeyzakharov

    Joined:
    Jul 2, 2014
    Posts:
    507
    We do extract attribute values in native code, but don't have any caching.
    I like the idea, but we need to evaluate what can the gains be here.
    Extracting attributes on native side is also expensive (at least in Mono gc allocations happen). Unlike type and method info which can be cached without a Mono object created (TypeCache creates Type or MethodInfo object when you access enumerator), attributes extraction always allocates. When we return MethodInfo or Type in the API it is System object which already contains internally a reference to the attributes slot in VTable. We need to test the perf impact - if the native shortcut would be faster than Mono implementation. My feeling is that we can only redistribute where we spend time here - during caching or getting the data. It is important to understand the use case here - e.g. if attribute values extraction happens only once for the domain lifetime, then there is no savings, but if there is a use case when we acquire attribute values multiple times, then yes, we can save time.
     
  23. SugoiDev

    SugoiDev

    Joined:
    Mar 27, 2013
    Posts:
    395
    If you're able to somehow persist the extracted/generated data across domain reloads, and only refresh the data for assemblies that have actually changed (ie, assemblies that were recompiled), it would be massive. I'm not sure if this is even possible with the current model without some high-order wizardry (that could very well end up being slower...)

    We end up in a situation where the solution tends to be .NET Core: no domains.
    Most slowness in reloads in the Editor comes from the fact that all assemblies are forced to reload when any assembly changes.
     
    alexeyzakharov and Peter77 like this.
  24. cecarlsen

    cecarlsen

    Joined:
    Jun 30, 2006
    Posts:
    858
    @alexeyzakharov, once the types are retrieved using TypeCache, what is the fastest way to get the paths to the script assets (MonoScript)?
     
    Last edited: Mar 2, 2020
  25. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    I often have a use case where I want to find instance method or fields decorated with a custom attribute. This means I need to have a reference to the actual target object in order to do anything with it. So far, the TypeCache only seems to allow me to retrieve static data, is that correct? To get instances I would need to find them in the scene or in memory first and then reflect on them manually or is there anything I can use in the TypeCache to make this faster? Or would it be possible for Unity to support the TypeCache for instance methods as well?
     
    alexeyzakharov likes this.
  26. alexeyzakharov

    alexeyzakharov

    Joined:
    Jul 2, 2014
    Posts:
    507
    Static data in terms of type annotations and compiled assemblies, yes. The type information is static then for the domain lifetime. And TypeCache doesn't support dynamically codegen methods or assemblies.

    Could you please explain a bit more - do you want to enumerate all instances of some particular type? Or you codegen methods at runtime and would like to have that info included in typecache?
     
  27. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    See this code example:

    Code (CSharp):
    1. public class MyMethodAttribute : Attribute
    2. {
    3. }
    4.  
    5. public class ExampleBehaviour : MonoBehaviour
    6. {
    7.     public int myValue;
    8.  
    9.     [MyMethod]
    10.     public static void DoStatic()
    11.     {
    12.         Debug.Log("No reference to any object instance here.");
    13.     }
    14.  
    15.     [MyMethod]
    16.     public void DoInstance()
    17.     {
    18.         Debug.Log("My object value: " + myValue);
    19.     }
    20. }
    21.  
    22. public class MyCustomTool : EditorWindow
    23. {
    24.     private void OnEnable()
    25.     {
    26.         var methods = TypeCache.GetMethodsWithAttribute<MyMethodAttribute>();
    27.         foreach (var method in methods)
    28.         {
    29.             // Can only invoke static method here.
    30.             method.Invoke(null, new object[0]);
    31.         }
    32.     }
    33. }
    With TypeCache it is possible to find all static methods decorated with the "MyMethod" attribute. However, I would like to use the attribute on instance methods as well. For example, a MonoBehaviour that uses serialized fields somewhere in the scene.

    Currently, I call FindObjectOfType<MonoBehaviour> to get all instances in a scene and then use reflection to get all methods and see if they have the "MyMethod" attribute. I was hoping that TypeCache could speed this up, but I also understand if it would be too much of a performance hit for Unity to cache all instance data.
     
    alexeyzakharov likes this.
  28. QFSW

    QFSW

    Joined:
    Mar 24, 2015
    Posts:
    2,906
    I don't think TypeCache will help at all for finding mono instances because that isn't related to type data (and isn't static so wouldn't be cacheable anyway)
     
    alexeyzakharov likes this.
  29. alexeyzakharov

    alexeyzakharov

    Joined:
    Jul 2, 2014
    Posts:
    507
    Hm, this should be possible already - DoInstance should be one of results in the GetMethodsWithAttribute returned enumerator. We don't have any restrictions atm wrt public/static/etc binding flags. Moreover it is covered by tests :)
    Did you try this?

    Although for your tool you might need to use a dictionary/hashtable to match objects to methods through type for invocation. This is what TypeCache doesn't support.
     
  30. Xarbrough

    Xarbrough

    Joined:
    Dec 11, 2014
    Posts:
    1,188
    So that was the core of my question. I do get the MethodInfo, but I also need an object reference to invoke the method, which TypeCache doesn't provide. In this case, the slow part of the operation will be to find all the object instances. I'll have to test if TypeCache is actually an improvement over just using reflection without caching in my scenario.

    But thanks for the clarification!
     
    alexeyzakharov likes this.