Search Unity

Question Shared components and burstified playbacks

Discussion in 'Entity Component System' started by sebas77, Jan 14, 2023.

  1. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    One thing I have been looking forward to from version 1.0 was to use SharedComponents with EntityCommandBuffers and enjoy a burstified playback.

    While now I can use 100% ECB and forget the entity manager, the playback doesn't seem to be burstified

    upload_2023-1-14_23-28-30.png

    what's going on?
     
  2. HyperionSniper

    HyperionSniper

    Joined:
    Jun 18, 2017
    Posts:
    30
    Instance methods aren't burstified on their own. For it to be bursted, it has to be within a BurstCompiled function like an ISystem OnUpdate.
     
  3. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    I'll try thanks

    edit: got it

    edit2: ok not really:

    System.InvalidOperationException: Jobs accessing the entity manager must issue a complete sync point
    This Exception was thrown from a job compiled with Burst, which has limited exception support.

    Code (CSharp):
    1. new PlaybackJob()
    2. {
    3.      _entityManager = EntityManager,
    4.      _entityCommandBuffer = _entityCommandBuffer
    5. }.Run();
    6.  
    7.  
    8. [BurstCompile]
    9. struct PlaybackJob:IJob
    10. {
    11.     public EntityCommandBuffer _entityCommandBuffer;
    12.     public EntityManager _entityManager;
    13.  
    14.     public void Execute()
    15.     {
    16.         _entityCommandBuffer.Playback(_entityManager);
    17.     }
    18. }
    edit3: I had to do a
    EntityManager.CompleteAllTrackedJobs(); to make it work. That's fine by me

    edit4: it worked but the exact same job running elsewhere gives a different error:

    Code (csharp):
    1.  
    2. InvalidOperationException: The native container may not be deallocated on a job.
    3. Use [DeallocateOnJobCompletionAttribute] to deallocate a native container safely on job completion.
    4. Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckDeallocateAndThrow (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) (at <9157942190d54845a13242589f74e64d>:0)
    5. Unity.Entities.ComponentSafetyHandles.CompleteAllJobsAndInvalidateArrays () (at ./Library/PackageCache/com.unity.entities@1.0.0-pre.15/Unity.Entities/ComponentSafetyHandles.cs:211)
    6.  
    seems a bit random :( what native container is talking about? the entity command buffer?
     
    Last edited: Jan 15, 2023
  4. HyperionSniper

    HyperionSniper

    Joined:
    Jun 18, 2017
    Posts:
    30
    You can only playback on the main thread.

    You probably ran the job on the main thread the first time, and it didn't work the other time because you're running it using Schedule, which runs on another thread, which isn't allowed.

    The deallocation error probably is the ECB manually disposing its internal buffer inside ECB.Playback, and you shouldn't be deallocating stuff manually inside jobs. To be clear, you shouldn't use Playback inside jobs.

    What you should be doing is something like this:

    Code (CSharp):
    1.  
    2. // ISystems can be bursted, but not SystemBase
    3. [BurstCompile]
    4. public partial struct MySystem : ISystem {
    5.  
    6.     // Everything inside this function will be bursted.
    7.     [BurstCompile]
    8.     public void OnUpdate(ref SystemState state) {
    9.         var jobHandle = new MyJob(){ ... }.Schedule(...);
    10.         ...
    11.         // The job has to finish writing everything to the ECB before Playback.
    12.         jobHandle.Complete();
    13.         // Playback has to be done on the main thread.
    14.         frameECB.Playback(state.EntityManager);
    15.     }
    16.  
    17.     // This job is BurstCompiled.
    18.     [BurstCompile]
    19.     public struct MyJob : IJob {
    20.         // If you want to use a parallel job, this should be EntityCommandBuffer.ParallelWriter.
    21.         EntityCommandBuffer FrameECB;
    22.  
    23.         public void Execute(){
    24.             // do bursted stuff with your ECB in the job
    25.         }
    26.     }
    27. }
     
    Last edited: Jan 16, 2023
  5. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    I thought was implicit that I did a complete. That's not the issue. I'll paste the whole code when I'll have time.
     
  6. elliotc-unity

    elliotc-unity

    Unity Technologies

    Joined:
    Nov 5, 2015
    Posts:
    230
    In general, playing back an ecb implies doing structural changes, which you can only do from one thread at a time. So, rather than doing it from a job, I'd just do it from the main thread, for example from a bursted ISystem. (This is assuming none of the existing ECB systems will do for playing back your ecb.)

    That said, if burst is on, the "inside unmanaged bits" of ECB playback (e.g. applying the changes to the unmanaged shared components and moving entities between chunks as a result) should be bursted whether or not you bursted the call to Playback() itself. I would be curious to see a breakdown of those 2.73ms, and whether for example there were just a lot of entities that got moved or something.

    Edit: If you really want to do structural changes from not the main thread, we do have a thing called an ExclusiveEntityTransaction for that purpose, or EET for short. We use this for streaming scenes in on worker threads. It basically designates some other thread as the only thread that gets to do structural changes for a particular world, which works well for streaming since we have separate streaming worlds.
     
  7. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    thanks a lot! I cannot use ISystem at the moment, so I use a job yes, but with Run or Complete. I don't care about doing it on other threads. I am more curious about the fact that you were expecting it to be burstfied regardless. More info will come when I am back to it.
     
    elliotc-unity likes this.
  8. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    Ok I am back to it and I am still stuck @elliotc-unity . This is the deal:

    The system is a SystemBase and must stay a systembase, at least until I understand how I can add an ISystem implementation manually into a world (I do not use automatic bootstrap)

    I then run a code similar to:

    Code (CSharp):
    1. void ConvertPendingEntities(JobHandle combinedHandle)
    2.         {
    3.             var cmd                 = _entityCommandBuffer.AsParallelWriter();
    4.  
    5.             _cachedList.Clear();
    6.  
    7. #if UNITY_ECS_100            
    8.             EntityManager.GetAllUniqueSharedComponentsManaged(_cachedList);
    9. #else          
    10.             EntityManager.GetAllUniqueSharedComponentData(_cachedList);
    11. #endif
    12.  
    13.             Dependency = JobHandle.CombineDependencies(Dependency, combinedHandle);
    14.  
    15.             //note if this is slow, the whole loop should be burstified instead (and move away from ForEach)
    16.             for (int i = 0; i < _cachedList.Count; i++)
    17.             {
    18.                 var dotsEntityToSetup = _cachedList[i];
    19.                 if (dotsEntityToSetup.@group == ExclusiveGroupStruct.Invalid) continue;
    20.  
    21.                 var mapper = entitiesDB.QueryNativeMappedEntities<DOTSEntityComponent>(dotsEntityToSetup.@group);
    22.  
    23.                 //Note: for some reason GetAllUniqueSharedComponentData returns DOTSEntityToSetup with valid values
    24.                 //that are not used anymore by any entity. Something to keep an eye on if fixed on future versions
    25.                 //of DOTS (this was with DOTS 0.17)
    26.                 Entities.ForEach((Entity entity, int entityInQueryIndex, in DOTSSveltoEGID egid) =>
    27.                 {
    28.                     mapper.Entity(egid.egid.entityID).dotsEntity = entity;
    29.                     cmd.RemoveComponent<DOTSEntityToSetup>(entityInQueryIndex, entity);
    30.                 }).WithSharedComponentFilter(dotsEntityToSetup).ScheduleParallel();
    31.             }
    32.          
    33.             Dependency.Complete();
    34.          
    35.             new PlaybackJob()
    36.             {
    37.                     entityManager = EntityManager,
    38.                     entityCommandBuffer = _entityCommandBuffer
    39.             }.Schedule(Dependency).Complete();
    40.       //      _entityCommandBuffer.Playback(EntityManager);
    41.             _entityCommandBuffer.Dispose();
    42.         }
    Note that the PlaybackJob, as previously shown, just executes the commandBuffer.Playback.
    if I run the Playback without job, everything runs fine, however the playback will NOT be burstified.
    if I run the job, regardless if I use Complete() or Run(), I will get :

    Code (CSharp):
    1. InvalidOperationException: The native container may not be deallocated on a job.
    2. Use [DeallocateOnJobCompletionAttribute] to deallocate a native container safely on job completion.
    3. Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.CheckDeallocateAndThrow (Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle handle) (at <9157942190d54845a13242589f74e64d>:0)
    4. Unity.Entities.ComponentSafetyHandles.CompleteAllJobsAndInvalidateArrays () (at ./Library/PackageCache/com.unity.entities@1.0.0-pre.15/Unity.Entities/ComponentSafetyHandles.cs:211)
    5. Unity.Entities.ComponentDependencyManager.CompleteAllJobsAndInvalidateArrays () (at ./Library/PackageCache/com.unity.entities@1.0.0-pre.15/Unity.Entities/ComponentDependencyManager.cs:161)
    6. Unity.Entities.EntityDataAccess.BeforeStructuralChange () (at ./Library/PackageCache/com.unity.entities@1.0.0-pre.15/Unity.Entities/EntityDataAccess.cs:424)
    7. Unity.Entities.EntityDataAccess.BeginStructuralChanges () (at ./Library/PackageCache/com.unity.entities@1.0.0-pre.15/Unity.Entities/EntityDataAccess.cs:430)
    8. Unity.Entities.EntityCommandBuffer+PlaybackProcessor.Init (Unity.Entities.EntityDataAccess* entityDataAccess, Unity.Entities.EntityCommandBufferData* data, Unity.Entities.SystemHandle& originSystemHandle) (at ./Library/PackageCache/com.unity.entities@1.0.0-pre.15/Unity.Entities/EntityCommandBuffer.cs:3708)
    9. Unity.Entities.EntityCommandBuffer.PlaybackInternal (Unity.Entities.EntityDataAccess* mgr) (at ./Library/PackageCache/com.unity.entities@1.0.0-pre.15/Unity.Entities/EntityCommandBuffer.cs:3279)
    10. Unity.Entities.EntityCommandBuffer.Playback (Unity.Entities.EntityManager mgr) (at ./Library/PackageCache/com.unity.entities@1.0.0-pre.15/Unity.Entities/EntityCommandBuffer.cs:3221)
    11. Svelto.ECS.SveltoOnDOTS.PlaybackJob.Execute () (at ./Packages/com.sebaslab.svelto.ecs/Extensions/Unity/DOTS/UECS/SveltoOnDOTSEntitiesSubmissionGroup.cs:23)
    12. Unity.Jobs.IJobExtensions+JobStruct`1[T].Execute (T& data, System.IntPtr additionalPtr, System.IntPtr bufferRangePatchData, Unity.Jobs.LowLevel.Unsafe.JobRanges& ranges, System.Int32 jobIndex) (at <9157942190d54845a13242589f74e64d>:0)
    13.  
    For the sake of completeness, the ECB is allocated every frame with a TempJob allocator,

    BTW if there is an alternative better solution to GetAllUniqueSharedComponentData, without changing the semantic of the ForEach that must stay in this way, please let me know.
     
  9. HyperionSniper

    HyperionSniper

    Joined:
    Jun 18, 2017
    Posts:
    30
    See Method CreateSystem | Entities | 1.0.0-pre.15 (unity3d.com).
    This method is specifically for creating ISystems.

    See my previous post, which you ignored all but one line of ;) :
    Called it (kind of).

    The error in your job is in fact caused by something called a PlaybackProcessor inside ECB.Playback, which you can see in the above stacktrace. It attempts to 'CompleteAllJobsAndInvalidateArrays' before preparing to playback your changes, which involves disposing stuff. It seems like you cannot use Playback inside a job due to this.

    I personally wouldn't get hung up over not being able to burst this, but that being said, a hack you could use to burst it would be to do this (I'm not completely sure if this is safe or not but it should be, and it compiles):
    The better option is of course to use ISystem.
    Code (CSharp):
    1.     [BurstCompile]
    2.     public static class Test {
    3.         [BurstCompile]
    4.         public static unsafe void Playback(EntityCommandBuffer* buf, EntityManager* mgr) {
    5.             buf->Playback(*mgr);
    6.         }
    7.     }
     
    sebas77 likes this.
  10. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    @HyperionSniper Thanks a lot! It's getting somewhere.

    Ok regarding ISystem, I am not sure why Rider was not suggesting that extension, however I can see it still wouldn't work for me, as I need to inject dependencies. I guess ISystem must be unmanaged struct to work?

    Regarding playback I am not sure why you think burstification is not helpful and I am not sure how previously 1.0 this problem was solved. I guess the burstified functor was the solution all along?
     
    Last edited: Jan 23, 2023
  11. elliotc-unity

    elliotc-unity

    Unity Technologies

    Joined:
    Nov 5, 2015
    Posts:
    230
    Yes, ISystem's have to be unmanaged structs.

    I continue to be unconvinced that any slowness is actually due to not bursting the call to Playback(), because as I said, the inside unmanaged bits of ECB playback are bursted irrespective. I continue to be interested in a profile of this code to see where it is actually spending time.

    I'm also curious how many entities are involved here and how many components are getting removed; my suspicion is that there are just a lot of structural changes involved here, and that just takes time to move data between chunks, even though it is already bursted. Depending on your use case, you might be able to avoid those structural changes by using IEnableableComponent, or something.
     
  12. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    I will give you more insight later, but I have noticed that a built client is tenfold faster than an editor, so it may get irrelevant. I am surprised that the playback method itself is not burstified. Then again I am not sure if you are saying that it will be or won't be burstified regardless of the calling method.
     
  13. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    Internals of Playback are burst compiled when it's possible. ECB Playback uses EcbWalker which is responsible for gathering and processing ECB chains, and inside it checks can it burst specific chain or not and depends on that it runs from bursted code or not (because not everything in ECB can be burst compiled, for example adding managed components)
    upload_2023-1-24_14-35-36.png
     
  14. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    thanks this is useful. In the past the only reason why it couldn't be burstified was the shared components, that's why I was looking forward for 1.0. It's all unmanaged.
     
  15. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    ok elliotc-unity, let's analyse this

    upload_2023-1-29_12-24-14.png

    first of all, I am gonna verify if the CanBurstPlayback is true. Because we said that it is not important to burstify the playback call, I am just doing:

    _entityCommandBuffer.Playback(EntityManager);
    _entityCommandBuffer.Dispose();

    according to what I see, it is burstified

    upload_2023-1-29_12-30-20.png

    still I am not sure why the profiler doesn't show it as a glowing green as other burstified methods are normally shown

    Hundreds of entities are changed during a submission (ofc structural changes, otherwise it would be too easy) and these are operations performed:

    Destroy:

    Code (CSharp):
    1. ECB.DestroyEntity(entityComponent.dotsEntity);
    Set Shared Component (change of state):

    Code (CSharp):
    1. ECB.SetSharedComponent(entityComponent.dotsEntity, new DOTSSveltoGroupID(toGroup));
    Creation (this is the most complex part):

    Code (CSharp):
    1. _ECB.AddSharedComponent(dotsEntity, new DOTSSveltoGroupID(egid.groupID));
    2. _ECB.AddComponent(dotsEntity, new DOTSSveltoEGID(egid));
    3. if (mustHandleDOTSComponent)
    4.     _ECB.AddSharedComponent(dotsEntity, new DOTSEntityToSetup(egid.groupID));
    then after the creation happens (first playback happens):

    Code (CSharp):
    1. Entities.ForEach(
    2.     (Entity entity, int entityInQueryIndex, in DOTSSveltoEGID egid) =>
    3.     {
    4.         ecb.RemoveComponent<DOTSEntityToSetup>(entityInQueryIndex, entity);
    5.     }).WithSharedComponentFilter(dotsEntityToSetup).ScheduleParallel();
    (second playback happens)

    according to what I see in the profiler, the screenshot I have attached is the first playback

    the execution overall is very fast, but the spikes are due to the first playback

    upload_2023-1-29_12-43-3.png

    upload_2023-1-29_12-43-14.png

    I can share the project, is one of my simple Svelto demo I use to show Svelto.ECS
     
    Last edited: Jan 29, 2023
  16. eizenhorn

    eizenhorn

    Joined:
    Oct 17, 2016
    Posts:
    2,685
    Is this from editor? If yes - that’s expected. Because burst code JIT compiled in editor and you need to “prewarm” burst calls, otherwise first call (and depends on compilation speed, execution frequency etc. couple of next calls) will be not burst compiled. If you wan measure real code execution speed - profile build where burst code AOT compiled, or profile in editor with prewarm and performance testing extension which gather not only “one moment” data but batch with medians etc.
     
    HyperionSniper likes this.
  17. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    No it's a built client
     
  18. elliotc-unity

    elliotc-unity

    Unity Technologies

    Joined:
    Nov 5, 2015
    Posts:
    230
    I'll look at the project if you send it, but this is consistent with the idea that structural changes are not fast, and when you add a shared component for the first time, it creates a new archetype and new chunks, and if you do that to hundreds of entities, it does that a lot of times, and that's not fast at all.

    What problem are you trying to solve overall with this mechanism? Is there a way to rejigger it so that it doesn't require structural changes to work at all?
     
  19. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    Structural changes are fundamental to define sets, especially with the DOTS ECS architecture. My suggestion is to spend time optimising this part. How else would you define sets of entities found in specific states? Moreover in this project most of the time is about adding and removing entities (I am highlighting this because adding and removing is even more fundamental that change state of the entity)

    In any case I am still perplexed by the fact that the profiler doesn't highlight the playback as burstified method. It cannot be found in the burst inspector either. Imo something is still wrong.
     
    Last edited: Jan 31, 2023
  20. elliotc-unity

    elliotc-unity

    Unity Technologies

    Joined:
    Nov 5, 2015
    Posts:
    230
    Defining sets of entities in particular states is actually fine to define with components; the problem is when they switch archetypes frequently. The two main ways I would attack the problem are a) a non-shared state component with an enum that I write to and then manually check when deciding how to process a given entity's data, or b) a collection of enableable tag components that I enable and disable to change state. Both of these are very fast, especially when done from a bursted ISystem, MUCH faster than structural changes. I agree it seems like unnecessary work, but if state changes are frequent, I think it's genuinely pretty optimal.

    Unfortunately, structural changes not being fast is fundamental to our architecture. Internally, the main proposal to address this is to provide a mode where they're prohibited. Nobody has any ideas for how they would end up fast without throwing the whole thing out and starting over from scratch with a different scheme and different tradeoffs.

    You didn't find the playback function in the burst inspector because it has a funny name. Check the Show Unity Namespace box and then search for "ProcessChainChunk", click the only hit and then in the bottom field in the right pane search for "PlaybackProcessor>.ProcessChain" (not a typo).

    The profiler probably doesn't highlight the burstedness because it's called via a burst function pointer, and the profiler doesn't really know about those. On top of that, if it showed it as purely bursted that would be misleading, because it does actually call back into managed land for any managed bits of the playback. It's plausible to me that we could get the profiler to be better about bursted function pointers, and that if we also changed the names of both the bursted main bit and the unbursted inside bit to be less funny, that would be an improvement. I'll bug the profiler people.
     
  21. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
  22. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    @elliotc-unity

    a) is the first an if to check the state? If so cannot work in my case
    b) is interesting, how do they work internally? Unfortunately cannot be a solution in my case either, because I am exploiting the shared component feature to have sets defined by specific values. A type based component wouldn't work for me.

    also I still don't see the burstified method. Until I don't see the burstify method, I won't believe is so slow.

    upload_2023-1-31_16-47-54.png
     
    Last edited: Jan 31, 2023
  23. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
  24. elliotc-unity

    elliotc-unity

    Unity Technologies

    Joined:
    Nov 5, 2015
    Posts:
    230
    @sebas77 did you check the Show Unity Namespace box?
     
  25. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    all right, that was it, it's there :(
     
  26. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    I wrote custom command buffers that do specific operations faster by taking advantage of memory cache streaming and only performing one type of operation each.
    Yes it is. I still use this tech today. There might be some differences today with some of Unity's batching methods. But compared to ECB, it is still a big difference.
     
    sebas77 likes this.
  27. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643

    so this shows that the current system can actually be already optimised, right?
     
  28. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    EntityCommandBuffer? Probably not.
    Your code? Probably.
     
  29. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    All right I probably took the "that do specific operations faster " too broadly. I guess your optimisation won't apply to my case then.
     
  30. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    If you are dead-set on using ECB and nothing else, then no. What I have won't help you. But if you want to get the most performance out of Unity's ECS, you will need to be a little less naïve with your conclusions. This stuff takes time to learn, even if you are already experienced with other ECS implementations.
     
  31. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    I cannot agree that the solution is to avoid structural changes (at least not as simple as what I am doing). Structural changes are fundamental for any archetype-based ECS implementation to model states. Suggesting to not use structural changes highlights a flawed implementation and as such, I am not sure why, as a user, I should invest time to find workarounds (even so complicated as yours)
    Now I can agree that I should use tag components to move entities between sets, I can accept that even if it's not suitable for my use case.
    Besides, I am not sure how ECB is related to all of this, I thought ECB was the most performant way to change entities.

    Note: if structural changes are so heavy, DOTS ECS should provide an alternative way to subset existing sets, like for example using sparse set within the entity sets (and using double indexing).
     
    Last edited: Feb 1, 2023
  32. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,271
    I didn't say that.
    ECB is not the most performant way to change entities in most situations. It is convenient and often good enough for small amounts of changes. But the fastest structural change APIs come from the EntityManager batch-change methods. My optimization was to create specialized command buffers which are significantly more limited in what they can each do, but in turn hit these faster structural change codepaths.

    In Unity Learn, there's a DOTS tutorial and in there is a guide sheet with the performance comparisons of different structural change operations.
     
    sebas77 likes this.
  33. sebas77

    sebas77

    Joined:
    Nov 4, 2011
    Posts:
    1,643
    I see, I have to reconsider my approach with ECB then!

    about the structural changes, I was referring to previous suggestions given not from you.