DOTS Processing hundreds of thousands of entities using NativeLists

Hello there, I have this code that loads/unloads chunks, generates entities for newly loaded chunks, but when i tried to test it by creating entity with component “Loader”, Unity completely freeze and the last thing i see is the “ms” in stats skyrocket to several thousands. I also get error that NativeCollection has not been deallocated and that there is a memory leak.


Now, I (probably) know whats happening, i have a bug in my code where some nativecontainer is not deallocated, but i couldn’t find yout where is the bug. It is definitely somewhere in the generation part, as before that it worked perfectly. Also it worked before, so I started working on destroying the entities in unloaded chunks before i noticed the error, hence the code for unloading.


Anyway heres my entire main code:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Entities;
using Unity.Transforms;
using Unity.Jobs;
using Unity.Burst;
using static WorldUtils;

public class WorldMaster : ComponentSystem
{
    public int ChunkSize = 20;
    public int TileSize = 1;

    EntityCommandBuffer ECB;
    EntityArchetype TileArchetype;

    NativeList<int2> ActiveChunks;
    NativeList<int2> LoadedChunks;
    NativeList<int2> PendingChunks;

    JobHandle Dependency;

    protected override void OnStartRunning()
    {
        TileArchetype = World.DefaultGameObjectInjectionWorld.EntityManager.CreateArchetype
                    (
                    typeof(Translation),
                    typeof(Unloadable)
                    );

        ActiveChunks = new NativeList<int2>(0, Allocator.Persistent);
        LoadedChunks = new NativeList<int2>(0, Allocator.Persistent);
        PendingChunks = new NativeList<int2>(0, Allocator.Persistent);
    }

    protected override void OnDestroy()
    {
        ActiveChunks.Dispose();
        LoadedChunks.Dispose();
        PendingChunks.Dispose();
    }

    protected override void OnUpdate()
    {
        ECB = World.DefaultGameObjectInjectionWorld.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>().CreateCommandBuffer();

        Entities.ForEach((ref Translation translation, ref Loader loader) =>
        {
            float2 Position = new float2(translation.Value.x, translation.Value.y);
            int LoadDistance = loader.LoadDistance;

            Dependency = ActivateChunks(Position, ChunkSize, LoadDistance, ActiveChunks);
            Dependency = LoadChunks(ActiveChunks, LoadedChunks, PendingChunks, Dependency);
            Dependency = GenerateChunks(ECB, TileArchetype, PendingChunks, ChunkSize, TileSize, Dependency);

            Dependency.Complete();
        });

        Entities.ForEach((Entity entity,ref Translation translation, ref Unloadable unloadable) =>
        {
            float2 Position = new float2(translation.Value.x, translation.Value.y);
            NativeArray<Entity> Entities = new NativeArray<Entity>(1, Allocator.TempJob);
            Entities[0] = entity;

            Dependency = UnloadEntity(Position, ChunkSize, LoadedChunks, Entities, ECB, Dependency);
            Dependency.Complete();
        });

    }

    public JobHandle ActivateChunks(float2 Position, int ChunkSize, int LoadDistance, NativeList<int2> ActiveChunks, JobHandle Dependency = default(JobHandle))
    {
        ActivateChunksJob job = new ActivateChunksJob();
        job.Position = Position;
        job.ChunkSize = ChunkSize;
        job.LoadDistance = LoadDistance;
        job.ActiveChunks = ActiveChunks;
        return job.Schedule(Dependency);
    }

    public JobHandle LoadChunks(NativeList<int2> ActiveChunks, NativeList<int2> LoadedChunks, NativeList<int2> PendingChunks, JobHandle Dependency = default(JobHandle))
    {
        LoadChunksJob job = new LoadChunksJob();
        job.ActiveChunks = ActiveChunks;
        job.LoadedChunks = LoadedChunks;
        job.PendingChunks = PendingChunks;
        return job.Schedule(Dependency);
    }

    public JobHandle GenerateChunks(EntityCommandBuffer ECB, EntityArchetype TileArchetype, NativeList<int2> PendingChunks, int ChunkSize, int TileSize, JobHandle Dependency = default(JobHandle))
    {
        GenerateJob job = new GenerateJob();
        job.ECB = ECB;
        job.TileArchetype = TileArchetype;
        job.PendingChunks = PendingChunks;
        job.ChunkSize = ChunkSize;
        job.TileSize = TileSize;
        return job.Schedule(Dependency);
    }

