Why is my job staying on the Main thread when using ScheduleParallel

Hello, dear DOTS community.

I’m exploring DOTS for a while and came across an issue I need help to understand.
The following system detects, for each player entity, enemy entites closer than 5 Units. A dynamic buffer this then filled with the reachable targets.

The system is working fine, but the job stays on the Main Thread, even when using ScheduleParallel.
I would explain that by the small number of players (6), but no matter how much player I add, the only impact is my frame rate tanking. Must this kind of system stay on the main thread?

I would be more than happy to have your feedback on this subject.
Thanks a lot for your help.

using System.Collections;
using System.Collections.Generic;
using Components;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

public partial class DetectMultipleNearestEnemySystem_JobChunk_Schedule : SystemBase
{
    private EntityQueryDesc m_queryDescription;
    private EntityQuery m_entityQuery;
   
    protected override void OnStartRunning()
    {
        m_queryDescription = new EntityQueryDesc
        {
            None = new ComponentType[] { typeof(AlreadyTargeted), typeof(SingleTargetComponent) },
            All = new ComponentType[]{ typeof(EnemyTagComponent) },
        };
       
        m_entityQuery = GetEntityQuery(m_queryDescription);
    }
    protected override void OnUpdate()
    {
        var translationTypeHandle = World.DefaultGameObjectInjectionWorld.EntityManager.GetComponentTypeHandle<Translation>(true);
        var archetypeChunkArray = m_entityQuery.CreateArchetypeChunkArray(Allocator.TempJob);
        var entityTypeHandle = GetEntityTypeHandle();

        Entities.
            WithBurst().
            WithAll<PlayerTagComponent>().
            WithReadOnly(translationTypeHandle).
            WithReadOnly(entityTypeHandle).
            WithReadOnly(archetypeChunkArray).
            ForEach(
                (ref DynamicBuffer<TargetItem> _targets, in Translation _localToWorld) =>
                {
                    _targets.Clear();
                   
                    for (var i = 0; i < archetypeChunkArray.Length; i++)
                    {
                        var chunk = archetypeChunkArray[i];
                        var nativeArrayOfEntities = chunk.GetNativeArray(entityTypeHandle);
                        var nativeArrayOfTranslations = chunk.GetNativeArray(translationTypeHandle);

                        for (var j = 0; j < chunk.Count; j++)
                        {
                            var distance = math.distancesq(_localToWorld.Value, nativeArrayOfTranslations[j].Value);
                            if (distance > 25) continue;
                            {
                                _targets.Add(new TargetItem
                                {
                                    m_entity = nativeArrayOfEntities[j]
                                });
                            }
                        }
                    }
                }).
            WithDisposeOnCompletion(translationTypeHandle).
            WithDisposeOnCompletion(entityTypeHandle).
            WithDisposeOnCompletion(archetypeChunkArray).
            ScheduleParallel();
    }
}

Can you show profiler

1 Like

Of course. Here it is, attached to this reply.

Based on the profiler, the main thread is being treated as a temporary worker thread to help out because the EntityCommandBufferSystem wants that job done ASAP.

What your picture does not show is what the other worker threads are doing.

2 Likes

Hi.
Here is a more detailed snapshot of the profiler.
In which conditions would the EntityCommandBufferSystem want to complete a job asap?
I have another system instantiating projectiles based on the dynamicbuffer content. Thanks for the time you spend helping me, guys.

using System.Net;
using Components;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

namespace Systems
{
    public partial class FireMultipleProjectilesTowardTargetsSystem_Schedule : SystemBase
    {
        private EndSimulationEntityCommandBufferSystem m_ecbs;

        protected override void OnCreate()
        {
            m_ecbs = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
        }

        protected override void OnUpdate()
        {
            var ecb = m_ecbs.CreateCommandBuffer().AsParallelWriter();
            var deltaTime = Time.DeltaTime;
           
            var localToWorldFromEntity = GetComponentDataFromEntity<LocalToWorld>(true);
            var alreadyTargetedList = GetComponentDataFromEntity<AlreadyTargeted>(true);

            Entities
                .WithReadOnly(localToWorldFromEntity)
                .WithReadOnly(alreadyTargetedList)
                .WithAll<PlayerTagComponent, MultiTargetTagComponent>()
                .ForEach((int entityInQueryIndex,ref FireWeaponComponent _weaponData, ref DynamicBuffer<TargetItem> _targets, in Translation _translation) =>
            {
                if (_weaponData.m_currentRate > 0)
                {
                    _weaponData.m_currentRate -= deltaTime;
                    return;
                }

                _weaponData.m_currentRate = _weaponData.m_fireRate;
               
                for (var i = 0; i < _targets.Length; i++)
                {
                    if (alreadyTargetedList.HasComponent(_targets[i].m_entity)) continue;
                   
                    var projectile  = ecb.Instantiate(entityInQueryIndex, _weaponData.m_projectileEntityPrefab);
                    ecb.SetComponent(entityInQueryIndex,projectile, new Translation
                    {
                        Value = _translation.Value+new float3(0,.5f,0)
                    });
                    ecb.AddComponent(entityInQueryIndex,projectile, new TargetComponent
                    {
                        m_entity = _targets[i].m_entity
                    });
                    var targetPosition = localToWorldFromEntity[_targets[i].m_entity];
                    var direction = math.normalize(targetPosition.Position - _translation.Value);
                   
                    ecb.AddComponent(entityInQueryIndex, projectile, new MoveDirectionComponent
                    {
                        m_value = direction
                    });
                   
                    ecb.AddComponent(entityInQueryIndex, _targets[i].m_entity, new AlreadyTargeted()
                    {
                    });
                   
                }
            }).
                WithDisposeOnCompletion(localToWorldFromEntity).
                WithDisposeOnCompletion(alreadyTargetedList).
                ScheduleParallel();
           
            m_ecbs.AddJobHandleForProducer(Dependency);
        }
    }

    public struct TargetComponent: IComponentData
    {
        public Entity m_entity;
    }
}

m_ecbs.AddJobHandleForProducer(Dependency);

This here makes the ECBS wait on the job to complete.

Ok, understood. Thanks a lot.
I need this entityCommandBuffer to instantiate my projectiles but it’s irrelevant to wait each frame for the completion of the detection job if I only create projectiles each .5f seconds (fire rate of the player).

How could I then condition the execution of “m_ecbs.AddJobHandleForProducer(Dependency)” for when it’s needed? This line is outside the Entities.ForEach loop, so it will execute each frame, no matter what happens in the loop. I would instead need a way to stop the execution of the System if not needed.
I recall a system would stop running if no entity is found by the EntityQuery hidden in the Entities.ForEach(). Setting a special “AllowsFireTag” on player ready to shoot and checking for that tag in the FireSystem may work.

But setting tags require another EntityCommandBuffer. Hum… I’m puzzled.