Skip to content

Subscription-based Events (legion-like) #2287

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
tower120 opened this issue May 31, 2021 · 2 comments
Closed

Subscription-based Events (legion-like) #2287

tower120 opened this issue May 31, 2021 · 2 comments
Labels
A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible C-Usability A targeted quality-of-life change that makes Bevy easier to use

Comments

@tower120
Copy link

tower120 commented May 31, 2021

What problem does this solve or what need does it fill?

Current Event system does not play well with out-of-sync readers. (systems with different FixedStep's, update on demand, etc...)

What solution would you like?

legion - like subscription based events. EventReader subscribes to EventSource. EventSource-EventReader keep track of EventData usage, and free memory after EventData was consumed.

I see 3 possible implementations. In all cases EventReader should be stored as system-local resource. EventSource can be global resource. All versions also could have unsubscribe().


First - EventSource does not store EventData queue, EventData copied into each EventReader queue.

//pseudocode in cpp
template<class Data>
struct EventSource {
     std::vector<EventReader<Data>*> subscribers;

     void trigger(Data data){
        for(EventReader* subscriber : subscribers)
           subscriber->event_list.push_back(data);
     }
}

template<class Data>
struct EventReader{
    circular_buffer<Data> event_list;
}

Simple to implement, read-efficient, easy to make thread-safe. Memory consumption and trigger event processing time grows linearly with EventReader's count.


Second - move each EventData in Arc, have Arc-pointers in EventReader - list.

//pseudocode in cpp
template<class Data>
struct EventSource {
     std::vector<EventReader<Data>*> subscribers;

     void trigger(Data data){
        auto shared_ptr = make_shared_pointer(data);
        for(EventReader* subscriber : subscribers)
           subscriber->event_list.push_back(shared_ptr);
     }
}

template<class Data>
struct EventReader{
    circular_buffer<shared_pointer<Data>> event_list;
}

Memory fragmentation, memory alloc/dealloc (can be solved with per EventSource memory-arena [custom Arc?]), Arc dereferencing. But trigger costs does not grow with EventReader's count.

N.B. Since events operate on inter-system level - it could turn out that memory fragmentation is not a problem... CPU cache probably will already be clogged with iterated components data.


Third - second version variation, using intrusive linked list and intrusive Arc.

//pseudocode in cpp
// lack of actual synchronization
template<class Data>
struct EventSource {

    struct EventData{
         Data data;
         atomic<int32> use_count;
         // intrusive linked list (indices as alternative)
         atomic<EventData*> next;
         atomic<EventData*> prev;
    }
    atomic<EventData*> event_list_begin;
    std::array<Data, 128> memory_arena;  // or in heap. Or maybe global memory arena?

   int event_readers = 0;

    void subscribe(EventReader& event_reader){
       event_reader.data_ptr= event_list_begin;
       ++event_readers;
    }

     void trigger(Data data){
        EventData* event = push_list_back(memory_arena, event_list_begin);
        event->use_count = event_readers;
     }
}

template<class Data>
struct EventReader{
    EventSource<Data>* src;
    EventData* data_ptr;  // can't be freed while in use

    optional<Data> consume(){
       data_ptr = list_next(src->event_list_begin, data_ptr);
       if (!data_ptr)
          return {};       

       Data data = data_ptr->data;
       data_ptr->use_count--;
       if (data_ptr->use_count == 0){
           list_free(src->event_list_begin, data_ptr);
       }
       return std::move(data);
    }
}

Synchronized Intrusive List may be somewhat tricky to implement. Lowest memory consumption. Reader have fixed memory consumption.

@tower120 tower120 added C-Feature A new feature, making something new possible S-Needs-Triage This issue needs to be labelled labels May 31, 2021
@NathanSWard NathanSWard added A-ECS Entities, components, systems, and events C-Usability A targeted quality-of-life change that makes Bevy easier to use and removed S-Needs-Triage This issue needs to be labelled labels May 31, 2021
@alice-i-cecile
Copy link
Member

bevyengine/rfcs#17 and bevyengine/rfcs#25 aim to solve different aspects of this problem domain. As you suspected, performance is a serious concern.

@tower120
Copy link
Author

tower120 commented Jun 8, 2021

Yes, looks like that rfcs do what I want. Closing this.

@tower120 tower120 closed this as completed Jun 8, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible C-Usability A targeted quality-of-life change that makes Bevy easier to use
Projects
None yet
Development

No branches or pull requests

3 participants