Skip to content

Commit ef243be

Browse files
committed
chore: add benchmark comparisons
# Conflicts: # ROADMAP.md
1 parent 88d2046 commit ef243be

4 files changed

Lines changed: 295 additions & 4 deletions

File tree

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ CDN usage:
2525
- **Procedural:** 2D/3D Perlin noise, Simplex noise, Worley (cellular) noise
2626
- **Spatial:** Quadtree, AABB helpers, SAT polygon intersection, circle-ray intersection, swept AABB
2727
- **Search & Text:** Fuzzy search/scoring, Trie autocomplete, binary search, Levenshtein distance
28-
- **Data:** Diff (LCS), deep clone, groupBy
29-
- **Web performance:** Debounce, throttle, LRU cache, memoize, request deduplication helper
28+
- **Data:** Diff (LCS), deep clone, groupBy, JSON diff/patch helpers
29+
- **Web performance:** Debounce, throttle, LRU cache, memoize, request deduplication helper, virtual scrolling range calculator
3030
- **Graph:** BFS distance map, DFS traversal, topological sort
3131
- **Visual & Geometry:** Convex hull, line intersection, point-in-polygon, easing presets, Bezier helpers
3232
- **AI Behaviours:** Steering behaviours (seek, flee, arrive, pursue, wander), boids flocking update, behaviour trees
@@ -37,6 +37,7 @@ npm run lint # ESLint + TypeScript rules
3737
npm run typecheck # Strict TypeScript validation
3838
npm run build # Emits dist/ with ESM + .d.ts
3939
npm test # Vitest suite
40+
npm run benchmark # Compare algorithm variants locally
4041
```
4142

4243
Examples live under `examples/` and can be executed with `tsx`/`ts-node` or compiled for the browser. See `examples/astar.ts`, `examples/steering.ts`, `examples/boids.ts`, `examples/requestDedup.ts`, `examples/sat.ts`, `examples/simplex.ts`, and `examples/worley.ts` for quick starts.

ROADMAP.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
- [x] Introduce request deduplication helper
2424
- [x] Ship virtual scrolling utilities
2525
- [x] Add diff/patch helpers for nested JSON structures
26-
- [ ] Create benchmarking scripts to compare algorithm variants
26+
- [x] Create benchmarking scripts to compare algorithm variants
2727
- [ ] Expand CI to include coverage gating and bundle size checks
2828

2929
## Milestone 1.0.0 – Production Readiness

benchmarks/runBenchmarks.mjs

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
#!/usr/bin/env node
2+
import { existsSync } from 'node:fs';
3+
import { join, dirname } from 'node:path';
4+
import { fileURLToPath } from 'node:url';
5+
import { performance } from 'node:perf_hooks';
6+
7+
const __dirname = dirname(fileURLToPath(import.meta.url));
8+
const distEntry = join(__dirname, '../dist/index.js');
9+
10+
if (!existsSync(distEntry)) {
11+
console.error('dist/index.js not found. Run "npm run build" before benchmarking.');
12+
process.exit(1);
13+
}
14+
15+
const {
16+
astar,
17+
dijkstra,
18+
manhattanDistance,
19+
graphBFS,
20+
perlin,
21+
worley,
22+
simplex2D,
23+
Quadtree,
24+
} = await import('../dist/index.js');
25+
26+
function runBenchmarks() {
27+
const benchmarks = [
28+
createNoiseBenchmark(),
29+
createSpatialBenchmark(),
30+
createGraphBenchmark(),
31+
];
32+
33+
console.log('LLM Algorithms – Benchmark Suite');
34+
console.log('===================================');
35+
36+
for (const benchmark of benchmarks) {
37+
executeBenchmark(benchmark);
38+
}
39+
}
40+
41+
function createRng(seed = 42) {
42+
let state = seed >>> 0;
43+
return () => {
44+
state = (state + 0x6d2b79f5) >>> 0;
45+
let t = Math.imul(state ^ (state >>> 15), 1 | state);
46+
t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
47+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
48+
};
49+
}
50+
51+
function createNoiseBenchmark() {
52+
const width = 128;
53+
const height = 128;
54+
const seed = 1337;
55+
56+
return {
57+
name: 'Procedural noise generation (128×128)',
58+
iterations: 5,
59+
context: { width, height, seed },
60+
variants: [
61+
{
62+
name: 'Perlin',
63+
run: ({ width: w, height: h, seed: s }) => aggregateGrid(perlin({ width: w, height: h, seed: s })),
64+
},
65+
{
66+
name: 'Worley',
67+
run: ({ width: w, height: h, seed: s }) => aggregateGrid(worley({ width: w, height: h, points: 32, seed: s })),
68+
},
69+
{
70+
name: 'Simplex',
71+
run: ({ width: w, height: h, seed: s }) => aggregateSimplex(w, h, s),
72+
},
73+
],
74+
};
75+
}
76+
77+
function aggregateGrid(grid) {
78+
let total = 0;
79+
for (const row of grid) {
80+
for (const value of row) {
81+
total += value;
82+
}
83+
}
84+
return total;
85+
}
86+
87+
function aggregateSimplex(width, height, seed) {
88+
let total = 0;
89+
for (let y = 0; y < height; y += 1) {
90+
for (let x = 0; x < width; x += 1) {
91+
total += simplex2D(x / width, y / height, seed);
92+
}
93+
}
94+
return total;
95+
}
96+
97+
function createSpatialBenchmark() {
98+
const pointCount = 10_000;
99+
const queryRect = { x: 200, y: 200, width: 300, height: 300 };
100+
const rng = createRng(123);
101+
102+
const points = Array.from({ length: pointCount }, () => ({
103+
x: rng() * 1_000,
104+
y: rng() * 1_000,
105+
}));
106+
107+
const tree = new Quadtree({ x: 0, y: 0, width: 1_000, height: 1_000 }, 8);
108+
for (const point of points) {
109+
tree.insert(point);
110+
}
111+
112+
return {
113+
name: 'Spatial query – quadtree vs linear scan',
114+
iterations: 50,
115+
context: { points, queryRect, tree },
116+
variants: [
117+
{
118+
name: 'Quadtree query',
119+
run: ({ tree: qt, queryRect: rect }) => qt.query(rect).length,
120+
},
121+
{
122+
name: 'Linear scan',
123+
run: ({ points: pts, queryRect: rect }) => {
124+
let hits = 0;
125+
const { x, y, width, height } = rect;
126+
const x2 = x + width;
127+
const y2 = y + height;
128+
for (const point of pts) {
129+
if (point.x >= x && point.x <= x2 && point.y >= y && point.y <= y2) {
130+
hits += 1;
131+
}
132+
}
133+
return hits;
134+
},
135+
},
136+
],
137+
};
138+
}
139+
140+
function createGraphBenchmark() {
141+
const dimension = 32;
142+
const obstacleChance = 0.18;
143+
const baseSeed = 9001;
144+
145+
let generated;
146+
let reachable = false;
147+
for (let attempt = 0; attempt < 8 && !reachable; attempt += 1) {
148+
generated = buildGridGraph(dimension, obstacleChance, baseSeed + attempt);
149+
const distances = graphBFS(generated.graph, generated.startNode);
150+
reachable = distances.has(generated.goalNode);
151+
}
152+
153+
if (!generated || !reachable) {
154+
throw new Error('Failed to generate a traversable grid graph for benchmarking.');
155+
}
156+
157+
return {
158+
name: 'Graph traversal – Dijkstra vs BFS vs A*',
159+
iterations: 25,
160+
context: generated,
161+
variants: [
162+
{
163+
name: 'Dijkstra (weighted)',
164+
run: ({ graph: g, startNode: s, goalNode: t }) => {
165+
const result = dijkstra({ graph: g, start: s, goal: t });
166+
return result ? result.cost : Number.POSITIVE_INFINITY;
167+
},
168+
},
169+
{
170+
name: 'BFS (unweighted)',
171+
run: ({ graph: g, startNode: s, goalNode: t }) => {
172+
const distances = graphBFS(g, s);
173+
return distances.get(t) ?? Number.POSITIVE_INFINITY;
174+
},
175+
},
176+
{
177+
name: 'A* Manhattan heuristic',
178+
run: ({ grid, startPoint, goalPoint }) => {
179+
const path = astar({ grid, start: startPoint, goal: goalPoint, heuristic: manhattanDistance, allowDiagonal: false });
180+
return path ? path.length : Number.POSITIVE_INFINITY;
181+
},
182+
},
183+
],
184+
};
185+
}
186+
187+
function buildGridGraph(size, obstacleChance, seed) {
188+
const rng = createRng(seed);
189+
const grid = Array.from({ length: size }, () => Array.from({ length: size }, () => 0));
190+
for (let y = 0; y < size; y += 1) {
191+
for (let x = 0; x < size; x += 1) {
192+
if ((x === 0 && y === 0) || (x === size - 1 && y === size - 1)) {
193+
continue;
194+
}
195+
grid[y][x] = rng() < obstacleChance ? 1 : 0;
196+
}
197+
}
198+
199+
const graph = {};
200+
for (let y = 0; y < size; y += 1) {
201+
for (let x = 0; x < size; x += 1) {
202+
if (grid[y][x] === 1) {
203+
continue;
204+
}
205+
const id = `${x},${y}`;
206+
graph[id] = [];
207+
const neighbors = [
208+
[1, 0],
209+
[-1, 0],
210+
[0, 1],
211+
[0, -1],
212+
];
213+
for (const [dx, dy] of neighbors) {
214+
const nx = x + dx;
215+
const ny = y + dy;
216+
if (nx < 0 || ny < 0 || nx >= size || ny >= size || grid[ny][nx] === 1) {
217+
continue;
218+
}
219+
graph[id].push({ node: `${nx},${ny}`, weight: 1 + rng() * 4 });
220+
}
221+
}
222+
}
223+
224+
return {
225+
graph,
226+
grid,
227+
startNode: '0,0',
228+
goalNode: `${size - 1},${size - 1}`,
229+
startPoint: { x: 0, y: 0 },
230+
goalPoint: { x: size - 1, y: size - 1 },
231+
};
232+
}
233+
234+
function executeBenchmark({ name, iterations, context, variants }) {
235+
console.log(`\n${name}`);
236+
console.log('-'.repeat(name.length));
237+
238+
const results = variants.map((variant) => measureVariant(variant, context, iterations));
239+
const table = results.map((result) => ({
240+
Variant: result.name,
241+
'Avg ms': result.avgMs.toFixed(3),
242+
'Ops/sec': result.opsPerSec.toFixed(2),
243+
Iterations: iterations,
244+
Checksum: result.checksum.toFixed(2),
245+
}));
246+
247+
console.table(table);
248+
}
249+
250+
function measureVariant(variant, context, iterations) {
251+
// Warm up once.
252+
variant.run(context);
253+
254+
const start = performance.now();
255+
let checksum = 0;
256+
for (let i = 0; i < iterations; i += 1) {
257+
const value = variant.run(context);
258+
checksum += normaliseResult(value);
259+
}
260+
const totalMs = performance.now() - start;
261+
const avgMs = totalMs / iterations;
262+
const opsPerSec = iterations / (totalMs / 1_000 || 1);
263+
264+
return { name: variant.name, avgMs, opsPerSec, checksum };
265+
}
266+
267+
function normaliseResult(value) {
268+
if (value === null || value === undefined) {
269+
return 0;
270+
}
271+
if (typeof value === 'number') {
272+
return value;
273+
}
274+
if (typeof value === 'string') {
275+
return value.length;
276+
}
277+
if (Array.isArray(value)) {
278+
return value.length;
279+
}
280+
if (value instanceof Map || value instanceof Set) {
281+
return value.size;
282+
}
283+
if (typeof value === 'object') {
284+
return Object.keys(value).length;
285+
}
286+
return 0;
287+
}
288+
289+
runBenchmarks();

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"test": "vitest run",
2828
"test:watch": "vitest",
2929
"lint": "eslint 'src/**/*.ts' 'tests/**/*.ts'",
30-
"format": "prettier --write ."
30+
"format": "prettier --write .",
31+
"benchmark": "node benchmarks/runBenchmarks.mjs"
3132
},
3233
"keywords": [
3334
"algorithms",

0 commit comments

Comments
 (0)