Reading and Writing shared variables produces a lot of garbage

Dimonasdf

New member
Let's say we have a simple MonoBehaviour component which has reference to BehaviorTree component on an NPC and does this (caches NPC rotation speed):

C#:
void Start()
{
    MaxLookAtRotationDelta = (SharedFloat)behaviorTree?.GetVariable("MaxLookAtRotationDelta");
}

In a scenario where there are 70 NPCs, spawning across 15 frames - 4-5 NPCs a frame, in each frame this code takes 30-46ms to compute and has 24000-30000 calls to GC with 1.1-1.4MB allocation. There is also a setting of patrol waypoints lists, and it takes the same amount of cpu effort to complete (in addition to this reading).
All these NPCs are a ~5 prefabs with BehaviorTree component on them and ~5 respective external behaviors assets assigned directly, there is no pooling or dynamic assignment of trees. NPCs are pooled as is.

I'm assuming this is not per design, but what can be done to make this right? Any insights and tips will be greatly appreciated.
 
I'm also concerned about this. Haven't done performance tests yet... But if it turns out to be so costly, seems we have to discard shared variables and use custom tasks with blackboard.
 
What does the callstack look like when you are seeing the huge amount of allocation? GetVariable is essentially just a dictionary lookup so it should be quick (as long as the tree has already been deserialized, which it would if it is active).

I'm also concerned about this. Haven't done performance tests yet... But if it turns out to be so costly, seems we have to discard shared variables and use custom tasks with blackboard.
The task variables are assigned differently compared to using GetVariable.
 
Reading and Writing shared variables produces a lot of garbage
Actually, must be something else. I was testing some stuff, and it turns out that reading/writing tons of shared variables on Update() doesn't allocate anything. Only when behavior is enabled, Behavior.CheckForSerialization() allocates a small amount and that's it.
 
@abc123 is right. It looks like your tree hasn't been deserialized yet so that's where the bottleneck is coming from. You can deserialize and pool the objects during a loading screen which will prevent that spike:

 
Hey there,

I have this exact problem. I try to GetVariable and change its value when I try to enable object but there are too much garbage.
Also I try to GetVariable and change the value in Awake but it throws an error for Value. (System.ArgumentNullException: Value cannot be null.)

I don't get it how to fix it.

1597300789692.png
 
Last edited:
You can use pooling to deserialize during a better time. The External Behavior Tree link has an example in how to use external trees with your object pooler.

It looks like the tree hasn't been deserialized yet from that screenshot which is why it first needs to be deserialized before getting the variable. Once it is deserialized getting the variable is O(1).
 
You can use pooling to deserialize during a better time. The External Behavior Tree link has an example in how to use external trees with your object pooler.

It looks like the tree hasn't been deserialized yet from that screenshot which is why it first needs to be deserialized before getting the variable. Once it is deserialized getting the variable is O(1).

I use pooling for creating External Behavior Tree in loading time then Init them after instantiation and when I want to active my agent, I assign its External Behavior from pooling but still that huge amount of garbage collection allocation is there, also performance issue.

But as you can see in Deep Profiling it is not because of GetVariable (It is low), it is because of setting External Behavior and enabling Behavior Tree.

What should I do ?

1597578931868.png
 
If the tree hasn't been enabled yet then it needs to generate the objects in order to be able to traverse the tree at runtime. You could start the tree during a loading screen and then just pause it with DisableBehavior(true) until you are ready to use the tree. This will prevent the tree from having to be reinitialized.

Enable async loading will also load the tree in a new thread so it won't eliminate any allocations but it will prevent the tree from initializing in the main thread.
 
If the tree hasn't been enabled yet then it needs to generate the objects in order to be able to traverse the tree at runtime. You could start the tree during a loading screen and then just pause it with DisableBehavior(true) until you are ready to use the tree. This will prevent the tree from having to be reinitialized.

Enable async loading will also load the tree in a new thread so it won't eliminate any allocations but it will prevent the tree from initializing in the main thread.

When there isn't any agent yet in loading screen how I should assign external behavior tree then enable behavior tree then just pause it ?
 
You will need to spawn the agent first, deactivate, and along with that deactivate the tree to pause it. Now that I think we have the reason why async loading isn't working I would first give that a try.
 
You will need to spawn the agent first, deactivate, and along with that deactivate the tree to pause it. Now that I think we have the reason why async loading isn't working I would first give that a try.
Hmm, I create agents in pool. So you mean, first I need Instantiate External Behaviors, then Init them then Instantiate agents, give them ExternalBehaviorTree then make them enable then pause them by disabling and problem will be solved ?
Also if I want to deactivate that agent again I should pause the behavior tree and for activating again I should enable and restart it ?

I can set ExternalBehaviorTree of agent exactly after Init pooled External Behavior Trees ?
 
Last edited:
Yes, your workflow should be similar to:

- Create pool of external behavior trees
- Create pool of agents
- Assign external tree to agent
- Call DisableBehavior(true) to pause the behavior tree
- Disable agent
---
- Enable agent, call EnableBehavior() to unpause

With this flow the tree will be initialized at the same time that you create your agents, and you can take the initialization performance hit during a loading screen.
 
Yes, your workflow should be similar to:

- Create pool of external behavior trees
- Create pool of agents
- Assign external tree to agent
- Call DisableBehavior(true) to pause the behavior tree
- Disable agent
---
- Enable agent, call EnableBehavior() to unpause

With this flow the tree will be initialized at the same time that you create your agents, and you can take the initialization performance hit during a loading screen.

I have a question: what are benefits of pausing behavior trees on start? Isn't it better to not start them at all, until you want the behavior to actually run? My algorithm looks like this:

- Create pool of external behaviors, Init() them
- Create pool of agents, add BehaviorTree components with StartWhenEnabled set to false
---
- When AI agent is enabled, assign unused external behavior from pool
- When AI agent is spawned on the map and ready to go, EnableBehavior()
- When AI agent is disabled, DisableBehavior(), unassign external behavior
 
Last edited:
The only benefit of pausing the behavior tree on the start is that the data structures within EnableBehavior will be created. After you resume the tree this will then allow the tree to be resumed without any allocations at all. The only downside of this method is that you are storing more in memory, but it does allow for an allocation free startup.
 
- Create pool of external behavior trees
- Create pool of agents
- Assign external tree to agent
- Call DisableBehavior(true) to pause the behavior tree
- Disable agent
When BT are already on the agent's prefab it would be?
  • instantiate prefab
  • call DisableBehavior(true)
 
Top