mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 19:46:43 +00:00
60f69a6408
Begins work on #3207 ## Description: This PR is the first optimization slice for #3207: it removes O(n) render lookups in `StructureIconsLayer` by replacing array-first render state with a unit-id keyed map, and tightens hot-path execution to reduce per-tick allocations. ### What changed - Refactored render state from array-first to `rendersByUnitId: Map<number, StructureRenderInfo>`. - Replaced O(n) lookup/delete paths with O(1) `Map#get` / `Map#delete`. - Replaced `seenUnits` object-identity tracking with `seenUnitIds: Set<number>`. - Removed `tick()` array/closure chain (`map(...).forEach(...)`) and switched to index-based loop. - Reduced ghost-path allocation pressure by reusing a layer-level `Set` for connected ally IDs instead of allocating `filter` + `map` + `new Set` per ghost query. - Added dirty-flag caching for structure visibility focus (`visibilityStateDirty`) so expensive visibility-state scans recompute only when toggles change. ### Performance validation (before/after) Benchmark added: `tests/perf/StructureIconsLayerLookupPerf.ts` Command: `npm run perf` Observed result: - `StructureIconsLayer BEFORE (array O(n) lookup/delete) x 0.33 ops/sec ±13.28%` - `StructureIconsLayer AFTER (unit-id map O(1) lookup/delete) x 95.65 ops/sec ±2.46%` - Fastest implementation: AFTER (unit-id map) #### Profiler screenshots are too noisy to be useful for such a focused change ### Verification - `npx tsc --noEmit` ✅ - `npx eslint src/client/graphics/layers/StructureIconsLayer.ts tests/perf/StructureIconsLayerLookupPerf.ts` ✅ - `npm run perf` ✅ ## Please complete the following: ~~- [ ] I have added screenshots for all UI updates~~ ~~- [ ] I process any text displayed to the user through translateText() and I've added it to the en.json file~~ - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: skigim
110 lines
3.0 KiB
TypeScript
110 lines
3.0 KiB
TypeScript
import Benchmark from "benchmark";
|
|
|
|
const STRUCTURE_COUNT = 50000;
|
|
const LOOKUP_COUNT = 50000;
|
|
const UPGRADE_LOOKUP_COUNT = 5000;
|
|
|
|
interface StructureRenderSample {
|
|
unitId: number;
|
|
ownerId: number;
|
|
level: number;
|
|
}
|
|
|
|
const rendersArray: StructureRenderSample[] = Array.from(
|
|
{ length: STRUCTURE_COUNT },
|
|
(_, index) => ({
|
|
unitId: index + 1,
|
|
ownerId: (index % 5) + 1,
|
|
level: (index % 4) + 1,
|
|
}),
|
|
);
|
|
|
|
const rendersMap = new Map<number, StructureRenderSample>();
|
|
for (const render of rendersArray) {
|
|
rendersMap.set(render.unitId, render);
|
|
}
|
|
|
|
const activeLookupIds = Array.from(
|
|
{ length: LOOKUP_COUNT },
|
|
(_, index) => ((index * 97) % STRUCTURE_COUNT) + 1,
|
|
);
|
|
|
|
const inactiveLookupIds = Array.from(
|
|
{ length: LOOKUP_COUNT },
|
|
(_, index) => ((index * 193) % STRUCTURE_COUNT) + 1,
|
|
);
|
|
|
|
const canUpgradeIds = Array.from(
|
|
{ length: UPGRADE_LOOKUP_COUNT },
|
|
(_, index) => ((index * 389) % STRUCTURE_COUNT) + 1,
|
|
);
|
|
|
|
const myOwnerId = 3;
|
|
const results: string[] = [];
|
|
|
|
new Benchmark.Suite()
|
|
.add("StructureIconsLayer BEFORE (array O(n) lookup/delete)", () => {
|
|
const localRenders = rendersArray.map((render) => ({ ...render }));
|
|
|
|
for (const unitId of activeLookupIds) {
|
|
const render = localRenders.find((entry) => entry.unitId === unitId);
|
|
if (render) {
|
|
render.level = render.level + 1;
|
|
}
|
|
}
|
|
|
|
for (const canUpgradeId of canUpgradeIds) {
|
|
const potentialUpgrade = localRenders.find(
|
|
(entry) => entry.unitId === canUpgradeId && entry.ownerId === myOwnerId,
|
|
);
|
|
if (potentialUpgrade) {
|
|
potentialUpgrade.level = potentialUpgrade.level + 1;
|
|
}
|
|
}
|
|
|
|
for (const unitId of inactiveLookupIds) {
|
|
const index = localRenders.findIndex((entry) => entry.unitId === unitId);
|
|
if (index !== -1) {
|
|
localRenders.splice(index, 1);
|
|
}
|
|
}
|
|
})
|
|
.add("StructureIconsLayer AFTER (unit-id map O(1) lookup/delete)", () => {
|
|
const localRenders = new Map<number, StructureRenderSample>();
|
|
for (const [unitId, render] of rendersMap) {
|
|
localRenders.set(unitId, { ...render });
|
|
}
|
|
|
|
for (const unitId of activeLookupIds) {
|
|
const render = localRenders.get(unitId);
|
|
if (render) {
|
|
render.level = render.level + 1;
|
|
}
|
|
}
|
|
|
|
for (const canUpgradeId of canUpgradeIds) {
|
|
const potentialUpgrade = localRenders.get(canUpgradeId);
|
|
if (potentialUpgrade && potentialUpgrade.ownerId === myOwnerId) {
|
|
potentialUpgrade.level = potentialUpgrade.level + 1;
|
|
}
|
|
}
|
|
|
|
for (const unitId of inactiveLookupIds) {
|
|
localRenders.delete(unitId);
|
|
}
|
|
})
|
|
.on("cycle", (event: Benchmark.Event) => {
|
|
results.push(String(event.target));
|
|
})
|
|
.on("complete", function () {
|
|
console.log("\n=== StructureIconsLayer Lookup Benchmark Results ===");
|
|
|
|
for (const result of results) {
|
|
console.log(result);
|
|
}
|
|
|
|
const fastest = this.filter("fastest").map("name");
|
|
console.log(`\nFastest implementation: ${fastest.join(", ")}`);
|
|
})
|
|
.run({ async: true });
|