MonoBehaviourGizmos constructor & destructor usage causes long Reload Script Assemblies time

I’ve been using the A* pathfinding project (which contains ALINE == drawing manager if I understand correctly) for almost a year now, and as my project has grown the “Reload Script Assemblies” step (doing domain reload among other things) that occurs when we making changes to our scripts has grown considerably. While in Unity 2020.3.21f1 it was very inconsistent, the long wait time became consistent in 2021.2.7f1 and thus I was able to start looking for the root cause, which turns out to be this plugin!

The reason is that the MonoBehaviourGizmos script is a MonoBehaviour which uses a constructor & a destructor that gets called for every instance that exists in the project during this domain reload/unload process. Since the project has grown considerabily, the amount of calls has grown to an amount where this has become a serious workflow issue. In short, it isn’t rare to need to wait for 1-2 minutes to wait for the reloading to finish… you can imagine thats not a nice way to develop a game ^^.

I’m not really sure why the monobehaviour constructor/destructor is called by Unity this way, but I do know that it is generally a bad idea to use these regardless… and think that Awake/OnDestroy can simply be used instead. Doing this locally seemed to not break anything in drawing, but I haven’t looked at it extensively.

Please let me know what you think!

More details
In my search for trying to find the “Reload Script Assembles” root cause, I encountered some information that I noted down in more detail here: https://forum.unity.com/threads/improving-iteration-time-on-c-script-changes.1184446/page-3#post-7798350

A screenshot of logs of register:
image

Hi

Which version of the package are you using?

At least in the latest version, the only thing it does in the constructor is to add itself to a list. Even with 80000 objects that shouldn’t take more than a few milliseconds.
Your debug log calls will slow things down considerably though, so make sure you profile without them.

I’m using A* Pathfinding Project 4.3.46, and yes the only thing it seems to do is register & unregister itself. In profiling, the registering & unregistering itself is doesn’t appear to be what takes a long time. Instead, the process of “unloading” the domain is what takes a long time. More detail as to why, or what it is doing is (afaik) impossible; Deep profiling the editor during assembly reload is basically impossible due to the memory requirements. (unity goes over 30GB and then crashes) I once had it not crash, and it didn’t give any useful additional information.

Note that my tests that come at at 1-2 minutes are without the debug logs. If I add those, the reload time increases to over 5 minutes. :slight_smile:

Hi

Can you try to profile the DrawingManager.RemoveDestroyedGizmoDrawers method itself (e.g. add a stopwatch and Debug.Log the time it takes).
If some part is slow, that’s probably the one.

This method only takes ~15 milliseconds for me. I’ve also timed the OnDisable & DelayedDestroy methods which are similar.

I think what is happening is in calling the constructor, any prefabs using a MonoBehaviourGizmos script will be loaded into the domain, which may trigger other scripts (& and/or art assets?) to be loaded as well, which then result in the unloading process also taking a long time. As such, the problem might not be any of your code RE: registering / unregistering drawers or the like, but rather the use of constructor & finalizer itself.

Is there a specific reason why you are using those, rather than just Awake & OnDestroy?

I don’t think so. Having code in the constructor doesn’t magically cause that constructor to be used. Unity will, however, load all of your scripts in prefabs and the scene after it re-compiles your scripts.

Nit, but I don’t use a finalizer. Finalizers have some limitations, which make them kinda slow to use and cause more GC pressure.

Primarily to make it easier for users. It’s very easy to override the Awake method without properly calling base.Awake(). For Awake it would just lead to no gizmos showing, but missing the OnDestroy could potentially lead to a memory leak, which would be bad. That’s why I use the constructor instead, and poll for destroyed objects in RemoveDestroyedGizmoDrawers.

Oh my, I your note about the finalizer triggered me, because my version does have it! It turns out that (over half a year ago) we ran into (runtime in editor) performance issues, and it was “solved” by adding a constructor/finalizer setup, which indeed is a bad idea… and caused this whole thing!

The thing it was solving is the DrawingManager’s “OnPostRender” method was taking over 60ms of CPU, because it would call RemoveDestroyedGizmoDrawers() every frame. So the custom modifications would simply NOT execute this & use the Awake() & OnDestroy() methods as you mentioned. (which I’m ok with, taking the usability issues you pointed out as well)

I’m still a bit puzzled as to why we decided to add the constructor/destructor as well, which the whole thing seems to work fine without. (AND we don’t accept any usage of it anywhere else in our own code either) Thats my problem to figure out ^^

Apologies for the confusion this undoubtably caused!

1 Like

Great to hear that you figured it out :slight_smile:

1 Like