Allocations when calling AIBase.SearchPath? (5.0.5 Pro)

I’m seeing significant allocations occurring every frame from agents when searching for Paths (using Unity’s Profiler). It looks like the call to AIBase.SearchPath eventually results in getting a pooled Path from the PathPool, but when the pooled Path calls Path.Reset it’ll create a new instance of PathNNConstraint when assigning a value to Path.nnConstraint. Since Path.nnConstraint already assigns a value of PathNNConstraint.Walkable during initialization, maybe Path.Reset could simply compare the PathNNConstraint.constrain value, and only call the PathNNConstraint.Walkable constructor when the value is false (?). This would potentially eliminate the allocation every time Path.Reset is called.

Probably worth noting that I’m using AIPath with an AutoRepathPolicy.Mode of Dynamic. I’ve kept the default values for maximumPeriod, period, and sensitivity, so the repathing isn’t too aggressive. Unfortunately the more agents one has, the more the auto repathing will kick in, which is what I’m seeing. The garbage collector can keep up with the allocations to a point, but it’ll eventually hit a threshold when the number of active agents means that every frame a subset of all agents will search their path, and thus create an allocation with that call to Path.Reset. Even when the subset of agents allocating per frame is around 5 it’s enough to drop my framerate by about 20-30-40fps. Pausing movement of all agents immediately brings the framerate back up. Also, as expected, reducing the number of active agents increases fps.

Also worth mentioning that I’m using Path pooling based on the Pooling documentation. With between 50-100 agents moving around very quickly between dynamically changing destinations, on average this is resulting in about 170-200 ABPaths created after a minute or two, and 5-10 ABPaths in the PathPool at any given time.

I guess my question is whether we could update Path.Reset to not allocate every time, or maybe have AIBase.SearchPath get a Path from the PathPool that doesn’t require an allocation (or something else altogether)?

Here’s a snapshot from the Profiler that shows the allocations occurring:

Hi

Yeah, this is an annoying allocation. I can’t really change it, though, since a lot of existing code is already using it, and they may store references to this NNConstraint. Backwards compatibility really sucks sometimes.
I have a task on my todo list to rewrite the whole NNConstraint system to use a struct instead, which wouldn’t cause any allocations.
It is only a few tens of bytes, per frame, though, so I very much doubt that this particular allocation is a major cause for concern.

This seems very significant. Are you sure it’s the path recalculations that drop your fps by this much, and not something else?

I hear you that you feel like it’s only a small number of bytes per frame, but the issue is that the more agents one has active at any given time the more allocations are going to occur, which is what I’m seeing. I’m doing some more testing to see if I can’t isolate where the bulk of the framerate drops are occurring, but as you can see in the Profiler snapshot above there’s 4.4% of the current frame being eaten up by these allocations (that test was with a fairly modest number of agents). This is occurring every frame, and that’s strictly limiting the number of agents I can have active at any given time.

How about simply having a conditional in Path.Reset that tests for whether Path.nnConstraint is null, OR if Path.nnConstraint.constrain value is set to false? If Path.nnConstraint is null, then set it with the call to PathNNConstraint.Walkable. Also, if Path.nnConstraint is not null, and the value of Path.nnConstraint.constrain is false, then call PathNNConstraint.Walkable:

if  (nnConstraint == null || nnConstraint.constrain == false)
{
    nnConstraint = PathNNConstraint.Walkable;
} 

Would it be possible to store a static instance of PathNNConstraint.Walkable, instead of having that call construct a new instance every call? Do all Paths need their own instance of PathNNConstraint?

There’s another persistent allocation I see in the Profiler at Core/misc/PathInterpolator.SetPath. If you add a conditional around setting the capacity of PathInterpolator.path, then that’ll eliminate an allocation that occurs every time AstarPath.Update calls PathReturnQueue.ReturnPaths. Also one of those types of allocations that gets worse with the number of agents, thus making it difficult to scale things up. This change would fix it in Core/misc/PathInterpolator.SetPath:

if (this.path.Capacity < path.Count)
{
    this.path.Capacity = path.Count
}

One other thing I noticed in tracking down these perf issues that is unrelated to the allocation mentioned above, is the use of Physics.SyncTransforms and Physics2D.SyncTransforms in AIBase.OnUpdate. There’s a call to both of these functions at the beginning and the end of OnUpdate, two calls total for both of these functions. My understanding of how these functions work is that they should be unnecessary to call from within OnUpdate, and could very well be adding significant overhead to the entire pathfinding project.

This thread talks about limiting the use of SyncTransforms, and makes a good case for not needing them to be called at the beginning and end of AIBase.OnUpdate, especially considering every agent will be making these calls which are global in nature.

I tested removing those calls entirely from Astar 5.0.5, and there was no negative impact.