Search Unity

Bug [InitializeOnLoadMethod] sometimes can run before the AssetDatabase is actually ready to load things

Discussion in 'Asset Database' started by Kybernetik, Aug 20, 2022.

  1. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    • My editor scripts use a ScriptableObject to store their settings.
    • There should only ever be one instance of the settings class.
    • It uses a [InitializeOnLoadMethod] to call
      AssetDatabase.FindAssets("t:MySettings");
      to find all instances and load one.
    • If it finds nothing, it creates a new instance and saves it.
    Most of the time it works fine, but if I do a full reimport of my project (such as when first checking it out from Git or using Assets/Reimport All) it fails to find the existing settings asset and creates a new one, causing conflicts.

    If I use AssetDatabase.GUIDToAssetPath with a hard coded GUID it gives the correct path, but AssetDatabase.LoadAsset returns null.

    Is there a different way I should be initializing things which actually waits for the AssetDatabase to be ready or at least a way to check if the AssetDatabase is ready? I can think of a couple of hacky ways of checking, but I can't be sure if they're actually reliable or if my scripts might get stuck thinking it's not ready when the settings asset actually doesn't exist or won't be able to load for some reason (script errors or something, I just don't know).

    I submitted a Bug Report: Case IN-14385
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,001
    I'm not aware of the exact order of method calls here. It may be normal that in InitializeOnLoadMethod the AssetDatabase is not fully initialized. In addition, importing is a concurrent process.

    You can try two things, one would be a hack: start a coroutine, wait for a while.

    The other would be to manually call AssetDatabase.ImportAsset() if LoadAsset returns null but you are certain that the asset is there. And use the ForceSynchroneousImport option. I suppose this could work, give it a try.
     
  3. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    Waiting would be easy with EditorApplication.delayCall or EditorApplication.update, but that's too much of a hack for my liking because there's nothing to indicate how long I should wait. The target file might exist but be not loadable for whatever reason and I don't want my system to just be inoperable while waiting indefinitely. This is for an Asset Store plugin where I've already had reports from users running into this problem so coming up with a proper fix is pretty important.

    Manually importing the asset was worth a shot, but unfortunately didn't help at all.
     
  4. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,001
    I thought it was going to be in the Asset Store and the hack wouldn't fare well. ;)

    Anyhow, I just checked and found this:

    Asset operations such as asset loading should be avoided in InitializeOnLoad methods. InitializeOnLoad methods are called before asset importing is completed and therefore the asset loading can fail resulting in a null object.

    It's not for InitializeOnLoadMethod but it could apply to that too.

    The solution is also mentioned:

    To do initialization after a domain reload which requires asset operations use the AssetPostprocessor.OnPostProcessAllAssets callback. This callback supports all asset operations and has a parameter signaling if there was a domain reload.
     
  5. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    More time wasted thanks to counter-intuitive designs and half-assed documentation ...

    I actually already had an OnPostProcessAllAssets method, but it looks like it's only called on a domain reload if you include the optional didDomainReload parameter which seems to have only been added in Unity 2021.

    Thanks for finding that, I'm glad I didn't have to waste time implementing an unreliable hack. I'll send you a free voucher for my asset.
     
  6. Unity_Javier

    Unity_Javier

    Unity Technologies

    Joined:
    Mar 7, 2018
    Posts:
    190
    Hi @Kybernetik ,

    I'd like to take a moment to explain why we return null inside of InitializeOnLoad, and highlight why we changed it from ADBV1 to ADBV2:

    The issue at hand here is consistency. In the scenario you describe, InitializeOnLoadMethod gets called when the editor is booting up. This is analogous to:
    1. Modifying a C# script
    2. Modifying an asset
    3. Calling refresh so both changes are picked up together.

    So, if we do try that out using ADBV1, in 2018.3, I can show you the biggest problem, please refer to the attached GIF.

    In this case, I have a texture (test_texture.png) and its dimensions are 256x128 pixels
    If I:
    1. Modify a C# script
    2. Change the Dimensions to be something else (i.e. 256x256)
    3. Let Auto-Refresh run

    You can see that inside the InitalizeOnLoad call, the dimensions get printed and they're not what's on disk!!!

    This is because the asset has not been imported yet, and so the last known version of the asset is what ends up getting loaded. This can cause a large number of issues, since you're essentially getting out-of-date data back.

    In ADBV2 we make sure to return NULL as a way to tell you that the asset is not available and we know its modified, thus preventing out of date data from being returned until it has been imported.

    This is definitely a behaviour change, but for the sake of consistency we chose to do this when ABDV2 was designed.

    I've attached a little GIF showing this off in 2018.3
     

    Attached Files:

    CodeSmile likes this.
  7. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    Thanks for the explanation, I really appreciate it.

    It seems to me that it would have made more sense to change [InitializeOnLoad] to run after other assets are imported (as OnPostProcessAllAssets does) and if there's actually a use case which needs it called first then give it a constructor parameter to choose when it runs (like [RuntimeInitializeOnLoadMethod] has). Using OnPostProcessAllAssets works, but needing to make a new class which inherits from AssetPostprocessor and implement that rather long method signature with so many parameters I don't care about is a lot more effort than just using an [InitializeOnLoadMethod] attribute which should intuitively run my code when the editor is actually ready to run my code.

    Returning null is a really bad solution though, because it's a silent failure with no way of differentiating between it and other forms of failure such as the file not existing or the file being invalid. For example, if I mess up my Git setup and end up with an LFS file pointer in place of the actual file it would exist with the right path on disk and there'd be no way for me to tell if it's actually not loadable or if it just can't be loaded yet. This is an exceptional circumstance so throwing an exception would be vastly preferable. It's understandable that you just want to return null for simplicity in regular circumstances like if the asset doesn't exist, but in this case using AssetDatabase.LoadAsset is explicitly wrong so throwing an exception is appropriate.
     
    Last edited: Aug 22, 2022
    CodeSmile likes this.
  8. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    6,001
    I agree, an exception would make more sense here. Or at a minimum a debug log warning or error. Similar to calling certain methods (SendMessage?) in editor mode from OnValidate or similar.

    Btw thanks for the voucher. ;)
     
  9. Unity_Javier

    Unity_Javier

    Unity Technologies

    Joined:
    Mar 7, 2018
    Posts:
    190
    We actually have had a task on our backlog to report errors if trying to load an asset during InitializeOnLoad.

    We've now formalized it (after adding the feedback you both supplied) and @polly_1f496 will be taking care of outputting something when a load operation is performed during InitializeOnLoad. Stay tuned for updates!
     
    CodeSmile and Kybernetik like this.
  10. Kybernetik

    Kybernetik

    Joined:
    Jan 3, 2013
    Posts:
    2,570
    Please make sure the errors also apply to any other methods that fail silently at the moment like FindAssets.
     
  11. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    1,091
    Just upgraded to 2022 and started getting errors, OnPostProcessAllAssets runs too late, later than OnEnable of gameobjects.

    I also get random "Unexpected: object already in ms_IDToPointer map." error there.