Search Unity

Best way to query entities from the main thread

Discussion in 'Entity Component System' started by sebas77, Apr 23, 2019.

  1. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,644
    I understand that the design is pushing the user to use the JobSystems as much as possible and I realise why, but for my purposes I need currently to run the iteration in the mainthread. If I use the ComponentSystem Update, it will complain saying that the same entities are being read/write inside a job. Knowing how multithreading works, I understand this too. Therefore my current solution is to start and complete a job from inside the mainthread which works (which means that luckily no mainthread synchronization points are involved). however in order to work, I will need to setup the job with the proper dependencies, which I can take only from the JobSystem OnUpdate function. Currently my work around is:


    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
    _deps = inputDeps;

    return inputDeps;
    }

    and from the mainthread do:


    var job = new SyncJob(array, chunkComponentType0, archetypeChunkArray);
    var inputDeps = job.Schedule(_deps);
    inputDeps.Complete();

    which works but looks hacky. Is there a better way to do this or this is fair enough?
     
  2. FrankvHoof

    FrankvHoof

    Joined:
    Nov 3, 2014
    Posts:
    258
  3. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,644
    OK I'll try with ForEach again. I abandoned it early on for performance reasons, but honestly may not have been so important.
     
  4. FrankvHoof

    FrankvHoof

    Joined:
    Nov 3, 2014
    Posts:
    258
    If you're calling JobHandle.Complete(), you're waiting for the Job to complete anyways (blocking wait). The only performance gain you may see with the use of a Job over ForEach is the ability to do multiple tasks in parallel (or some gains with Burst).
    Not sure if Burst is ever coming to ForEach though.
     
  5. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,644
    Burst is surely a good point, do you think my job method is viable anyway?
     
  6. FrankvHoof

    FrankvHoof

    Joined:
    Nov 3, 2014
    Posts:
    258
    Depends on the parallelisation of your job, as there's overhead in scheduling it.
    If you have a small batch, with a lot of work per object, go main-thread.
    If you have a large batch, with a small amount of work per object, multi-threading could still provide performance-increases (as you're splitting it over your worker threads).
     
    sebas77 likes this.
  7. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,644
    yes of course, I was thinking more about the Burst case in case I was going to handle hundreds or thousands of entities, although of course parallelism would help in this case too.
     
  8. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    Unless you need the result of the iteration on the main thread within the same system, this should not be the case. I may be wrong, but I am guessing you need to keep some sort of persistent tally in your iteration across all entities. In that case, I believe ScheduleSingle is what you want.
     
  9. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,644
    it may be true that jobs never operate inside the ComponentSystem OnUpdate time frame, but my main thread can happen at any time. To be honest I think it was coinciding with the normal monobehaviour Update() but I am not 100% sure it coincides with the ComponentSystem OnUpdate callback time frame.

    Edit: correction, I call it in the monobehaviour late update time, so this is very likely the problem, although I want the freedom to do so even if I realise it doesn't make much sense nowadays.
     
    Last edited: Apr 24, 2019
  10. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    That doesn't make a lot of sense. Two things can't be happening on the main thread concurrently. And the only way ECS systems would interleave with MonoBehaviour updates is if MonoBehaviours are calling Update on the ECS systems or vice versa. Attempting to read component data on the main thread after scheduling a job should trigger the automatic dependency management system to complete the job for you. So if that's not the case, that means you aren't giving ECS all the job handles for jobs you schedule.
     
  11. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,644
    Talking about main thread is always awkward but a main thread frame is a long lasting execution and the unity engineers may want to exploit this time to execute in parallel job. I didn't confirm my hypothesis in any way, but as far as I got, the job system is synchronised with the main thread in such a way the main thread can execute safely on entities, but only during the componentsystem on update execution time. My hypothesis is that during The rest of the main thread jobs could executed, thus it's not safe to iterate in entities on the mainhtread outside the on update component system time. Does this make sense to you? And most of all is it correct?
     
  12. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    I think I understand your confusion now. And no your hypothesis is not correct. Jobs can be running during a system's OnUpdate. Otherwise that would be potential performance loss and ECS is smarter about jobs than that.

    So a World and everything in it share an automatic dependency management mechanism. This mechanism keeps track of types (IComponentData and IBufferElementData) used by everything living in the world and all the jobs they schedule and whenever it gets surprised by a new type request, it will automatically complete all necessary jobs using that type on the spot. In the case of JobComponentSystems OnUpdate, it will look at what you have requested in the past and pass you a JobHandle of all jobs using those components and buffer types. Those jobs are probably still running, but you can safely append to their chain to keep your worker threads busy longer. In a ComponentSystem it will look at what you have requested in the past and complete all jobs using those components for you before OnUpdate gets called (there's several internal methods that run between Update and OnUpdate). This really only applies to EntityQueries which is what Entities.ForEach and IJobForEach are built on. EntityManager has its own rules.

    IJobForEach depends on the automatic dependency mechanism to schedule properly. So while it is possible to schedule such a job outside of a ComponentSystem or JobComponentSystem, the rules are complicated and I cannot recommend it without first knowing the detailed specifics and motivations for your use case.

    Instead, I can give you a few pointers. First, OnUpdate always gets called from the main thread. That means you can access MonoBehaviours from there just fine. Second, if you want to launch jobs during a MonoBehaviour's Update for some reason, you can make the MonoBehaviour create a system and manually call Update on it. Just make sure you use World.CreateSystem rather than World.GetOrCreateSystem, in your Start magic method (so that each MonoBehaviour instance "has" its own version of the system.

    I won't discuss other ways to iterate entities on the main thread since they won't have Burst and you already mentioned you were having performance problems with Entities.ForEach. But it is worth noting that the EntityManager is also part of the automatic dependency management mechanism and all interactions with it should be handled correctly regardless of where on the main thread it is accessed from.

    Does this clear things up? Does it provide you an alternate approach to solve your problem?
     
  13. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,644
    Thanks a lot! So if I understood you correctly, the synchronization happens at component level and only when needed. In this case I actually though about that too (because it was making sense, not because I verified it), but I forgot about it when I wrote my explanation.

    This means that using the main thread will have always an impact, which is fine at the moment for me, as I use the main thread only for synchronization (copying data around)

    What I didn't understand properly by your explanation is if this main thread synchronization can happen at any point, which should be the case if I can call the ComponentSystem OnUpdate method whenever I want. In this case I am not sure how Unity can work out when to complete the jobs operating on the components I am going to use though, but I guess the EntityManager knows everything about it!
     
  14. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    Yes, the main thread can be triggered to sync whenever you want to call Update on a system, which can be pretty much any point during the main thread's frame cycle. Any interaction with a system or the EntityManager will likely involve the automatic dependency management mechanism behind the scenes, so it always stays up to date on the main thread and is always ready to complete the right jobs when you request access to a component.
     
  15. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,644
    Hey @DreamingImLatios

    I am moving my code to use IJobChunk with filtered group. it's promising, but I didn't get one thing. The JobComponentSystem OnUpdate is called on the mainthread right? If so, when is it actually called? Once the previously scheduled job from this system is completed or every mainthread frame?
     
  16. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    It’s calling every frame.
     
  17. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,644
    Thanks @eizenhorn then I don't understand, how can it be sure the the previous scheduled job are ended before to schedule new ones? Or they are simply queued?
     
  18. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    JobComponentSystem handles that for you. This is from ComponentSystem.cs
    Code (CSharp):
    1. public abstract class JobComponentSystem : ComponentSystemBase
    2.     {
    3.         JobHandle m_PreviousFrameDependency;
    4.  
    5.         unsafe JobHandle BeforeOnUpdate()
    6.         {
    7.             BeforeUpdateVersioning();
    8.  
    9.             // We need to wait on all previous frame dependencies, otherwise it is possible that we create infinitely long dependency chains
    10.             // without anyone ever waiting on it
    11.             m_PreviousFrameDependency.Complete();
    12.  
    13.             return GetDependency();
    14.         }
     
    sebas77 likes this.
  19. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,644
    Wow this stuff is so unintuitive. So this means that the main thread frame rate is affected by the job time execution.
     
  20. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    >Wow this stuff is so unintuitive. So this means that the main thread frame rate is affected by the job time execution.
    It waits on the previous frames job. That's generally not a thing that will happen, because usually at the end of the frame rendering has some sync points that require most things to complete.


    What specifically is unintuitive. It's pretty simple, main thread / player loop runs on main thread. Systems are scheduled in specific order on main thread using various [updatebefore, updategroup etc].

    They schedule jobs. Jobs are designed with deterministic execution & no race condition guarantee in mind.
    Essentially the code should gurantee same results if it runs on main thread as it does when running multithreaded.
    If main thread needs to access data it waits on the jobs that produce that specific data, so that other jobs can continue to run.

    Structural changes, change data layout so doing precise change tracking is quite difficult. We haven't done that. So we basically wait on all job at that point. For this reason using ECB is useful so you can put them all in just a couple EntityCommandBufferSystem, meaning you only have hard sync points on those specific systems.

    The main point of the system is to make it so you can write modular code and that system order won't break job dependencies / race conditions. It should always work as if the code was running on the main thread. Dependency handling is thus done for the user based on read & write access against component types.

    The only thing that is truly separated from each other are worlds. Thus can be used to pre-populate data completely asynchronously.


    Looking at system debugger shows you the order of systems. Looking at timeline profiler shows you both jobs & system ordering.
     
    MNNoxMortem likes this.
  21. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,644
    Thanks @Joachim_Ante for the complete explanation . If I think in terms of standard multi threading with parallel execution where the concept of mainthread is meaningless, then the approach is counter intuitive. However the determinism argument makes a lot of sense and it's something I actually pondered on as well, so once I understand the mechanics it becomes clear. I haven't used the ECB yet, I'll look in to it too!
     
    Last edited: Apr 30, 2019
  22. cwennchen

    cwennchen

    Joined:
    Nov 22, 2019
    Posts:
    7
    Is calling JobHandle.Schedule from MonoBehaviour.Update the same as executing from JobSystem.OnUpdate?
     
  23. Guedez

    Guedez

    Joined:
    Jun 1, 2012
    Posts:
    827
    I use this, no idea if it's performant or not
    Code (CSharp):
    1.                 EntityQuery query = i.DumbGetComponentGroup(typeof(T));
    2.                 NativeArray<Entity> ents = query.ToEntityArray(Allocator.TempJob);
    3.                 foreach (Entity ent in ents) {
    4.                     db.Insert(new SharedDataToEntity<T>(lastId, ent));
    5.                 }
    6.                 ents.Dispose();
    DumbGetComponentGroup
    is some method I made to get easy access to the
    ComponentSystem
    's
    GetEntityQuery
    method