loopier / animatron

Animatron for Godot 4.x <
MIT License
16 stars 1 forks source link

Using large number of actors with `/rand` pauses the app #61

Open loopier opened 1 month ago

loopier commented 1 month ago

When using /rand with large groups, the app pauses.

/load square

/for i 500 /create a$i square # this already pauses the app

/size a* 0.01 # works as expected

# the next lines pauses the app
/rand /x a* 0 1920
/rand /y a* 0 1080 

This might be related to #51 and #52.

In the evalCommand func in Main.gd there are a few if statements that need to be resolved for each command. I'm starting to suspect that this is what is slowing everything down.

totalgee commented 4 weeks ago

It seems to be something on O(n^2), because tripling the number of actors to 1500 means the /rand lines take around 10x longer to execute, compared to 500 actors. Little optimizations like moving initializations outside loops or trying to reduce object allocations don't seem to help.

totalgee commented 3 weeks ago

The command arguments processing code is doing lots of repeated work, in particular when getting Actors by name. For example, in the /rand command of your example, it finds all the matching Actors ("a*"), and calls the sub-command /x for each of them. Lower, down, once the /property command is called, each Actor (e.g. "a0", "a1", ...) is also "resolved" by calling getActors("a0"), and so on...by string name (in this case, not a wildcard, but it could be. But we can't just replace the Actor name by the Actor object, because in "lower down" command/def calls, they all expect the Actor argument to be a string ("actor:s").

I think one way we could optimize this quite a bit would be to make Actor arguments have their own "type", maybe something like "actor:a". This special "type specification" would support either a string (name lookup with possible wildcards, same as currently), or else an Actor object, or even an array of Actors. If we already have an Actor or array of them, we just return that, without searching repeatedly (again) for actors in the scene hierarchy by string/name... This will give us quite a speed improvement, I'm pretty sure.

I don't have time to implement that tonight, but I was able to at least do some profiling and stepping into the code to understand what is slow. The main performance bottleneck is calling getActors() 500 times, in the above /rand example with 500 Actors in the scene.

totalgee commented 3 weeks ago

I added this test command, to check the performance for calling getActors() 500 times, and jus that alone takes basically the same time as calling your command: /rand /x a* 0 1920 (about 650ms on my machine)

# Add to the command map at the top of CommandInterface.gd:
    "/perfTest": CommandDescription.new(perfTest, "", "Do some custom performance/profiling check"),

# And add this function:
func perfTest() -> Status:
    var startTime := Time.get_ticks_usec()
    for i in 500:
        var result = getActors("a*")
    var endTime := Time.get_ticks_usec()
    print("Test end -- delta time %f ms" % [(endTime - startTime) * 1e-3])
    return Status.ok(true)