    public JobHandle UnloadEntity(float2 Position, int ChunkSize, NativeList<int2> LoadedChunks, NativeArray<Entity> Entities, EntityCommandBuffer ECB, JobHandle Dependency = default(JobHandle))
    {
        UnloadJob job = new UnloadJob();
        job.Position = Position;
        job.ChunkSize = ChunkSize;
        job.LoadedChunks = LoadedChunks;
        job.Entities = Entities;
        job.ECB = ECB;
        return job.Schedule(Dependency);
    }

}

[BurstCompile]
public struct ActivateChunksJob : IJob
{
    public float2 Position;
    public int ChunkSize;
    public int LoadDistance;
    public NativeList<int2> ActiveChunks;

    public void Execute()
    {
        ActiveChunks.Clear();
        for (int x = 0; x < LoadDistance; x++)
        {
            for (int y = 0; y < LoadDistance; y++)
            {
                int2 chunk = GetChunkFromWorld(Position, ChunkSize, new float2(0, 0)) - LoadDistance / 2 + new int2(x, y);
                if (!ActiveChunks.Contains(chunk))
                {
                    ActiveChunks.Add(chunk);
                }
            }
        }
    }
}

[BurstCompile]
public struct LoadChunksJob : IJob
{
    public NativeList<int2> ActiveChunks;
    public NativeList<int2> LoadedChunks;
    public NativeList<int2> PendingChunks;

    public void Execute()
    {
        for (int i = 0; i < ActiveChunks.Length; i++)
        {
            int2 chunk = ActiveChunks*;
            if (!LoadedChunks.Contains(chunk))
            {
                LoadedChunks.Add(chunk);
                PendingChunks.Add(chunk);
            }
        }
        for (int i = LoadedChunks.Length - 1; i != -1; i--)
        {
            int2 chunk = LoadedChunks*;
            if (!ActiveChunks.Contains(chunk))
            {
                LoadedChunks.RemoveAt(LoadedChunks.IndexOf(chunk));
            }
        }
    }
}

[BurstCompile]
public struct GenerateJob : IJob
{
    public EntityCommandBuffer ECB;
    public EntityArchetype TileArchetype;
    public NativeList<int2> PendingChunks;
    public int ChunkSize;
    public int TileSize;
    public void Execute()
    {
        for (int i = PendingChunks.Length - 1; i != -1; i--)
        {
            int2 chunk = PendingChunks*;
            for (int x = 0; x < ChunkSize; x++)
            {
                for (int y = 0; y < ChunkSize; y++)
                {
                    Entity entity = ECB.CreateEntity(TileArchetype);
                    float2 position = GetWorldFromChunk(chunk, ChunkSize, new float2(0,0)) + GetTileCenter(new int2(x, y), TileSize);
                    ECB.SetComponent(entity, new Translation { Value = new float3(position.x, position.y, 0) });
                }
            }
            PendingChunks.RemoveAt(PendingChunks.IndexOf(chunk));
        }
    }
}

[BurstCompile]
public struct UnloadJob : IJob
{
    public float2 Position;
    public int ChunkSize;
    public NativeList<int2> LoadedChunks;
    public NativeArray<Entity> Entities;
    public EntityCommandBuffer ECB;
    public void Execute()
    {
        int2 Chunk;
        Chunk = GetChunkFromWorld(Position, ChunkSize, new float2(0,0));
        if (!LoadedChunks.Contains(Chunk))
        {
            ECB.DestroyEntity(Entities[0]);
        }
    }
}

I am also adding code from my library that was used in it:

public static float2 GetTileCenter(int2 TilePosition, float TileSize)
{
    return new float2((TilePosition.x + (TileSize * 0.5f)), (TilePosition.y + (TileSize * 0.5f)));
}
public static int2 GetChunkFromWorld(float2 WorldPosition, int ChunkSize, float2 OriginPosition)
{
    int2 ChunkPosition;
    ChunkPosition.x = (int)(math.floor((WorldPosition.x - OriginPosition.x) / ChunkSize));
    ChunkPosition.y = (int)(math.floor((WorldPosition.y - OriginPosition.y) / ChunkSize));
    return ChunkPosition;
}
public static float2 GetWorldFromChunk(int2 ChunkPosition, int ChunkSize, float2 OriginPosition)
{
    float2 WorldPosition;
    WorldPosition.x = (ChunkPosition.x * ChunkSize) + OriginPosition.x;
    WorldPosition.y = (ChunkPosition.y * ChunkSize) + OriginPosition.y;
    return WorldPosition;
}

