Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Scene referred ScriptableObject and the Where the F is my memory?

Discussion in 'Scripting' started by LightStriker, Dec 7, 2019.

  1. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    For years I have been under specific impression on how assets reference and management occurs between scenes. Our current project - triggering out of memories constantly on a very limited platform - appears to be proving me I was dead wrong.

    When loading the main menu, we have a bunch of managers loading bunches of definition; localization, fonts, UI, etc. At that moment, we hover around 650Mb. Looking up the loaded asset, it's all as expected.

    When loading the first gameplay scene, the memory climbs up to 2.4Gb. Also as expected. We have large scene, lot of enemies, lightings. It's a full fledged third person RPG.

    When quitting the game and returning to the main menu, we load an empty scene, call Resources.UnloadUnusedAsset and... 1.1Gb. What? For a while I thought it was maybe just Unity shuffling around memory, keeping it to itself to re-allocated it faster. Except that memory would keep climbing between different scenes. Something was sticking around.

    Until I pulled the Memory Profiler:


    A ScriptableObject with 0 Ref Count. This ScritableObject was referenced from the unloaded scene; it contains the definition of an enemy, a reference to the prefab, the animation set, the different weapons it uses and it's stats. In the scene, a spawner script uses that definition to build up a character.

    I have pretty much 550Mb of characters data - mesh, animation, textures - floating around memory, with a ScriptableObject referencing it, but nothing referencing the ScriptableObject. And Unity is clearly not disposing it.

    Why is Unity keeping those around? How do I prevent that from occurring? I always read that SO were to be handled like any other asset. But clearly that's now what's happening here. If nothing references a texture, the texture is unloaded. Do I have to crawl around object types with some nasty FindAllObjectsOfType and do something with them? If so, what exactly?

    Resources.UnloadAsset doesn't appear to be doing anything.
    Resources.UnloadUnusedAssets clearly doesn't catch those.
    Destroy sounds dangerous, especially while playing in the editor, as those are "disk asset", loaded by a scene reference.
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,312
    I think you're okay on this and here's why:

    ScriptableObjects in the editor have a subtly different lifecycle from MonoBehaviors.

    MonoBehaviors come into existence and blink back out as expected, as they live on a GameObject.

    ScriptableObjects come into existence when you do anything to look at it, but there is a persistent linkage that bidirectionally connects the SO with the disk asset.

    To prove this to yourself, put an OnEnable() and OnDisable() method in your ScriptableObject class, and make it print out on the debug log, or else put a breakpoint in it. Then open/close the editor, switch scenes, etc.

    You'll see the OnEnable() on your first use of the SO, but in the editor it will never OnDisable(). However, pretty sure in the target build it will go away. Give it a test!
     
  3. JoshuaMcKenzie

    JoshuaMcKenzie

    Joined:
    Jun 20, 2015
    Posts:
    911
    Can you check that scriptableobject's hideflags? It could be set to dontunloadunusedasset, dontsave, or hideanddontsave. These flags prevent the asset from being able to be unloaded via Resources.UnloadUnusedAssets().

    Likewise scriptableobjects created manually (via new keyword) in the scene MUST be manually destroyed via DestroyImmeadiate when you are done with it. Unity will not clean it up for you automatically.
     
  4. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    The data/memory snapshot I'm displaying is on a build on the target platform. Not from editor data.

    Those SOs are on disk, referenced by different MonoBehaviour in scenes. They are not created at runtime. I'll check their hide flags, but I don't change them. So... Do they have some odd flags by default?
     
  5. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,312
    Well that's interesting! I had the opposite observation using ScriptableObjects in my little Datasacks utility. But I'm also running on an older version of Unity3D.

    As @JoshuaMcKenzie noted above, did you get a chance to dump out the hideFlags bits, see what they are? I'd be careful changing those if they are instrumental in SO behavior vis-a-vis the live editing and connection to disk assets, but it might give you a clue.
     
  6. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    This is getting worst, and I'm REALLY not happy about it.

    Object.FindObjectsOfType<T>() doesn't return those lingering object.
    Resources.FindObjectsOfTypeAll<T>() does. At least that works.

    They don't have any hide flags. (HideFlags.None)

    I've managed to unload them using Resources.UnloadAsset. I can see OnDisable getting triggered, and they don't show up anymore on the Memory Profiler. Win? Not quite.

    The asset those SO referenced... Are still there! And now they don't have references to them. I have some free-floating GameObject, not attached to any scene and not getting clean.
     
  7. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,312
    Try this: you probably have a bunch of singletons: blow them all away (destroy the GameObject-attached ones, make extra code (if possible) to blow away the other ones).

    While you're at it, gather all Transforms with FindObjectsOfType<Transform>, then blow away all their root gameObjects just in case too.

    Wrap each destroy in a pokemon try/catch in case anybody inside there complains, and at the end of it all switch away to a single empty scene using a vanilla Unity SceneManager call to do it.

    If the objects go away then you know there actually IS something still referencing them.

    To truly destroy some singletons, it may be necessary to put in a boolean that permanently poisons them from recreating themselves on demand, in case some other logic tries to make a fresh one.
     
  8. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    That's the issue, the Memory Profiler shows no ref count towards those objects. So if there's a reference, it's native? That would be bad, as it means something Unity is doing.

    My next try is to manually call Destroy on the prefabs from the OnDisable of the ScriptableObject. Feels like pulling a loose strings of something very bad.
     
    Last edited: Dec 8, 2019
  9. lordconstant

    lordconstant

    Joined:
    Jul 4, 2013
    Posts:
    389
    Just to be sure, are you calling Resources.UnloadUnusedAssets after you unload the scriptable object? Also a call to GC.Collect may be needed. It may just unload that scriptable object & not the assets that it was referencing.
     
  10. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Currently the "Load empty scene and unload everything" looks like:

    Code (CSharp):
    1.         private static IEnumerator UnloadScene()
    2.         {
    3.             UDebug.Log("- Load Empty Scene.");
    4.             yield return SceneManager.LoadSceneAsync(1);
    5.  
    6.             yield return null;
    7.  
    8.             CharacterSpawner.ClearSpawners();
    9.             AIManager.Clear();
    10.             ClearPrefabs();
    11.             IsAttacking.Clear();
    12.             Breach.ClearAll();
    13.             SectorActivator.ClearAll();
    14.             LODManager.ClearAll();
    15.             DepthOcclusionDrawer.ClearAll();
    16.             AssetBundleManager.ClearLoadedAssets();
    17.  
    18.             var spawnerDatas = Resources.FindObjectsOfTypeAll<SpawnerData>();
    19.             foreach (var spawnData in spawnerDatas)
    20.             {
    21.                 if (!spawnData.Persistent)
    22.                     Resources.UnloadAsset(spawnData);
    23.             }
    24.  
    25.             yield return null;
    26.  
    27.             GC.Collect(0, GCCollectionMode.Forced, true, true);
    28.  
    29.             yield return null;
    30.  
    31.             yield return Resources.UnloadUnusedAssets();
    32.  
    33.             yield return null;
    34.         }
     
  11. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    COME ON!!!

    Now I have materials and textures floating around without ref to them!
     
  12. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    chrismarch likes this.
  13. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,312
    A happy fully-explained engineering outcome is my favorite kinda story. Good to know about the defect reference reports... Hrm.
     
  14. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,406
    @LightStriker good to hear you found a way to resolve this. Could you maybe go into more detail of the nature of the references missed by the memory Profiler and ideally please also file a bug report, so that we can fix this and improve the tool for everyone?
     
  15. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,406
    The bug report would just need a snapshot file from each tool and a description of what to look for :)
     
  16. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Solve the issue of references. However, fragmentation is a dirty bitch. I hate how there's simply no solution except assume it will crash if you play long enough. Non-compacting GC is just horrible.

    How is the native object of a material exist without any reference? That should never happens, so having an object shows up 0 ref count shouldn't happen, unless it's manually flag to be kept alive.

    Heap Explorer showed there was a static field with a list of C# object referencing a scene spawner (destroyed on scene unload, but C# still there) that had a reference to that ScriptableObject.

    A VERY useful features of Heap Explorer is a root-hierarchy listing. For any object, how it's linked to a root objects. (Static, scene, Don't Destroy On Load, etc) So it shows you why this object is still alive.

    While being there:



    What's the dark blue?
     
    futurlab_xbox likes this.
  17. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,312
    The memory fragments that are sad because they'll never be part of a large malloc() again?

    But seriously, when I look here:

    https://forum.unity.com/threads/wip...filer-debugger-and-analyzer-for-unity.527949/

    I see the following caveat:

    "Known Issues

    Heap Explorer uses Unity’s experimental MemoryProfiling API, which contains various bugs, from cosmetics to major issues that make memory snapshots not trustworthy.

    These bugs occur in every application that use Unity’s MemoryProfiling API, such as Unity’s own MemoryProfiler tool. I hope Unity Technologies is going to fix them."

    Did those get fixed and the ones you are seeing are new? Or does HeapExplorer get info from some other mechanism?
     
  18. MartinTilo

    MartinTilo

    Unity Technologies

    Joined:
    Aug 16, 2017
    Posts:
    2,406
    First of all, what Unity Version are you on? The Memory Profiler API got mayor fixes in 2019.3, including with regard to the collection of object connections, trimming them down to what references actually matter for GC and UnloadUnusedAssets. So if we're talking about latest 2019.3, reasons could be: HideFlags; it not yet being collected by UnloadUnusedAssets; a Memory Profiler crawler failure (see below); a genuine bug in Unity systems where this is being leaked, please report it as a bug.


    Right, this sounds like a bug in our snapshot crawler that might've discarded a connection due to a faulty assumption. We're aware of this and will fix it in a future version of the UI.


    Paths To Root visualization is in our backlog. Our higher priority was to trim the collected connections first (see above comment about 2019.3) so we A) snapshot faster and B) don't need to iterate over bloated connection data to find the paths that matter.


    As the legend at the top of this view states: these are allocated scripting memory. So in essence, this is either space in managed heap allocations that doesn't have any (non collected) objects in it, or scripting VM memory used my Mono or IL2CPP for e.g. Scripting Type Metadata. So heap fragmentation, reflection and generics are big contributors to this. We're planning to provide a more detailed breakdowns for these in future unity versions.


    I just checked all that were listed in that document and all but one are closed (the remaining one being about missing data on ExecutablesAndDLLs memory). Also any other such bugs that we're aware of are fixed in 2019.3. The reason for why HeapExplorer might've gotten more info out of the snapshot in this case is the mentioned flaw in the Memory Profiler Crawler which should only fail very infrequently at this point but might e.g. fail when managed fields where stripped with reference fields following these fields.
     
    Last edited: Dec 16, 2019
    Kurt-Dekker and Peter77 like this.
  19. LightStriker

    LightStriker

    Joined:
    Aug 3, 2013
    Posts:
    2,717
    Thanks a lot for the info.

    Honestly, how hard would it be to have the GC be compacting? I simply don't see a way I could reduce fragmentation ENOUGH for a low RAM platform such as the Switch. I say that because there's no way to control where memory is allocated. It's like playing Tetris where you don't know the layout of the board and which piece you're handling.