Observer ordering/scheduling
What problem does this solve or what need does it fill?
Sometimes an observer can rely on things added by other observers. But it is currently not possible to explicitly order the observers.
What solution would you like?
I would like the ability to mark an observer as having to run after or before another one.
I have two ideas on what the API side can look like:
- Having something like
ObserverEntityCommands::after(Entity), so that you can run it after a specific observer entity- This has the downside that you would need to store that entity somewhere if you want to use it in another plugin.
- Having an
ObserverSettype, that you can add to observers, similar toSystemSet.- You could then schedule after/before a given
ObserverSetwithout having to worry about the individual observer entities. It's the option I prefer
- You could then schedule after/before a given
This is needed in #12365 as well.
We definitely want this, just figuring out the architecture and API here is tricky. I'd really like to not be blocked on systems-as-entities and a relationship-powered schedule graph, so a decent temporary solution would be nice here.
I experimented with this idea a little today and there's definitely some non-trivial design constraints we need to consider.
Initially I thought the most obvious answer would be to store observers in a Schedule (rather than the current CachedObservers struct) per event-type E. My rationale was "We want system-like ordering, so why not use the existing solution to that problem?". Since observers are just systems anyway, this makes sense. However, there are some major issues with this approach:
- Events propagate, so observers must run one-by-one for a particular event. This means the ability for the schedule to run observers in parallel doesn't matter.
- Some observers only watch particular entities or particular components. This isn't unworkable, since we could encode this into run-conditions in a
Schedule, but it may be less performant than the current solution (EntityHashMapfor observer systems). - The
Scheduleis a large struct (1kB). Having 1 schedule per event type is just too much overhead.
Unfortunately, I don't have any affirmative ideas on how we should pursue this feature, just the above findings on a way we probably shouldn't do it.
It's a shame events need to propagate (it's non negotiable, too important for UI), because the other two concerns might be counteracted by the new-found ability to run observers in parallel, rather than one-by-one in a single-threaded for-loop (as they currently are). Might be worth changing how Trigger works to make stopping propagation something that's requested separately, and thus observable in the type system. I imagine no matter what we do here, there's a performance win that could come from knowing which observers could be run in parallel, and doing so.
I imagine no matter what we do here, there's a performance win that could come from knowing which observers could be run in parallel, and doing so.
Amdahl's Law is gonna bite you there :) Observers almost always do tiny bits of work: it's unlikely that parallelizing them is worth the overhead.
The fact that ObserverMap is a hashmap (https://github.com/bevyengine/bevy/blob/main/crates/bevy_ecs/src/observer/mod.rs#L324) is almost never used apart from observer despawn, so one option could be to switch from a hashmap to a Vec<(Entity, ObserveRunner)>.
Some unclear parts:
- what would the api look like? Simply
app.add_observer(a.after(b))? - how would we implement ordering constraints between observers in different maps (global vs per-entity vs per-component observers). I would be fine with defining a fixed order between these (maybe global first, then per-entity, then per-entity) as a first pass
Just want to mention my situation relating to this. I'm trying to use observers as a way to represent passive abilities on cards. Each card can have a number of abilities and the user might want the abilities to run in a specific order for synergy or math reasons (of course it would only matter if they have the same triggers) . So i can imagine a list of these, with arrows to be able to move them up or down in that list which would control the order of the abilities.
When i first looked into the ObservedBy component, i assumed that changing the Vec inside it would be the way to do it. But I didnt even try it cus i was told early on in the discord that it wasnt going to work. So I think maybe thats another thing to consider if ur thinking about the API for this. I personally would not benefit too much from the before/after API, but thats with respect to my specific situation. I can see that API being useful in other situations so an API that allows for both would be cool. I dont think it matters that it happens specifically in ObservedBy , but that was the first place i looked so maybe its not a bad idea to put it there.
I have a win-condition observer that I would like to run after several other observers. Would like to have these observers in a systemset-like set so I can run the win-condition after the set. Would be nice.
Hm. Observer handles an event, event happens in specific context (world as of time of event creation). In distributed systems (e.g., in Matrix (social network) - Matrix Event Graph (MEG), in many CRDTs) an event bakes causal past (so we have that "world as of time of event creation"). Seems that if we only allow observers to use commands to modify the world - then all observers that handle a specific event will have exact world as of time of event creation, and can run in parallel. I.e., forbid direct world mutation from a handler. Then issued commands get applied / flushed, and we move to handling next event.