And also the testing code i used to create entity with “Loader Component”:

using System.Collections;
using System.Collections.Generic;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
public class Test : MonoBehaviour
{
    public float2 Position;
    EntityManager EntityManager;
    private void Start()
    {
        EntityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
    }
    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.G))
        {
            Entity entity = EntityManager.CreateEntity(typeof (Translation), typeof (Loader));
            EntityManager.SetComponentData(entity, new Translation { Value = new float3(Position.x, Position.y, 0) });
            EntityManager.SetComponentData(entity, new Loader { LoadDistance = 40 });
        }
    }
}

if you managed to read this entire behemoth of a code and not fall asleep while doing so AND you would even decide to help me, i would be very, very thankful :smiley:

So trought extensive testing i found out actually the unloading caused the freeze, while generation caused the error message. I am actively trying to fix both.


Edit: Yep i was right, it was the Entities.ForEach trying to cycle trough 640 000 entities every frame. It was stupid to think it would not cause problems. As for the error message about leak i found out it was because i allocated the nativelists in OnStartRunning and I fixed it by changing it to OnCreate.

Before:

  • 640.000 entities

  • 200 to 400ms

Now:

  • 640.000 entities (changed nothing here)

  • 5ms (and spread across available cpu cores in 1.5ms slices)

What was the absolutely biggest #1 performance offender?

