How to decrease delay between receiving tag based "events"?

I’ve got a following dilemma. Here’s a simplified description:

A system acts as trigger and detects entities based on their simplified “physics” properties. Which is a Detector. It has DynamicBuffer of the hits resolved, and it “ref” writes to the DynamicBuffer;

Another system fetches <DynamicBuffer, Tag> query.
Then, processes it by generating a request to do something for each HitData.Entity and attaches request to the hit entity by using EntityCommandBuffer;

This creates one frame delay between generated event and actual action for it.
But, systems that perform <DynamicBuffer, Tag> query would run each frame. (Since .WithChangedFilter will trigger every frame)
Meaning even with completely empty buffer systems will still schedule jobs which will create an overhead each frame.

So I’ve thought about adding a HasHitsTag component for the initial Detector.
(frame)
Generate hits (0), attach HasHitsTag (0), fetch <HasHitsTag, DynamicBuffer, Tag> (1), generate action request (1), resolve action request (2);

This will take now two frames to react to. I don’t like the idea of introducing a sync point in the middle of the frame either.

Am I doing it wrong or overthinking, and the delay is no big deal?

Iterating through the dynamic buffer to check if its length is zero is more expensive than the structural change to tag the entity? I highly doubt that and would encourage you to profile before adding frame delays to your logic.

1 Like

Problem is, every subsequent “receiver” system would require main thread ops in this case. And which is worse - manual implementation of the job to supply only required DynamicBuffers.

Also, random access. DynamicBuffers won’t be aligned properly.
Also, I’m not adding entity directly, I’m using EntityCommandBuffers for that. Which is what causing delays, since they don’t cause structural changes in the middle of the frame.

Why? I don’t understand where that conclusion comes from, especially since you could easily put an early out here:

You are doing that with an Entities.ForEach, right?

Because that would require:

  • To fetch BufferFromEntity array on the main thread based on query;
  • Check every one of them if DynamicBuffer.Length != 0,
  • Then store results somehow (preferrably excluding random access);
  • And then wind up a job that runs on the results.

Looks like a lot of boilerplate to me, and it doesn’t prevent receiver systems from running well the system.
And probably would be just cheaper to keep it as it is, just scheduling.

Entities.ForEach((/* ... */ in DynamicBuffer<HitData> hits) =>
{
    if (hits.Length <= 0)
        return;

    //Begin hits processing work
    //...
    //...

}).ScheduleParallel();

It doesn’t prevent jobs from scheduling. That’s exactly what I’m doing right now.

Fun fact, filters on entity queries don’t prevent jobs from running. The early-out only happens if the query without filters fails to match any chunks. The filtering step creates an additional job.

Well, I guess then there’s no magical way around for fast exit.
Its either scheduling each frame, or introducing a delay to funnel chunks by tag query.

The real issue here is that the main thread has no way of knowing whether the reading jobs should be scheduled until the writing job finishes. So either you need a sync point for the main thread to learn that info, or you need to always schedule jobs.

How much overhead is scheduling all jobs costing you? How many jobs are getting scheduled?

Not a lot right now. Guesstimated total job count would be no less than all “respose” systems.

I think by the time you get to the point where you have performance problems (if you ever get there), C# source generators will be a thing and you may be able to leverage it to merge jobs. I wouldn’t worry about it too much right now.

1 Like

So I’ve managed to find a really simple and at the same time ellegant solution which doesn’t require adding extra sync points.

I’ve moved Detector system to run before actual BeginSimulationEntityCommandBufferSystem and used it as an already existing sync point.

Since system itself does not rely on any other data / system, and only generates data for the simulation - there’s no real penalty of doing so.

As a result:

  • Tag gets attached on the same frame as system runs, which is a big plus. (No extra delays)
  • Prevents running reactive systems each frame (due to the chunk funneling via tag).
  • Reactive systems no longer need to be marked to update before “Detector” (always ran after in simulation group)

Best part - I’ve already had “BeforeSimulationGroup”, so no extra work involved setting it up.

Sometimes best solution is to think outside of “box”. In this case - SimulationGroup.

I’m glad you’ve found a solution, though I have to say, I’ve been a bit baffled about the problem from the beginning. Even if you are using tag components as events, I still don’t see why you couldn’t have used the pattern:

  1. System schedules jobs which catch and ‘raise and event’, by scheduling an ECB to add an event tag component.

  2. run that ECB System to add the event component.

  3. A listener System acts on all of the event components.

That all happens in one frame, albeit with a sync point to add the tags. There are models for ECS event systems which don’t require using components as events, and therefore avoid the sync point. But nothing about using a sync point should require you to skip a frame to check for them.

Sorry if I’m asking questions you’ve already answered. I’m having trouble wrapping my head around why it’s a problem.

Modifying data is what changes things. Without structural changes there’s no way to notify changes in data in a manner that doesn’t involve constantly running jobs or updates or extra ops on top of existing ones.
And without actual data - there’s no way to react on it. This where add sync point vs performance argument comes in.
Add too many sync points - and there will stalls for the whole application.

As for the diffrent models themselves - I haven’t looked for them to be honest. I’ve just made what works for me right now, and that seems to be good enough. There’s a potential bottleneck with moving chunks around, but its like miles away from where current data processing load is. So I’ll just leave it as it is.

Any links to ECS “event” patterns would be nice though. Thanks.

1 Like

You’re right that using ECBs to add event components can slow things down. But what confused me was when you were fighting frame delays - It sounded like you were having trouble finding a design that would let you both raise and respond to an event in the same frame. I was confused about why that was a problem.

I’ve had trouble finding design that:

  1. Doesn’t block multithreading / jobs (no extra sync points);
  2. Doesn’t run systems where they should not be running (constantly);
  3. Doesn’t have extensive delays;
  4. Doesn’t require writing extra code per reactive system or introduce extra complexity to the initial one;
  5. All at the same time.

In this case proper ordering was the answer. And I’ve realized that not everything should be executed right away. Splitting load across multiple frames can have benefits, although not in all cases.

1 Like

Thanks for the breakdown, and good luck with your patterns. :slight_smile:

1 Like

If you’re ever looking for some alternative event patterns to use, which don’t require sync points, here are two popular options people have used:

  1. Store all events of a type in a DynamicBuffer. That buffer is preallocated to the largest needed size. Each event producer is given an assigned index range which they can safely write to. This allows multiple producers to write to the same DynamicBuffer in separate, parallel jobs. All event “listener” jobs would be scheduled to be dependent on all event “producer” jobs completing first. Clear the event buffer after all listener jobs have completed.

  2. There are a lot of solutions out there which use Native collections in various ways to store events. NativeArray, NativeQueue, and NativeStream are all popular. Some of these are quite advanced, like the one tertle has made public.

1 Like