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 { Entity } from "../entity";
|
||||||
import type { WorldEvent, QueryUpdate, RelationshipUpdate } from "./events";
|
import type { WorldEvent, QueryUpdate, RelationshipUpdate } from "./events";
|
||||||
import type { RelationshipDef } from "../relationship";
|
import type { RelationshipDef } from "../relationship";
|
||||||
|
import type { ComponentDef } from "../component";
|
||||||
|
|
||||||
// ── Internal state ───────────────────────────────────
|
// ── Internal state ───────────────────────────────────
|
||||||
interface QueryObserverState {
|
interface QueryObserverState {
|
||||||
|
|
@ -24,6 +25,9 @@ export class ObservableLayer {
|
||||||
private _observers: QueryObserverState[] = [];
|
private _observers: QueryObserverState[] = [];
|
||||||
private _relObservers: RelationshipObserverState[] = [];
|
private _relObservers: RelationshipObserverState[] = [];
|
||||||
|
|
||||||
|
// ── Observer index: component key → observers that care ──
|
||||||
|
private _compIndex = new Map<symbol, Set<QueryObserverState>>();
|
||||||
|
|
||||||
// ── Query observers ─────────────────────────────
|
// ── Query observers ─────────────────────────────
|
||||||
|
|
||||||
observe(query: Query): Subject<QueryUpdate> {
|
observe(query: Query): Subject<QueryUpdate> {
|
||||||
|
|
@ -38,6 +42,7 @@ export class ObservableLayer {
|
||||||
subject: new Subject<QueryUpdate>(),
|
subject: new Subject<QueryUpdate>(),
|
||||||
};
|
};
|
||||||
this._observers.push(state);
|
this._observers.push(state);
|
||||||
|
this._indexObserver(state);
|
||||||
return state.subject;
|
return state.subject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,24 +88,100 @@ export class ObservableLayer {
|
||||||
): void {
|
): void {
|
||||||
this.events$.next(event);
|
this.events$.next(event);
|
||||||
|
|
||||||
for (const o of this._observers) {
|
this._dispatchToObservers(event, queryMatches);
|
||||||
this._updateObserver(o, event, queryMatches);
|
|
||||||
}
|
|
||||||
for (const o of this._relObservers) {
|
for (const o of this._relObservers) {
|
||||||
this._updateRelObserver(o, event);
|
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(
|
private _updateObserver(
|
||||||
obs: QueryObserverState,
|
obs: QueryObserverState,
|
||||||
event: WorldEvent,
|
event: WorldEvent,
|
||||||
queryMatches: (query: Query, e: Entity) => boolean,
|
queryMatches: (query: Query, e: Entity) => boolean,
|
||||||
): void {
|
): void {
|
||||||
// Only entity-bearing events affect queries
|
|
||||||
if (!("entity" in event)) return;
|
|
||||||
|
|
||||||
const e = event.entity!;
|
const e = event.entity!;
|
||||||
const wasMatched = obs.matched.has(e);
|
const wasMatched = obs.matched.has(e);
|
||||||
const nowMatches = queryMatches(obs.query, e);
|
const nowMatches = queryMatches(obs.query, e);
|
||||||
|
|
@ -167,17 +248,12 @@ export class ObservableLayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
case "destroyed": {
|
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 }[] = [];
|
const removed: { source: Entity; target: Entity }[] = [];
|
||||||
for (const key of obs.edges) {
|
for (const key of obs.edges) {
|
||||||
const [si, ti] = key.split(":").map(Number);
|
const [si, ti] = key.split(":").map(Number);
|
||||||
const idx = event.entity & 0xfffff;
|
const idx = event.entity & 0xfffff;
|
||||||
if (si === idx || ti === idx) {
|
if (si === idx || ti === idx) {
|
||||||
removed.push({
|
removed.push({ source: si as Entity, target: ti as Entity });
|
||||||
source: si as Entity,
|
|
||||||
target: ti as Entity,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const r of removed) {
|
for (const r of removed) {
|
||||||
|
|
@ -199,6 +275,7 @@ export class ObservableLayer {
|
||||||
o.matched.clear();
|
o.matched.clear();
|
||||||
}
|
}
|
||||||
this._observers = [];
|
this._observers = [];
|
||||||
|
this._compIndex.clear();
|
||||||
|
|
||||||
for (const o of this._relObservers) {
|
for (const o of this._relObservers) {
|
||||||
o.subject.complete();
|
o.subject.complete();
|
||||||
|
|
@ -211,6 +288,7 @@ export class ObservableLayer {
|
||||||
this.events$.complete();
|
this.events$.complete();
|
||||||
for (const o of this._observers) o.subject.complete();
|
for (const o of this._observers) o.subject.complete();
|
||||||
this._observers = [];
|
this._observers = [];
|
||||||
|
this._compIndex.clear();
|
||||||
for (const o of this._relObservers) o.subject.complete();
|
for (const o of this._relObservers) o.subject.complete();
|
||||||
this._relObservers = [];
|
this._relObservers = [];
|
||||||
}
|
}
|
||||||
|
|
@ -218,6 +296,9 @@ export class ObservableLayer {
|
||||||
|
|
||||||
// ── Helpers ─────────────────────────────────────
|
// ── Helpers ─────────────────────────────────────
|
||||||
|
|
||||||
|
/** Sentinel key for observers that must be notified on spawn/destroy. */
|
||||||
|
const ANY_KEY = Symbol("any");
|
||||||
|
|
||||||
function queriesEqual(a: Query, b: Query): boolean {
|
function queriesEqual(a: Query, b: Query): boolean {
|
||||||
if (a.with.length !== b.with.length) return false;
|
if (a.with.length !== b.with.length) return false;
|
||||||
if (a.not.length !== b.not.length) return false;
|
if (a.not.length !== b.not.length) return false;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue