Pooling, Pathfinding without seeker, garbage problem

Hey,
i’m trying to use the System without the Seeker component on thousands of entities (to avoid the monobehaviour overhead). So far it works ok, but somehow seems to generate alot of garbage in ABPath.Construct (where it calls Activator.CreateInstance). I though that using Claim() and Release() would make the paths get pooled, to avoid that. Maybe i’m just misunderstanding or missing something; but i also looked at the Seeker to verify that i’m not doing something super wrong. I claim and release the path inside the completed callback, calling them almost directly after each other.
Is this normal behaviour or can i improve something here?

I’m surprised that, while looking at the PathPool GetPath method, i can’t actually find Activator to be involved. But the unity profiler lists it (?)

Hi

Try to add the ‘Pathfinding Debugger’ component to a single object in your scene. Then enable the ‘Show Path Profile’ toggle. When you start the game it should show you how large the pool for each type of path object is, and how many paths of each type that have been created during the game. If everything is working correctly I would expect the ‘total created’ value to level off after a short time.

I’m not sure why Activator.CreateInstance shows up. Possibly that’s how Mono implements ‘new T()’ where T is a generic type?
Also note that currently it is not possible to get rid of all allocations. For each path an NNConstraint object is allocated (in the ABPath.Reset method) which I haven’t been able to get rid of due to backwards compatibility reasons. However it is much smaller than a whole path (in the profiler you can see that it makes up only around 15% of the allocations from paths).

Interestingly, when i just let it play and look at that debug output, it seems normal (around 80/100), but when i start the profiler for the first time, both numbers rise enormously (to around 1600), when i stop the profiler it stays at those high numbers with the first fluctuating normally like it did when in the low range. When i reenable the profiler the first number begins to drop until zero, at which point the second number begins to rise again (infinitely).
I have Time.time based re-requests of paths, so i’m not sure if it’s the fault of my code or really a bug.
Profiler is deep profiling btw.
Edit2: And i see the 65k cleanup after just 2 minutes, maybe i’m doing something wrong i’m not aware of.

Hi

That might be because the profiler slows everything down (esp. with deep profiling), but if you are still calculating paths every N seconds they might get queued up. If you are recalculating paths for a single unit, it is best to make sure that the previous path has been calculated before you start the next one. If you are requesting paths faster than the system can calculate them then the queue will grow without bounds (which is usually bad).
See Path.CompleteState: https://arongranberg.com/astar/docs/class_pathfinding_1_1_path.php#ae7e61764a09f9184c43bc8723f803e6d

That’s just because you have calculated 65000 paths. There are some cleanup operations it has to do at that time but it is nothing to worry about. I probably should remove that debug message to reduce confusion.

That seems odd though. I would expect the first one to drop and then the second number to increase. Possibly if you are calculating a large number of paths in one batch the first one would drop and the second one increase, and then after a small amount of time the first one would increase as well (when the paths are pooled again).

According to your post it looks like it’s in my code.
Is canceling the path with Error() correct?
I’ll try to see if a completestate check can do it. Can I take NotCalculated and Partial both as ‘In Progress’ ?
Edit: i see, the value is to be checked inside the pathCompleted callback, it’s not a general state and my previous assumption is wrong.

Yes, that should work.

Only NotCalculates indicates that it is in progress. Partial means a path that didn’t actually make it to the target, but it still considered calculated. This feature is experimental at the moment and is not used by any built-in paths (unless some very specific settings are set).

No, it is a state that you can check at any time, not just in the pathCompleted callback. In the pathCompleted callback it is guaranteed not to be ‘NotCalculated’ however. Was there anything in the docs that lead you to this conclusion?

