Why is this class creating a memory leak?

Below is a class I use to cache Texture2D for my custom editor. Caching the scriptable objects icons significantly improves the custom editors load time. Below is the class responsible for caching all of the scriptable objects icons for all class that inherit type JungleNode.

[InitializeOnLoad]
    internal static class JungleIconCache
    {
        private static Dictionary<Type, Texture2D> _cache = new();

        static JungleIconCache()
        {
            _cache.Clear();
            var jungleNodeTypes = TypeCache.GetTypesDerivedFrom(typeof(JungleNode)).ToList();
            jungleNodeTypes = jungleNodeTypes.FindAll(t => !t.IsAbstract);
            foreach (var type in jungleNodeTypes)
            {
                var instance = ScriptableObject.CreateInstance(type);
                var image = EditorGUIUtility.ObjectContent(instance, type).image as Texture2D;
                UnityEngine.Object.DestroyImmediate(instance);
                _cache.Add(type, image);
            }
        }
     
        internal static Texture2D GetIcon(Type type)
        {
            _cache.TryGetValue(type, out var texture2D);
            return texture2D;
        }
    }

Each time I recompile, the memory used by the editor goes up and up. I tried destroying the scriptable object instance I temporarily make, but that didn’t help. What part of my class could be causing the memory leak?

Thanks in advance!

If you force a GC.Collect() does it reduce the consumed memory? I wouldn’t necessarily recommend it as a solution, but I would be curious to see if that memory simply hasn’t been garbage collected yet.

Sadly that didn’t fix it. Memory is still filling up.

I would probably break out the memory profiler to see where the memory is actually coming from, rather than assuming it’s from this.

I also wonder if there’s a better way of doing this than creating and destroying a bunch of scriptable objects on every domain reload.

2 Likes

The memory leak is caused by the class.
As for if there’s a better way to get object content from a scriptable object, I couldn’t find a better solution.

Have you done tests to know that it is? (Task Manager doesn’t count)

For this kind of editor stuff, I just make an editor-only scriptable object (such as using ScriptableSingleton<T0>) to just author this via the inspector. Then icons can just be looked up via types or instance of these SO’s.

I have done tests. Removing the class fixed the memory leak.

That would be a perfect solution for making the process of caching the icons faster but even doing it that way would not fix the memory leak issue. :slight_smile:

Have you tried to use AssetPreview.GetMiniTypeThumbnail instead?
https://docs.unity3d.com/ScriptReference/AssetPreview.GetMiniTypeThumbnail.html

Just tried it out… Seems the AssetPreview.GetMiniTypeThumbnail method only works with Unity’s components.

Icons when using AssetPreview.GetMiniTypeThumbnail:
9450308--1326698--upload_2023-11-4_14-28-51.png

Icons when using AssetPreview.GetMiniTypeThumbnail(typeof(SphereCollider)):
(Using type sphere collider as an example)
9450308--1326701--upload_2023-11-4_14-29-28.png

Icons when using AssetPreview.GetMiniThumbnail:
9450308--1326704--upload_2023-11-4_14-30-25.png

It seems AssetPreview.GetMiniThumbnail and EditorGUIUtility.ObjectContent do the same thing. Although, Get Mini Thumbnail is a lot cleaner. I’ll do some more playing around to see if I could get the type thumbnail to work with my scriptable object icons. :slight_smile:

Can you check if UnityEditor.Search.SearchUtils.GetTypeIcon is doing what you’re looking for?

You probably shouldn’t be doing anything in a static constructor against the Unity engine.

I’m surprised you’re not getting angry messages from Unity about doing things off the main thread or otherwise before things are ready…

Probably because whatever you’re doing is failing to be recorded for subsequent cleanup, since it was allocated from C#/.NET before the engine was up and able to deal with things.

Do it lazy: make a static-access pattern loader that does all the above the first time you tickle the .instance field.

That way at least it all happens from a normal place in your threading.

1 Like

Static constructors have always been fine. [InitializeOnLoad] literally exists to call static constructors in the editor.

2 Likes

Does not work due to it being an internal method. Also, UnityEditor.Search.SearchUtils.GetTypeIcon is just a wrapper method that uses AssetPreview.GetMiniThumbnail. I’ll attach a screenshot of the method below:
9451862--1327124--upload_2023-11-5_17-26-46.png

I also figured out that AssetPreview.GetMiniTypeThumbnail only works on types that inherit from Mono Behavior. Since what I need the icons from inherits from type Scriptable Object, the AssetPreview.GetMiniTypeThumbnail method doesn’t work.

Also not sure if you understood me, but I meant just have these sprites serialised into a scriptable singleton via some look-up table and use these to load your icons. Nothing to cause a memory leak. None of this preview-thumbnail stuff.

1 Like

I don’t think EditorGUIUtility.ObjectContent() is meant to be used outside of OnGUI or to get anything other than a temporary copy of an icon. I haven’t looked at the code yet but this is a pattern (i.e. not using new for this) that’s usually used for fleeting temporary usages without needing to do a full loading of it. Or if it is loaded, that it gets unloaded automatically and your static references might be keeping them alive at the moment when it would otherwise get cleaned up by the Editor.

Generally this is a bit of a mess but you can check out what we’re doing for that in the Memory Profiler code here

And here

Including the cleanup logic.

