Skip to content

Add observer ordering via ObserverSet and topo-sorted dispatch#24328

Draft
caniko wants to merge 13 commits into
bevyengine:mainfrom
caniko:feat/observer-ordering
Draft

Add observer ordering via ObserverSet and topo-sorted dispatch#24328
caniko wants to merge 13 commits into
bevyengine:mainfrom
caniko:feat/observer-ordering

Conversation

@caniko
Copy link
Copy Markdown

@caniko caniko commented May 17, 2026

Add observer ordering via ObserverSet and topo-sorted dispatch

Resolves #14890.

What this changes

Observers can be ordered against each other using a SystemSet-style API:

app.add_observer(score.in_set(WinCheck));
app.add_observer(announce.after(WinCheck));

Cross-bucket ordering works natively: a global observer can be ordered against a per-entity or per-component observer. The implementation uses one topo sort per event key, then reuses the sorted order across all dispatch sites. The existing archetype-flag fast skip is preserved.

Approach

  • ObserverSet trait + derive, mirroring SystemSet. Identities are stored as Interned<dyn ObserverSet>.
  • CachedObservers replaces the old EntityHashMap buckets with one node table, one topo-sorted order, and sorted inverted indices per bucket.
  • Topo sort reuses bevy_ecs::schedule::graph::DiGraph, so observers and systems share the same ordering kernel.
  • Dispatch in event/trigger.rs goes through one run_ordered helper that performs a k-way merge over sorted NodeId streams. No per-dispatch allocation.
  • Dynamic trigger helpers are routed through the same ordered dispatch path.

Breaking changes

  1. ObserverMap type alias removed. Replaced by indexed-Vec storage. Downstream code reading world.observers() to enumerate observers needs to use a new iterator API. There are very few such users in the ecosystem (mostly debug tools).
  2. CachedObservers / CachedComponentObservers field shapes change. The fields were already private; only the accessors (global_observers(), etc.) were public. The new accessors return sorted slices of NodeId plus a nodes() table.
  3. Iteration order becomes deterministic even for callers who never touch ObserverSet. Strictly stronger than today's contract; nothing correct can break, but anything that relied on undefined order (test asserts, frame replays) will need updates. Call out in the PR.
  4. Dispatch sites in event/trigger.rs change shape. Internal, no Trigger trait signature changes, but custom Trigger impls in the wild (Bevy picking is the largest) need a quick port to the new run_ordered helper. Bevy maintainers can drive this in the same PR.

What's NOT in this PR

  • Parallel observer execution. This needs SystemParam access analysis per observer to build a conflict graph.
  • .run_if(condition) on observers. Adjacent, but kept out to constrain review scope.
  • Propagation hop reordering. Hop order is owned by Traversal; per-hop dispatch now uses the sorted observer order.
  • ambiguous_with for observers. Only meaningful once parallel observer execution exists.

Design Notes

Design summary

The key design choice is one event-local observer graph, not four separately sorted buckets. This enables edges across dispatch buckets while avoiding four topo sorts per event. Each registered observer/event pair becomes an ObserverNode with a stable insertion sort key. CachedObservers stores a dense node table, an event-wide sorted order, and bucket indices containing NodeIds sorted by that event-wide order.

Dispatch sites pass the relevant sorted streams into run_ordered. For one stream, dispatch is a direct dense slice walk. For multiple streams without ordering edges, dispatch preserves existing bucket order without allocation. If ordering edges exist, run_ordered walks the event-wide order and runs nodes present in the active streams, which gives cross-bucket ordering without allocating a merged list.

Archetype flags remain the fast skip for lifecycle component observers. Register/unregister returns the same component-observer deltas needed to update those flags, now backed by the new component bucket indices.

Test coverage

New and updated bevy_ecs observer coverage includes:

  • ObserverSet derive and set membership.
  • before/after ordering by observer entity and by set.
  • multiple-set membership and empty-set targets.
  • cycle detection behavior.
  • unregister-preserves-order.
  • cross-bucket ordering.
  • archetype-flag preservation.
  • propagation using sorted dispatch per hop.
  • nested sets.
  • dispatch_order_for accessor coverage.
  • dynamic trigger helpers routed through ordered dispatch.
  • existing lifecycle order tests kept intact and ported to the current Discard lifecycle terminology.

Local validation

  • cargo check --workspace: passed.
  • cargo test -p bevy_ecs --doc: passed.
  • cargo clippy -p bevy_ecs -- -D warnings: passed.
  • cargo test -p bevy_ecs: observer tests pass, but the full suite is blocked locally by error::bevy_error::tests::filtered_backtrace_test. I verified the same test fails on untouched upstream main (370be1b02) under Rust 1.95.0 with the same assertion, so this is not caused by this branch.

caniko added 9 commits May 17, 2026 12:23
Foundation for observer ordering (bevyengine#14890). No behaviour change yet — descriptor edges are unread by storage and dispatch; that wiring lands in later commits.
All four dispatch sites in event/trigger.rs now route through a single ordered dispatch helper over sorted NodeId streams. Archetype-flag fast skip is preserved. Cross-bucket ordering is now observable at runtime when observer ordering edges exist.
@github-actions
Copy link
Copy Markdown
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide, as well as our policy regarding AI usage, and we look forward to reviewing your pull request shortly ✨

@alice-i-cecile alice-i-cecile modified the milestones: 0.19, 0.20 May 17, 2026
@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events M-Release-Note Work that should be called out in the blog due to impact S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels May 17, 2026
@github-project-automation github-project-automation Bot moved this to Needs SME Triage in ECS May 17, 2026
@github-actions
Copy link
Copy Markdown
Contributor

It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note.

Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes.

@alice-i-cecile
Copy link
Copy Markdown
Member

run_if already works now :) Add a test for the interaction?

@caniko
Copy link
Copy Markdown
Author

caniko commented May 17, 2026

Added follow-up commits completing the public API surface to match SystemSet's shape:

  • add_observers((a, b, c)) plural entry point.
  • IntoObserverConfigs tuple trait via all_tuples!.
  • Tuple-level .in_set / .before / .after / .chain modifiers.
  • configure_observer_sets((A, B).chain()) for set-level ordering.
  • Observer::with_name(&str) for diagnostic labels.
  • dispatch_order_for_set / _for_target / _with_names accessor variants.

Storage and dispatch are unchanged from the previous commits; this is additive on the builder and diagnostics layer. The new observer tests cover tuple chaining, tuple-wide modifiers, set chaining, naming, and filtered dispatch-order accessors.

I also pushed a small follow-up formatting commit after CI caught stable clippy's semicolon_if_nothing_returned lint in the new tests.

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 M-Release-Note Work that should be called out in the blog due to impact S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged

Projects

Status: Needs SME Triage

Development

Successfully merging this pull request may close these issues.

Observer ordering/scheduling

2 participants