archetype-ecs 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -14
- package/bench/allocations-1m.js +1 -1
- package/bench/component-churn-bench.js +193 -0
- package/bench/iterate.wat +95 -0
- package/bench/multi-ecs-bench.js +1 -1
- package/bench/run-js-vs-go-ts.sh +147 -0
- package/bench/typed-vs-bitecs-1m.js +1 -1
- package/bench/typed-vs-untyped.js +1 -1
- package/bench/vs-bitecs.js +1 -1
- package/bench/wasm-iteration-bench.js +289 -0
- package/dist/{src/ComponentRegistry.d.ts → ComponentRegistry.d.ts} +8 -3
- package/dist/{src/EntityManager.d.ts → EntityManager.d.ts} +15 -10
- package/dist/{src/EntityManager.js → EntityManager.js} +196 -95
- package/dist/{src/System.d.ts → System.d.ts} +4 -0
- package/dist/{src/System.js → System.js} +25 -5
- package/dist/WasmArena.d.ts +13 -0
- package/dist/WasmArena.js +48 -0
- package/dist/{src/index.d.ts → index.d.ts} +7 -9
- package/dist/{src/index.js → index.js} +2 -0
- package/dist/wasm-kernels.d.ts +10 -0
- package/dist/wasm-kernels.js +59 -0
- package/package.json +12 -7
- package/src/ComponentRegistry.ts +7 -3
- package/src/EntityManager.ts +209 -119
- package/src/System.ts +34 -9
- package/src/WasmArena.ts +83 -0
- package/src/index.ts +16 -11
- package/src/iterate.wat +135 -0
- package/src/wasm-kernels.ts +68 -0
- package/tests/EntityManager.test.ts +51 -86
- package/tests/System.test.ts +184 -0
- package/tests/types.ts +1 -1
- package/tsconfig.json +2 -2
- package/tsconfig.test.json +13 -0
- package/dist/tests/EntityManager.test.d.ts +0 -1
- package/dist/tests/EntityManager.test.js +0 -651
- package/dist/tests/System.test.d.ts +0 -1
- package/dist/tests/System.test.js +0 -630
- package/dist/tests/types.d.ts +0 -1
- package/dist/tests/types.js +0 -129
- /package/dist/{src/ComponentRegistry.js → ComponentRegistry.js} +0 -0
- /package/dist/{src/Profiler.d.ts → Profiler.d.ts} +0 -0
- /package/dist/{src/Profiler.js → Profiler.js} +0 -0
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ em.forEach([Position, Velocity], (arch) => {
|
|
|
51
51
|
### Why archetype-ecs?
|
|
52
52
|
|
|
53
53
|
<table>
|
|
54
|
-
<tr><td><strong>Fast iteration</strong></td><td>
|
|
54
|
+
<tr><td><strong>Fast iteration</strong></td><td>0.5 ms/frame over 1M entities with auto-detected <a href="#wasm-simd">WASM SIMD</a>. Faster than bitecs, wolf-ecs, harmony-ecs — see <a href="#benchmarks">benchmarks</a>.</td></tr>
|
|
55
55
|
<tr><td><strong>Low memory</strong></td><td>86 MB for 1M entities. Sparse-array ECS libraries use up to 2.4x more.</td></tr>
|
|
56
56
|
<tr><td><strong>No allocations</strong></td><td><code>get</code>, <code>set</code>, and <code>forEach</code> don't allocate.</td></tr>
|
|
57
57
|
<tr><td><strong>Typed</strong></td><td>TypeScript generics throughout. Field names autocomplete, wrong fields don't compile.</td></tr>
|
|
@@ -228,8 +228,8 @@ A functional API is also available:
|
|
|
228
228
|
import { createSystem } from 'archetype-ecs'
|
|
229
229
|
|
|
230
230
|
const deathSystem = createSystem(em, (sys) => {
|
|
231
|
-
sys.onAdded(Health, (id
|
|
232
|
-
sys.onRemoved(Health, (id
|
|
231
|
+
sys.onAdded(Health, (id) => console.log(`${id} spawned`))
|
|
232
|
+
sys.onRemoved(Health, (id) => console.log(`${id} died`))
|
|
233
233
|
})
|
|
234
234
|
|
|
235
235
|
em.flushHooks()
|
|
@@ -254,6 +254,47 @@ em.deserialize(JSON.parse(json), { Position, Velocity, Health })
|
|
|
254
254
|
|
|
255
255
|
Supports stripping components, skipping entities, and custom serializers.
|
|
256
256
|
|
|
257
|
+
### WASM SIMD
|
|
258
|
+
|
|
259
|
+
When WebAssembly SIMD is available (all modern browsers and Node.js 16+), archetype-ecs automatically allocates numeric TypedArrays on a shared `WebAssembly.Memory`. This enables SIMD-accelerated batch operations like `fieldAdd()` — no manual WASM code needed.
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
em.forEach([Position, Velocity], (arch) => {
|
|
263
|
+
arch.fieldAdd(Position.x, Velocity.vx) // px[i] += vx[i], SIMD-accelerated
|
|
264
|
+
arch.fieldAdd(Position.y, Velocity.vy) // py[i] += vy[i], SIMD-accelerated
|
|
265
|
+
})
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`fieldAdd(target, source)` dispatches to a `f32x4.add` SIMD kernel (4 floats per instruction) when available, and falls back to a scalar JS loop otherwise. Your system code stays identical either way — no feature detection needed.
|
|
269
|
+
|
|
270
|
+
To disable WASM mode (e.g. for testing or environments without WebAssembly):
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
const em = createEntityManager({ wasm: false })
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
For advanced use cases, you can write your own WASM modules and use `fieldOffset()` + `em.wasmMemory` to operate directly on the raw memory:
|
|
277
|
+
|
|
278
|
+
```ts
|
|
279
|
+
import { instantiateKernels } from 'archetype-ecs'
|
|
280
|
+
|
|
281
|
+
const kernels = await instantiateKernels(em.wasmMemory!)
|
|
282
|
+
|
|
283
|
+
em.forEach([Position, Velocity], (arch) => {
|
|
284
|
+
kernels.iterate_simd(
|
|
285
|
+
arch.fieldOffset(Position.x), arch.fieldOffset(Position.y),
|
|
286
|
+
arch.fieldOffset(Velocity.vx), arch.fieldOffset(Velocity.vy),
|
|
287
|
+
arch.count,
|
|
288
|
+
)
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Details:**
|
|
293
|
+
- The arena reserves 128 MB virtual address space (lazily committed — no physical RAM cost on most OSes)
|
|
294
|
+
- String fields always fall back to regular JS arrays
|
|
295
|
+
- `fieldAdd` uses SIMD for `Float32Array` fields; other types fall back to scalar
|
|
296
|
+
- The bump allocator doesn't reclaim memory — frequent archetype churn may waste space
|
|
297
|
+
|
|
257
298
|
---
|
|
258
299
|
|
|
259
300
|
## TypeScript
|
|
@@ -261,7 +302,7 @@ Supports stripping components, skipping entities, and custom serializers.
|
|
|
261
302
|
Component types are inferred from their definition. Field names autocomplete, wrong fields are compile errors.
|
|
262
303
|
|
|
263
304
|
```ts
|
|
264
|
-
//
|
|
305
|
+
// Schema is inferred — Position becomes ComponentDef<'x' | 'y'>
|
|
265
306
|
const Position = component('Position', 'f32', ['x', 'y'])
|
|
266
307
|
|
|
267
308
|
Position.x // FieldRef — autocompletes to .x and .y
|
|
@@ -298,9 +339,9 @@ Schema component with mixed field types.
|
|
|
298
339
|
const Item = component('Item', { name: 'string', weight: 'f32', armor: 'u8' })
|
|
299
340
|
```
|
|
300
341
|
|
|
301
|
-
### `createEntityManager()`
|
|
342
|
+
### `createEntityManager(options?)`
|
|
302
343
|
|
|
303
|
-
Returns an entity manager
|
|
344
|
+
Returns an entity manager. WASM SIMD is auto-detected and enabled by default. Pass `{ wasm: false }` to force JS-only mode.
|
|
304
345
|
|
|
305
346
|
| Method | Description |
|
|
306
347
|
|---|---|
|
|
@@ -321,6 +362,21 @@ Returns an entity manager with the following methods:
|
|
|
321
362
|
| `flushHooks()` | Collect pending add/remove events for registered hooks |
|
|
322
363
|
| `serialize(symbolToName, strip?, skip?, opts?)` | Serialize world to JSON-friendly object |
|
|
323
364
|
| `deserialize(data, nameToSymbol, opts?)` | Restore world from serialized data |
|
|
365
|
+
| `wasmMemory` | `WebAssembly.Memory \| null` — the underlying memory (WASM mode only) |
|
|
366
|
+
|
|
367
|
+
The `forEach` callback receives an `ArchetypeView` with:
|
|
368
|
+
|
|
369
|
+
| Method | Description |
|
|
370
|
+
|---|---|
|
|
371
|
+
| `field(ref)` | Get the backing TypedArray for a field |
|
|
372
|
+
| `fieldStride(ref)` | Elements per entity (1 for scalars, N for arrays) |
|
|
373
|
+
| `fieldOffset(ref)` | Byte offset in `wasmMemory` (-1 if not in WASM mode) |
|
|
374
|
+
| `fieldAdd(target, source)` | `target[i] += source[i]` — uses SIMD in WASM mode, scalar loop otherwise |
|
|
375
|
+
| `snapshot(ref)` | Get the snapshot TypedArray (change tracking) |
|
|
376
|
+
|
|
377
|
+
### `instantiateKernels(memory)`
|
|
378
|
+
|
|
379
|
+
Loads the built-in WASM SIMD module onto the given `WebAssembly.Memory`. Returns `{ iterate_scalar, iterate_simd }` — both take `(pxOffset, pyOffset, vxOffset, vyOffset, count)` as byte offsets.
|
|
324
380
|
|
|
325
381
|
### `System`
|
|
326
382
|
|
|
@@ -349,16 +405,16 @@ Creates a pipeline from an array of class-based (`System` subclasses) and/or fun
|
|
|
349
405
|
|
|
350
406
|
1M entities, Position += Velocity, 5 runs (median), Node.js:
|
|
351
407
|
|
|
352
|
-
| | archetype-ecs | [bitecs](https://github.com/NateTheGreatt/bitECS) | [wolf-ecs](https://github.com/EnderShadow8/wolf-ecs) | [harmony-ecs](https://github.com/3mcd/harmony-ecs) | [miniplex](https://github.com/hmans/miniplex) |
|
|
353
|
-
|
|
354
|
-
| **Iteration** (ms/frame) |
|
|
355
|
-
| **Entity creation** (ms) | 401 | 366 | **106** | 248 | 265 |
|
|
356
|
-
| **Memory** (MB) | 86 | 204 | 60 | **31** | 166 |
|
|
408
|
+
| | archetype-ecs | archetype-ecs (WASM SIMD) | [bitecs](https://github.com/NateTheGreatt/bitECS) | [wolf-ecs](https://github.com/EnderShadow8/wolf-ecs) | [harmony-ecs](https://github.com/3mcd/harmony-ecs) | [miniplex](https://github.com/hmans/miniplex) |
|
|
409
|
+
|---|---:|---:|---:|---:|---:|---:|
|
|
410
|
+
| **Iteration** (ms/frame) | 1.5 | **0.5** | 2.2 | 2.2 | 1.8 | 32.5 |
|
|
411
|
+
| **Entity creation** (ms) | 401 | 401 | 366 | **106** | 248 | 265 |
|
|
412
|
+
| **Memory** (MB) | 86 | 86+128 | 204 | 60 | **31** | 166 |
|
|
357
413
|
|
|
358
414
|
Each library runs the same test — iterate 1M entities over 500 frames:
|
|
359
415
|
|
|
360
416
|
```ts
|
|
361
|
-
// archetype-ecs
|
|
417
|
+
// archetype-ecs (default) — manual loop
|
|
362
418
|
em.forEach([Position, Velocity], (arch) => {
|
|
363
419
|
const px = arch.field(Position.x) // Float32Array, dense
|
|
364
420
|
const py = arch.field(Position.y)
|
|
@@ -369,14 +425,22 @@ em.forEach([Position, Velocity], (arch) => {
|
|
|
369
425
|
py[i] += vy[i]
|
|
370
426
|
}
|
|
371
427
|
})
|
|
428
|
+
|
|
429
|
+
// archetype-ecs (WASM SIMD) — same code, 2.5x faster
|
|
430
|
+
const em = createEntityManager({ wasm: true })
|
|
431
|
+
em.forEach([Position, Velocity], (arch) => {
|
|
432
|
+
arch.fieldAdd(Position.x, Velocity.vx)
|
|
433
|
+
arch.fieldAdd(Position.y, Velocity.vy)
|
|
434
|
+
})
|
|
372
435
|
```
|
|
373
436
|
|
|
374
|
-
|
|
437
|
+
The WASM SIMD kernel processes 4 floats per instruction (`f32x4.add`) and avoids V8's `f32→f64→f32` conversion overhead.
|
|
375
438
|
|
|
376
439
|
Run them yourself:
|
|
377
440
|
|
|
378
441
|
```bash
|
|
379
|
-
npm run bench
|
|
442
|
+
npm run bench # vs other ECS libraries
|
|
443
|
+
node --expose-gc bench/wasm-iteration-bench.js # WASM SIMD benchmark
|
|
380
444
|
```
|
|
381
445
|
|
|
382
446
|
---
|
|
@@ -389,6 +453,7 @@ Compared against other JS ECS libraries:
|
|
|
389
453
|
|
|
390
454
|
| Feature | archetype-ecs | bitecs | wolf-ecs | harmony-ecs | miniplex |
|
|
391
455
|
|---|:---:|:---:|:---:|:---:|:---:|
|
|
456
|
+
| WASM SIMD iteration (opt-in) | ✓ | — | — | — | — |
|
|
392
457
|
| String SoA storage | ✓ | — | — | — | — |
|
|
393
458
|
| Mixed string + numeric components | ✓ | — | — | — | — |
|
|
394
459
|
| `forEach` with dense TypedArray field access | ✓ | — | — | — | — |
|
package/bench/allocations-1m.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import {
|
|
5
5
|
createWorld, addEntity, addComponent, removeEntity, query
|
|
6
6
|
} from 'bitecs';
|
|
7
|
-
import { createEntityManager, component } from '../dist/
|
|
7
|
+
import { createEntityManager, component } from '../dist/index.js';
|
|
8
8
|
|
|
9
9
|
const COUNT = 1_000_000;
|
|
10
10
|
const RUNS = 5;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Component Churn Benchmark: 1M entities, component add/remove over 500 frames
|
|
2
|
+
// Vergelijkt runtime van code gecompileerd met tsc (JS) vs tsgo (Go)
|
|
3
|
+
// Run met: node --expose-gc bench/component-churn-bench.js
|
|
4
|
+
// Of via: bash bench/run-js-vs-go-ts.sh
|
|
5
|
+
|
|
6
|
+
import { createEntityManager, component } from '../dist/index.js';
|
|
7
|
+
|
|
8
|
+
const COUNT = 1_000_000;
|
|
9
|
+
const FRAMES = 500;
|
|
10
|
+
const CHURN_BATCH = 1_000;
|
|
11
|
+
const RUNS = 5;
|
|
12
|
+
|
|
13
|
+
// ── Utilities ────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const median = (arr) => {
|
|
16
|
+
const s = [...arr].sort((a, b) => a - b);
|
|
17
|
+
const mid = s.length >> 1;
|
|
18
|
+
return s.length & 1 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const memMB = () => {
|
|
22
|
+
if (globalThis.gc) globalThis.gc();
|
|
23
|
+
return process.memoryUsage().heapUsed / 1024 / 1024;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// ── Components ───────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
const Position = component('ChurnPos', { x: 'f32', y: 'f32' });
|
|
29
|
+
const Velocity = component('ChurnVel', { vx: 'f32', vy: 'f32' });
|
|
30
|
+
const Active = component('ChurnActive', { frame: 'i32' });
|
|
31
|
+
|
|
32
|
+
// ── Benchmark: iteration only (baseline) ─────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function benchIterationOnly() {
|
|
35
|
+
const em = createEntityManager();
|
|
36
|
+
for (let i = 0; i < COUNT; i++) {
|
|
37
|
+
em.createEntityWith(
|
|
38
|
+
Position, { x: i * 0.1, y: i * 0.1 },
|
|
39
|
+
Velocity, { vx: 1, vy: 1 },
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Warmup
|
|
44
|
+
for (let f = 0; f < 10; f++) {
|
|
45
|
+
em.forEach([Position, Velocity], (arch) => {
|
|
46
|
+
const px = arch.field(Position.x);
|
|
47
|
+
const py = arch.field(Position.y);
|
|
48
|
+
const vx = arch.field(Velocity.vx);
|
|
49
|
+
const vy = arch.field(Velocity.vy);
|
|
50
|
+
for (let i = 0; i < arch.count; i++) {
|
|
51
|
+
px[i] += vx[i];
|
|
52
|
+
py[i] += vy[i];
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const t0 = performance.now();
|
|
58
|
+
for (let f = 0; f < FRAMES; f++) {
|
|
59
|
+
em.forEach([Position, Velocity], (arch) => {
|
|
60
|
+
const px = arch.field(Position.x);
|
|
61
|
+
const py = arch.field(Position.y);
|
|
62
|
+
const vx = arch.field(Velocity.vx);
|
|
63
|
+
const vy = arch.field(Velocity.vy);
|
|
64
|
+
for (let i = 0; i < arch.count; i++) {
|
|
65
|
+
px[i] += vx[i];
|
|
66
|
+
py[i] += vy[i];
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return (performance.now() - t0) / FRAMES;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Benchmark: iteration + component churn ───────────────────────────────────
|
|
74
|
+
|
|
75
|
+
function benchComponentChurn() {
|
|
76
|
+
const em = createEntityManager();
|
|
77
|
+
|
|
78
|
+
const entityIds = new Array(COUNT);
|
|
79
|
+
for (let i = 0; i < COUNT; i++) {
|
|
80
|
+
entityIds[i] = em.createEntityWith(
|
|
81
|
+
Position, { x: i * 0.1, y: i * 0.1 },
|
|
82
|
+
Velocity, { vx: 1, vy: 1 },
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let activeIds = [];
|
|
87
|
+
|
|
88
|
+
// Warmup
|
|
89
|
+
for (let f = 0; f < 10; f++) {
|
|
90
|
+
em.forEach([Position, Velocity], (arch) => {
|
|
91
|
+
const px = arch.field(Position.x);
|
|
92
|
+
const py = arch.field(Position.y);
|
|
93
|
+
const vx = arch.field(Velocity.vx);
|
|
94
|
+
const vy = arch.field(Velocity.vy);
|
|
95
|
+
for (let i = 0; i < arch.count; i++) {
|
|
96
|
+
px[i] += vx[i];
|
|
97
|
+
py[i] += vy[i];
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const t0 = performance.now();
|
|
103
|
+
for (let f = 0; f < FRAMES; f++) {
|
|
104
|
+
// 1. Movement system: iterate all entities with Position + Velocity
|
|
105
|
+
em.forEach([Position, Velocity], (arch) => {
|
|
106
|
+
const px = arch.field(Position.x);
|
|
107
|
+
const py = arch.field(Position.y);
|
|
108
|
+
const vx = arch.field(Velocity.vx);
|
|
109
|
+
const vy = arch.field(Velocity.vy);
|
|
110
|
+
for (let i = 0; i < arch.count; i++) {
|
|
111
|
+
px[i] += vx[i];
|
|
112
|
+
py[i] += vy[i];
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// 2. Remove Active from previous batch (archetype migration)
|
|
117
|
+
for (const id of activeIds) {
|
|
118
|
+
em.removeComponent(id, Active);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 3. Add Active to new batch (archetype migration, rotating through entities)
|
|
122
|
+
activeIds = [];
|
|
123
|
+
const base = (f * CHURN_BATCH) % (COUNT - CHURN_BATCH);
|
|
124
|
+
for (let i = 0; i < CHURN_BATCH; i++) {
|
|
125
|
+
const id = entityIds[base + i];
|
|
126
|
+
em.addComponent(id, Active, { frame: f });
|
|
127
|
+
activeIds.push(id);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 4. Process Active entities (small set, tests multi-archetype iteration)
|
|
131
|
+
em.forEach([Position, Active], (arch) => {
|
|
132
|
+
const px = arch.field(Position.x);
|
|
133
|
+
const frame = arch.field(Active.frame);
|
|
134
|
+
for (let i = 0; i < arch.count; i++) {
|
|
135
|
+
px[i] += frame[i] * 0.001;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return (performance.now() - t0) / FRAMES;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
function main() {
|
|
145
|
+
const compiler = process.env.TS_COMPILER || 'unknown';
|
|
146
|
+
|
|
147
|
+
console.log(`=== Component Churn Benchmark ===`);
|
|
148
|
+
console.log(` ${(COUNT / 1e6).toFixed(0)}M entities | ${FRAMES} frames | ${CHURN_BATCH} churn/frame | ${RUNS} runs`);
|
|
149
|
+
console.log(` Compiler: ${compiler}`);
|
|
150
|
+
console.log();
|
|
151
|
+
|
|
152
|
+
const memBefore = memMB();
|
|
153
|
+
|
|
154
|
+
// ── Baseline: iteration only ──────────────────────────────────────────────
|
|
155
|
+
console.log(' [1/2] Iteration only (baseline)');
|
|
156
|
+
const iterResults = [];
|
|
157
|
+
for (let r = 0; r < RUNS; r++) {
|
|
158
|
+
process.stdout.write(` Run ${r + 1}/${RUNS}...`);
|
|
159
|
+
const ms = benchIterationOnly();
|
|
160
|
+
iterResults.push(ms);
|
|
161
|
+
console.log(` ${ms.toFixed(2)} ms/frame`);
|
|
162
|
+
}
|
|
163
|
+
const iterMedian = median(iterResults);
|
|
164
|
+
|
|
165
|
+
// ── Component churn ───────────────────────────────────────────────────────
|
|
166
|
+
console.log(' [2/2] Iteration + component churn');
|
|
167
|
+
const churnResults = [];
|
|
168
|
+
for (let r = 0; r < RUNS; r++) {
|
|
169
|
+
process.stdout.write(` Run ${r + 1}/${RUNS}...`);
|
|
170
|
+
const ms = benchComponentChurn();
|
|
171
|
+
churnResults.push(ms);
|
|
172
|
+
console.log(` ${ms.toFixed(2)} ms/frame`);
|
|
173
|
+
}
|
|
174
|
+
const churnMedian = median(churnResults);
|
|
175
|
+
|
|
176
|
+
const memAfter = memMB();
|
|
177
|
+
const churnOverhead = ((churnMedian / iterMedian - 1) * 100).toFixed(1);
|
|
178
|
+
|
|
179
|
+
// ── Resultaten ────────────────────────────────────────────────────────────
|
|
180
|
+
console.log();
|
|
181
|
+
console.log(` Resultaten (${compiler}):`);
|
|
182
|
+
console.log(` Iteratie baseline: ${iterMedian.toFixed(2)} ms/frame`);
|
|
183
|
+
console.log(` Met component churn: ${churnMedian.toFixed(2)} ms/frame`);
|
|
184
|
+
console.log(` Churn overhead: +${churnOverhead}%`);
|
|
185
|
+
console.log(` Geheugen (heap): ~${(memAfter - memBefore).toFixed(0)} MB`);
|
|
186
|
+
console.log();
|
|
187
|
+
|
|
188
|
+
// Machine-readable output voor runner script
|
|
189
|
+
console.log(`__ITER__=${iterMedian.toFixed(4)}`);
|
|
190
|
+
console.log(`__CHURN__=${churnMedian.toFixed(4)}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
main();
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
(module
|
|
2
|
+
(memory (import "env" "memory") 1)
|
|
3
|
+
|
|
4
|
+
;; Scalar loop: px[i] += vx[i]; py[i] += vy[i]
|
|
5
|
+
;; params: byte offsets for px, py, vx, vy arrays + element count
|
|
6
|
+
(func (export "iterate_scalar")
|
|
7
|
+
(param $px i32) (param $py i32) (param $vx i32) (param $vy i32) (param $count i32)
|
|
8
|
+
(local $i i32)
|
|
9
|
+
(local $off i32)
|
|
10
|
+
(local.set $i (i32.const 0))
|
|
11
|
+
(block $break
|
|
12
|
+
(loop $loop
|
|
13
|
+
(br_if $break (i32.ge_u (local.get $i) (local.get $count)))
|
|
14
|
+
(local.set $off (i32.shl (local.get $i) (i32.const 2)))
|
|
15
|
+
;; px[i] += vx[i]
|
|
16
|
+
(f32.store
|
|
17
|
+
(i32.add (local.get $px) (local.get $off))
|
|
18
|
+
(f32.add
|
|
19
|
+
(f32.load (i32.add (local.get $px) (local.get $off)))
|
|
20
|
+
(f32.load (i32.add (local.get $vx) (local.get $off)))
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
;; py[i] += vy[i]
|
|
24
|
+
(f32.store
|
|
25
|
+
(i32.add (local.get $py) (local.get $off))
|
|
26
|
+
(f32.add
|
|
27
|
+
(f32.load (i32.add (local.get $py) (local.get $off)))
|
|
28
|
+
(f32.load (i32.add (local.get $vy) (local.get $off)))
|
|
29
|
+
)
|
|
30
|
+
)
|
|
31
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
32
|
+
(br $loop)
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
;; SIMD loop: processes 4 floats at a time using v128 / f32x4
|
|
38
|
+
(func (export "iterate_simd")
|
|
39
|
+
(param $px i32) (param $py i32) (param $vx i32) (param $vy i32) (param $count i32)
|
|
40
|
+
(local $i i32)
|
|
41
|
+
(local $off i32)
|
|
42
|
+
(local $end4 i32)
|
|
43
|
+
;; end4 = count & ~3 (round down to multiple of 4)
|
|
44
|
+
(local.set $end4 (i32.and (local.get $count) (i32.const -4)))
|
|
45
|
+
;; SIMD loop: 4 elements per iteration
|
|
46
|
+
(local.set $i (i32.const 0))
|
|
47
|
+
(block $break
|
|
48
|
+
(loop $loop
|
|
49
|
+
(br_if $break (i32.ge_u (local.get $i) (local.get $end4)))
|
|
50
|
+
(local.set $off (i32.shl (local.get $i) (i32.const 2)))
|
|
51
|
+
;; px[i..i+4] += vx[i..i+4]
|
|
52
|
+
(v128.store
|
|
53
|
+
(i32.add (local.get $px) (local.get $off))
|
|
54
|
+
(f32x4.add
|
|
55
|
+
(v128.load (i32.add (local.get $px) (local.get $off)))
|
|
56
|
+
(v128.load (i32.add (local.get $vx) (local.get $off)))
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
;; py[i..i+4] += vy[i..i+4]
|
|
60
|
+
(v128.store
|
|
61
|
+
(i32.add (local.get $py) (local.get $off))
|
|
62
|
+
(f32x4.add
|
|
63
|
+
(v128.load (i32.add (local.get $py) (local.get $off)))
|
|
64
|
+
(v128.load (i32.add (local.get $vy) (local.get $off)))
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
(local.set $i (i32.add (local.get $i) (i32.const 4)))
|
|
68
|
+
(br $loop)
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
;; Scalar remainder
|
|
72
|
+
(block $break2
|
|
73
|
+
(loop $loop2
|
|
74
|
+
(br_if $break2 (i32.ge_u (local.get $i) (local.get $count)))
|
|
75
|
+
(local.set $off (i32.shl (local.get $i) (i32.const 2)))
|
|
76
|
+
(f32.store
|
|
77
|
+
(i32.add (local.get $px) (local.get $off))
|
|
78
|
+
(f32.add
|
|
79
|
+
(f32.load (i32.add (local.get $px) (local.get $off)))
|
|
80
|
+
(f32.load (i32.add (local.get $vx) (local.get $off)))
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
(f32.store
|
|
84
|
+
(i32.add (local.get $py) (local.get $off))
|
|
85
|
+
(f32.add
|
|
86
|
+
(f32.load (i32.add (local.get $py) (local.get $off)))
|
|
87
|
+
(f32.load (i32.add (local.get $vy) (local.get $off)))
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
(local.set $i (i32.add (local.get $i) (i32.const 1)))
|
|
91
|
+
(br $loop2)
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
)
|
package/bench/multi-ecs-bench.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Tests: iteration (Position += Velocity), entity creation, memory usage
|
|
3
3
|
// Run with: node --expose-gc bench/multi-ecs-bench.js
|
|
4
4
|
|
|
5
|
-
import { createEntityManager, component } from '../dist/
|
|
5
|
+
import { createEntityManager, component } from '../dist/index.js';
|
|
6
6
|
|
|
7
7
|
const COUNT = 1_000_000;
|
|
8
8
|
const FRAMES = 500;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# ── JS TypeScript (tsc) vs Go TypeScript (tsgo) Benchmark Runner ──────────────
|
|
3
|
+
# Compileert archetype-ecs met beide compilers en vergelijkt de runtime.
|
|
4
|
+
#
|
|
5
|
+
# Gebruik: bash bench/run-js-vs-go-ts.sh
|
|
6
|
+
# Vereist: npm install @typescript/native-preview
|
|
7
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
cd "$(dirname "$0")/.."
|
|
11
|
+
|
|
12
|
+
BENCH="bench/component-churn-bench.js"
|
|
13
|
+
NODE_FLAGS="--expose-gc"
|
|
14
|
+
|
|
15
|
+
# Kleuren
|
|
16
|
+
RED='\033[0;31m'
|
|
17
|
+
GREEN='\033[0;32m'
|
|
18
|
+
CYAN='\033[0;36m'
|
|
19
|
+
BOLD='\033[1m'
|
|
20
|
+
NC='\033[0m'
|
|
21
|
+
|
|
22
|
+
echo -e "${BOLD}=== archetype-ecs: tsc vs tsgo ===${NC}"
|
|
23
|
+
echo ""
|
|
24
|
+
|
|
25
|
+
# ── Check of tsgo beschikbaar is ─────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
if ! npx tsgo --version &>/dev/null; then
|
|
28
|
+
echo -e "${RED}tsgo niet gevonden. Installeer met:${NC}"
|
|
29
|
+
echo " npm install -D @typescript/native-preview"
|
|
30
|
+
exit 1
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
TSC_VERSION=$(npx tsc --version 2>/dev/null || echo "unknown")
|
|
34
|
+
TSGO_VERSION=$(npx tsgo --version 2>/dev/null || echo "unknown")
|
|
35
|
+
echo -e " tsc: ${CYAN}${TSC_VERSION}${NC}"
|
|
36
|
+
echo -e " tsgo: ${CYAN}${TSGO_VERSION}${NC}"
|
|
37
|
+
echo ""
|
|
38
|
+
|
|
39
|
+
# ── Backup bestaande dist ───────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
DIST_BACKUP=""
|
|
42
|
+
if [ -d dist ]; then
|
|
43
|
+
DIST_BACKUP=$(mktemp -d)
|
|
44
|
+
cp -r dist/* "$DIST_BACKUP/" 2>/dev/null || true
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
cleanup() {
|
|
48
|
+
# Herstel originele dist
|
|
49
|
+
if [ -n "$DIST_BACKUP" ] && [ -d "$DIST_BACKUP" ]; then
|
|
50
|
+
rm -rf dist
|
|
51
|
+
mkdir -p dist
|
|
52
|
+
cp -r "$DIST_BACKUP"/* dist/ 2>/dev/null || true
|
|
53
|
+
rm -rf "$DIST_BACKUP"
|
|
54
|
+
fi
|
|
55
|
+
}
|
|
56
|
+
trap cleanup EXIT
|
|
57
|
+
|
|
58
|
+
# ── 1. Compileer en benchmark met tsc (JS TypeScript) ───────────────────────
|
|
59
|
+
|
|
60
|
+
echo -e "${BOLD}--- [1/2] tsc (JS TypeScript) ---${NC}"
|
|
61
|
+
rm -rf dist
|
|
62
|
+
|
|
63
|
+
TSC_START=$(date +%s%N)
|
|
64
|
+
npx tsc
|
|
65
|
+
TSC_END=$(date +%s%N)
|
|
66
|
+
TSC_COMPILE=$(( (TSC_END - TSC_START) / 1000000 ))
|
|
67
|
+
echo -e " Compile time: ${GREEN}${TSC_COMPILE}ms${NC}"
|
|
68
|
+
echo ""
|
|
69
|
+
|
|
70
|
+
TSC_OUTPUT=$(TS_COMPILER="tsc (JS)" node $NODE_FLAGS "$BENCH")
|
|
71
|
+
echo "$TSC_OUTPUT"
|
|
72
|
+
|
|
73
|
+
TSC_ITER=$(echo "$TSC_OUTPUT" | grep '__ITER__=' | cut -d= -f2)
|
|
74
|
+
TSC_CHURN=$(echo "$TSC_OUTPUT" | grep '__CHURN__=' | cut -d= -f2)
|
|
75
|
+
|
|
76
|
+
echo ""
|
|
77
|
+
|
|
78
|
+
# ── 2. Compileer en benchmark met tsgo (Go TypeScript) ──────────────────────
|
|
79
|
+
|
|
80
|
+
echo -e "${BOLD}--- [2/2] tsgo (Go TypeScript) ---${NC}"
|
|
81
|
+
rm -rf dist
|
|
82
|
+
|
|
83
|
+
TSGO_START=$(date +%s%N)
|
|
84
|
+
npx tsgo
|
|
85
|
+
TSGO_END=$(date +%s%N)
|
|
86
|
+
TSGO_COMPILE=$(( (TSGO_END - TSGO_START) / 1000000 ))
|
|
87
|
+
echo -e " Compile time: ${GREEN}${TSGO_COMPILE}ms${NC}"
|
|
88
|
+
echo ""
|
|
89
|
+
|
|
90
|
+
TSGO_OUTPUT=$(TS_COMPILER="tsgo (Go)" node $NODE_FLAGS "$BENCH")
|
|
91
|
+
echo "$TSGO_OUTPUT"
|
|
92
|
+
|
|
93
|
+
TSGO_ITER=$(echo "$TSGO_OUTPUT" | grep '__ITER__=' | cut -d= -f2)
|
|
94
|
+
TSGO_CHURN=$(echo "$TSGO_OUTPUT" | grep '__CHURN__=' | cut -d= -f2)
|
|
95
|
+
|
|
96
|
+
echo ""
|
|
97
|
+
|
|
98
|
+
# ── Samenvatting ─────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
echo -e "${BOLD}=== Samenvatting ===${NC}"
|
|
101
|
+
echo ""
|
|
102
|
+
|
|
103
|
+
# Compile time vergelijking
|
|
104
|
+
if [ "$TSGO_COMPILE" -gt 0 ]; then
|
|
105
|
+
COMPILE_RATIO=$(echo "scale=1; $TSC_COMPILE / $TSGO_COMPILE" | bc 2>/dev/null || echo "?")
|
|
106
|
+
else
|
|
107
|
+
COMPILE_RATIO="?"
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
echo " Compile time:"
|
|
111
|
+
echo " tsc (JS): ${TSC_COMPILE}ms"
|
|
112
|
+
echo " tsgo (Go): ${TSGO_COMPILE}ms"
|
|
113
|
+
echo -e " ${GREEN}tsgo is ${COMPILE_RATIO}x sneller${NC}"
|
|
114
|
+
echo ""
|
|
115
|
+
|
|
116
|
+
# Runtime vergelijking
|
|
117
|
+
echo " Runtime — iteratie baseline (ms/frame):"
|
|
118
|
+
echo " tsc (JS): ${TSC_ITER}"
|
|
119
|
+
echo " tsgo (Go): ${TSGO_ITER}"
|
|
120
|
+
if [ -n "$TSC_ITER" ] && [ -n "$TSGO_ITER" ]; then
|
|
121
|
+
ITER_RATIO=$(echo "scale=2; $TSC_ITER / $TSGO_ITER" | bc 2>/dev/null || echo "?")
|
|
122
|
+
if (( $(echo "$ITER_RATIO > 1.05" | bc -l 2>/dev/null || echo 0) )); then
|
|
123
|
+
echo -e " ${GREEN}tsgo is ${ITER_RATIO}x sneller${NC}"
|
|
124
|
+
elif (( $(echo "$ITER_RATIO < 0.95" | bc -l 2>/dev/null || echo 0) )); then
|
|
125
|
+
INV=$(echo "scale=2; 1 / $ITER_RATIO" | bc 2>/dev/null || echo "?")
|
|
126
|
+
echo -e " ${RED}tsc is ${INV}x sneller${NC}"
|
|
127
|
+
else
|
|
128
|
+
echo " ~gelijk"
|
|
129
|
+
fi
|
|
130
|
+
fi
|
|
131
|
+
echo ""
|
|
132
|
+
|
|
133
|
+
echo " Runtime — component churn (ms/frame):"
|
|
134
|
+
echo " tsc (JS): ${TSC_CHURN}"
|
|
135
|
+
echo " tsgo (Go): ${TSGO_CHURN}"
|
|
136
|
+
if [ -n "$TSC_CHURN" ] && [ -n "$TSGO_CHURN" ]; then
|
|
137
|
+
CHURN_RATIO=$(echo "scale=2; $TSC_CHURN / $TSGO_CHURN" | bc 2>/dev/null || echo "?")
|
|
138
|
+
if (( $(echo "$CHURN_RATIO > 1.05" | bc -l 2>/dev/null || echo 0) )); then
|
|
139
|
+
echo -e " ${GREEN}tsgo is ${CHURN_RATIO}x sneller${NC}"
|
|
140
|
+
elif (( $(echo "$CHURN_RATIO < 0.95" | bc -l 2>/dev/null || echo 0) )); then
|
|
141
|
+
INV=$(echo "scale=2; 1 / $CHURN_RATIO" | bc 2>/dev/null || echo "?")
|
|
142
|
+
echo -e " ${RED}tsc is ${INV}x sneller${NC}"
|
|
143
|
+
else
|
|
144
|
+
echo " ~gelijk"
|
|
145
|
+
fi
|
|
146
|
+
fi
|
|
147
|
+
echo ""
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import {
|
|
4
4
|
createWorld, addEntity, addComponent, query
|
|
5
5
|
} from 'bitecs';
|
|
6
|
-
import { createEntityManager, component } from '../dist/
|
|
6
|
+
import { createEntityManager, component } from '../dist/index.js';
|
|
7
7
|
|
|
8
8
|
const COUNT = 1_000_000;
|
|
9
9
|
const FRAMES = 500;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Benchmark: forEach+field (bulk TypedArray) vs query+get/set (per-entity) vs query+getComponent (allocating)
|
|
2
2
|
|
|
3
|
-
import { createEntityManager, component } from '../dist/
|
|
3
|
+
import { createEntityManager, component } from '../dist/index.js';
|
|
4
4
|
|
|
5
5
|
const COUNT = 1_000_000;
|
|
6
6
|
const FRAMES = 200;
|
package/bench/vs-bitecs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Benchmark: archetype-ecs vs bitECS
|
|
2
2
|
// Tests: system loop, entity creation, component add/remove churn
|
|
3
3
|
|
|
4
|
-
import { createEntityManager, component } from '../dist/
|
|
4
|
+
import { createEntityManager, component } from '../dist/index.js';
|
|
5
5
|
import {
|
|
6
6
|
createWorld, addEntity,
|
|
7
7
|
addComponent, removeComponent,
|