perf(observable): index observers by component for faster dispatch
Introduce a component-to-observer index to avoid iterating over all observers for every world event. This optimizes event dispatching by only notifying observers whose queries are relevant to the specific component being added, removed, or changed.
This commit is contained in:
parent
1c55485f9f
commit
24616a0855
|
|
@ -3,6 +3,7 @@ import type { Query } from "../query";
|
|||
import type { Entity } from "../entity";
|
||||
import type { WorldEvent, QueryUpdate, RelationshipUpdate } from "./events";
|
||||
import type { RelationshipDef } from "../relationship";
|
||||
import type { ComponentDef } from "../component";
|
||||
|
||||
// ── Internal state ───────────────────────────────────
|
||||
interface QueryObserverState {
|
||||
|
|
@ -24,6 +25,9 @@ export class ObservableLayer {
|
|||
private _observers: QueryObserverState[] = [];
|
||||
private _relObservers: RelationshipObserverState[] = [];
|
||||
|
||||
// ── Observer index: component key → observers that care ──
|
||||
private _compIndex = new Map<symbol, Set<QueryObserverState>>();
|
||||
|
||||
// ── Query observers ─────────────────────────────
|
||||
|
||||
observe(query: Query): Subject<QueryUpdate> {
|
||||
|
|
@ -38,6 +42,7 @@ export class ObservableLayer {
|
|||
subject: new Subject<QueryUpdate>(),
|
||||
};
|
||||
this._observers.push(state);
|
||||
this._indexObserver(state);
|
||||
return state.subject;
|
||||
}
|
||||
|
||||
|
|
@ -83,24 +88,100 @@ export class ObservableLayer {
|
|||
): void {
|
||||
this.events$.next(event);
|
||||
|
||||
for (const o of this._observers) {
|
||||
this._updateObserver(o, event, queryMatches);
|
||||
}
|
||||
this._dispatchToObservers(event, queryMatches);
|
||||
|
||||
for (const o of this._relObservers) {
|
||||
this._updateRelObserver(o, event);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Private ──────────────────────────────────────
|
||||
// ── Private: observer indexing ───────────────────
|
||||
|
||||
/** Add an observer to the component index. */
|
||||
private _indexObserver(state: QueryObserverState): void {
|
||||
for (const def of state.query.with) {
|
||||
this._addToIndex(def._key, state);
|
||||
}
|
||||
for (const def of state.query.not) {
|
||||
this._addToIndex(def._key, state);
|
||||
}
|
||||
// Also index under a well-known symbol for spawn/destroy events
|
||||
// (those always fan out to all observers).
|
||||
this._addToIndex(ANY_KEY, state);
|
||||
}
|
||||
|
||||
/** Remove an observer from the component index. */
|
||||
private _unindexObserver(state: QueryObserverState): void {
|
||||
for (const def of state.query.with) {
|
||||
this._remFromIndex(def._key, state);
|
||||
}
|
||||
for (const def of state.query.not) {
|
||||
this._remFromIndex(def._key, state);
|
||||
}
|
||||
this._remFromIndex(ANY_KEY, state);
|
||||
}
|
||||
|
||||
private _addToIndex(key: symbol, state: QueryObserverState): void {
|
||||
let set = this._compIndex.get(key);
|
||||
if (!set) {
|
||||
set = new Set();
|
||||
this._compIndex.set(key, set);
|
||||
}
|
||||
set.add(state);
|
||||
}
|
||||
|
||||
private _remFromIndex(key: symbol, state: QueryObserverState): void {
|
||||
const set = this._compIndex.get(key);
|
||||
if (!set) return;
|
||||
set.delete(state);
|
||||
if (set.size === 0) this._compIndex.delete(key);
|
||||
}
|
||||
|
||||
/** Dispatch to only the relevant observers. */
|
||||
private _dispatchToObservers(
|
||||
event: WorldEvent,
|
||||
queryMatches: (query: Query, e: Entity) => boolean,
|
||||
): void {
|
||||
if (!("entity" in event)) return;
|
||||
|
||||
// Determine which component keys are relevant
|
||||
let keys: symbol[] = [];
|
||||
|
||||
switch (event.type) {
|
||||
case "spawned":
|
||||
case "destroyed":
|
||||
// Check all observers (via the ANY_KEY set)
|
||||
keys = [ANY_KEY];
|
||||
break;
|
||||
|
||||
case "componentAdded":
|
||||
case "componentRemoved":
|
||||
case "componentChanged":
|
||||
keys = [event.component._key];
|
||||
break;
|
||||
}
|
||||
|
||||
// Collect unique observers to update (deduplicate across keys)
|
||||
const seen = new Set<QueryObserverState>();
|
||||
for (const key of keys) {
|
||||
const set = this._compIndex.get(key);
|
||||
if (!set) continue;
|
||||
for (const o of set) {
|
||||
if (!seen.has(o)) {
|
||||
seen.add(o);
|
||||
this._updateObserver(o, event, queryMatches);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Private: observer update logic ───────────────
|
||||
|
||||
private _updateObserver(
|
||||
obs: QueryObserverState,
|
||||
event: WorldEvent,
|
||||
queryMatches: (query: Query, e: Entity) => boolean,
|
||||
): void {
|
||||
// Only entity-bearing events affect queries
|
||||
if (!("entity" in event)) return;
|
||||
|
||||
const e = event.entity!;
|
||||
const wasMatched = obs.matched.has(e);
|
||||
const nowMatches = queryMatches(obs.query, e);
|
||||
|
|
@ -167,17 +248,12 @@ export class ObservableLayer {
|
|||
}
|
||||
|
||||
case "destroyed": {
|
||||
// World emits relationshipRemoved for each edge before destroy,
|
||||
// so those fire first. Then we arrive here — just clean up.
|
||||
const removed: { source: Entity; target: Entity }[] = [];
|
||||
for (const key of obs.edges) {
|
||||
const [si, ti] = key.split(":").map(Number);
|
||||
const idx = event.entity & 0xfffff;
|
||||
if (si === idx || ti === idx) {
|
||||
removed.push({
|
||||
source: si as Entity,
|
||||
target: ti as Entity,
|
||||
});
|
||||
removed.push({ source: si as Entity, target: ti as Entity });
|
||||
}
|
||||
}
|
||||
for (const r of removed) {
|
||||
|
|
@ -199,6 +275,7 @@ export class ObservableLayer {
|
|||
o.matched.clear();
|
||||
}
|
||||
this._observers = [];
|
||||
this._compIndex.clear();
|
||||
|
||||
for (const o of this._relObservers) {
|
||||
o.subject.complete();
|
||||
|
|
@ -211,6 +288,7 @@ export class ObservableLayer {
|
|||
this.events$.complete();
|
||||
for (const o of this._observers) o.subject.complete();
|
||||
this._observers = [];
|
||||
this._compIndex.clear();
|
||||
for (const o of this._relObservers) o.subject.complete();
|
||||
this._relObservers = [];
|
||||
}
|
||||
|
|
@ -218,6 +296,9 @@ export class ObservableLayer {
|
|||
|
||||
// ── Helpers ─────────────────────────────────────
|
||||
|
||||
/** Sentinel key for observers that must be notified on spawn/destroy. */
|
||||
const ANY_KEY = Symbol("any");
|
||||
|
||||
function queriesEqual(a: Query, b: Query): boolean {
|
||||
if (a.with.length !== b.with.length) return false;
|
||||
if (a.not.length !== b.not.length) return false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue