perf: track component and relationship counts per entity

Introduce component and relationship counters to optimize entity
destruction and component checks. This allows for short-circuiting
the destruction process for bare entities and provides an O(1)
check for `hasAnyComponent`.
This commit is contained in:
hypercross 2026-06-01 14:34:51 +08:00
parent 9e788b135b
commit 2fe9203be9
1 changed files with 27 additions and 4 deletions

View File

@ -21,6 +21,8 @@ export class World {
// ── Entity pools ────────────────────────────────── // ── Entity pools ──────────────────────────────────
private _generations: number[] = []; private _generations: number[] = [];
private _free: number[] = []; private _free: number[] = [];
private _componentCounts: number[] = [];
private _relCounts: number[] = [];
// ── Component storage ───────────────────────────── // ── Component storage ─────────────────────────────
private _components = new Map<symbol, SparseSet<any>>(); private _components = new Map<symbol, SparseSet<any>>();
@ -52,6 +54,8 @@ export class World {
if (this._free.length > 0) { if (this._free.length > 0) {
const idx = this._free.pop()!; const idx = this._free.pop()!;
const gen = this._generations[idx]; const gen = this._generations[idx];
this._componentCounts[idx] = 0;
this._relCounts[idx] = 0;
const e = makeEntity(idx, gen); const e = makeEntity(idx, gen);
this._emit({ type: "spawned", entity: e }); this._emit({ type: "spawned", entity: e });
return e; return e;
@ -59,6 +63,8 @@ export class World {
const idx = this._generations.length; const idx = this._generations.length;
this._generations.push(1); this._generations.push(1);
this._componentCounts.push(0);
this._relCounts.push(0);
const e = makeEntity(idx, 1); const e = makeEntity(idx, 1);
this._emit({ type: "spawned", entity: e }); this._emit({ type: "spawned", entity: e });
return e; return e;
@ -69,6 +75,14 @@ export class World {
const idx = entityIndex(entity); const idx = entityIndex(entity);
if (!this._isAlive(idx, entity)) return; if (!this._isAlive(idx, entity)) return;
// Short-circuit: truly bare entities have nothing to clean up
if (this._componentCounts[idx] === 0 && this._relCounts[idx] === 0) {
this._generations[idx]++;
this._free.push(idx);
this._emit({ type: "destroyed", entity });
return;
}
// Clean relationships before components // Clean relationships before components
for (const [key] of this._relForward) { for (const [key] of this._relForward) {
// Entity as source // Entity as source
@ -99,6 +113,8 @@ export class World {
dirty.delete(idx); dirty.delete(idx);
} }
this._componentCounts[idx] = 0;
this._relCounts[idx] = 0;
this._generations[idx]++; this._generations[idx]++;
this._free.push(idx); this._free.push(idx);
@ -114,10 +130,7 @@ export class World {
hasAnyComponent(entity: Entity): boolean { hasAnyComponent(entity: Entity): boolean {
const idx = entityIndex(entity); const idx = entityIndex(entity);
if (!this._isAlive(idx, entity)) return false; if (!this._isAlive(idx, entity)) return false;
for (const store of this._components.values()) { return this._componentCounts[idx] > 0;
if (store.has(idx)) return true;
}
return false;
} }
// ── Component operations ────────────────────────── // ── Component operations ──────────────────────────
@ -131,9 +144,12 @@ export class World {
this._assertAlive(idx, entity); this._assertAlive(idx, entity);
const store = this._getOrCreateStore(def); const store = this._getOrCreateStore(def);
const existed = store.has(idx);
const value = { ...def.defaults, ...init }; const value = { ...def.defaults, ...init };
store.set(idx, value); store.set(idx, value);
if (!existed) this._componentCounts[idx]++;
this._emit({ type: "componentAdded", entity, component: def }); this._emit({ type: "componentAdded", entity, component: def });
return value; return value;
} }
@ -149,6 +165,7 @@ export class World {
this._dirty.get(def._key)?.delete(idx); this._dirty.get(def._key)?.delete(idx);
if (removed) { if (removed) {
this._componentCounts[idx]--;
this._emit({ type: "componentRemoved", entity, component: def }); this._emit({ type: "componentRemoved", entity, component: def });
} }
} }
@ -265,6 +282,9 @@ export class World {
} }
rev.add(si); rev.add(si);
this._relCounts[si]++;
this._relCounts[ti]++;
this._emit({ this._emit({
type: "relationshipAdded", type: "relationshipAdded",
source, source,
@ -570,6 +590,9 @@ export class World {
} }
} }
this._relCounts[si]--;
this._relCounts[ti]--;
this._emit({ this._emit({
type: "relationshipRemoved", type: "relationshipRemoved",
source, source,