Performance issue when loading behaviour tree

Nenad

New member
Hi,

I have performance issue every time I instantiate enemy from pool. Does anybody have some tips or idea how to optimize this?

1106
 
It looks like the tree is being deserialized. Take a look at this page for how to preload the tree to allow you to deserialize the tree during initialization:

 
Hi Justin,

Thank you for your answer. I looked at link that you posted and it helped me with deserialize. Now I have other problem. When I set ExternalBehaviour form my stack I have spike on BehaviourManager.EnableBehaviour.

"behaviorTree.ExternalBehavior = ExternalBehaviourStack.Pop();"

My BehaviourManager update is set to manual and I tick specific enemy with BehaviorManager.instance.Tick(_allEnemies[index]);

Is there anything I can do about this?
 

Attachments

  • BehaviourTreePool.PNG
    BehaviourTreePool.PNG
    147.3 KB · Views: 18
Do you have Reset Values When Complete enabled? I've seen this sometimes cause performance hiccups. How large is your tree?
 
As for size of my tree here it is, I don't know if this is considered like big tree? Also Behaviour Tree settings are attached.

I also set Behaviour Name to none, because I set external behavior manually from tree. Can I have Behaviour Tree without any behavior, like empty shell that will be used with pooled behaviours? Also I don't use any variables with my tree and all actions are custom implementation of enemy actions.
 

Attachments

  • Tree.jpg
    Tree.jpg
    93.6 KB · Views: 41
  • Tree settings.JPG
    Tree settings.JPG
    24.4 KB · Views: 38
Your tree doesn't look that big so that aspect shouldn't be a problem. Is that bottle neck when you first enable the behavior tree or on a later succession of enabling it? Since you have Pause When Disabled selected it should be really quick to reenable after it has been enabled once.

There isn't much that you can do to get around the initial data structure creation process on first enable. You can hide it though by enabling all of your trees immediately after they are deserialized and then pause the tree right after. This will keep those data structures in memory so it doesn't have to go through the initialization process again.
 
There isn't much that you can do to get around the initial data structure creation process on first enable.

Hi Justin, found this post as I was having the same issue. In my game, I have potentially several hundred entities running around each with a behavior tree running. I'm using a pooling system (if the entities get reused from the pool I don't see another tree deserialization), all of the entities are referencing the same external behavior tree, the tree is small (14 nodes).

Each time I instantiate a new entity, it's triggering BinaryDeserialization.Load, taking ~15ms. Am I reading the above correctly that there's no way around this?

I'm hoping I'm reading it wrong, that there's some way to run the deserialization once only!

Screen Shot 2020-07-21 at 5.47.17 PM.png
 
It looks like that slowdown is caused by loading the deserialization process. In this case you should be able to pool the tree using the external behavior tree pooling method when the loading screen is shown. Alternatively if you spawn a bunch of GameObjects with the tree and then deactivate them while the loading screen is shown that will also get you past the deserialization .
 
In this case you should be able to pool ...

Thanks for the reply ... in my case, this would add something like a 3-5s operation while I pre-pooled hundreds of trees ... and this is running on my local computer, I'm assuming it'll be a lot slower on some mobile devices which would push it into the non-viable realm.

I did notice that using JSON serialization gives a significant performance improvement (~10ms vs ~15ms for Binary). Might it be possible to use an even faster serializer to work around this? I found this excellent comparison article https://aloiskraus.wordpress.com/2017/04/23/the-definitive-serialization-performance-guide/ while pondering this.

TBH it feels like I'm missing some newbie fundamental here ... I need hundreds of copies of the same thing, why do I need to deserialize it hundreds of times over?

If I clone a GameObject with a behavior tree on it (which takes ~1ms) it'll work as expected but if I then try to set any tree variable value it'll trigger deserialization ... even though it's already running ... Is that a bug?
 
Are you using external trees? This should allow you to pool those trees in their own instance and then reassign the tree when a new agent spawns.

I need hundreds of copies of the same thing, why do I need to deserialize it hundreds of times over?
The tree structure is the same, but each tree needs to have its own instance of the object in order to hold it's own values for that agent.

If I clone a GameObject with a behavior tree on it (which takes ~1ms) it'll work as expected but if I then try to set any tree variable value it'll trigger deserialization ... even though it's already running ... Is that a bug?
It doesn't sound like the tree is deserialized after you do the clone. If the tree is running then the tree would have been deserialized.
 
Are you using external trees?

Yup ... each monster entity/gameobject/agent only uses the same, single, external tree. If I spawn 100 entities the deserialization runs 100 times, If I then remove them to their pool and respawn them it doesn't run again. Is this expected behavior?

... each tree needs to have its own instance of the object in order to hold it's own values for that agent.

Sure ... I'm just missing the underlying reason why it has to create that instance via (slow) deserialization, could this not be dramatically sped up by cloning after the first deserialization?

It doesn't sound like the tree is deserialized after you do the clone.

Ugh, sorry, this was my fail looking at the profiler ... I was manually calling tree.Start() when spawning which immediately triggers deserialization. The Instantiate() call was taking <1ms but didn't instantly trigger deserialization until later in the player loop as the behavior tree was set to 'Start When Enabled'. So ... both trees running, both incurred the same deserialization cost.
 
Last edited:
If I spawn 100 entities the deserialization runs 100 times, If I then remove them to their pool and respawn them it doesn't run again. Is this expected behavior?
That's correct.

I'm just missing the underlying reason why it has to create that instance via (slow) deserialization, could this not be dramatically sped up by cloning after the first deserialization?
All of the fields need to be traversed to match up the shared variables and to ensure the variables are pointing to the correct references. I played with this idea awhile back and found that the slowdown was from the reflection and walking these fields, which unfortunately cannot be avoided. I would love to be proven wrong on this case - since it has been awhile since I last looked at this I'll take another jab at it.
 
I haven't had a chance to look at it yet, it's on my list to look at before I release the next update though. If you want to take a look at it ahead of time you can download the runtime source and try to do a mem copy instead of initializing a new within the Behavior Manager. If you do take a look at it send me a message at support@opsive.com and I can work through it with you.
 
So I did some testing in this area (to try and avoid multiple de-serializations when instantiating the same prefab). My use case was when warming up the pool the majority of the time was spent de-serializing the JSON.

Using reflection to do deep copy is slow especially for tasks with nested classes. It's faster than deserializing though if the tasks are relatively flat. If we change Task and add a Clone() method it would be very quick, but that adds a lot of overhead to each task.

Another alternative is to provide pooling of behaviors in BehaviorManager. This way `JSONDeserialization.DeserializeTask()` could create more instances of the tree and only need to deserialize the JSON once. This also requires some changes in the internals of Behavior Manager.. so it adds a lot of overhead/complexity to the implementation and would only see benefit at reducing start-up speeds and if you don't need to re-size the pool.

So probably for most people async loading + pre warming a bunch of behaviors behind a loading screen is good enough. Smart usage of smaller external trees could also help split the GC of deserialization across more frames.
 
Top