More like inverted thinking. Below that enum i saw the PathState enum and immediately jumped to the conclusion that this one must be what is used to check a state, but i was also wrong with that. I’m just confused i guess.
My goal is to have a method that i can call anytime with a target position, that returns the state of movement (like searchingPath, targetNotReachable, Moving). And i also wanted to have an active path while another is already queued for the next destination to not stutter. And that while reserving the option to switch between direct and pathing movement. I think i just need to redesign that alltogether, as the cancelling, switching and tracking of the queued paths obviously doesn’t seem to work well.
But to get a better understanding:
After starting a path i can check IsDone() for a correct ‘In progress’ state, for canceling i use Error() and check path.error to sort out the invalid. If no error i check CompleteState for error or complete (ignoring partial and NotCalculated?). If complete i manually check the distance between the last vectorpath position and the target destination to see if it actually reached it.
Anytime:
(!path.IsDone() = calculating path)
OnPathCompleted:
(path.error || CompleteState.Error = target not reachable)
(CompleteState.Complete && InRange(lastvectorpathpos, destination) = target reachable)
Is that correct?

Right, yes you can use IsDone, that is more idiomatic. The implementation is simply

public bool IsDone () {
	return CompleteState != PathCompleteState.NotCalculated;
}

Similarly, you do not have to check for if CompleteState is set to error if you are already checking path.error, the implementation for path.error is simply

 public bool error { get { return CompleteState == PathCompleteState.Error; } }

Yeah, that enum is used internally by the pathfinding scripts to keep track of the path. You can use it if you really want to, but the CompleteState enum is easier to use.

One minor detail that I should point out. It probably will not affect you, but if you would be affected by it it could cause a lot of confusion later on. That a path has been calculated does not necessarily mean that the callback has been called. Since pathfinding runs in a separate thread it may complete the path at any time, however path callbacks must run in the Unity thread, so most likely the callback will be called during the current/next frame.

I’m not completely sure what you want to achieve, but maybe it is easier with coroutines?
See https://arongranberg.com/astar/docs/class_pathfinding_1_1_path.php#accfdd05d449188d1d7f74cf32bce687a

Something like that is likely the source of the problem (overwriting in callbacks/update, requeuing etc.)… just in my code.
Maybe a Coroutine approach is indeed a solution for that, I’ll see. Thanks so far!

Do you have a tip how i could get rid of the NNConstraint allocation? It seems to work fine so far, but because i have a few thousand entities moving, those allocations seem to pile up (i can’t get under 30kb per frame and this is the remaining part i’d like to address, to see if it helps)

Edit: It only seems to make up a small part of the footprint (around 2kb), not sure if i should care about it.

If you aren’t modifying the NNConstraints on the paths that you create, you should be able to do this:
Open the Path.cs file, find this line (in the Reset method)

nnConstraint = PathNNConstraint.Default;

and replace it with

nnConstraint = ObjectPoolSimple<PathNNConstraint>.Claim();
nnConstraint.constrainArea = false;

then in the OnEnterPool method, add this line

ObjectPoolSimple<PathNNConstraint>.Release(ref nnConstraint);

I think that should do it.

I may have an alternate solution, but not sure if it would work in all cases, since it should have the same effect as what you’re suggesting. What would break if we do modify the NNConstraints?

My alternate solution is to simply create a Reset() method for the NNConstraint which sets all values to default, which I call in Path.Reset():

private static NNConstraint defaultPathValues = PathNNConstraint.Default;
public void ResetToPathDefault()
{

this.area = defaultPathValues.area;
this.constrainArea = defaultPathValues.constrainArea;

this.constrainDistance = defaultPathValues.constrainDistance;
this.constrainTags = defaultPathValues.constrainTags;
this.constrainWalkability = defaultPathValues.constrainWalkability;
this.distanceXZ = defaultPathValues.distanceXZ;
this.graphMask = defaultPathValues.graphMask;
this.tags = defaultPathValues.tags;
this.walkable = defaultPathValues.walkable;

}

Extra details:
I’m “modifying” the constraints to match the graphMask & tasks of seeker I’m using to check the distance from a target.

It works as long as you make sure that your scripts don’t keep references to those NNConstraints that you reset. None of the pathfinding scripts do this, so if you are sure your own scripts don’t, then everything should be fine.

1 Like