Search Unity

Resolved NativeArray wont Dispose. Why?

Discussion in 'Entity Component System' started by JustTiredOfEverything, Aug 28, 2022.

  1. JustTiredOfEverything

    JustTiredOfEverything

    Joined:
    Aug 4, 2022
    Posts:
    80
    I wrote some pathfinding scripts that seems to works just fine in play mode, but throws errors on playmode exit. It says "A Native Collection has not been disposed, resulting in a memory leak".

    So I called Dispose() for the NativeArray in OnDestroy. This time on exit, it gives me the same error, plus one before it saying the array " has been deallocated, it is not allowed to access it".

    Is this actually a problem if I just don't fix it? What problems will it cause? The script seems to work just fine, so what happens if you don't dispose of a NativeArray exactly, anything bad? Or can I just leave it as is?
    Will this prevent unity from making a build? How would this affect a build?

    And, finally, how would I fix this?
     
    Last edited: Aug 28, 2022
  2. JustTiredOfEverything

    JustTiredOfEverything

    Joined:
    Aug 4, 2022
    Posts:
    80
    Here are my scripts. I wrote them following the DOTS A* Pathfinding tutorial from CodeMonkey.

    PathNode:
    Code (CSharp):
    1. public struct PathNode {
    2.     public int x;
    3.     public int y;
    4.     public int index;
    5.     public int gCost;
    6.     public int hCost;
    7.     public int fCost;
    8.     public bool isWalkable;
    9.     public int cameFromNodeIndex;
    10.     public void CalculateFCost() {
    11.         fCost = gCost + hCost;
    12.     }
    13.     public void SetIsWalkable(bool isWalkable) {
    14.         this.isWalkable = isWalkable;
    15.     }
    16. }
    Pathfinding:
    Code (CSharp):
    1. using UnityEngine;
    2. using Unity.Mathematics;
    3. using Unity.Collections;
    4. using Unity.Jobs;
    5. using Unity.Burst;
    6. public class Pathfinding : MonoBehaviour {
    7.     public float pathTimeInterval = 1f;
    8.     const int MOVE_STRAIGHT_COST = 10;
    9.     const int MOVE_DIAGONAL_COST = 14;
    10.     Grid grid;
    11.     public int findPathJobCount = 1;
    12.  
    13.  
    14.     public int2 startPosition = new int2(0, 0),
    15.                 endPosition = new int2(5, 5);
    16.     float intervalTimer;
    17.     void Awake() {
    18.         grid = FindObjectOfType<Grid>();
    19.     }
    20.     void Update() {
    21.         intervalTimer += Time.deltaTime;
    22.         if (intervalTimer >= pathTimeInterval) {
    23.             DoJobShit();
    24.             intervalTimer = 0;
    25.         }
    26.     }
    27.     void DoJobShit() {
    28.         float startTime = Time.realtimeSinceStartup;
    29.         NativeArray<JobHandle> jobHandleArray = new NativeArray<JobHandle>(findPathJobCount, Allocator.TempJob);
    30.         for (int i = 0; i < findPathJobCount; i++) {
    31.             FindPathJob findPathJob = new FindPathJob {
    32.                 _startPosition = startPosition,
    33.                 _endPosition = endPosition,
    34.                 _gridSize = grid.gridSize,
    35.                 _pathNodeArray = grid.grid
    36.             };
    37.             if (i == 0) {
    38.                 jobHandleArray[i] = findPathJob.Schedule();
    39.             } else {
    40.                 jobHandleArray[i] = findPathJob.Schedule(jobHandleArray[i - 1]);
    41.             }
    42.         }
    43.         JobHandle.CompleteAll(jobHandleArray);
    44.         jobHandleArray.Dispose();
    45.         Debug.Log("Time: " + ((Time.realtimeSinceStartup - startTime) * 1000f));
    46.     }
    47.     [BurstCompile]
    48.     struct FindPathJob : IJob {
    49.         public int2 _startPosition, _endPosition, _gridSize;
    50.         public NativeArray<PathNode> _pathNodeArray;
    51.         public void Execute() {
    52.             for (int x = 0; x < _gridSize.x; x++) {
    53.                 for (int y = 0; y < _gridSize.y; y++) {
    54.                     PathNode pathNode = new PathNode();
    55.                     pathNode.gCost = int.MaxValue;
    56.                     pathNode.hCost = CalculateDistanceCost(new int2(x, y), _endPosition);
    57.                     pathNode.CalculateFCost();
    58.                 }
    59.             }
    60.        
    61.             NativeArray<int2> neighbourOffsetArray = new NativeArray<int2>(8, Allocator.Temp);
    62.             neighbourOffsetArray[0] = new int2(-1, 0); // Left
    63.             neighbourOffsetArray[1] = new int2(+1, 0); // Right
    64.             neighbourOffsetArray[2] = new int2(0, +1); // Up
    65.             neighbourOffsetArray[3] = new int2(0, -1); // Down
    66.             neighbourOffsetArray[4] = new int2(-1, -1); // Left Down
    67.             neighbourOffsetArray[5] = new int2(-1, +1); // Left Up
    68.             neighbourOffsetArray[6] = new int2(+1, -1); // Right Down
    69.             neighbourOffsetArray[7] = new int2(+1, +1); // Right Up
    70.             int endNodeIndex = CalculateIndex(_endPosition.x, _endPosition.y, _gridSize.x);
    71.             PathNode startNode = _pathNodeArray[CalculateIndex(_startPosition.x, _startPosition.y, _gridSize.x)];
    72.             startNode.gCost = 0;
    73.             startNode.CalculateFCost();
    74.             _pathNodeArray[startNode.index] = startNode;
    75.             NativeList<int> openList = new NativeList<int>(Allocator.Temp);
    76.             NativeList<int> closedList = new NativeList<int>(Allocator.Temp);
    77.             openList.Add(startNode.index);
    78.             while (openList.Length > 0) {
    79.                 int currentNodeIndex = GetLowestCostFNodeIndex(openList, _pathNodeArray);
    80.                 PathNode currentNode = _pathNodeArray[currentNodeIndex];
    81.                 if (currentNodeIndex == endNodeIndex) {
    82.                     // Reached our destination!
    83.                     break;
    84.                 }
    85.                 // Remove current node from Open List
    86.                 for (int i = 0; i < openList.Length; i++) {
    87.                     if (openList[i] == currentNodeIndex) {
    88.                         openList.RemoveAtSwapBack(i);
    89.                         break;
    90.                     }
    91.                 }
    92.                 closedList.Add(currentNodeIndex);
    93.                 for (int i = 0; i < neighbourOffsetArray.Length; i++) {
    94.                     int2 neighbourOffset = neighbourOffsetArray[i];
    95.                     int2 neighbourPosition = new int2(currentNode.x + neighbourOffset.x, currentNode.y + neighbourOffset.y);
    96.                     if (!IsPositionInsideGrid(neighbourPosition, _gridSize)) {
    97.                         // Neighbour not valid position
    98.                         continue;
    99.                     }
    100.                     int neighbourNodeIndex = CalculateIndex(neighbourPosition.x, neighbourPosition.y, _gridSize.x);
    101.                     if (closedList.Contains(neighbourNodeIndex)) {
    102.                         // Already searched this node
    103.                         continue;
    104.                     }
    105.                     PathNode neighbourNode = _pathNodeArray[neighbourNodeIndex];
    106.                     if (!neighbourNode.isWalkable) {
    107.                         // Not walkable
    108.                         continue;
    109.                     }
    110.                     int2 currentNodePosition = new int2(currentNode.x, currentNode.y);
    111.                     int tentativeGCost = currentNode.gCost + CalculateDistanceCost(currentNodePosition, neighbourPosition);
    112.                     if (tentativeGCost < neighbourNode.gCost) {
    113.                         neighbourNode.cameFromNodeIndex = currentNodeIndex;
    114.                         neighbourNode.gCost = tentativeGCost;
    115.                         neighbourNode.CalculateFCost();
    116.                         _pathNodeArray[neighbourNodeIndex] = neighbourNode;
    117.                         if (!openList.Contains(neighbourNode.index)) {
    118.                             openList.Add(neighbourNode.index);
    119.                         }
    120.                     }
    121.                 }
    122.             }
    123.             PathNode endNode = _pathNodeArray[endNodeIndex];
    124.             if (endNode.cameFromNodeIndex == -1) {
    125.                 // Didn't find a path!
    126.                 //Debug.Log("Didn't find a path!");
    127.             } else {
    128.                 // Found a path
    129.                 NativeList<int2> path = CalculatePath(_pathNodeArray, endNode);
    130.                 /*
    131.                 foreach (int2 pathPosition in path) {
    132.                     Debug.Log(pathPosition);
    133.                 }
    134.                 */
    135.                 path.Dispose();
    136.             }
    137.             //_pathNodeArray.Dispose();
    138.             neighbourOffsetArray.Dispose();
    139.             openList.Dispose();
    140.             closedList.Dispose();
    141.         }
    142.         NativeList<int2> CalculatePath(NativeArray<PathNode> pathNodeArray, PathNode endNode) {
    143.             if (endNode.cameFromNodeIndex == -1) {
    144.                 // Couldn't find a path!
    145.                 return new NativeList<int2>(Allocator.Temp);
    146.             } else {
    147.                 // Found a path
    148.                 NativeList<int2> path = new NativeList<int2>(Allocator.Temp);
    149.                 path.Add(new int2(endNode.x, endNode.y));
    150.                 PathNode currentNode = endNode;
    151.                 while (currentNode.cameFromNodeIndex != -1) {
    152.                     PathNode cameFromNode = pathNodeArray[currentNode.cameFromNodeIndex];
    153.                     path.Add(new int2(cameFromNode.x, cameFromNode.y));
    154.                     currentNode = cameFromNode;
    155.                 }
    156.                 return path;
    157.             }
    158.         }
    159.         bool IsPositionInsideGrid(int2 gridPosition, int2 gridSize) {
    160.             return
    161.                 gridPosition.x >= 0 &&
    162.                 gridPosition.y >= 0 &&
    163.                 gridPosition.x < gridSize.x &&
    164.                 gridPosition.y < gridSize.y;
    165.         }
    166.         int CalculateIndex(int x, int y, int gridWidth) {
    167.             return x + y * gridWidth;
    168.         }
    169.         int CalculateDistanceCost(int2 aPosition, int2 bPosition) {
    170.             int xDistance = math.abs(aPosition.x - bPosition.x);
    171.             int yDistance = math.abs(aPosition.y - bPosition.y);
    172.             int remaining = math.abs(xDistance - yDistance);
    173.             return MOVE_DIAGONAL_COST * math.min(xDistance, yDistance) + MOVE_STRAIGHT_COST * remaining;
    174.         }
    175.         int GetLowestCostFNodeIndex(NativeList<int> openList, NativeArray<PathNode> pathNodeArray) {
    176.             PathNode lowestCostPathNode = pathNodeArray[openList[0]];
    177.             for (int i = 1; i < openList.Length; i++) {
    178.                 PathNode testPathNode = pathNodeArray[openList[i]];
    179.                 if (testPathNode.fCost < lowestCostPathNode.fCost) {
    180.                     lowestCostPathNode = testPathNode;
    181.                 }
    182.             }
    183.             return lowestCostPathNode.index;
    184.         }
    185.  
    186.     }
    187. }
    188.  

    And the Grid, where dispose is called:
    Code (CSharp):
    1. using UnityEngine;
    2. using Unity.Mathematics;
    3. using Unity.Collections;
    4. using Unity.Jobs;
    5. using Unity.Burst;
    6.  
    7. public class Grid : MonoBehaviour {
    8.     public NativeArray<PathNode> pathNodeArray, grid;
    9.     JobHandle jobHandle;
    10.     GenerateGridJob gridJob;
    11.     public int2 gridSize = new int2(10, 10);
    12.     void Awake() {
    13.         pathNodeArray = new NativeArray<PathNode>(gridSize.x * gridSize.y, Allocator.Persistent);
    14.         grid = new NativeArray<PathNode>(gridSize.x * gridSize.y, Allocator.Persistent);
    15.         GenerateGrid();
    16.     }
    17.     void Update() {
    18.         if (jobHandle.IsCompleted) {
    19.             jobHandle.Complete();
    20.             grid = gridJob._pathNodeArray;
    21.         }
    22.     }
    23.     void GenerateGrid() {
    24.         jobHandle = new JobHandle();
    25.         gridJob = new GenerateGridJob {
    26.             _gridSize = gridSize,
    27.             _pathNodeArray = pathNodeArray
    28.         };
    29.         jobHandle = gridJob.Schedule();
    30.     }
    31.     void OnDestroy() {
    32.         pathNodeArray.Dispose();
    33.         grid.Dispose();
    34.     }
    35.  
    36.     [BurstCompile]
    37.     struct GenerateGridJob : IJob {
    38.         public int2 _gridSize;
    39.         public NativeArray<PathNode> _pathNodeArray;
    40.  
    41.         public void Execute() {
    42.             for (int x = 0; x < _gridSize.x; x++) {
    43.                 for (int y = 0; y < _gridSize.y; y++) {
    44.                     PathNode pathNode = new PathNode();
    45.                     pathNode.x = x;
    46.                     pathNode.y = y;
    47.                     pathNode.index = CalculateIndex(x, y, _gridSize.x);
    48.                     //pathNode.gCost = int.MaxValue;
    49.                     //pathNode.hCost = CalculateDistanceCost(new int2(x, y), _endPosition);
    50.                     //pathNode.CalculateFCost();
    51.  
    52.  
    53.                     //replace later
    54.                     pathNode.isWalkable = true;
    55.  
    56.  
    57.  
    58.                     pathNode.cameFromNodeIndex = -1;
    59.                     _pathNodeArray[pathNode.index] = pathNode;
    60.                 }
    61.             }
    62.             /*
    63.             // Place Testing Walls
    64.             {
    65.                 PathNode walkablePathNode = pathNodeArray[CalculateIndex(1, 0, gridSize.x)];
    66.                 walkablePathNode.SetIsWalkable(false);
    67.                 pathNodeArray[CalculateIndex(1, 0, gridSize.x)] = walkablePathNode;
    68.  
    69.                 walkablePathNode = pathNodeArray[CalculateIndex(1, 1, gridSize.x)];
    70.                 walkablePathNode.SetIsWalkable(false);
    71.                 pathNodeArray[CalculateIndex(1, 1, gridSize.x)] = walkablePathNode;
    72.  
    73.                 walkablePathNode = pathNodeArray[CalculateIndex(1, 2, gridSize.x)];
    74.                 walkablePathNode.SetIsWalkable(false);
    75.                 pathNodeArray[CalculateIndex(1, 2, gridSize.x)] = walkablePathNode;
    76.             }
    77.             */
    78.  
    79.             //_pathNodeArray.Dispose();
    80.         }
    81.         int CalculateIndex(int x, int y, int gridWidth) {
    82.             return x + y * gridWidth;
    83.         }
    84.     }
    85. }
    86.  
     
    Last edited: Aug 28, 2022
  3. JustTiredOfEverything

    JustTiredOfEverything

    Joined:
    Aug 4, 2022
    Posts:
    80
    And the errors:
    Code (CSharp):
    1. ObjectDisposedException: The Unity.Collections.NativeArray`1[PathNode] has been deallocated, it is not allowed to access it
    2. Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckDeallocateAndThrow (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) (at <40205bb2cb25478a9cb0f5e54cf11441>:0)
    3. Unity.Collections.LowLevel.Unsafe.DisposeSentinel.Dispose (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle& safety, Unity.Collections.LowLevel.Unsafe.DisposeSentinel& sentinel) (at <40205bb2cb25478a9cb0f5e54cf11441>:0)
    4. Unity.Collections.NativeArray`1[T].Dispose () (at <40205bb2cb25478a9cb0f5e54cf11441>:0)
    5. Grid.OnDestroy () (at Assets/Grid.cs:33)
    Code (CSharp):
    1. A Native Collection has not been disposed, resulting in a memory leak. Allocated from:
    2. Unity.Collections.NativeArray`1:.ctor(Int32, Allocator, NativeArrayOptions)
    3. Grid:Awake() (at Assets\Grid.cs:14)
    4.  
     
    Last edited: Aug 28, 2022
  4. Neto_Kokku

    Neto_Kokku

    Joined:
    Feb 15, 2018
    Posts:
    1,751
    The problem is on line 20 of the grid script:

    Code (CSharp):
    1. grid = gridJob._pathNodeArray;
    This line is causing the variable grid to point to the same array as pathNodeArray. So when you try to dispose both arrays in OnDestroy, you get an error because they are actually the same array (so you're trying to dispose an already disposed array), and you get the memory leak error when exciting play mode since the original array assigned to grid was never disposed.
     
    JustTiredOfEverything likes this.
  5. joshrs926

    joshrs926

    Joined:
    Jan 31, 2021
    Posts:
    115
    Inside the Grid MonoBehaviour it looks like you allocate 2 native arrays: pathNodeArray and grid. The GenerateGridJob struct also has a native array called _pathNodeArray. When creating the job you assign parhNodeArray to _pathNodeArray then when the job completes you assign _pathNodeArray to grid. So you no longer have the original Native Array that was allocated and assigned to grid. Now both grid and pathNodeArray are equal to pathNodeArray. So when you try to dispose both, you’re really trying to dispose the same one twice.


    You probably don’t need 2 native arrays. Just have the one grid Native array. Create it in Awake, assign it to the job’s native array field, complete the job, then that’s it. Now the original grid NativeArray has the data you want. Code Monkey actually has a great video about how to get data out of a job using Native Arrays.


    One other possible issue is that PathNode has a bool field and bools are managed objects, I believe. I remember having issues trying to put bools in Native Arrays. Seems like a silly issue though so hopefully Unity made an exception for bools and somehow made them allowed. If you end up having problems you could do something like this:

    byte v;

    public bool IsValueTrue

    {

    get => v > 0;

    set => v = value ? 1 : 0;

    }

    But that’s horribly verbose compared to just using a bool


    Good job! This looks really cool. I’ll have to check out his pathfinding tutorial at some point.
     
    JustTiredOfEverything likes this.
  6. joshrs926

    joshrs926

    Joined:
    Jan 31, 2021
    Posts:
    115
    Dang you beat me to it while I was writing mine!
     
  7. JustTiredOfEverything

    JustTiredOfEverything

    Joined:
    Aug 4, 2022
    Posts:
    80
    I appreciate it. I got rid of this line and took out the grid variable. I'm running into more trouble, but that specific issue went away.
     
  8. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,882
    Hmmm ... I wonder ... because of the further issues: I think it's possible for Destroy() to be called while a job is still running. In that case Destroy() should call Complete() on the JobHandle before disposing the arrays used by the job.
     
  9. JustTiredOfEverything

    JustTiredOfEverything

    Joined:
    Aug 4, 2022
    Posts:
    80
    How do make it NOT do that?
    Like, how do I copy it instead of pointing towards it?
     
    Last edited: Aug 28, 2022
  10. JustTiredOfEverything

    JustTiredOfEverything

    Joined:
    Aug 4, 2022
    Posts:
    80
    I switched it to an int just to be safe.
    It still gave me issues.

    video link?
     
    Last edited: Aug 28, 2022
  11. joshrs926

    joshrs926

    Joined:
    Jan 31, 2021
    Posts:
    115
    Code Monkey - How to get Output from the Unity Job System


    If you post code/ error logs of the issues you're having I'd be happy to take a look.
     
  12. joshrs926

    joshrs926

    Joined:
    Jan 31, 2021
    Posts:
    115
    One thing to keep in mind:
    NativeArrays (and all other Native Collections) are technically structs. So they are copied by-value, not by-reference. BUT... one of their fields is a pointer to memory, so they kind of are hybrid value-reference types.
    Idk what the actual fields of NativeArray are, but pretend they're this:

    Code (CSharp):
    1. public struct NativeArray<T>
    2. {
    3.      // just for pretend
    4.      T* pointer;
    5.      int length;
    6. }
    All of the values that are "in" the native array are really in memory somewhere and that pointer points to them. So I can make a hundred copies of this native array and they will all point to the same collection of values. That was the problem you were having earlier. You overwrote one of your native arrays with another, so the original was still left in memory undisposed.