Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

[Feedback] CommandBuffer usage with EntityExclusiveTransaction

Discussion in 'Entity Component System' started by Soaryn, Aug 11, 2019.

  1. Soaryn

    Soaryn

    Joined:
    Apr 17, 2015
    Posts:
    328
    So I have been toying around trying to reduce some overhead with creating entities. One concept I thought of was to go wide and construct the components in parallel since I don't know what components are to be on this entity until I loop over a server sent message (3rd party api message); however, with the EntityExclusiveTransaction, parallelism doesn't seem to be a possibility. I can only utilize that Transaction on a single job at a time.

    Then a thought occurred back to the EntityCommandBuffer. The only problem I have usually with it is that it is on the main thread and because of this can stall the system if enough entities are constructed. The fault here is that it only accepts a EntityManager in the Playback.

    However, if the EntityCommandBuffer.Playback could be given the ability to accept an EntityExclusiveTransaction as a parameter to allow for it to be used in a job for playback, this would merge the best of both worlds. My question was how feasible would this be? @Joachim_Ante
     
  2. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,223
    You can use multiple worlds with multiple EETs and use NativeSlice to split your message. Also, depending on the complexity of the message parsing, NativeStream might be helpful to your problem.

    ECBs can be used to modify already existing entities, so giving it an EET from a different world sounds like an easy mistake with very bad consequences. Unless the parsing of the server message is significantly more costly than parsing an ECB, what you are asking for likely will not give you any speedup.
     
  3. Soaryn

    Soaryn

    Joined:
    Apr 17, 2015
    Posts:
    328
    Multiple worlds is not really a good scaling answer to this if under the consideration of 10k entities at a time to enqueue. Note I don't think it is a bad idea, but that should be the last solution, not the first.

    Note that my intention is to build it off main thread. All an ECB does is store commands, to iterate over on main thread. My hope was to be able to play them back to an Exclusive as it is a EntityManager of another world, but on a thread. If we had an ECB specifically designed off main thread that could be used, that too would be fine. Moving still would be on main thread but that is roughly .14ms assuming there are entities for all entities once.

    The reason this would have a speedup is due to the fact I could parallelize the parsing of the message to become components. Right now I have to do it sequentially which takes 0.135ms just for 1 entity. If I could break this apart then it would be roughly 1/4th or so of that at least.


    Some Stats for ExclusiveEntityTransaction:
    • Creating a single Entity: 0.002ms
    • Decomposing the Received JSON: 0.020ms
    • Adding Components: 0.060ms
      • Translating Json to ComponentData: [Defined by component]
      • Adding ReadOnly Component if Success: 0.007ms
      • Setting ComponentData: 0.001ms
    The largest value I had with the translation, was about 0.020ms and that is just one component. Since all of these are sequential I can't mitigate that any other way than to reduce the amount of entities handled from the queue in one frame, and creating additional handler worlds. Each world however adds on ~0.140ms just to call MoveFrom and that ONLY helps if I am spawning multiple entities rather than scaling the data on per entity.

    If I could store even ComponentData in a NativeList/Queue then it would likely be doable without adding a parameter to ECB.Playback


    I'm unsure if you were aware I have no intention of using an existing EntityCommandBuffer, but rather one dedicated to my system.
     
  4. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,223
    I don't understand your argument here. Why would this not scale for 10k entities?

    This is exactly why ECB is just a performance waste for your problem. All you are doing is generating buffers to consume later when you could just consume the results immediately and skip the extra writing and reading.

    There's only one case I can imagine where having ECB-like playback using a single EET in a job makes sense, and that's if your 10k entities are cross-referencing each other randomly. In that case, you can use NativeStream to build a multi-producer-single-consumer queue.

    But if it were me going for maximum performance, I would build all the entities using multiple worlds and use a NativeStream to enqueue commands for setting components with Entity cross-references. Then I would merge all those worlds into a single staging world, dispatch a singular job with EET to play back those commands, and then finally merge that world into the main world.

    And all that is assuming that I can't make any assumptions whatsoever about archetypes nor can use reflection to generate and use generic Burst jobs with ComponentDataFromEntity after batch-instantiating archetypes.
     
  5. Soaryn

    Soaryn

    Joined:
    Apr 17, 2015
    Posts:
    328
    So to reiterate, the longest comsumer of time is translating from json to component data. Say I have 10 component data to add that are increasing in complexity of how the data is to be parsed. This is where scaling breaks, as every translation is sequential in its current state as I can't store a buffer of random ComponentData and their types for consumption later.

    IF I had an ECB playback to an exclusive transaction within a job, I could at the very least translate the json on say a ParallelJob, add to the buffer, and then on a single job once finished, playback add components and set the data. Note still on a thread not main thread.


    A NativeStream, while neat as a concept, is not at all related here.
     
  6. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,223
    You are contradicting yourself here. If you can use a parallel job to split up your json parsing, then you are also capable of splitting up your parsing into multiple parallel IJobs each with an EET. If you don't need determinism, you could probably even pack your EETs into an unsafe array and launch a parallelFor using thread index for accessing an EET.

    However, I really suggest you take a step back and look at your input and output data and also examine how much work you are willing to put into a solution versus waiting potentially indefinitely for Unity to implement one for you. Also evaluate how much effort you are willing to put in for how much performance gain. The reason I bring this all up is because you are completely missing out on Burst right now. I can think of several approaches which involve counting archetypes while rewriting your json into tightly packed binary, then instantiating archetypes, and finally writing your data to the entities in the chunks.

    NativeStream is exactly what you are asking for here:
     
  7. Soaryn

    Soaryn

    Joined:
    Apr 17, 2015
    Posts:
    328
    Uh. How?

    Problems. I don't know the structure of the entity until runtime, more specifically until I get the json and iterate over each translation. This is due to only registered parts of the json being used by the developer. There is no collection I can use to store the translated json data components as all I have at that very moment is a random type, and an IComponent interface. You CANNOT store the interface in any Native collection due to it being an interface (non-blittable).

    Have you ever actually used the ExclusiveEntityTransaction? There is no contradiction. The command buffer would be the storage for the random data components. There should be no reason to reinvent the wheel as it exists. The "buffer" in the first statement is reference to a storage of ComponentData as I just mentioned I can't store an arbitrary type in a NativeCollection, where as an ECB could just add a component command. You can only have one EET accessor. That is kind of the point, you have passed off a EntityManager to a thread and said "Here you go, you have EXCLUSIVE" access to it. It does not work like the EntityCommandBuffer.Concurrent.


    Again, while the NativeStream is neat, and I likely could write arbitrary data to it, how do you propose I read those values back without knowing the data types? If I can then awesome I'd love to use it; however, I fear this is just a fancy new toy that has been mentioned in a few other threads that a few want to use everywhere.

    I don't fully understand your opposition to having the ECB have a playback featuring the EntityExclusiveTransaction. Anyone manually calling playback should at that point know what they are doing with it, otherwise you should never call playback on some random ECB you got access to. I'm wanting to instantiate a new one and run with that for this system, not take away functionality: augment.


    As for your note about burst I have already made posts regarding this on other threads, there is nothing I can do until it can support strings as this is highly dependent on a 3rd party api message. Burst does not support (to my knowledge) ECB, EET, nor strings for the json decomposition, as well as passing around an entity (I got around this by passing a pointer).
     
    Last edited: Aug 12, 2019
  8. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,223
    Ah. I found the point of confusion.

    The thing is, each World has its own EntityManager. Those EntityManagers are completely independent of each other. And consequently, each EntityManager can have a unique EET. That means that if you have 4 Worlds, you can have 4 EETs each running an IJob in a separate thread parallel to each other.

    All you need to do is enumerate the possible types the json can produce, and write that value (and optionally a size in bytes) as a header to your arbitrary data. Of course you can define alternative encoding schemes that better organize entities, but that's the gist of it.

    My main opposition to it is that there is better tool for the job which is available in the current API that you can use today (multiple worlds). Don't get me wrong, having EETs playback ECBs would be useful in multi-world simulations where multiple ECBs could be played back in parallel on different threads. But I would rather Unity take their time and finish the refactoring of EntityComponentStore before adding such a feature rather than spend extra development time trying to add it in early when it really isn't actually needed yet.

    "Nothing I can do" is a strong phrase. The archetype counting technique I proposed does not rely on ECB nor EET. Depending on how you convert json to strongly typed data, you can use one of several other techniques like ComponentDataFromEntity to write back to entities from something like a NativeStream. That just leaves the jobs that parse the json, count the archetypes, and write the binary representation of json to some intermediate structure.

    Now for strings in Burst, the solution is pretty straightforward. First you get a non-Burst parallelFor job to convert your string into a NativeArray<Byte>. Then you import some C++ json parsing dll into Burst context and use that for your parsing. That should make 80-90% of your work Burst-able, which would likely be a bigger win than multi-threading. But it is a lot of work. Hence why I mentioned needing to take a step back and evaluate how much work you were willing to put in for how much performance gains versus waiting for Unity to maybe implement something that might help you in some approximate future.
     
  9. Soaryn

    Soaryn

    Joined:
    Apr 17, 2015
    Posts:
    328
    I understood that, not really a point of confusion . This goes back to our earlier debate on multi worlds being a last resort improvement rather than a first. My biggest opposition to doing this is the time it takes to move entities from one world to another is already high enough without having 4. If I could say pull all entities from the 4 worlds in the same amount of time then it would be fine. But it takes about 0.2ms per world's EntityManager.

    This one may be a long or short list, so I will look into how feasible it would be.


    This makes sense. I wasn't aware they were refactoring it. That one I can wait on



    I personally would love it if there was a Burstable JsonUtility. I'd rather not personally fumble through trying to make a burstable Json parser with the way it stands right now.

    For NativeStreams, I have yet to see an example of how to get it to work, as they appear to be setup slightly differently from other collections for jobs.
     
  10. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,223
    I'm having a hard time imaging that ECB playback is faster than EETs (even with the world merging penalty) for that many entities. EET has the ability to create an Entity from an archetype, so entities would be moved around a lot less using EET.

    Out of curiosity (don't feel like you have to answer), how are you converting json to strong typing? You aren't using reflection, are you? Also assuming multiple worlds is super slow for some odd reason, what main thread exclusive task would you be doing while the EET would be processing the ECB? To me it is more intuitive to schedule some heavy non-ECS jobs (ToComponentDataArray and other data structures) right before the sync point and let those run while the main thread syncs.

    I guess I have quite different opinions regarding effort/performance tradeoffs. 3rd party APIs are always a huge pain to integrate with when performance matters.

    NativeStreams are typeless containers, and have an individual stream per foreach index. You just allocate how many parallelFor indices you need, then wrap around each index access with BeginForeachIndex and EndForEachIndex. The reader lets you peek at data without marking the data as read, so you can use it to read a header before doing a typed read if that's something you need.
     
  11. Soaryn

    Soaryn

    Joined:
    Apr 17, 2015
    Posts:
    328
    Basically, I have a FunctionPointer that is registered by the user. It uses the JsonUtility.fromJson<T> and then returns an interface that is implemented by T. That interface is then sent to an indeterminate amount of function pointer invokes that all cast the interface to the original structure T. The user is in charge of how it translates T to the ComponentData or SharedData that is to be added to the entity via the interface. IF the translation fails, (either errors or returns false) then the component is not added nor set. This is where the archetype part breaks down a little bit as I can't assume the structure going in. It is highly dependent on the message received. However, the structure changing actually doesn't really effect performance in a different world in this case, as I mentioned I have profiled extensively and the most consuming aspect is the translation done sequentially. So if I have 2 long translations I add their time together rather than take the longest time. I'll look into using the NativeStream to alleviate some of this, but it just sounds like reinventing the wheel of the EntityCommandBuffer.

    Note, no reflection is being used here, just abuse of generics and function pointers (which are oddly burstable but almost every part of the rest of the system breaks in Burst).


    My point was being to use both ECB and EET together, just to use the ECB as the playback buffer, then on a single IJob playback to the EET. The only Main thread tasks being created in this scenario is, if the EET Dependency is completed, stop the transaction, Move all entities from OtherWorld to MainWorld, then Schedule the next batch NativeQueue of messages Beginning a new transaction Start.

    Note, the second longest aspect of this system is the ConcurrentQueue -> NativeQueue as the 3rd party messages are received from a websocket. And until Unity has their next transport system finalized, I'm not even going to worry about changing that as it is unfortunately its own threaded system rather than a Jobified system.
     
  12. DreamingImLatios

    DreamingImLatios

    Joined:
    Jun 3, 2017
    Posts:
    4,223
    Oh. I was thinking this was a deserialization problem, but it sounds like it is more like GameObjectConversion in which custom code can hook in and modify the resulting entities however it sees fit. There isn't much you can do about that optimization-wise without putting constraints on the custom code in some way. Your solution is a little too general OOP for me to give any suggestions. I'm still don't understand what benefit you would have performing playback in an IJob instead of the main thread in this particular use case. From what you described it sounds like the main thread would spin and wait for completion to move the new entities from OtherWorld to MainWorld rather than do any useful main thread-exclusive work.

    But besides that lack of understanding, what you are asking for sort of makes sense. It is definitely not performant though. There's no way you are making that beast performant without some aggressive redesigns and constraints of how you are transforming your json into entities.