@driftengine/script 3.61.1 → 3.61.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/script",
3
- "version": "3.61.1",
3
+ "version": "3.61.2",
4
4
  "description": "The drift/* capability bindings: what this engine provides to DriftScript, and the only place the two are coupled",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -21,6 +21,7 @@
21
21
  "!src/**/*.test.mjs",
22
22
  "!src/**/__snapshots__",
23
23
  "capabilities.json",
24
+ "scripts",
24
25
  "README.md",
25
26
  "LICENSE",
26
27
  "NOTICE"
@@ -30,17 +31,17 @@
30
31
  "driftscript": "1.13.0"
31
32
  },
32
33
  "peerDependencies": {
33
- "@driftengine/ai": "3.61.1",
34
- "@driftengine/animation": "3.61.1",
35
- "@driftengine/audio": "3.61.1",
36
- "@driftengine/chemistry": "3.61.1",
37
- "@driftengine/core": "3.61.1",
38
- "@driftengine/editor": "3.61.1",
39
- "@driftengine/entities": "3.61.1",
40
- "@driftengine/network": "3.61.1",
41
- "@driftengine/terrain": "3.61.1",
42
- "@driftengine/ui2d": "3.61.1",
43
- "@driftengine/xr": "3.61.1"
34
+ "@driftengine/ai": "3.61.2",
35
+ "@driftengine/animation": "3.61.2",
36
+ "@driftengine/audio": "3.61.2",
37
+ "@driftengine/chemistry": "3.61.2",
38
+ "@driftengine/core": "3.61.2",
39
+ "@driftengine/editor": "3.61.2",
40
+ "@driftengine/entities": "3.61.2",
41
+ "@driftengine/network": "3.61.2",
42
+ "@driftengine/terrain": "3.61.2",
43
+ "@driftengine/ui2d": "3.61.2",
44
+ "@driftengine/xr": "3.61.2"
44
45
  },
45
46
  "author": "Drift Technologies",
46
47
  "repository": {
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * What a hoisted view buys over a host call per field.
4
+ *
5
+ * The design's decision 2 rests on a claim — that indexing a live view is meaningfully cheaper than
6
+ * calling the host for every field of every entity of every frame — and a design that asserts a
7
+ * number without measuring it is the failure `docs/README.md` opens by describing. This is the
8
+ * measurement.
9
+ *
10
+ * ```sh
11
+ * node --experimental-strip-types packages/script/scripts/query-bench.mjs generated
12
+ * node --experimental-strip-types packages/script/scripts/query-bench.mjs handwritten
13
+ * node --experimental-strip-types packages/script/scripts/query-bench.mjs calls
14
+ * ```
15
+ *
16
+ * ---
17
+ *
18
+ * ## One implementation per process, and that is not tidiness
19
+ *
20
+ * The first DriftScript benchmark called three implementations through one `fn` parameter, which
21
+ * made the call site **megamorphic**: whichever ran first got a monomorphic site and won. The same
22
+ * generated function measured 3.80 ns when it ran first and 10.01 ns when it ran second, and the
23
+ * first ordering reported the language *faster* than the JavaScript a person would write — a result
24
+ * that is not a thing that happens, believed for a while because it was measured.
25
+ *
26
+ * So this script runs exactly one implementation and exits. Comparing two means running it twice.
27
+ *
28
+ * ## It binds the module by hand rather than through `host.ts`
29
+ *
30
+ * **Node can only load the engine packages whose relative imports name a `.ts` file.** `driftscript`
31
+ * does, because it ships a Vite plugin the toolchain loads; `@driftengine/entities` does, because it
32
+ * depends on `driftscript`. Every other engine package uses extensionless imports, which a bundler
33
+ * resolves and Node does not — so importing `host.ts` pulls in `@driftengine/audio` and the script
34
+ * cannot start.
35
+ *
36
+ * The two pieces this needs are both reachable, so it calls `__bind` with the one implementation map
37
+ * it wants. **What that costs** is that this measures the binding rather than the host's assembly of
38
+ * it. **What would make it wrong** is a host that did something to the implementations on the way
39
+ * through, which `bindModule` does not: it checks requirements and passes the map along.
40
+ */
41
+ import { World, buildSchedule, defineComponent, runSchedule } from '@driftengine/entities';
42
+ import { compileDriftScript, singleFileHost } from 'driftscript/compiler';
43
+ import { loadModule } from 'driftscript';
44
+ import { registerEntityModule } from '../src/entityHost.ts';
45
+ import { entitiesImplementation } from '../src/bindings/entities.ts';
46
+
47
+ const ENTITIES = 20_000;
48
+ const TICKS = 200;
49
+
50
+ const SOURCE = `
51
+ component Position { x: f64 = 0, y: f64 = 0 }
52
+ component Velocity { x: f64 = 0, y: f64 = 0 }
53
+
54
+ system Movement {
55
+ reads Velocity
56
+ writes Position
57
+
58
+ update {
59
+ for e in query<Position, Velocity>() {
60
+ e.Position.x = e.Position.x + e.Velocity.x
61
+ e.Position.y = e.Position.y + e.Velocity.y
62
+ }
63
+ }
64
+ }
65
+ `;
66
+
67
+ /** A world with `ENTITIES` movers, and the two component types. */
68
+ function populate() {
69
+ const Position = defineComponent('Position', { x: 'f64', y: 'f64' });
70
+ const Velocity = defineComponent('Velocity', { x: 'f64', y: 'f64' });
71
+ const world = new World();
72
+ for (let i = 0; i < ENTITIES; i += 1) {
73
+ const e = world.create();
74
+ world.add(e, Position, { x: 0, y: 0 });
75
+ world.add(e, Velocity, { x: 1, y: 1 });
76
+ }
77
+ return { world, Position, Velocity };
78
+ }
79
+
80
+ /** Time `TICKS` runs of `tick`, best of three, in nanoseconds per entity. */
81
+ function measure(tick) {
82
+ for (let warm = 0; warm < 20; warm += 1) tick();
83
+ let best = Infinity;
84
+ for (let round = 0; round < 3; round += 1) {
85
+ const started = process.hrtime.bigint();
86
+ for (let t = 0; t < TICKS; t += 1) tick();
87
+ const elapsed = Number(process.hrtime.bigint() - started);
88
+ best = Math.min(best, elapsed / (TICKS * ENTITIES));
89
+ }
90
+ return best;
91
+ }
92
+
93
+ /** The compiled `.drs` system, run through the schedule exactly as a host would. */
94
+ async function generated() {
95
+ /* No registry: this module imports no capability by name, and the query form's requirement is
96
+ added by the compiler. Passing one would pull in the host assembly this script cannot load. */
97
+ const compiled = compileDriftScript(SOURCE, {
98
+ filename: 'move.drs',
99
+ mode: 'production',
100
+ manifest: { name: 'bench', provides: ['drift/ecs'] },
101
+ host: singleFileHost(),
102
+ });
103
+ const errors = compiled.diagnostics.filter((d) => d.severity === 'error');
104
+ if (errors.length > 0) throw new Error(errors.map((d) => `${d.code} ${d.message}`).join('\n'));
105
+
106
+ const namespace = await import(`data:text/javascript,${encodeURIComponent(compiled.code)}`);
107
+ const module = loadModule(namespace);
108
+ const registry = new Map();
109
+ const { systems } = registerEntityModule(module, registry);
110
+ module.exports.__bind({ 'drift/ecs': entitiesImplementation({ components: registry }) });
111
+
112
+ const Position = registry.get('Position');
113
+ const Velocity = registry.get('Velocity');
114
+ const world = new World();
115
+ for (let i = 0; i < ENTITIES; i += 1) {
116
+ const e = world.create();
117
+ world.add(e, Position, { x: 0, y: 0 });
118
+ world.add(e, Velocity, { x: 1, y: 1 });
119
+ }
120
+ const schedule = buildSchedule(systems);
121
+ let tick = 0;
122
+ return measure(() => {
123
+ runSchedule(world, schedule, tick);
124
+ tick += 1;
125
+ });
126
+ }
127
+
128
+ /** What a person would write against the engine directly: the same views, by hand. */
129
+ function handwritten() {
130
+ const { world, Position, Velocity } = populate();
131
+ return measure(() => {
132
+ const cursor = world.query(Position, Velocity);
133
+ const position = world.view(Position);
134
+ const velocity = world.view(Velocity);
135
+ for (;;) {
136
+ const step = cursor.next();
137
+ if (step.done === true) break;
138
+ const e = step.value;
139
+ const p = position.sparse[e % 2 ** 26];
140
+ const v = velocity.sparse[e % 2 ** 26];
141
+ position.x[p] = position.x[p] + velocity.x[v];
142
+ position.y[p] = position.y[p] + velocity.y[v];
143
+ }
144
+ });
145
+ }
146
+
147
+ /** The interim shape: a host call per field, which is what the query form replaced. */
148
+ function calls() {
149
+ const { world, Position, Velocity } = populate();
150
+ const ecs = entitiesImplementation({
151
+ components: new Map([
152
+ ['Position', Position],
153
+ ['Velocity', Velocity],
154
+ ]),
155
+ });
156
+ return measure(() => {
157
+ const count = ecs.count(world, 'Position');
158
+ for (let i = 0; i < count; i += 1) {
159
+ const e = ecs.at(world, 'Position', i);
160
+ ecs.write(
161
+ world,
162
+ e,
163
+ 'Position',
164
+ 'x',
165
+ ecs.read(world, e, 'Position', 'x') + ecs.read(world, e, 'Velocity', 'x'),
166
+ );
167
+ ecs.write(
168
+ world,
169
+ e,
170
+ 'Position',
171
+ 'y',
172
+ ecs.read(world, e, 'Position', 'y') + ecs.read(world, e, 'Velocity', 'y'),
173
+ );
174
+ }
175
+ });
176
+ }
177
+
178
+ const which = process.argv[2];
179
+ const implementations = { generated, handwritten, calls };
180
+ const chosen = implementations[which];
181
+ if (chosen === undefined) {
182
+ console.error(`usage: query-bench.mjs <${Object.keys(implementations).join('|')}>`);
183
+ process.exit(1);
184
+ }
185
+
186
+ const nanoseconds = await chosen();
187
+ console.log(
188
+ `${which}: ${nanoseconds.toFixed(2)} ns per entity ` +
189
+ `(${ENTITIES.toLocaleString('en-GB')} entities x ${TICKS} ticks, best of three)`,
190
+ );