@adobe/ecs 0.2.3 → 0.2.4
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/dist/implementation/compiler/body-access.d.ts +18 -1
- package/dist/implementation/compiler/body-access.js +104 -15
- package/dist/implementation/compiler/body-access.js.map +1 -1
- package/dist/implementation/compiler/estree.d.ts +5 -2
- package/dist/implementation/compiler/estree.js +8 -5
- package/dist/implementation/compiler/estree.js.map +1 -1
- package/dist/implementation/compiler/inferred.js +4 -2
- package/dist/implementation/compiler/inferred.js.map +1 -1
- package/dist/implementation/compiler/transform.js +5 -4
- package/dist/implementation/compiler/transform.js.map +1 -1
- package/dist/implementation/world/activate.js +14 -2
- package/dist/implementation/world/activate.js.map +1 -1
- package/dist/implementation/world/buffer.d.ts +7 -0
- package/dist/implementation/world/buffer.js +7 -0
- package/dist/implementation/world/buffer.js.map +1 -1
- package/dist/implementation/world/cascade.d.ts +10 -3
- package/dist/implementation/world/cascade.js +48 -23
- package/dist/implementation/world/cascade.js.map +1 -1
- package/dist/implementation/world/commands.js +40 -5
- package/dist/implementation/world/commands.js.map +1 -1
- package/dist/implementation/world/create.js +53 -2
- package/dist/implementation/world/create.js.map +1 -1
- package/dist/implementation/world/execute.d.ts +4 -4
- package/dist/implementation/world/execute.js +132 -83
- package/dist/implementation/world/execute.js.map +1 -1
- package/dist/implementation/world/finalizers.d.ts +2 -0
- package/dist/implementation/world/finalizers.js +24 -3
- package/dist/implementation/world/finalizers.js.map +1 -1
- package/dist/implementation/world/join.js +12 -2
- package/dist/implementation/world/join.js.map +1 -1
- package/dist/implementation/world/resource-changes.d.ts +18 -0
- package/dist/implementation/world/resource-changes.js +66 -0
- package/dist/implementation/world/resource-changes.js.map +1 -0
- package/dist/implementation/world/rows.js +21 -16
- package/dist/implementation/world/rows.js.map +1 -1
- package/dist/implementation/world/run-changes.d.ts +38 -0
- package/dist/implementation/world/run-changes.js +138 -0
- package/dist/implementation/world/run-changes.js.map +1 -0
- package/dist/implementation/world/runner.js +16 -12
- package/dist/implementation/world/runner.js.map +1 -1
- package/dist/implementation/world/state.d.ts +13 -0
- package/dist/implementation/world/state.js +13 -1
- package/dist/implementation/world/state.js.map +1 -1
- package/dist/implementation/world/synchronize.js +72 -22
- package/dist/implementation/world/synchronize.js.map +1 -1
- package/dist/implementation/world/tables-read.d.ts +5 -3
- package/dist/implementation/world/tables-read.js +16 -4
- package/dist/implementation/world/tables-read.js.map +1 -1
- package/dist/implementation/world/tables.d.ts +3 -2
- package/dist/implementation/world/tables.js +83 -37
- package/dist/implementation/world/tables.js.map +1 -1
- package/dist/implementation/world/thread-observer.js +1 -1
- package/dist/implementation/world/thread-observer.js.map +1 -1
- package/dist/implementation/world/thread-systems.js +10 -6
- package/dist/implementation/world/thread-systems.js.map +1 -1
- package/dist/implementation/world/up.js +7 -1
- package/dist/implementation/world/up.js.map +1 -1
- package/dist/implementation/world/where.d.ts +8 -0
- package/dist/implementation/world/where.js +40 -11
- package/dist/implementation/world/where.js.map +1 -1
- package/dist/interface/finalizer/body.d.ts +18 -3
- package/dist/interface/finalizer/create.d.ts +7 -5
- package/dist/interface/finalizer/create.js +8 -5
- package/dist/interface/finalizer/create.js.map +1 -1
- package/dist/interface/finalizer/declaration.d.ts +4 -2
- package/dist/interface/indexes/declaration-of.d.ts +17 -0
- package/dist/interface/indexes/declaration-of.js +20 -0
- package/dist/interface/indexes/declaration-of.js.map +1 -0
- package/dist/interface/indexes/index.d.ts +1 -0
- package/dist/interface/indexes/index.js +1 -0
- package/dist/interface/indexes/index.js.map +1 -1
- package/dist/interface/registry/schedule.d.ts +28 -1
- package/dist/interface/registry/schedule.js +182 -21
- package/dist/interface/registry/schedule.js.map +1 -1
- package/dist/interface/system/access.d.ts +15 -0
- package/dist/interface/system/check.d.ts +7 -0
- package/dist/interface/system/check.js +17 -3
- package/dist/interface/system/check.js.map +1 -1
- package/dist/interface/system/commands.d.ts +26 -5
- package/dist/interface/system/create-resolved.js +1 -1
- package/dist/interface/system/create-resolved.js.map +1 -1
- package/dist/interface/system/create.d.ts +10 -6
- package/dist/interface/system/create.js +5 -4
- package/dist/interface/system/create.js.map +1 -1
- package/dist/interface/system/describe.d.ts +120 -0
- package/dist/interface/system/describe.js +116 -0
- package/dist/interface/system/describe.js.map +1 -0
- package/dist/interface/system/index.d.ts +1 -0
- package/dist/interface/system/index.js +1 -0
- package/dist/interface/system/index.js.map +1 -1
- package/dist/interface/system/leaf.d.ts +6 -0
- package/dist/interface/system/render.js +1 -0
- package/dist/interface/system/render.js.map +1 -1
- package/dist/interface/system/resolve.d.ts +5 -1
- package/dist/interface/system/resolve.js +19 -1
- package/dist/interface/system/resolve.js.map +1 -1
- package/dist/interface/system/shared-only.d.ts +7 -0
- package/dist/interface/system/shared-only.js +2 -0
- package/dist/interface/system/shared-only.js.map +1 -0
- package/dist/interface/system/tables.d.ts +9 -1
- package/dist/interface/world/table/index.d.ts +3 -2
- package/dist/interface/world/world.d.ts +15 -0
- package/package.json +11 -2
- package/src/guide/03-systems.out +1 -1
- package/src/guide/06-sync-points.md +12 -8
- package/src/guide/06-sync-points.out +1 -2
- package/src/guide/06-sync-points.ts +9 -5
- package/src/guide/18-identity.out +1 -1
- package/src/guide/guide.test.ts +3 -150
- package/src/implementation/compiler/body-access.ts +102 -12
- package/src/implementation/compiler/estree.ts +9 -6
- package/src/implementation/compiler/inferred.test.ts +1 -1
- package/src/implementation/compiler/inferred.ts +4 -2
- package/src/implementation/compiler/transform.test.ts +41 -21
- package/src/implementation/compiler/transform.ts +5 -3
- package/src/implementation/world/activate.ts +11 -2
- package/src/implementation/world/batches.test.ts +3 -3
- package/src/implementation/world/buffer.ts +7 -0
- package/src/implementation/world/cascade.ts +46 -22
- package/src/implementation/world/commands.ts +36 -5
- package/src/implementation/world/create.test.ts +8 -8
- package/src/implementation/world/create.ts +57 -2
- package/src/implementation/world/despawn-where.test.ts +287 -0
- package/src/implementation/world/execute.ts +125 -78
- package/src/implementation/world/finalizers.ts +27 -4
- package/src/implementation/world/ids.test.ts +1 -1
- package/src/implementation/world/indexes.test.ts +2 -1
- package/src/implementation/world/join.test.ts +19 -0
- package/src/implementation/world/join.ts +16 -2
- package/src/implementation/world/names.test.ts +1 -2
- package/src/implementation/world/perf.md +3 -1
- package/src/implementation/world/resource-changes.test.ts +61 -0
- package/src/implementation/world/resource-changes.ts +67 -0
- package/src/implementation/world/rows.ts +21 -14
- package/src/implementation/world/rules.test.ts +37 -0
- package/src/implementation/world/run-changes.test.ts +88 -0
- package/src/implementation/world/run-changes.ts +160 -0
- package/src/implementation/world/runner.ts +17 -13
- package/src/implementation/world/shared.test.ts +2 -2
- package/src/implementation/world/state.ts +19 -0
- package/src/implementation/world/sync-points.test.ts +135 -0
- package/src/implementation/world/synchronize.ts +65 -18
- package/src/implementation/world/tables-read.ts +16 -5
- package/src/implementation/world/tables.test.ts +106 -4
- package/src/implementation/world/tables.ts +71 -33
- package/src/implementation/world/thread-observer.ts +1 -1
- package/src/implementation/world/thread-systems.ts +10 -6
- package/src/implementation/world/up.test.ts +13 -1
- package/src/implementation/world/up.ts +6 -1
- package/src/implementation/world/where.ts +38 -10
- package/src/interface/finalizer/body.ts +13 -3
- package/src/interface/finalizer/create.test.ts +3 -1
- package/src/interface/finalizer/create.ts +12 -7
- package/src/interface/finalizer/declaration.ts +4 -2
- package/src/interface/indexes/create.test.ts +10 -0
- package/src/interface/indexes/declaration-of.ts +34 -0
- package/src/interface/indexes/index.ts +1 -0
- package/src/interface/registry/schedule.ts +196 -23
- package/src/interface/system/access.ts +16 -0
- package/src/interface/system/check.ts +15 -2
- package/src/interface/system/commands.ts +23 -4
- package/src/interface/system/commands.type-test.ts +31 -0
- package/src/interface/system/create-resolved.ts +1 -1
- package/src/interface/system/create.test.ts +8 -0
- package/src/interface/system/create.ts +9 -5
- package/src/interface/system/describe.test.ts +79 -0
- package/src/interface/system/describe.ts +122 -0
- package/src/interface/system/index.ts +1 -0
- package/src/interface/system/leaf.ts +6 -0
- package/src/interface/system/render.ts +1 -0
- package/src/interface/system/resolve.ts +25 -2
- package/src/interface/system/shared-only.ts +5 -0
- package/src/interface/system/tables.ts +7 -1
- package/src/interface/world/run.type-test.ts +11 -1
- package/src/interface/world/table/index.ts +3 -2
- package/src/interface/world/world.ts +13 -0
- package/src/perftest/benchmarks.ts +18 -0
- package/src/perftest/structural.ts +20 -0
- package/src/perftest/tables.ts +107 -0
|
@@ -39,10 +39,10 @@ export type ExecuteContext = {
|
|
|
39
39
|
};
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Runs a schedule tier by tier,
|
|
43
|
-
* first so workers run while main-thread systems do; commands merge in
|
|
44
|
-
* ran them. A failing system stops the run after its tier finishes:
|
|
45
|
-
* apply. `system` is the one the run was given, which the GPU's errors are reported under.
|
|
42
|
+
* Runs a schedule tier by tier, applying a sync point after each tier its schedule says and after the last, and reports
|
|
43
|
+
* what went wrong. Within a tier, kernels start first so workers run while main-thread systems do; commands merge in
|
|
44
|
+
* tier order, then chunk order, whichever thread ran them. A failing system stops the run after its tier finishes:
|
|
45
|
+
* later tiers are skipped, and commands so far still apply. `system` is the one the run was given, which the GPU's errors are reported under.
|
|
46
46
|
*/
|
|
47
47
|
export const execute = (context: ExecuteContext, schedule: Schedule, args: unknown, system: { readonly name: string }): RunReport | Promise<RunReport> => {
|
|
48
48
|
const { gpu } = context.state;
|
|
@@ -128,6 +128,8 @@ type Prepared = {
|
|
|
128
128
|
chunk: number;
|
|
129
129
|
error: unknown;
|
|
130
130
|
tierFailed: boolean;
|
|
131
|
+
/** How many commands the run's sync points dropped so far. */
|
|
132
|
+
dropped: number;
|
|
131
133
|
};
|
|
132
134
|
|
|
133
135
|
/** A schedule's tails: the schedule without them, and their runners, leaves and tiers in schedule order. */
|
|
@@ -139,6 +141,7 @@ const tailingOf = (context: ExecuteContext, schedule: Schedule): Tailing | null
|
|
|
139
141
|
const leaves: Leaf[] = [];
|
|
140
142
|
const tiers: number[] = [];
|
|
141
143
|
const rest: Leaf[][] = [];
|
|
144
|
+
const syncs: (Schedule["syncs"][number])[] = [];
|
|
142
145
|
schedule.tiers.forEach((tier, t) => {
|
|
143
146
|
const kept = tier.filter((leaf, i) => {
|
|
144
147
|
if (!flags[t][i]) return true;
|
|
@@ -147,9 +150,13 @@ const tailingOf = (context: ExecuteContext, schedule: Schedule): Tailing | null
|
|
|
147
150
|
return false;
|
|
148
151
|
});
|
|
149
152
|
if (kept.length > 0) rest.push(kept);
|
|
153
|
+
// A sync point after a tier left empty follows the tier kept before it.
|
|
154
|
+
const reasons = schedule.syncs[t];
|
|
155
|
+
if (reasons !== null && rest.length > 0) syncs[rest.length - 1] = [...syncs[rest.length - 1] ?? [], ...reasons];
|
|
150
156
|
});
|
|
151
157
|
if (leaves.length === 0) return null;
|
|
152
|
-
|
|
158
|
+
const kept = rest.map((_, t) => t < rest.length - 1 ? syncs[t] ?? null : null);
|
|
159
|
+
return { schedule: { tiers: rest, reductions: schedule.reductions, observer: schedule.observer, syncs: kept }, runners: leaves.map(context.runnerOf), leaves, tiers };
|
|
153
160
|
};
|
|
154
161
|
|
|
155
162
|
type Outcome = { failures: Failure[] | null; skipped: Leaf[] | null };
|
|
@@ -164,7 +171,7 @@ const preparedFor = (context: ExecuteContext, schedule: Schedule): Prepared => {
|
|
|
164
171
|
runners: schedule.tiers.map(tier => tier.map(context.runnerOf)), remotes: schedule.tiers.map(() => 0), uses: schedule.tiers.map(() => []), version: -1,
|
|
165
172
|
started: schedule.tiers.map(tier => new Array<readonly Chunk[]>(tier.length)),
|
|
166
173
|
systems: [], tiers: [], tasks: new Map(), warnings: [], warningCount: 0, warned: null, outcome: { failures: null, skipped: null },
|
|
167
|
-
placement: -1, tailing: null, settling: 0, chunk: 0, error: undefined, tierFailed: false,
|
|
174
|
+
placement: -1, tailing: null, settling: 0, chunk: 0, error: undefined, tierFailed: false, dropped: 0,
|
|
168
175
|
};
|
|
169
176
|
bySchedule.set(schedule, prepared);
|
|
170
177
|
}
|
|
@@ -315,8 +322,8 @@ const runFused = async (context: ExecuteContext, schedule: Schedule, prepared: P
|
|
|
315
322
|
const { gpu } = state;
|
|
316
323
|
let last = first;
|
|
317
324
|
if (prepared.version !== state.runnersVersion) refresh(context, schedule, prepared);
|
|
318
|
-
// A tier using an index starts a task of its own, after the index is refreshed for it.
|
|
319
|
-
while (last + 1 < schedule.tiers.length && allOn(prepared.runners[last + 1], channel) && prepared.uses[last + 1].length === 0) last++;
|
|
325
|
+
// A tier using an index starts a task of its own, after the index is refreshed for it, as does one after a sync point.
|
|
326
|
+
while (last + 1 < schedule.tiers.length && schedule.syncs[last] === null && allOn(prepared.runners[last + 1], channel) && prepared.uses[last + 1].length === 0) last++;
|
|
320
327
|
if (channel.busy()) await channel.idle();
|
|
321
328
|
for (let v = first; v <= last; v++) {
|
|
322
329
|
if (state.tails.length !== 0) {
|
|
@@ -431,7 +438,7 @@ const endTier = (state: State, gpu: GpuEngine | null, prepared: Prepared, schedu
|
|
|
431
438
|
};
|
|
432
439
|
|
|
433
440
|
/**
|
|
434
|
-
* Runs a schedule tier by tier
|
|
441
|
+
* Runs a schedule tier by tier with its sync points, and reports what went wrong (`execute`). Runs with no GPU,
|
|
435
442
|
* tails or observer posts awaiting nothing, so a run that waits for nothing allocates no async frame: at the first
|
|
436
443
|
* tier on a joined thread, index build across the pool or chunk on workers, `executeFrom` goes on from there.
|
|
437
444
|
*/
|
|
@@ -454,11 +461,11 @@ const executeTiers = (context: ExecuteContext, whole: Schedule, args: unknown):
|
|
|
454
461
|
if (schedule.reductions.size !== 0) for (const [key, contributors] of schedule.reductions) (folds ??= new Map()).set(key, { target: null, values: [], remaining: contributors, ran: false, failed: false });
|
|
455
462
|
const { outcome } = prepared;
|
|
456
463
|
prepared.warningCount = 0;
|
|
464
|
+
prepared.dropped = 0;
|
|
457
465
|
outcome.failures = null;
|
|
458
466
|
outcome.skipped = null;
|
|
459
467
|
if (state.gpu !== null || tailing !== null || state.tails.length !== 0 || state.observer.posts.length !== 0) return executeFrom(context, schedule, prepared, folds, args, tailing, 0, null, null);
|
|
460
468
|
let handed: Promise<RunReport> | null = null;
|
|
461
|
-
let dropped = 0;
|
|
462
469
|
try {
|
|
463
470
|
for (let t = 0; t < schedule.tiers.length; t++) {
|
|
464
471
|
// A tick in progress takes up runners replaced since it started from its next tier on.
|
|
@@ -474,37 +481,35 @@ const executeTiers = (context: ExecuteContext, whole: Schedule, args: unknown):
|
|
|
474
481
|
const waiting = settleTier(state, folds, prepared, schedule.tiers[t], t, nothing);
|
|
475
482
|
if (waiting !== null) return handed = executeFrom(context, schedule, prepared, folds, args, null, t, null, waiting);
|
|
476
483
|
if (endTier(state, null, prepared, schedule, t)) break;
|
|
484
|
+
if (schedule.syncs[t] !== null) syncNow(context, prepared, false);
|
|
477
485
|
}
|
|
478
486
|
}
|
|
479
487
|
finally {
|
|
480
|
-
if (handed === null)
|
|
488
|
+
if (handed === null) syncNow(context, prepared, true);
|
|
481
489
|
}
|
|
482
|
-
return reportOf(state, prepared
|
|
490
|
+
return reportOf(state, prepared);
|
|
483
491
|
};
|
|
484
492
|
|
|
485
493
|
/**
|
|
486
|
-
* A
|
|
487
|
-
*
|
|
494
|
+
* A sync point with nothing to wait for: no GPU, tails or observer posts, which `executeTiers` checked before each
|
|
495
|
+
* tier and bodies can't add. The run's `last` lets go of strings no reader holds.
|
|
488
496
|
*/
|
|
489
|
-
const
|
|
497
|
+
const syncNow = (context: ExecuteContext, prepared: Prepared, last: boolean): void => {
|
|
490
498
|
const { state, workers, publish } = context;
|
|
491
|
-
|
|
492
|
-
let dropped = 0;
|
|
493
|
-
// This run's tasks on joined threads are done: they return to their event loops while its sync point applies.
|
|
499
|
+
// The run's tasks on joined threads so far are done: they return to their event loops while its sync point applies.
|
|
494
500
|
for (let k = 0; k < state.logs.length; k++) markRun(state.logs[k]);
|
|
495
501
|
try {
|
|
496
502
|
if (prepareSync(state)) {
|
|
497
|
-
dropped
|
|
498
|
-
for (const { finalizer, error, kind } of applySync(state)) (outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
|
|
503
|
+
prepared.dropped += state.plan.dropped;
|
|
504
|
+
for (const { finalizer, error, kind } of applySync(state)) (prepared.outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
|
|
499
505
|
}
|
|
500
506
|
}
|
|
501
507
|
finally {
|
|
502
508
|
// Strings let go before the last run's sync point have no reader left: this run's waited for every one.
|
|
503
|
-
state.strings?.settle();
|
|
509
|
+
if (last) state.strings?.settle();
|
|
504
510
|
rewind(state, workers);
|
|
505
511
|
publish();
|
|
506
512
|
}
|
|
507
|
-
return dropped;
|
|
508
513
|
};
|
|
509
514
|
|
|
510
515
|
/**
|
|
@@ -512,12 +517,11 @@ const finishSync = (context: ExecuteContext, prepared: Prepared): number => {
|
|
|
512
517
|
* indexes' build, is done, or once `settling`, what settling it waits for, settles.
|
|
513
518
|
*/
|
|
514
519
|
const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared: Prepared, folds: Map<unknown, Fold> | null, args: unknown, tailing: Tailing | null, first: number, building: Promise<void> | null, settling: Promise<Settled> | null): Promise<RunReport> => {
|
|
515
|
-
const { state, workers
|
|
520
|
+
const { state, workers } = context;
|
|
516
521
|
const { gpu } = state;
|
|
517
522
|
const { outcome } = prepared;
|
|
518
523
|
let built = building;
|
|
519
524
|
let waited = settling;
|
|
520
|
-
let dropped = 0;
|
|
521
525
|
try {
|
|
522
526
|
for (let t = first; t < schedule.tiers.length; t++) {
|
|
523
527
|
const tier = schedule.tiers[t];
|
|
@@ -553,6 +557,10 @@ const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared
|
|
|
553
557
|
const last = await runFused(context, schedule, prepared, folds, args, t, only);
|
|
554
558
|
if (last < 0) break;
|
|
555
559
|
t = last;
|
|
560
|
+
if (schedule.syncs[t] !== null) {
|
|
561
|
+
const pending = syncPoint(context, prepared, false);
|
|
562
|
+
if (pending !== null) await pending;
|
|
563
|
+
}
|
|
556
564
|
continue;
|
|
557
565
|
}
|
|
558
566
|
if (gpu !== null) await readAhead(state, gpu, runners);
|
|
@@ -578,60 +586,15 @@ const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared
|
|
|
578
586
|
let waiting = settleTier(state, folds, prepared, tier, t, value);
|
|
579
587
|
while (waiting !== null) waiting = settleTier(state, folds, prepared, tier, t, await waiting);
|
|
580
588
|
if (endTier(state, gpu, prepared, schedule, t)) break;
|
|
589
|
+
if (schedule.syncs[t] !== null) {
|
|
590
|
+
const pending = syncPoint(context, prepared, false);
|
|
591
|
+
if (pending !== null) await pending;
|
|
592
|
+
}
|
|
581
593
|
}
|
|
582
594
|
}
|
|
583
595
|
finally {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
try {
|
|
587
|
-
if (gpu !== null) {
|
|
588
|
-
gpu.submit();
|
|
589
|
-
gpu.beforeSync();
|
|
590
|
-
}
|
|
591
|
-
try {
|
|
592
|
-
// A sync point counts as a write to everything a tail still reads.
|
|
593
|
-
if (state.tails.length !== 0) {
|
|
594
|
-
activate(state);
|
|
595
|
-
const waiting = syncPending(state) ? settleAll(state) : null;
|
|
596
|
-
if (waiting !== null) await waiting;
|
|
597
|
-
}
|
|
598
|
-
// Each sync point waits for observer systems on joined threads in progress, and cancels the rest.
|
|
599
|
-
if (state.observer.posts.length !== 0) {
|
|
600
|
-
const waiting = settlePosts(state);
|
|
601
|
-
if (waiting !== null) await waiting;
|
|
602
|
-
}
|
|
603
|
-
if (prepareSync(state)) {
|
|
604
|
-
try {
|
|
605
|
-
if (gpu !== null) {
|
|
606
|
-
// The sync point moves and writes existing rows of these archetypes on the CPU, and
|
|
607
|
-
// finalizers read them, so it needs the GPU's writes there first.
|
|
608
|
-
const disturbed = disturbedBy(state);
|
|
609
|
-
gpu.disturb(disturbed.archetypes);
|
|
610
|
-
if (gpu.anyAhead()) await gpu.readback(disturbed.storages);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
finally {
|
|
614
|
-
// Applied even when the readback fails, which only a lost device makes it do: the CPU state
|
|
615
|
-
// stays whole.
|
|
616
|
-
dropped = state.plan.dropped;
|
|
617
|
-
for (const { finalizer, error, kind } of applySync(state)) (outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
finally {
|
|
622
|
-
// Even a sync point that failed partway moved rows.
|
|
623
|
-
if (gpu !== null && state.touched !== null) {
|
|
624
|
-
gpu.touched(state.touched);
|
|
625
|
-
state.touched.clear();
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
finally {
|
|
630
|
-
// Strings let go before the last run's sync point have no reader left: this run's waited for every one.
|
|
631
|
-
state.strings?.settle();
|
|
632
|
-
rewind(state, workers);
|
|
633
|
-
publish();
|
|
634
|
-
}
|
|
596
|
+
const pending = syncPoint(context, prepared, true);
|
|
597
|
+
if (pending !== null) await pending;
|
|
635
598
|
}
|
|
636
599
|
if (tailing !== null) {
|
|
637
600
|
// Placed again since, as when a joined thread is gone: they're found again next run.
|
|
@@ -644,12 +607,96 @@ const executeFrom = async (context: ExecuteContext, schedule: Schedule, prepared
|
|
|
644
607
|
countRan(state, tailing.runners);
|
|
645
608
|
}
|
|
646
609
|
}
|
|
647
|
-
return reportOf(state, prepared
|
|
610
|
+
return reportOf(state, prepared);
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Applies the commands so far as a sync point, once what it changes is read: by tails and observer systems on joined
|
|
615
|
+
* threads still reading it, and where it moves rows, by the GPU. One within a run lets the tiers after it see them;
|
|
616
|
+
* the run's `last` lets go of strings no reader holds. Returns what to wait for, or null once it applied, so a sync
|
|
617
|
+
* point with nothing to wait for takes no turn of the event loop before what follows it.
|
|
618
|
+
*/
|
|
619
|
+
const syncPoint = (context: ExecuteContext, prepared: Prepared, last: boolean): Promise<void> | null => {
|
|
620
|
+
const { state } = context;
|
|
621
|
+
const { gpu } = state;
|
|
622
|
+
// The run's tasks on joined threads so far are done: they return to their event loops while its sync point applies.
|
|
623
|
+
for (let k = 0; k < state.logs.length; k++) markRun(state.logs[k]);
|
|
624
|
+
let pending: Promise<void> | null = null;
|
|
625
|
+
try {
|
|
626
|
+
if (gpu !== null) {
|
|
627
|
+
gpu.submit();
|
|
628
|
+
gpu.beforeSync();
|
|
629
|
+
}
|
|
630
|
+
const reading = readersDone(state);
|
|
631
|
+
pending = reading === null ? applyOnDevice(state, prepared) : reading.then(() => applyOnDevice(state, prepared) ?? undefined);
|
|
632
|
+
}
|
|
633
|
+
finally {
|
|
634
|
+
if (pending === null) afterSync(context, last);
|
|
635
|
+
}
|
|
636
|
+
return pending === null ? null : pending.finally(() => afterSync(context, last));
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Waits for the readers a sync point changes what they read for: tails, as it counts as a write to everything they
|
|
641
|
+
* read, then observer systems on joined threads in progress, cancelling the rest; null when none needs waiting for.
|
|
642
|
+
*/
|
|
643
|
+
const readersDone = (state: State): Promise<void> | null => {
|
|
644
|
+
if (state.tails.length !== 0) {
|
|
645
|
+
activate(state);
|
|
646
|
+
const waiting = syncPending(state) ? settleAll(state) : null;
|
|
647
|
+
if (waiting !== null) return waiting.then(() => (state.observer.posts.length === 0 ? null : settlePosts(state)) ?? undefined);
|
|
648
|
+
}
|
|
649
|
+
return state.observer.posts.length === 0 ? null : settlePosts(state);
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Prepares and applies a sync point, reading back first what the GPU wrote where it moves and writes existing rows,
|
|
654
|
+
* which finalizers read too; applied even when the readback fails, which only a lost device makes it do, so the CPU
|
|
655
|
+
* state stays whole.
|
|
656
|
+
*/
|
|
657
|
+
const applyOnDevice = (state: State, prepared: Prepared): Promise<void> | null => {
|
|
658
|
+
if (!prepareSync(state)) return null;
|
|
659
|
+
const { gpu } = state;
|
|
660
|
+
let reading: Promise<void> | null = null;
|
|
661
|
+
try {
|
|
662
|
+
if (gpu !== null) {
|
|
663
|
+
const disturbed = disturbedBy(state);
|
|
664
|
+
gpu.disturb(disturbed.archetypes);
|
|
665
|
+
if (gpu.anyAhead()) reading = gpu.readback(disturbed.storages);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
finally {
|
|
669
|
+
if (reading === null) apply(state, prepared);
|
|
670
|
+
}
|
|
671
|
+
return reading === null ? null : reading.finally(() => apply(state, prepared));
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
const apply = (state: State, prepared: Prepared): void => {
|
|
675
|
+
prepared.dropped += state.plan.dropped;
|
|
676
|
+
for (const { finalizer, error, kind } of applySync(state)) (prepared.outcome.failures ??= []).push(failureOf(kind ?? "finalizer", finalizer, error));
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
/** Once a sync point applied or failed: the rows it moved, even partway, the strings it let go, and its buffers. */
|
|
680
|
+
const afterSync = (context: ExecuteContext, last: boolean): void => {
|
|
681
|
+
const { state, workers, publish } = context;
|
|
682
|
+
const { gpu } = state;
|
|
683
|
+
try {
|
|
684
|
+
if (gpu !== null && state.touched !== null) {
|
|
685
|
+
gpu.touched(state.touched);
|
|
686
|
+
state.touched.clear();
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
finally {
|
|
690
|
+
// Strings let go before the last run's sync point have no reader left: this run's waited for every one.
|
|
691
|
+
if (last) state.strings?.settle();
|
|
692
|
+
rewind(state, workers);
|
|
693
|
+
publish();
|
|
694
|
+
}
|
|
648
695
|
};
|
|
649
696
|
|
|
650
697
|
/** A run's report once its sync point applied, its index conflicts included: a shared one when nothing went wrong. */
|
|
651
|
-
const reportOf = (state: State, prepared: Prepared
|
|
652
|
-
const { outcome } = prepared;
|
|
698
|
+
const reportOf = (state: State, prepared: Prepared): RunReport => {
|
|
699
|
+
const { outcome, dropped } = prepared;
|
|
653
700
|
// Conflicts unique indexes found while refreshing for finalizers.
|
|
654
701
|
if (state.indexFailures.length !== 0) (outcome.failures ??= []).push(...state.indexFailures.splice(0));
|
|
655
702
|
const { failures, skipped } = outcome;
|
|
@@ -20,6 +20,8 @@ import type { TaskChannel } from './remote-systems.js';
|
|
|
20
20
|
import { resourceOffsetsOf, writeStops, type StopsOf } from './stop-lists.js';
|
|
21
21
|
import { acquireLookups, refreshUses, type IndexLookups } from './indexes.js';
|
|
22
22
|
import { indexWatchesOf } from './lookup-keys.js';
|
|
23
|
+
import { takeResourceChanges, watchResource, type ResourceChange } from './resource-changes.js';
|
|
24
|
+
import type { Resource } from '../../interface/resource/resource.js';
|
|
23
25
|
|
|
24
26
|
/** A registered finalizer, resolved for a world: what it matches or watches, its context, and the buffer its commands go to. */
|
|
25
27
|
export type FinalizerRunner = {
|
|
@@ -28,6 +30,8 @@ export type FinalizerRunner = {
|
|
|
28
30
|
readonly excluded: readonly number[];
|
|
29
31
|
/** For a changes finalizer: whether it watches a component, by id. */
|
|
30
32
|
readonly watches: (id: number) => boolean;
|
|
33
|
+
/** For a changes finalizer: the resources it watches, by id. */
|
|
34
|
+
readonly resourceWatches: readonly number[];
|
|
31
35
|
/** By archetype id: 1 if it matches, 2 if not, 0 until known. */
|
|
32
36
|
readonly matches: number[];
|
|
33
37
|
readonly context: Context;
|
|
@@ -61,9 +65,16 @@ export const createFinalizerRunner = (state: State, finalizer: Finalizer): Final
|
|
|
61
65
|
// the objects a joined thread fills, whose storage here holds only tags.
|
|
62
66
|
let watches = (id: number): boolean => !state.derived[id] && !state.resources.has(id) && state.remote[id] === undefined;
|
|
63
67
|
const watch = finalizer.kind === "changes" ? finalizer.declaration.watch : undefined;
|
|
68
|
+
const resourceWatches: number[] = [];
|
|
64
69
|
if (watch !== undefined) {
|
|
65
70
|
// `Finalizer.changes` rejected derived components.
|
|
66
71
|
const watched = new Set(watch.map(key => idOf(state, key)));
|
|
72
|
+
for (const id of watched) {
|
|
73
|
+
if (!state.resources.has(id)) continue;
|
|
74
|
+
watched.delete(id);
|
|
75
|
+
resourceWatches.push(id);
|
|
76
|
+
watchResource(state, id);
|
|
77
|
+
}
|
|
67
78
|
watches = id => watched.has(id);
|
|
68
79
|
}
|
|
69
80
|
const target = { buffer: createBuffer() };
|
|
@@ -79,6 +90,7 @@ export const createFinalizerRunner = (state: State, finalizer: Finalizer): Final
|
|
|
79
90
|
required: [...new Set([...reads, ...query?.has ?? []])].filter(key => !(query?.optional ?? []).includes(key)).map(key => idOf(state, key)),
|
|
80
91
|
excluded: (query?.not ?? []).map(key => idOf(state, key)),
|
|
81
92
|
watches,
|
|
93
|
+
resourceWatches,
|
|
82
94
|
matches: [],
|
|
83
95
|
context: createContext(state, declaration, commanding ? { commands, indexes: indexes.byName } : { indexes: indexes.byName }),
|
|
84
96
|
target,
|
|
@@ -248,9 +260,14 @@ type Grouped = { from: Essential[] | null; to: Essential[] | null; added: Essent
|
|
|
248
260
|
|
|
249
261
|
/** No components, shared by every group that replaces none. */
|
|
250
262
|
const none: readonly number[] = [];
|
|
263
|
+
const noResources: readonly never[] = [];
|
|
264
|
+
const noResourceChanges: readonly ResourceChange[] = [];
|
|
251
265
|
|
|
252
|
-
/**
|
|
253
|
-
|
|
266
|
+
/**
|
|
267
|
+
* The sync point's structural changes to a changes finalizer's components, grouped by how they changed, and of
|
|
268
|
+
* `written`, the resources it watches.
|
|
269
|
+
*/
|
|
270
|
+
const changesOf = (state: State, runner: FinalizerRunner, queued: readonly Queued[], written: readonly ResourceChange[]): ChangeSet<Essential, Resource.Key> => {
|
|
254
271
|
const { plan, archetypes, entities } = state;
|
|
255
272
|
const { watches } = runner;
|
|
256
273
|
const names = (archetype: Archetype | null): Essential[] | null =>
|
|
@@ -366,8 +383,12 @@ const changesOf = (state: State, runner: FinalizerRunner, queued: readonly Queue
|
|
|
366
383
|
const value = commandValue(state, queued, entry, id);
|
|
367
384
|
return isMissing(value) ? live(entity, id) : value;
|
|
368
385
|
};
|
|
386
|
+
const resources = written.length === 0 || runner.resourceWatches.length === 0 ? noResources
|
|
387
|
+
: written.filter(change => runner.resourceWatches.includes(change.id)).map(({ id, previous, next }) => ({ key: newestKeyOf(state.keys[id]), previous, next }));
|
|
369
388
|
return {
|
|
370
389
|
groups: [...groups.values()],
|
|
390
|
+
// Stored values match each resource's registered schema, so they have its type.
|
|
391
|
+
resources: resources as ChangeSet<Essential, Resource.Key>["resources"],
|
|
371
392
|
// Stored and commanded values match each component's registered schema, so they have its type.
|
|
372
393
|
previous: <K extends Essential>(entity: Entity, key: K) => (bornHere(entity) ? null : live(entity, idOf(state, key))) as ReadonlyType<K> | null,
|
|
373
394
|
next: <K extends Essential>(entity: Entity, key: K) => next(entity, idOf(state, key)) as ReadonlyType<K> | null,
|
|
@@ -388,6 +409,8 @@ const staged = (state: State): boolean => {
|
|
|
388
409
|
export const runFinalizers = (state: State, queued: readonly Queued[], born: Map<number, number>, tick: number): Thrown[] => {
|
|
389
410
|
const errors: Thrown[] = [];
|
|
390
411
|
state.born = born;
|
|
412
|
+
// Taken before any finalizer runs, so what finalizers write is seen at the next sync point.
|
|
413
|
+
const written = state.resourcesSeen.size === 0 ? noResourceChanges : takeResourceChanges(state);
|
|
391
414
|
try {
|
|
392
415
|
for (const runner of state.finalizers) {
|
|
393
416
|
const { finalizer, remote } = runner;
|
|
@@ -399,8 +422,8 @@ export const runFinalizers = (state: State, queued: readonly Queued[], born: Map
|
|
|
399
422
|
if (finalizer.kind === "finalizer") finalizeStops(state, runner, finalizer, errors);
|
|
400
423
|
else {
|
|
401
424
|
// The context is built to the finalizer's declaration, so its body accepts it.
|
|
402
|
-
const changes: { body(changes: ChangeSet<Essential>, context: object): void } = finalizer;
|
|
403
|
-
guarded(state, errors, finalizer, () => changes.body(changesOf(state, runner, queued), runner.context.value));
|
|
425
|
+
const changes: { body(changes: ChangeSet<Essential, Resource.Key>, context: object): void } = finalizer;
|
|
426
|
+
guarded(state, errors, finalizer, () => changes.body(changesOf(state, runner, queued, written), runner.context.value));
|
|
404
427
|
}
|
|
405
428
|
if (runner.target.buffer.count > 0) {
|
|
406
429
|
state.queued.push({ buffer: runner.target.buffer, guarded: true });
|
|
@@ -19,7 +19,7 @@ describe("a world given keys with ids", () => {
|
|
|
19
19
|
const again: typeof import("./ids-keys.test-helper.js") = await import(/* @vite-ignore */ `./ids-keys.test-helper.js?again=${Date.now()}`);
|
|
20
20
|
expect(again.score).not.toBe(score);
|
|
21
21
|
const spawnAgain = System.create({}, ({ commands }) => { spawned.push(commands.spawn([again.score])({ [again.score]: 2 })); });
|
|
22
|
-
const add = System.create({ components: { with: [again.score] } }, row => { row[again.score] += 10; });
|
|
22
|
+
const add = System.create({ components: { with: [again.score], whole: true } }, row => { row[again.score] += 10; });
|
|
23
23
|
await world.run(spawnAgain);
|
|
24
24
|
await world.run(add);
|
|
25
25
|
const [first, second] = spawned;
|
|
@@ -253,7 +253,8 @@ const keepTicking = System.create({}, () => {});
|
|
|
253
253
|
const watchGrid = System.create({ lookup: { near: { index: grid } } }, ({ lookup }) => { void lookup.near([0, 0]).length; });
|
|
254
254
|
|
|
255
255
|
describe("index lifetime", () => {
|
|
256
|
-
|
|
256
|
+
// Flaky on CI's runners, which runs no flaky test: run locally until it's hardened (`followups.md`, Correctness).
|
|
257
|
+
it.skipIf(process.env["CI"] !== undefined)("builds an index the first time a system uses it and releases it, arena blocks included, once none does", async () => {
|
|
257
258
|
const driver = backToBack();
|
|
258
259
|
await using world = createWorld({ [workers]: 0, [always]: true }, { loop: driver });
|
|
259
260
|
await world.run(spawnCells, { cells: [[0, 0], [1, 1]] });
|
|
@@ -62,6 +62,11 @@ const readTotal = System.create({ readonly: true }, ({ resources }) => resources
|
|
|
62
62
|
const mix = System.create({}, ({ resources }, input: { readonly level: number }) => {
|
|
63
63
|
resources[levels] = [...resources[levels], input.level];
|
|
64
64
|
});
|
|
65
|
+
/** Runs on the thread claiming joinTestAudio, as `mix` does, and despawns by a query, which only the world's thread does. */
|
|
66
|
+
const mixAndClear = System.create({}, ({ resources, commands }) => {
|
|
67
|
+
resources[levels] = [...resources[levels], 0];
|
|
68
|
+
commands.despawnWhere({ with: [marked] });
|
|
69
|
+
});
|
|
65
70
|
/** An observer system recording each total it reads into `into`. */
|
|
66
71
|
const totalsInto = (into: number[]) => System.create({ readonly: true }, ({ resources }) => { into.push(resources[total]); });
|
|
67
72
|
/** An observer system recording how many levels it reads into `into`. */
|
|
@@ -155,6 +160,12 @@ describe("joining", () => {
|
|
|
155
160
|
await remote.dispose();
|
|
156
161
|
await world.dispose();
|
|
157
162
|
});
|
|
163
|
+
it("rejects a run asking for its changes, which only the world's thread records", async () => {
|
|
164
|
+
const { world, remote } = await joined();
|
|
165
|
+
await expect(remote.run(idle, { changes: true })).rejects.toThrow("a run's changes are recorded only on the world's thread");
|
|
166
|
+
await remote.dispose();
|
|
167
|
+
await world.dispose();
|
|
168
|
+
});
|
|
158
169
|
it("rejects a run that failed, naming its system, and a run of a system without an id", async () => {
|
|
159
170
|
const { world, remote } = await joined();
|
|
160
171
|
await expect(remote.run(fail)).rejects.toThrow(/^System "fail" \(.*join\.test\.ts#fail\) failed: joinTest failure$/);
|
|
@@ -216,6 +227,14 @@ describe("the join signal", () => {
|
|
|
216
227
|
await world.run(mix, { level: 1 });
|
|
217
228
|
expect(await eventually(() => heard.includes(1))).toBe(true);
|
|
218
229
|
});
|
|
230
|
+
it("fails a system on a joined thread that despawns by a query, which runs on the world's thread", async () => {
|
|
231
|
+
await using world = createWorld({ [workers]: 0 });
|
|
232
|
+
const channel = new MessageChannel();
|
|
233
|
+
const shared = world.share(channel.port1);
|
|
234
|
+
await using _remote = await join(channel.port2, { threads: ["joinTestAudio"] });
|
|
235
|
+
await shared;
|
|
236
|
+
await expect(world.run(mixAndClear)).rejects.toThrow("commands.despawnWhere runs on the world's thread");
|
|
237
|
+
});
|
|
219
238
|
it("resolves several joins at once, each with its own claims, and each of their observer systems sees the world's next run", async () => {
|
|
220
239
|
await using world = createWorld({ [workers]: 0 });
|
|
221
240
|
const claims: readonly (readonly ("main" | "joinTestAudio")[])[] = [[], ["main"], ["joinTestAudio"]];
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { asksForChanges, takesInput } from './run-changes.js';
|
|
3
|
+
import type { ChangeSet } from '../../interface/finalizer/body.js';
|
|
4
|
+
import type { Essential } from '../../interface/component/value.js';
|
|
5
|
+
import type { Resource } from '../../interface/resource/resource.js';
|
|
2
6
|
import type { Thread } from '../../interface/thread/thread.js';
|
|
3
7
|
import type { World } from '../../interface/world/world.js';
|
|
4
8
|
import type { Input } from '../../interface/system/input.js';
|
|
5
9
|
import type { System } from '../../interface/system/system.js';
|
|
6
|
-
import { declarationOf, identifiedItems, idOfItem, idOfKey, indexOfLookup, keyOfId, labelOf, requireDefinition, threadOf, threadOfDeclaration, watchedBy, type Definition } from '../../interface/registry/index.js';
|
|
10
|
+
import { declarationOf, definitionOf, identifiedItems, idOfItem, idOfKey, indexOfLookup, keyOfId, labelOf, requireDefinition, threadOf, threadOfDeclaration, watchedBy, type Definition } from '../../interface/registry/index.js';
|
|
7
11
|
import { unsharedKeyOf } from './index-keys.js';
|
|
8
12
|
import { layoutOf, type Views } from '../../interface/registry/layout.js';
|
|
9
13
|
import {
|
|
@@ -681,9 +685,19 @@ export const joinWorld = (port: Parameters<World["share"]>[0], options: { readon
|
|
|
681
685
|
return runNumbered(numberOf(id, true), definition, input, null);
|
|
682
686
|
};
|
|
683
687
|
|
|
688
|
+
function run<I, R>(system: System<I, R>, ...input: Input<NoInfer<I>>): Promise<R>;
|
|
689
|
+
function run<I, R>(system: System<I, R>, ...input: [...Input<NoInfer<I>>, options: { readonly changes: true }]): Promise<ChangeSet<Essential, Resource.Key>>;
|
|
690
|
+
function run(system: System<unknown, unknown>, ...args: readonly unknown[]): Promise<unknown> {
|
|
691
|
+
const definition = definitionOf(system);
|
|
692
|
+
// A system taking input whose input is all it was given runs with it.
|
|
693
|
+
const asked = args.length > 0 && asksForChanges(args[args.length - 1]) && definition !== undefined && !(args.length < 2 && takesInput(definition));
|
|
694
|
+
if (asked) return Promise.reject(new Error(`System "${system.name}": a run's changes are recorded only on the world's thread.`));
|
|
695
|
+
return runOn(system, args);
|
|
696
|
+
}
|
|
697
|
+
|
|
684
698
|
const world: World = {
|
|
685
699
|
// A run gives what its system's body returned, `R` by the system's type.
|
|
686
|
-
run
|
|
700
|
+
run,
|
|
687
701
|
add: system => {
|
|
688
702
|
const definition = requireDefinition(system);
|
|
689
703
|
const leaves = observerLeavesOf(definition);
|
|
@@ -43,8 +43,7 @@ const failOnce = System.create(() => { throw new Error("unnamed"); });
|
|
|
43
43
|
|
|
44
44
|
describe("systems, rules and finalizers of one label", () => {
|
|
45
45
|
it("are distinct in schedules, both run, and are told apart in reports by their ids", async () => {
|
|
46
|
-
|
|
47
|
-
const linked = (after: readonly System[], body: (row: { [value]: number }) => void): System => System.create({ components: { with: [value] }, after }, row => body(row));
|
|
46
|
+
const linked = (after: readonly System[], body: (row: { [value]: number }) => void): System => System.create({ components: { with: [value], whole: true }, after }, row => body(row));
|
|
48
47
|
const a = linked([], row => { ran.push("a"); row[value] += 1; });
|
|
49
48
|
const b = linked([a], row => { ran.push("b"); row[value] += 1; throw new Error("b failed"); });
|
|
50
49
|
const c = linked([b], row => { ran.push(`c${row[value]}`); });
|
|
@@ -220,7 +220,7 @@ Shared components and `where`, against `12e366d`, node: the whole suite three ti
|
|
|
220
220
|
- **A shared value read in a row costs what a column's does:** `shared read 100K` 4.04–4.10 ns a row against 4.24–4.27 for `column read 100K`, the value bound with the row once per archetype. In a kernel it's a parameter of each call: `shared read 100K kernel` 0.40–0.41 against 0.50 for `column read 100K kernel`, one column fewer to stream.
|
|
221
221
|
- **Spawning with a shared constant:** `spawn bulk shared`, one spawner reused, 17.3–18.1 ns a spawn against 33.3–34.3 for `spawn bulk plain`, whose cell is a column; `spawn singles shared`, its spawner found by its constants each call, 313–323 ns against 229 for `spawn singles plain`: about 90 ns a call to find the spawner, a tuple's leaves walked through a tree of its stored leaves (`shared.ts`) and each constant in turn through the shape's tree of spawners, with no text built.
|
|
222
222
|
- **`where` over 1K cells,** 3 × 3 of them in view: `where 1K cells still` 0.49 ns a cell, 0.5 µs a run with nothing evaluated; `where 1K cells moving`, every predicate evaluated again as the camera moves each run, 18.7–19.0 ns a cell, each predicate's resources read once per evaluation.
|
|
223
|
-
- **Many small archetypes:** `small archetypes 1K`, 10K rows over 1K archetypes of one cell each,
|
|
223
|
+
- **Many small archetypes:** `small archetypes 1K`, 10K rows over 1K archetypes of one cell each, 6.8–7.0 ns a row against 3.04 for `one archetype 10K`: about 38 ns an archetype, binding its row, which a shared component's values multiply.
|
|
224
224
|
|
|
225
225
|
Review fixes to shared components and `where`, node, alternating against the commit before each, medians of four to six rounds:
|
|
226
226
|
- **Arrivals precise** (`arrivals.ts`): a system with `where` and change filters reads a ring of the moves since its last run, each archetype moved into owing the point of the one its rows came from, where every arrival made the archetype count from the oldest point of all matches, found by a scan of them. New `where changed 1K cells drifting in view`, every cell in view, 1% of 10K rows moving to the next cell before each run: 123.6 → 69.2 ns a cell; `where changed 1K cells drifting`, 3 × 3 cells in view, 2.0 → 2.8 ns a cell, the ring's 102 entries a run read, about 7 ns each, and cells moved into from culled ones scanned since that one's point; `changed 1K cells drifting`, without `where`, 52.3 → 52.0. `where 1K cells still` and `moving` within noise.
|
|
@@ -241,6 +241,8 @@ Step figures marked (vitest) are from the node vitest lane: read them as relativ
|
|
|
241
241
|
kernel, the second call stayed a call and boxed each entity, a double past 2^32. `despawn bulk kernel` 4.60 → 4.12 ms;
|
|
242
242
|
`despawn bulk` compiled 4.55 ms against 4.32 as written before, 4.10 after.
|
|
243
243
|
- Inline value writes instead of closures, an 8-shape spawn cache, edges indexed by component id, archetypes grown once per sync point: create 38 → 27.6 ms.
|
|
244
|
+
- Holders of references spawned in a sync point are linked a block at a time before the cascade, from their columns, rather than listed as triples for the cascade and for the holder index each; and `commands.spawnTables` lists owners between its rows only when one is dropped, and reserves a table's entities at once when every row spawns: `spawn tables 1M owner` 160 → 53 ms, `spawn owned 1M` 133 → 64 ms, against 10 ms for `spawn tables 1M`. Each holder is journaled once, so an `up` tree builds again past 4096 journal entries and an eighth of its rows, as indexes do: `up load` 183 → 120 ms.
|
|
245
|
+
- A row loop reads the body into a local once, which V8 otherwise loads again each row, since the call could change it; rows bind without allocating, and filters are found by archetype id: `with 100K` 4.24 → 3.97 ns a row, `one archetype 10K` 3.47 → 3.04, `small archetypes 1K` 7.74 → 6.90, `shared read 100K` 4.07 → 3.78.
|
|
244
246
|
|
|
245
247
|
**Change filters.**
|
|
246
248
|
- Chunk ticks, the newest tick per 1024 rows, so filters skip unchanged chunks: clustered sparse to ~0.09 ms.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// © 2026 Adobe. MIT License. See /LICENSE for details.
|
|
2
|
+
import { expect, it } from "vitest";
|
|
3
|
+
import { Finalizer, System } from "../../index.js";
|
|
4
|
+
import { Observe } from "../../observe/index.js";
|
|
5
|
+
import { createWorld } from "../index.js";
|
|
6
|
+
import { sameValue } from "./resource-changes.js";
|
|
7
|
+
|
|
8
|
+
const size = Symbol("resourceChanges/size");
|
|
9
|
+
const corner = Symbol("resourceChanges/corner");
|
|
10
|
+
const unwatched = Symbol("resourceChanges/unwatched");
|
|
11
|
+
const log = Symbol("resourceChanges/log");
|
|
12
|
+
const label = Symbol("resourceChanges/label");
|
|
13
|
+
|
|
14
|
+
declare module "@adobe/ecs" {
|
|
15
|
+
interface Components {
|
|
16
|
+
[label]: { type: string };
|
|
17
|
+
}
|
|
18
|
+
interface Resources {
|
|
19
|
+
[size]: { type: number; default: 1 };
|
|
20
|
+
[corner]: { type: readonly [number, number] };
|
|
21
|
+
[unwatched]: { type: number };
|
|
22
|
+
[log]: { type: readonly string[]; default: [] };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Watching only resources, it sees no component's changes.
|
|
27
|
+
Finalizer.changes({ watch: [size, corner] }, (changes, { resources }) => {
|
|
28
|
+
const lines = changes.resources.map(change => change.key === size ? `size ${change.previous} → ${change.next}` : `corner ${change.previous.join(",")} → ${change.next.join(",")}`);
|
|
29
|
+
resources[log] = [...resources[log], ...changes.groups.map(() => "group"), ...lines];
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const resize = System.create({}, ({ resources }, input: { readonly to: number }) => {
|
|
33
|
+
resources[size] = input.to;
|
|
34
|
+
resources[size] = input.to + 1;
|
|
35
|
+
});
|
|
36
|
+
const move = System.create({}, ({ resources }, input: { readonly x: number }) => { resources[corner] = [input.x, 0]; });
|
|
37
|
+
const touch = System.create({}, ({ resources }) => { resources[unwatched] += 1; });
|
|
38
|
+
const spawn = System.create({}, ({ commands }) => { commands.spawn([label])({ [label]: "a" }); });
|
|
39
|
+
const read = System.create({ readonly: true }, ({ resources }) => resources[log]);
|
|
40
|
+
|
|
41
|
+
it("reports each watched resource written since its finalizers last ran, with its first and last value, once", async () => {
|
|
42
|
+
await using world = createWorld();
|
|
43
|
+
// A run writing nothing else still has a sync point, so its finalizers see the write.
|
|
44
|
+
await world.run(resize, { to: 4 });
|
|
45
|
+
expect(await world.run(read)).toEqual(["size 1 → 5"]);
|
|
46
|
+
await world.run(spawn);
|
|
47
|
+
await world.run(touch);
|
|
48
|
+
expect(await world.run(read)).toEqual(["size 1 → 5"]);
|
|
49
|
+
await world.run(move, { x: 2 });
|
|
50
|
+
// Written back to an equal value, leaf by leaf: not reported.
|
|
51
|
+
await world.run(move, { x: 2 });
|
|
52
|
+
await world.run(resize, { to: 4 });
|
|
53
|
+
expect(await world.run(read)).toEqual(["size 1 → 5", "corner 0,0 → 2,0"]);
|
|
54
|
+
expect(await Observe.toPromise(Observe.resource(world, size))).toBe(5);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("compares data leaf by leaf and other objects by identity", () => {
|
|
58
|
+
const map = new Map();
|
|
59
|
+
expect([sameValue([1, { a: -0 }], [1, { a: -0 }]), sameValue({ a: 1 }, { a: 1, b: 2 }), sameValue(0, -0), sameValue(NaN, NaN), sameValue(map, new Map()), sameValue(map, map)])
|
|
60
|
+
.toEqual([true, false, false, true, false, true]);
|
|
61
|
+
});
|