From 4a1e8948ddcea57bd9ca438d409fb715243920ab Mon Sep 17 00:00:00 2001 From: Kris Nye Date: Sun, 1 Nov 2026 14:56:09 -0800 Subject: [PATCH] ecs: sync the ecs repository at 171dab8: a pass counts what it ran linearly in its tiers Co-Authored-By: Claude Opus 5.5 --- .../ecs/src/implementation/world/execute.ts | 26 +++++++++++++------ .../implementation/world/run-results.test.ts | 17 ++++++++++++ .../ecs/src/implementation/world/state.ts | 8 ++++++ 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/ecs/packages/ecs/src/implementation/world/execute.ts b/ecs/packages/ecs/src/implementation/world/execute.ts index 48e5a57d..72d34e16 100644 --- a/ecs/packages/ecs/src/implementation/world/execute.ts +++ b/ecs/packages/ecs/src/implementation/world/execute.ts @@ -3,7 +3,7 @@ import type { Leaf } from '../../interface/system/leaf.js'; import type { Chunk, ChunkResult, Runner, Workers } from './runner.js'; import type { Schedule } from '../../interface/registry/schedule.js'; import type { Archetype, Storage } from './archetype.js'; -import { countRan, idOf, startRan, type State } from './state.js'; +import { countRan, countRanHere, idOf, startRan, type State } from './state.js'; import type { GpuEngine } from './gpu.js'; import { applySync, prepareSync, syncPending } from './synchronize.js'; import { resourcesWritten } from './resource-changes.js'; @@ -25,7 +25,6 @@ import { mergeUses, refreshUses, type IndexUse } from './indexes.js'; /** Shared by every report without failures or skipped systems: they're read-only, so none needs its own. */ const none: readonly never[] = []; const noChunks: readonly Chunk[] = []; -const noRunners: readonly Runner[] = []; /** The report of a run with nothing to report. */ export const clean: RunReport = { failures: none, skipped: none, dropped: 0, warnings: none }; @@ -109,8 +108,12 @@ type Prepared = { * tier for any other. */ readonly passes: number[]; - /** By tier starting a pass of several, the runners of the whole pass, which it counts as it ran at once. */ - readonly passRunners: (readonly Runner[])[]; + /** + * By tier within a pass of several, how many runners it and the pass's later tiers hold, and whether any runs on the + * GPU: what a pass entered there counts once it ran to its end. + */ + readonly passSystems: number[]; + readonly passGpu: boolean[]; /** The world's `runnersVersion` its runners were read at. */ version: number; readonly started: (readonly Chunk[])[][]; @@ -179,7 +182,7 @@ const preparedFor = (context: ExecuteContext, schedule: Schedule): Prepared => { if (prepared === undefined) { prepared = { runners: schedule.tiers.map(tier => tier.map(context.runnerOf)), remotes: schedule.tiers.map(() => 0), uses: schedule.tiers.map(() => []), - passes: schedule.tiers.map((_, t) => t), passRunners: schedule.tiers.map(() => noRunners), version: -1, + passes: schedule.tiers.map((_, t) => t), passSystems: schedule.tiers.map(() => 0), passGpu: schedule.tiers.map(() => false), version: -1, started: schedule.tiers.map(tier => new Array(tier.length)), systems: [], tiers: [], tasks: new Map(), warnings: [], warningCount: 0, warned: null, outcome: { failures: null, skipped: null }, placement: -1, tailing: null, settling: 0, chunk: 0, error: undefined, tierFailed: false, dropped: 0, @@ -212,7 +215,7 @@ const asWritten = (runners: readonly Runner[]): boolean => { /** Finds a schedule's passes (`Prepared["passes"]`) from its runners as read now. */ const passesOf = (schedule: Schedule, prepared: Prepared): void => { - const { runners, uses, passes, passRunners } = prepared; + const { runners, uses, passes, passSystems, passGpu } = prepared; let last = -1; for (let t = 0; t < schedule.tiers.length; t++) { if (!asWritten(runners[t])) last = t; @@ -221,7 +224,14 @@ const passesOf = (schedule: Schedule, prepared: Prepared): void => { while (last + 1 < schedule.tiers.length && uses[last + 1].length === 0 && asWritten(runners[last + 1])) last++; } passes[t] = last; - passRunners[t] = last === t ? noRunners : runners.slice(t, last + 1).flat(); + } + for (let t = schedule.tiers.length - 1; t >= 0; t--) { + const tier = runners[t]; + const continues = passes[t] > t; + let gpu = continues && passGpu[t + 1]; + for (let i = 0; i < tier.length; i++) if (tier[i].placement !== "cpu") gpu = true; + passSystems[t] = tier.length + (continues ? passSystems[t + 1] : 0); + passGpu[t] = gpu; } }; @@ -556,7 +566,7 @@ const runPass = (context: ExecuteContext, prepared: Prepared, schedule: Schedule return -1; } } - countRan(state, prepared.passRunners[first]); + countRanHere(state, prepared.passSystems[first], prepared.passGpu[first]); return last; }; diff --git a/ecs/packages/ecs/src/implementation/world/run-results.test.ts b/ecs/packages/ecs/src/implementation/world/run-results.test.ts index 1d461e0c..26759d76 100644 --- a/ecs/packages/ecs/src/implementation/world/run-results.test.ts +++ b/ecs/packages/ecs/src/implementation/world/run-results.test.ts @@ -354,6 +354,23 @@ describe("what a run gathers for tests", () => { expect(await runRan(world, System.create([nothing, raise]))).toEqual({ systems: 2, workers: 0, joined: 0, main: true, gpu: false }); expect(await runRan(world, readLevel)).toEqual({ systems: 0, workers: 0, joined: 0, main: false, gpu: false }); }); + + it("counts each system of a chain once, run whole or entered after a sync point that applies something", async () => { + await using world = createWorld({ [workers]: 0 }); + /** Systems each after the one before, the first after `first`. */ + const chainOf = (length: number, first: readonly System[]): System[] => { + const links: System[] = []; + for (let k = 0; k < length; k++) links.push(System.create({ after: links.length === 0 ? first : links.slice(-1) }, ({ resources }) => { + resources[frames] = k; + })); + return links; + }; + expect(await runRan(world, System.create(chainOf(64, [])))).toEqual({ systems: 64, workers: 0, joined: 0, main: true, gpu: false }); + const reads = System.create({ after: [spawn], components: { with: [amount] } }, row => { + counted += row[amount]; + }); + expect(await runRan(world, System.create([spawn, reads, ...chainOf(8, [reads])]))).toEqual({ systems: 10, workers: 0, joined: 0, main: true, gpu: false }); + }); }); describe("a value a body returned", () => { diff --git a/ecs/packages/ecs/src/implementation/world/state.ts b/ecs/packages/ecs/src/implementation/world/state.ts index 7161bd42..32b1fcb8 100644 --- a/ecs/packages/ecs/src/implementation/world/state.ts +++ b/ecs/packages/ecs/src/implementation/world/state.ts @@ -78,6 +78,14 @@ export const countRan = (state: State, runners: readonly RanRunner[]): void => { for (let k = 0; k < tier.logs.length; k++) ran.joined.add(tier.logs[k]); }; +/** Counts runners into `ran` that all ran here, none on a joined thread: on `main` unless a joined thread claims it. */ +export const countRanHere = (state: State, systems: number, gpu: boolean): void => { + const { ran } = state; + ran.systems += systems; + if (systems !== 0 && state.claims.get("main") === undefined) ran.main = true; + if (gpu) ran.gpu = true; +}; + /** Starts gathering what a run ran. */ export const startRan = (ran: Ran): void => { ran.systems = 0;