LoadedChunks being a NativeList i.e. O(n) .Contains() cost


    using UnityEngine;
    using Unity.Collections;
    using Unity.Mathematics;
    using Unity.Entities;
    using Unity.Transforms;
    using Unity.Jobs;
    public class WorldMaster : SystemBase
    {
    	public int ChunkSize = 20;
    	public int TileSize = 1;
    	EndSimulationEntityCommandBufferSystem _endSimulationEcbSystem;
    	EntityArchetype TileArchetype;
    	NativeList<int2> ActiveChunks;
    	NativeHashSet<int2> LoadedChunks;
    	NativeList<int2> PendingChunks;
    	Entity loaderEntity;
    	float loaderMovingSpeed = 1f;
    	protected override void OnCreate ()
    	{
    		_endSimulationEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    		TileArchetype = EntityManager.CreateArchetype( typeof(Unloadable) , typeof(Cluster) , typeof(Translation) );
    		ActiveChunks = new NativeList<int2>(128, Allocator.Persistent);
    		LoadedChunks = new NativeHashSet<int2>(128, Allocator.Persistent);
    		PendingChunks = new NativeList<int2>(128, Allocator.Persistent);
    
    		loaderEntity = EntityManager.CreateEntity( typeof(Loader) , typeof(Cluster) , typeof(Translation) );
    		EntityManager.SetComponentData<Translation>( loaderEntity , new Translation{ Value=new float3{ x=UnityEngine.Random.Range(-10,10) , y=UnityEngine.Random.Range(-10,10) , z=0 } });
    		EntityManager.SetComponentData( loaderEntity , new Loader{ LoadDistance=40 });
    	}
    	protected override void OnDestroy ()
    	{
    		ActiveChunks.Dispose();
    		LoadedChunks.Dispose();
    		PendingChunks.Dispose();
    	}
    	protected override void OnUpdate ()
    	{
    		// loader moves:
    		float3 loaderPositionDelta = new float3{ x=Input.GetAxis("Horizontal") , y=Input.GetAxis("Vertical") , z=0 } * loaderMovingSpeed;
    		if( math.lengthsq(loaderPositionDelta)>0f )
    		{
    			var translation = EntityManager.GetComponentData<Translation>( loaderEntity );
    			translation.Value += loaderPositionDelta;
    			EntityManager.SetComponentData( loaderEntity , translation );
    		}
    
    		// aliases:
    		var chunkSize = ChunkSize;
    		var tileSize = TileSize;
    		var activeChunks = ActiveChunks;
    		var loadedChunks = LoadedChunks;
    		var pendingChunks = PendingChunks;
    		var tileArchetype = TileArchetype;
    		
    		ActiveChunks.Clear();
    		var ecb = _endSimulationEcbSystem.CreateCommandBuffer();
    		var ecb_pw = ecb.AsParallelWriter();
    		
    		Entities
    			.WithName("update_cluster_job")
    			.WithChangeFilter<Translation>()// excludes stationary entities
    			.ForEach( ( ref Cluster cluster , in Translation translation ) =>
    			{
    				cluster.Value = GetChunkFromWorld( new float2{ x=translation.Value.x, y=translation.Value.y } , chunkSize );
    			})
    			.WithBurst().ScheduleParallel();
    
    		Entities
    			.WithName("get_active_chunks_job")
    			.ForEach( ( in Loader loader , in Cluster cluster ) =>
    			{
    				int loadDistance = loader.LoadDistance;
    				for( int x=0 ; x<loadDistance ; x++ )
    				for( int y=0 ; y<loadDistance ; y++ )
    				{
    					int2 chunk = cluster.Value - loadDistance/2 + new int2(x,y);
    					if( !activeChunks.Contains(chunk) )
    						activeChunks.Add(chunk);
    				}
    			})
    			.WithBurst().Schedule();
    
    		Job
    			.WithName("load_chunks_job")
    			.WithReadOnly( activeChunks )
    			.WithCode( ()=>
    			{
    				for( int i=0 ; i<activeChunks.Length ; i++ )
    				{
    					int2 chunk = activeChunks*;
					if( !loadedChunks.Contains(chunk) )
					{
						loadedChunks.Add(chunk);
						pendingChunks.Add(chunk);
					}
				}
				var activeChunksAsArray = activeChunks.AsArray();
				for( int i=activeChunksAsArray.Length-1 ; i!=-1 ; i-- )
				{
					int2 chunk = activeChunksAsArray*;
 					if( !activeChunks.Contains(chunk) )
 						loadedChunks.Remove( chunk );
 				}
 			})
 			.WithBurst().Schedule();
 
 		Job
			.WithName("generate_chunks_job")
 			.WithCode( ()=>
 			{
 				for( int i=pendingChunks.Length-1 ; i!=-1 ; i-- )
 				{
					int2 chunk = pendingChunks*;
 					for( int x=0 ; x<chunkSize ; x++ )
 					for( int y=0 ; y<chunkSize ; y++ )
 					{
 						Entity entity = ecb.CreateEntity( tileArchetype );
 						float2 position = GetWorldFromChunk(chunk,chunkSize) + GetTileCenter(new int2(x,y),tileSize);
 						ecb.SetComponent( entity , new Translation{ Value = new float3(position.x, position.y, 0) } );
 					}
 					pendingChunks.RemoveAt( pendingChunks.IndexOf(chunk) );
 				}
 			})
 			.WithBurst().Schedule();
 
 		Entities
			.WithName("unload_chunks_job")
 			.WithReadOnly( loadedChunks )
 			.WithAll<Unloadable>()
 			.ForEach( ( int entityInQueryIndex , Entity entity , in Cluster cluster ) =>
 			{
 				int2 chunk = cluster;
 				if( !loadedChunks.Contains(chunk) )
					ecb_pw.DestroyEntity( entityInQueryIndex , entity );
 			})
 			.WithBurst().ScheduleParallel();
 
		_endSimulationEcbSystem.AddJobHandleForProducer( Dependency );
 	}
	public static float2 GetTileCenter ( int2 tilePosition , float tileSize ) => (float2)tilePosition + tileSize*0.5f;
 	public static int2 GetChunkFromWorld ( float2 worldPosition , int chunkSize ) => (int2) math.floor( worldPosition/chunkSize );
	public static float2 GetWorldFromChunk ( int2 chunkPosition , int chunkSize ) => (float2)chunkPosition * (float)chunkSize;
 }
 
 public struct Cluster : IComponentData
 {
 	public int2 Value;
 	public static implicit operator int2 ( Cluster obj ) => obj.Value;
 }
 
 [System.Obsolete("I am a substitute for absent component")]
 public struct Unloadable : IComponentData {}
 [System.Obsolete("I am a substitute for absent component")]
 public struct Loader : IComponentData
 {
 	public int LoadDistance;
 }

You’re welcome :slight_smile: