From 31cd725ff2476c77a77b2867dfbb256d8abc951c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:44:13 +0000 Subject: [PATCH 1/4] Initial plan From 4c8e6f7163b55adb80b8bc623a69333a606e4882 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:48:28 +0000 Subject: [PATCH 2/4] Optimize query caching, system enable/disable, and reduce allocations Co-authored-by: Byloth <14953974+Byloth@users.noreply.github.com> --- src/query/manager.ts | 86 +++++++++++++++++++++++++------------------- src/world.ts | 21 +++++++++-- 2 files changed, 68 insertions(+), 39 deletions(-) diff --git a/src/query/manager.ts b/src/query/manager.ts index 69aa866..e2bde3d 100644 --- a/src/query/manager.ts +++ b/src/query/manager.ts @@ -8,20 +8,35 @@ import type Entity from "../entity.js"; import type Component from "../component.js"; import type { ComponentType, Instances } from "../types.js"; +// Cache for query keys to avoid regenerating them +const _queryKeyCache = new WeakMap(); + function _getQueryKey(types: ComponentType[]): string { + const cached = _queryKeyCache.get(types); + if (cached !== undefined) { return cached; } + const length = types.length; - if (length === 1) { return `${types[0].Id}`; } + let key: string; - const ids = new Array(length); - for (let i = 0; i < length; i += 1) + if (length === 1) { - ids[i] = types[i].Id; + key = `${types[0].Id}`; } + else + { + const ids = new Array(length); + for (let i = 0; i < length; i += 1) + { + ids[i] = types[i].Id; + } - ids.sort((a, b) => (a - b)); + ids.sort((a, b) => (a - b)); + key = ids.join(","); + } - return ids.join(","); + _queryKeyCache.set(types, key); + return key; } function _setMaskBit(mask: number[], typeId: number): void @@ -43,14 +58,21 @@ function _unsetMaskBit(mask: number[], typeId: number): void mask[index] &= ~(bit); } -function _createMask(types: ComponentType[]): number[] +// Cache for query masks to avoid recreating them +const _queryMaskCache = new Map(); + +function _createMask(key: string, types: ComponentType[]): number[] { + const cached = _queryMaskCache.get(key); + if (cached !== undefined) { return cached; } + const mask: number[] = []; for (const type of types) { _setMaskBit(mask, type.Id); } + _queryMaskCache.set(key, mask); return mask; } function _matchMask(entityMask: number[], queryMask: number[]): boolean @@ -66,6 +88,19 @@ function _matchMask(entityMask: number[], queryMask: number[]): boolean return true; } +// Helper function to gather components from an entity +// This centralizes the component gathering logic to reduce code duplication +function _gatherComponents(entity: Entity, types: ComponentType[], length: number): Component[] +{ + const components = new Array(length); + for (let i = 0; i < length; i += 1) + { + components[i] = entity.components.get(types[i])!; + } + + return components; +} + // eslint-disable-next-line @typescript-eslint/no-empty-object-type export default class QueryManager = { }> { @@ -121,12 +156,7 @@ export default class QueryManager = { }> if (!(_matchMask(entityMask, queryMask))) { continue; } const types = this._keyTypes.get(key)!; - const length = types.length; - const components = new Array(length); - for (let i = 0; i < length; i += 1) - { - components[i] = entity.components.get(types[i])!; - } + const components = _gatherComponents(entity, types, types.length); view.set(entity, components); } @@ -199,7 +229,7 @@ export default class QueryManager = { }> const view = this._views.get(key) as QueryView | undefined; if (view) { return view.components[0]; } - const queryMask = _createMask(types); + const queryMask = _createMask(key, types); const length = types.length; for (const entity of this._entities.values()) @@ -209,13 +239,7 @@ export default class QueryManager = { }> const entityMask = this._entityMasks.get(entity); if (!(entityMask) || !(_matchMask(entityMask, queryMask))) { continue; } - const components = new Array(length); - for (let i = 0; i < length; i += 1) - { - components[i] = entity.components.get(types[i])!; - } - - return components as R; + return _gatherComponents(entity, types, length) as R; } return undefined; @@ -235,7 +259,7 @@ export default class QueryManager = { }> const entities = this._entities; const entityMasks = this._entityMasks; - const queryMask = _createMask(types); + const queryMask = _createMask(key, types); const length = types.length; return new SmartIterator(function* (): Generator @@ -247,13 +271,7 @@ export default class QueryManager = { }> const entityMask = entityMasks.get(entity); if (!(entityMask) || !(_matchMask(entityMask, queryMask))) { continue; } - const components = new Array(length); - for (let i = 0; i < length; i += 1) - { - components[i] = entity.components.get(types[i])!; - } - - yield components as R; + yield _gatherComponents(entity, types, length) as R; } }); } @@ -271,7 +289,7 @@ export default class QueryManager = { }> let view = this._views.get(key) as QueryView | undefined; if (view) { return view; } - const queryMask = _createMask(types); + const queryMask = _createMask(key, types); const length = types.length; view = new QueryView(); @@ -282,13 +300,7 @@ export default class QueryManager = { }> const entityMask = this._entityMasks.get(entity); if (!(entityMask) || !(_matchMask(entityMask, queryMask))) { continue; } - const components = new Array(length); - for (let i = 0; i < length; i += 1) - { - components[i] = entity.components.get(types[i])!; - } - - view.set(entity, components as R); + view.set(entity, _gatherComponents(entity, types, length) as R); } this._views.set(key, view); diff --git a/src/world.ts b/src/world.ts index fdc1028..44596b2 100644 --- a/src/world.ts +++ b/src/world.ts @@ -26,6 +26,7 @@ export default class World = { }> private readonly _systems: Map; private readonly _enabledSystems: System[]; + private readonly _enabledSystemsIndex: Map; // O(1) lookup for disable public get systems(): ReadonlyMap { return this._systems; } private readonly _contexts: Map>; @@ -56,6 +57,7 @@ export default class World = { }> this._systems = new Map(); this._enabledSystems = []; + this._enabledSystemsIndex = new Map(); this._contexts = new Map(); this._dependencies = new Map(); @@ -106,14 +108,28 @@ export default class World = { }> else { left = middle + 1; } } + // Update indices for all systems after insertion point + for (let i = left; i < this._enabledSystems.length; i += 1) + { + this._enabledSystemsIndex.set(this._enabledSystems[i], i + 1); + } + this._enabledSystems.splice(left, 0, system); + this._enabledSystemsIndex.set(system, left); } private _disableSystem(system: System): void { - const index = this._enabledSystems.indexOf(system); - if (index === -1) { return; } + const index = this._enabledSystemsIndex.get(system); + if (index === undefined) { return; } this._enabledSystems.splice(index, 1); + this._enabledSystemsIndex.delete(system); + + // Update indices for all systems after removal point + for (let i = index; i < this._enabledSystems.length; i += 1) + { + this._enabledSystemsIndex.set(this._enabledSystems[i], i); + } } private _addDependency(system: System, type: ResourceType): Resource @@ -528,6 +544,7 @@ export default class World = { }> this._systems.clear(); this._enabledSystems.length = 0; + this._enabledSystemsIndex.clear(); try { From 2a47859fa2fc58fa910cdf1d16f25faae1b35767 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:49:48 +0000 Subject: [PATCH 3/4] Add component-to-entity index for faster single-component queries Co-authored-by: Byloth <14953974+Byloth@users.noreply.github.com> --- src/query/manager.ts | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/query/manager.ts b/src/query/manager.ts index e2bde3d..fe258ee 100644 --- a/src/query/manager.ts +++ b/src/query/manager.ts @@ -110,6 +110,9 @@ export default class QueryManager = { }> private readonly _queryMasks: Map; private readonly _entityMasks: WeakMap; + // Component-to-entity index for faster single-component queries + private readonly _componentIndex: Map>; + private readonly _entities: ReadonlyMap; private readonly _views: Map>; @@ -121,6 +124,8 @@ export default class QueryManager = { }> this._queryMasks = new Map(); this._entityMasks = new WeakMap(); + this._componentIndex = new Map(); + this._entities = entities; this._views = new Map(); } @@ -144,6 +149,15 @@ export default class QueryManager = { }> const entityMask = this._getEntityMask(entity); _setMaskBit(entityMask, type.Id); + // Update component index + let entities = this._componentIndex.get(type); + if (!(entities)) + { + entities = new Set(); + this._componentIndex.set(type, entities); + } + entities.add(entity); + const keys = this._typeKeys.get(type); if (!(keys)) { return; } @@ -168,6 +182,14 @@ export default class QueryManager = { }> const entityMask = this._entityMasks.get(entity); if (entityMask) { _unsetMaskBit(entityMask, type.Id); } + // Update component index + const entities = this._componentIndex.get(type); + if (entities) + { + entities.delete(entity); + if (entities.size === 0) { this._componentIndex.delete(type); } + } + const keys = this._typeKeys.get(type); if (!(keys)) { return; } @@ -205,12 +227,17 @@ export default class QueryManager = { }> const view = this._views.get(key) as QueryView<[R]> | undefined; if (view) { return view.components[0][0]; } - for (const entity of this._entities.values()) + // Use component index for faster lookup + const entities = this._componentIndex.get(type); + if (entities) { - if (!(entity.isEnabled)) { continue; } + for (const entity of entities) + { + if (!(entity.isEnabled)) { continue; } - const component = entity.components.get(type); - if (component?.isEnabled) { return component as R; } + const component = entity.components.get(type); + if (component?.isEnabled) { return component as R; } + } } return undefined; @@ -321,5 +348,6 @@ export default class QueryManager = { }> this._keyTypes.clear(); this._typeKeys.clear(); + this._componentIndex.clear(); } } From a8253bc7cdc0b2402f3bdd2d74d80e13bab9b28f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:51:58 +0000 Subject: [PATCH 4/4] Add performance benchmark tests to validate improvements Co-authored-by: Byloth <14953974+Byloth@users.noreply.github.com> --- tests/performance.test.ts | 189 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 tests/performance.test.ts diff --git a/tests/performance.test.ts b/tests/performance.test.ts new file mode 100644 index 0000000..b53b6fd --- /dev/null +++ b/tests/performance.test.ts @@ -0,0 +1,189 @@ +import { beforeEach, describe, expect, it } from "vitest"; + +import { Random } from "@byloth/core"; + +import { Component, Entity, System, World } from "../src/index.js"; +import type { ComponentType } from "../src/index.js"; + +const NUM_ENTITIES = 5000; +const NUM_COMPONENT_TYPES = 32; +const NUM_ITERATIONS = 1000; + +// Create component types +const ComponentTypes: ComponentType[] = []; +for (let i = 0; i < NUM_COMPONENT_TYPES; i += 1) +{ + const ComponentClass = class extends Component + { + public value = Math.random(); + }; + + Object.defineProperty(ComponentClass, "name", { value: `PerfComponent${i}` }); + ComponentTypes.push(ComponentClass); +} + +describe("Performance Benchmarks", () => +{ + let _world: World; + let _entities: Entity[]; + + beforeEach(() => + { + _world = new World(); + _entities = []; + + // Create entities with random components + for (let i = 0; i < NUM_ENTITIES; i += 1) + { + const entity = new Entity(); + const numComponents = Random.Integer(3, 10); + const selectedTypes = Random.Sample(ComponentTypes, numComponents); + + for (const ComponentType of selectedTypes) + { + entity.addComponent(new ComponentType()); + } + + _world.addEntity(entity); + _entities.push(entity); + } + }); + + it("Should handle repeated single-component queries efficiently", () => + { + const startTime = performance.now(); + + for (let i = 0; i < NUM_ITERATIONS; i += 1) + { + const type = ComponentTypes[i % ComponentTypes.length]; + const component = _world.getFirstComponent(type); + expect(component).toBeDefined(); + } + + const duration = performance.now() - startTime; + + // This should be fast due to component index + expect(duration).toBeLessThan(100); // Should complete in < 100ms + }); + + it("Should handle repeated multi-component queries efficiently", () => + { + const startTime = performance.now(); + + for (let i = 0; i < NUM_ITERATIONS; i += 1) + { + const type1 = ComponentTypes[i % ComponentTypes.length]; + const type2 = ComponentTypes[(i + 1) % ComponentTypes.length]; + _world.getFirstComponents(type1, type2); + } + + const duration = performance.now() - startTime; + + // This should be fast due to query caching + expect(duration).toBeLessThan(150); // Should complete in < 150ms + }); + + it("Should handle view creation and iteration efficiently", () => + { + const type1 = ComponentTypes[0]; + const type2 = ComponentTypes[1]; + const type3 = ComponentTypes[2]; + + const startTime = performance.now(); + + // First call creates view + const view = _world.getComponentView(type1, type2, type3); + + // Subsequent calls should return cached view + for (let i = 0; i < 100; i += 1) + { + const cachedView = _world.getComponentView(type1, type2, type3); + expect(cachedView).toBe(view); // Same reference + } + + // Iterate through components + let count = 0; + for (const components of view.components) + { + expect(components).toHaveLength(3); + count += 1; + } + + const duration = performance.now() - startTime; + + expect(count).toBeGreaterThan(0); + expect(duration).toBeLessThan(50); // Should complete in < 50ms + }); + + it("Should handle system enable/disable efficiently", () => + { + // Create systems with different priorities and unique types + const systems = []; + for (let i = 0; i < 100; i += 1) + { + class TestSystem extends System + { + public constructor() + { + super(Random.Integer(0, 10), true); + } + public override update(): void { /* noop */ } + } + const system = new TestSystem(); + systems.push(system); + _world.addSystem(system); + } + + const startTime = performance.now(); + + // Toggle systems multiple times + for (let i = 0; i < 1000; i += 1) + { + const system = systems[i % systems.length]; + if (system.isEnabled) + { + system.disable(); + } + else + { + system.enable(); + } + } + + const duration = performance.now() - startTime; + + // This should be fast due to O(1) lookup with index + expect(duration).toBeLessThan(50); // Should complete in < 50ms + }); + + it("Should handle world update efficiently", () => + { + // Create systems with unique types + let updateCount = 0; + for (let i = 0; i < 50; i += 1) + { + class TestSystem extends System + { + public constructor(priority: number) + { + super(priority, true); + } + public override update(): void { updateCount += 1; } + } + _world.addSystem(new TestSystem(i)); + } + + const startTime = performance.now(); + + // Run many update frames + for (let i = 0; i < 1000; i += 1) + { + _world.update(0.016); // 60 FPS + } + + const duration = performance.now() - startTime; + + expect(updateCount).toBe(50 * 1000); // 50 systems * 1000 frames + expect(duration).toBeLessThan(100); // Should complete in < 100ms + }); +});