diff --git a/bench/churn-sweep.csv b/bench/churn-sweep.csv new file mode 100644 index 0000000..2d57e9f --- /dev/null +++ b/bench/churn-sweep.csv @@ -0,0 +1,22 @@ +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 new file mode 100644 index 0000000..a34a18a --- /dev/null +++ b/bench/churn-sweep.ts @@ -0,0 +1,89 @@ +// 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 new file mode 100644 index 0000000..45f07bc --- /dev/null +++ b/bench/churn.ts @@ -0,0 +1,77 @@ +// 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);