diff --git a/bench/churn-sweep.csv b/bench/churn-sweep.csv deleted file mode 100644 index 2d57e9f..0000000 --- a/bench/churn-sweep.csv +++ /dev/null @@ -1,22 +0,0 @@ -structure,size,latency_ns_avg,latency_ns_med -MapArray,100,0.03,0.03 -Dictionary,100,0.00,0.00 -Map,100,0.00,0.00 -MapArray,250,0.05,0.05 -Dictionary,250,0.00,0.00 -Map,250,0.00,0.00 -MapArray,500,0.09,0.09 -Dictionary,500,0.00,0.00 -Map,500,0.00,0.00 -MapArray,999,0.16,0.16 -Dictionary,999,0.01,0.00 -Map,999,0.00,0.00 -MapArray,2500,0.42,0.42 -Dictionary,2500,0.01,0.01 -Map,2500,0.01,0.01 -MapArray,5000,0.81,0.80 -Dictionary,5000,0.02,0.02 -Map,5000,0.01,0.01 -MapArray,10000,1.66,1.66 -Dictionary,10000,0.05,0.04 -Map,10000,0.02,0.02 \ No newline at end of file diff --git a/bench/churn-sweep.ts b/bench/churn-sweep.ts deleted file mode 100644 index a34a18a..0000000 --- a/bench/churn-sweep.ts +++ /dev/null @@ -1,89 +0,0 @@ -// bench/churn-sweep.ts -import { Bench } from "tinybench"; -import { type Box, box } from "../engine/lib/box.ts"; -import { - createDictionary, - deleteEntry, - setEntry, -} from "../engine/lib/Dictionary.ts"; -import MapArray from "../engine/lib/MapArray.ts"; - -const SIZES = [100, 250, 500, 999, 2500, 5000, 10000]; -const CHURN_N = 100; -const churnKeys = Array.from({ length: CHURN_N }, (_, i) => `churn-${i}`); - -let sink = 0; - -function makeBaseData(size: number): Array<[string, Box]> { - const data: Array<[string, Box]> = []; - let i = size; - while (i-- > 0) data.push([String(i), box(i)]); - return data; -} - -type Row = { - structure: string; - size: number; - latencyNsAvg: number; - latencyNsMed: number; -}; -const rows: Row[] = []; - -for (const size of SIZES) { - const baseData = makeBaseData(size); - - const ma = new MapArray(structuredClone(baseData)); - const dict = createDictionary(structuredClone(baseData)); - const map = new Map(structuredClone(baseData)); - - const bench = new Bench({ name: `churn @ size=${size}`, time: 500 }); - - bench - .add("MapArray", () => { - for (const key of churnKeys) ma.set(key, box(1)); - for (const key of churnKeys) ma.delete(key); - sink += ma.entries().length; - }) - .add("Dictionary", () => { - for (const key of churnKeys) setEntry(dict, [key, box(1)]); - for (const key of churnKeys) deleteEntry(dict, key); - sink += dict.entries.length; - }) - .add("Map", () => { - for (const key of churnKeys) map.set(key, box(1)); - for (const key of churnKeys) map.delete(key); - sink += map.size; - }); - - await bench.run(); - - for (const task of bench.tasks) { - const res = task.result; - if (!res) continue; - rows.push({ - structure: task.name, - size, - latencyNsAvg: res.latency.mean, - latencyNsMed: res.latency.p50 ?? res.latency.mean, - }); - } - - console.log(`-- size=${size} done --`); - console.table(bench.table()); -} - -// CSV output -const header = "structure,size,latency_ns_avg,latency_ns_med"; -const csvLines = rows.map( - (r) => - `${r.structure},${r.size},${r.latencyNsAvg.toFixed(2)},${r.latencyNsMed.toFixed(2)}`, -); -const csv = [header, ...csvLines].join("\n"); - -(await Deno.writeTextFile?.("churn-sweep.csv", csv)) ?? - (await import("node:fs/promises")).then((fs) => - fs.writeFile("churn-sweep.csv", csv), - ); - -console.log("\nWrote churn-sweep.csv"); -console.log("sink (ignore):", sink); diff --git a/bench/churn.ts b/bench/churn.ts deleted file mode 100644 index 45f07bc..0000000 --- a/bench/churn.ts +++ /dev/null @@ -1,77 +0,0 @@ -// bench/churn.ts -import { Bench } from "tinybench"; -import { type Box, box } from "./../engine/lib/box.ts"; -import { - createDictionary, - deleteEntry, - getEntry, - setEntry, -} from "./../engine/lib/Dictionary.ts"; -import MapArray from "./../engine/lib/MapArray.ts"; - -const bench = new Bench({ name: "churn", time: 1000 }); - -const SIZE = 999; -const baseData: Array<[string, Box]> = []; -{ - let i = SIZE; - while (i-- > 0) baseData.push([String(i), box(i)]); -} - -// Churn keys: a rotating window of "new" entities not in the base set, -// so every set/delete pair actually does real insert+remove work -// rather than repeatedly hitting the same already-resident key. -const CHURN_N = 100; -const churnKeys = Array.from({ length: CHURN_N }, (_, i) => `churn-${i}`); - -let sink = 0; // anti-DCE: something observable reads this later - -function freshMapArray() { - return new MapArray(structuredClone(baseData)); -} -function freshDictionary() { - return createDictionary(structuredClone(baseData)); -} -function freshMap() { - return new Map(structuredClone(baseData)); -} - -// Each task rebuilds nothing per-iteration except doing insert+remove -// on a fixed churn window against a persistent SIZE-element base structure — -// this mirrors "N entities steady-state, M entering/leaving per frame". -const ma = freshMapArray(); -const dict = freshDictionary(); -const map = freshMap(); - -bench - .add("MapArray churn (set+delete)", () => { - for (const key of churnKeys) { - ma.set(key, box(1)); - } - for (const key of churnKeys) { - ma.delete(key); - } - sink += ma.entries().length; - }) - .add("Dictionary churn (set+delete)", () => { - for (const key of churnKeys) { - setEntry(dict, [key, box(1)]); - } - for (const key of churnKeys) { - deleteEntry(dict, key); - } - sink += dict.entries.length; - }) - .add("Map churn (set+delete)", () => { - for (const key of churnKeys) { - map.set(key, box(1)); - } - for (const key of churnKeys) { - map.delete(key); - } - sink += map.size; - }); - -await bench.run(); -console.table(bench.table()); -console.log("sink (ignore):", sink); diff --git a/bench/index.ts b/bench/index.ts index ad638a9..72da475 100644 --- a/bench/index.ts +++ b/bench/index.ts @@ -1,53 +1,11 @@ import { Bench } from "tinybench"; -import { type Box, box } from "./../engine/lib/box.ts"; -import { - createDictionary, - forEntry, - getEntry, -} from "./../engine/lib/Dictionary.ts"; -import MapArray from "./../engine/lib/MapArray.ts"; -const bench = new Bench({ name: "MapArray get", time: 3000 }); +const bench = new Bench({ name: "", time: 1000 }); let sink = 0; -const testData: Array<[string, Box]> = []; -let i = 999; -while (i-- > 0) { - testData.push([String(i), box(i)]); -} - -const ma = new MapArray(structuredClone(testData)); -const dict = createDictionary(structuredClone(testData)); -const map = new Map(structuredClone(testData)); - -bench - .add("MapArray iter/get reads", () => { - let sum = 0; - - for (const [key] of ma.entries()) { - sum += ma.get(key)!.value; - } - - sink += sum; - }) - .add("Dictionary iter/get reads", () => { - let sum = 0; - - forEntry(dict, (_, key) => { - sum += getEntry(dict, key)!.value; - }); - - sink += sum; - }) - .add("Map iter/get reads", () => { - let sum = 0; - - for (const [key] of map.entries()) { - sum += map.get(key)!.value; - } - - sink += sum; - }); +bench.add("", () => { + sink += 1; +}); await bench.run(); diff --git a/engine/Scene.ts b/engine/Scene.ts index a2da9bb..cb0d41f 100644 --- a/engine/Scene.ts +++ b/engine/Scene.ts @@ -4,9 +4,8 @@ import { entityProcessMap, } from "#/engine/Entity.ts"; import { type Graph, getNeighbours } from "#/engine/Graph.ts"; -import { box } from "#/engine/lib/box"; +import { type Box, box } from "#/engine/lib/box"; import { memoize } from "#/engine/lib/cache.ts"; -import MapArray from "#/engine/lib/MapArray.ts"; export type Process = (ctx: CanvasRenderingContext2D, delta: number) => void; export type PreProcess = () => void; @@ -24,7 +23,7 @@ export type SceneData = { entities: Array>; }; -export type ComponentsMap = Map>; +export type ComponentsMap = Map>>; export type Scene = { data: SceneData; @@ -113,7 +112,7 @@ const [loadScene, sceneCache] = memoize(async (name: string) => { const localEntityProcessMap = new Map(); sceneEntityProcessMap.set(name, localEntityProcessMap); - const componentsMap = new Map>(); + const componentsMap: ComponentsMap = new Map(); sceneComponentsMap.set(name, componentsMap); await Promise.all( @@ -138,7 +137,7 @@ const [loadScene, sceneCache] = memoize(async (name: string) => { if (componentsMap.has(key)) { componentsMap.get(key)!.set(id, boxedValue); } else { - componentsMap.set(key, new MapArray([[id, boxedValue]])); + componentsMap.set(key, new Map([[id, boxedValue]])); } } }, @@ -179,6 +178,6 @@ export async function setScene(name: string): Promise { } } -export default function (name: string): MapArray { - return currentScene.components.get(name) as MapArray; +export default function (name: string): Map> { + return currentScene.components.get(name) as Map>; } diff --git a/engine/lib/Dictionary.test.ts b/engine/lib/Dictionary.test.ts deleted file mode 100644 index 19fee93..0000000 --- a/engine/lib/Dictionary.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { expect, test } from "vitest"; -import { - createDictionary, - type Dictionary, - deleteEntrySave, - forEntry, - getEntry, - setEntrySave, -} from "./Dictionary.ts"; - -test("Dictionary", () => { - const dict: Dictionary = createDictionary([ - ["a", 1], - ["b", 2], - ["c", 3], - ]); - setEntrySave(dict, "foo", 42); - setEntrySave(dict, "bar", 0); - deleteEntrySave(dict, "bar"); - - expect(getEntry(dict, "foo")).toBe(42); - expect(getEntry(dict, "bar")).toBe(undefined); - expect(dict.indices.size).toBe(dict.entries.length); - - let sum = 0; - forEntry(dict, (value) => { - sum += value; - }); - expect(sum).toBe(48); -}); - -test("Dictionary don't mutate, when used 'save', while iterating", () => { - const dict: Dictionary = createDictionary([ - ["a", 1], - ["b", 2], - ["c", 3], - ]); - - let sum = 0; - forEntry(dict, (value, _, index) => { - if (index < 1) { - setEntrySave(dict, "d", 4); - setEntrySave(dict, "e", 5); - } - - sum += value; - }); - - expect(sum).toBe(6); - expect(dict.indices.size === 5).toEqual(dict.entries.length === 5); - - sum = 0; - forEntry(dict, (value, _, index) => { - if (index < 1) { - deleteEntrySave(dict, "d"); - deleteEntrySave(dict, "e"); - } - - sum += value; - }); - - expect(sum).toBe(15); - expect(dict.indices.size === 3).toEqual(dict.entries.length === 3); -}); diff --git a/engine/lib/Dictionary.ts b/engine/lib/Dictionary.ts deleted file mode 100644 index bd28e9f..0000000 --- a/engine/lib/Dictionary.ts +++ /dev/null @@ -1,95 +0,0 @@ -export type Dictionary = { - entries: Array<[K, V]>; - indices: Map; - isIterating: boolean; - deleteQueue: Array; - setQueue: Array<[K, V]>; -}; - -export function createDictionary( - iterable?: Iterable<[K, V]>, -): Dictionary { - const entries = iterable ? [...iterable] : []; - const indices = new Map(entries.map(([k], i) => [k, i])); - - return { - entries, - indices, - isIterating: false, - deleteQueue: [], - setQueue: [], - }; -} - -export function deleteEntry(dict: Dictionary, key: K): void { - const index = dict.indices.get(key); - if (index === undefined) { - return; - } - - const last = dict.entries.length - 1; - if (index !== last) { - dict.entries[index] = dict.entries[last]; - dict.indices.set(dict.entries[index][0], index); - } - - dict.entries.pop(); - dict.indices.delete(key); -} - -export function deleteEntrySave(dict: Dictionary, key: K): void { - if (dict.isIterating) { - dict.deleteQueue.push(); - } else { - deleteEntry(dict, key); - } -} - -export function setEntry( - dict: Dictionary, - [key, value]: [K, V], -): void { - const index = dict.indices.get(key); - if (index === undefined) { - dict.indices.set(key, dict.entries.length); - dict.entries.push([key, value]); - } else { - dict.entries[index][1] = value; - } -} - -export function setEntrySave( - dict: Dictionary, - key: K, - value: V, -): void { - if (dict.isIterating) { - dict.setQueue.push([key, value]); - } else { - setEntry(dict, [key, value]); - } -} - -export function getEntry(dict: Dictionary, key: K): V | undefined { - const index = dict.indices.get(key); - return index === undefined ? undefined : dict.entries[index][1]; -} - -export function forEntry( - dict: Dictionary, - callback: (value: V, key: K, index: number) => void, -): void { - dict.isIterating = true; - for (let i = 0; i < dict.entries.length; i++) { - callback(dict.entries[i][1], dict.entries[i][0], i); - } - dict.isIterating = false; - - while (dict.deleteQueue.length > 0) { - deleteEntry(dict, dict.deleteQueue.pop()!); - } - - while (dict.setQueue.length > 0) { - setEntry(dict, dict.setQueue.pop()!); - } -} diff --git a/engine/lib/MapArray.test.ts b/engine/lib/MapArray.test.ts deleted file mode 100644 index 238390a..0000000 --- a/engine/lib/MapArray.test.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { expect, test } from "vitest"; -import { box } from "./box.ts"; -import MapArray from "./MapArray.ts"; - -test("MapArray", () => { - const mapArray = new MapArray([ - ["a", box(1)], - ["b", box(2)], - ["c", box(3)], - ]); - mapArray.set("foo", box(42)); - mapArray.set("bar", box(0)); - mapArray.delete("bar"); - - expect(mapArray.get("foo")!.value).toBe(42); - expect(mapArray.get("bar")).toBe(undefined); - expect(mapArray.map.size).toBe(mapArray.array.length); - expect( - mapArray.entries().reduce( - (a, c) => { - a[1].value = a[1].value + c[1].value; - return a; - }, - ["_", box(0)], - )[1].value, - ).toBe(48); -}); diff --git a/engine/lib/MapArray.ts b/engine/lib/MapArray.ts deleted file mode 100644 index 633b9a2..0000000 --- a/engine/lib/MapArray.ts +++ /dev/null @@ -1,37 +0,0 @@ -import type { Box } from "./box.ts"; - -export default class MapArray { - map: Map>; - array: Array<[K, Box]>; - - constructor(iterable?: Iterable<[K, Box]>) { - this.map = new Map(iterable); - this.array = iterable ? [...iterable] : []; - } - - delete(key: K): void { - if (this.map.delete(key)) { - this.array[this.array.findIndex((e) => e[0] === key)] = - this.array[this.array.length - 1]; - this.array.pop(); - } - } - - set(key: K, value: Box): void { - if (this.map.has(key)) { - this.array.find((e) => e[0] === key)![1] = value; - } else { - this.array.push([key, value]); - } - - this.map.set(key, value); - } - - get(key: K): Box | undefined { - return this.map.get(key); - } - - entries(): Array<[K, Box]> { - return this.array; - } -} diff --git a/engine/system/drawSprites.ts b/engine/system/drawSprites.ts index f42b353..72bb160 100644 --- a/engine/system/drawSprites.ts +++ b/engine/system/drawSprites.ts @@ -13,13 +13,13 @@ sceneChangeSet.add((sceneData) => { drawables.length = 0; const sprites = [ - ...sceneComponentsMap.get(sceneData.name)!.get("sprite")!.map.values(), + ...sceneComponentsMap.get(sceneData.name)!.get("sprite")!.values(), ] as Array>; sceneComponentsMap .get(sceneData.name)! .get("position")! - .map.values() + .values() .forEach((box, index) => { drawables.push({ position: box as Box,