Also, yes, please do use the Memory Profiler Package to analyze this and let me know if and if so how it fails to help.

I have spent all yesterday and today trying to figure out a solution and FINALLY, I have found something that works!
Here’s the code:

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEngine;

[FilePath("MyIcon.cache", FilePathAttribute.Location.ProjectFolder)]
public class MyIconCache : ScriptableSingleton<MyIconCache>
{
    private const int MAX_CACHE_SIZE = 32;
 
    [SerializeField]
    private List<IconContainer> cache = new();
 
    [Serializable]
    private struct IconContainer
    {
        [SerializeField]
        private List<string> types;
        [SerializeField]
        private Texture icon;
         
        public IconContainer(List<Type> types, Texture icon)
        {
            types ??= new List<Type>();
            this.types = types.ConvertAll(type => type.AssemblyQualifiedName);
            this.icon = icon;
        }
         
        public Texture GetIcon()
        {
            return icon;
        }
     
        public int GetIconInstanceID()
        {
            return icon.GetInstanceID();
        }
         
        public bool HasType(Type type)
        {
            types ??= new List<string>();
            return types.Contains(type.AssemblyQualifiedName);
        }
         
        public void AddType(Type type)
        {
            types ??= new List<string>();
            if (!types.Contains(type.AssemblyQualifiedName))
                types.Add(type.AssemblyQualifiedName);
        }
         
        public void RemoveType(Type type)
        {
            types ??= new List<string>();
            if (types.Contains(type.AssemblyQualifiedName))
                types.Remove(type.AssemblyQualifiedName);
        }
    }
 
    public void BuildCache()
    {
        cache ??= new List<IconContainer>();
     
        // *SET THE TYPE TO DERIVE FROM TO WHATEVER YOU WANT TO BUILD THE CACHE FOR*
        var types = TypeCache.GetTypesDerivedFrom<ScriptableObject>().ToList();
        types = types.FindAll(jungleNodeType => !jungleNodeType.IsAbstract);
        // *SET THE TYPE TO DERIVE FROM TO WHATEVER YOU WANT TO BUILD THE CACHE FOR*
     
        var cacheSize = 0;
        var addIconInstanceIDs = new List<int>();
         
        foreach (var type in types)
        {
            // If the cache is full, stop
            if (cacheSize > MAX_CACHE_SIZE)
                break;
             
            var typeIcon = GetTypeThumbnail(type);
             
            // Remove any types that might already exist in the cache
            // *Only is the icon is different from the one already in the cache
            if (cache.Any(container => container.HasType(type)))
            {
                var cacheToRemoveTypeFrom = cache.Find(container => container.HasType(type));
                if (cacheToRemoveTypeFrom.GetIconInstanceID() != typeIcon.GetInstanceID())
                    cacheToRemoveTypeFrom.RemoveType(type);
            }
             
            // If the icon already exists in the cache, only add the type to it
            if (cache.Any(container => container.GetIconInstanceID() == typeIcon.GetInstanceID()))
            {
                var cacheToAddTypeTo = cache.Find(container => container.GetIconInstanceID() == typeIcon.GetInstanceID());
                cacheToAddTypeTo.AddType(type);
                continue;
            }
             
            cache.Add(new IconContainer
            (
                new List<Type> { type },
                typeIcon
            ));
            addIconInstanceIDs.Add(typeIcon.GetInstanceID());
            cacheSize++;
        }
         
        // Remove any icons that are no longer in use
        cache.RemoveAll(container => !addIconInstanceIDs.Contains(container.GetIconInstanceID()));
    }
     
    public void ClearCache()
    {
        cache ??= new List<IconContainer>();
        cache.Clear();
    }
     
    public Texture GetIcon(Type type)
    {
        foreach (var container in cache)
        {
            if (!container.HasType(type))
                continue;
            return container.GetIcon();
        }
        return GetTypeThumbnail(type);
    }
     
    private Texture GetTypeThumbnail(Type type)
    {
        var temporaryInstance = CreateInstance(type);
        var icon = AssetPreview.GetMiniThumbnail(temporaryInstance);
        DestroyImmediate(temporaryInstance);
        return icon;
    }
}

public static class MyIconCacheInitializer
{
    [InitializeOnLoadMethod]
    private static void EditorLoadCallback()
    {
        EditorApplication.delayCall = () =>
        {
            MyIconCache.instance.BuildCache();
        };
    }
}

This solution uses a struct called IconContainer to store the data. The cache is a serialized list of IconContainers. When the editor loads, the icon cache is automatically rebuilt. I added a constant at the top called MAX_CACHE_SIZE and set it to 32. This value limits the amount of containers that can be built. 32 should be plenty, but if you need more, feel free to crank that value up. This is a safe way to prevent the editor from eating up too much memory.

To get a types icon, call the public Texture GetIcon(Type type) method. If you attempt to get an icon from a type that was not cached, the icon returned will instead be fetched from the AssetPreview.GetMiniThumbnail method.
Example:

private void MyMethod()
{
     var myIcon = MyIconCache.instance.GetIcon(typeof(SphereCollider));
}

This solution also caches each icon only once. This is done using the IconContainer. The container has a reference to the icon texture and a serialized list of all the types (serialized as strings) that use that icon. This way, no duplicate containers are created making the cache smaller.

This took a while to figure out so I hope this helps! :slight_smile:

1 Like