Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions bench/iterate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,41 @@ export function makeEcsiaPinnedIter(n: number): IterCase {
}
}

// The compiled-ergonomic variant: the SAME readable `.each` body the `ecsia` bucket runs, but handed to
// q.compile, which rewrites `e.<comp>.<field>` to direct column indexing and codegens the bindColumns-
// shape loop. It should land near ecsia-pinned while keeping the proxy-path syntax — the point of the
// bucket is to show the ergonomic path no longer pays the per-row proxy tax.
export function makeEcsiaCompiledIter(n: number): IterCase {
const Position = defineComponent({ x: 'f32', y: 'f32' }, { name: 'position' })
const Velocity = defineComponent({ dx: 'f32', dy: 'f32' }, { name: 'velocity' })
const world = createWorld({ components: [Position, Velocity], maxEntities: nextPow2(n) })
let first = 0 as unknown as ReturnType<typeof world.spawnWith>
for (let i = 0; i < n; i++) {
const h = world.spawnWith(Position, Velocity)
if (i === 0) first = h
const v = world.entity(h).write(Velocity) as { dx: number; dy: number }
v.dx = 1
v.dy = 0.5
}
const q = world.query(write(Position), write(Velocity)) as unknown as {
compile<Ctx>(b: (e: { position: { x: number; y: number }; velocity: { dx: number; dy: number } }, ctx: Ctx) => void): (ctx: Ctx) => void
}
const run = q.compile<{ dt: number }>((e, ctx) => {
e.position.x += e.velocity.dx * ctx.dt
e.position.y += e.velocity.dy * ctx.dt
})
const ctx = { dt: DT }
return {
name: 'ecsia-compiled',
step() {
run(ctx)
},
sampleX() {
return (world.entity(first).read(Position) as { x: number }).x
},
}
}

interface MiniEntity {
position: { x: number; y: number }
velocity: { dx: number; dy: number }
Expand Down
3 changes: 2 additions & 1 deletion bench/regression-baseline.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"_comment": "CI bench regression ceilings — MAX allowed ns/entity RATIO of each ecsia path vs a SAME-RUN bitECS control (so machine drift cancels). A real regression (e.g. codegen breaking → bindColumns deopts from ~0.72x to ~1.5x) trips the ceiling; ~10% run-to-run noise does not. RATCHET: when a path durably improves, lower its ceiling here. Measured 2026-06-08: bindColumns ~0.72x, eachChunk ~1.08x, each ~7.4x.",
"_comment": "CI bench regression ceilings — MAX allowed ns/entity RATIO of each ecsia path vs a SAME-RUN bitECS control (so machine drift cancels). A real regression (e.g. codegen breaking → bindColumns deopts from ~0.72x to ~1.5x) trips the ceiling; ~10% run-to-run noise does not. RATCHET: when a path durably improves, lower its ceiling here. Measured 2026-06-08: bindColumns ~0.72x, compile ~0.75x (the ergonomic body, codegen'd — should track bindColumns), eachChunk ~1.08x, each ~7.4x.",
"ratiosVsBitecs": {
"bindColumns": 0.9,
"compile": 1.0,
"eachChunk": 1.3,
"each": 9.0
}
Expand Down
9 changes: 8 additions & 1 deletion bench/test/regression.bench.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
import { describe, expect, test } from 'vitest'
import { readFileSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { makeEcsiaIter, makeEcsiaCursorIter, makeEcsiaPinnedIter, makeBitEcsIter } from '../iterate.js'
import {
makeEcsiaIter,
makeEcsiaCursorIter,
makeEcsiaPinnedIter,
makeEcsiaCompiledIter,
makeBitEcsIter,
} from '../iterate.js'
import type { IterCase } from '../iterate.js'

const ENABLED = process.env['BENCH_REGRESSION'] === '1'
Expand Down Expand Up @@ -51,6 +57,7 @@ describe.skipIf(!ENABLED)('bench regression — ecsia/bitECS ns/entity ratios un

test.each([
['bindColumns', makeEcsiaPinnedIter as (n: number) => CtxIter],
['compile', makeEcsiaCompiledIter as (n: number) => CtxIter],
['eachChunk', makeEcsiaCursorIter as (n: number) => CtxIter],
['each', makeEcsiaIter as (n: number) => CtxIter],
])('%s ratio vs bitECS stays under its ceiling', (name, make) => {
Expand Down
12 changes: 6 additions & 6 deletions bundle-budget.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
"_comment": "Bundle-size budgets: max min+gzip BYTES per tree-shaken entry (scripts/size-check.mjs). The honest \"lean install\" number. Ratchet DOWN with `node scripts/size-check.mjs --update` when a build shrinks; CI fails if a build grows past budget*1.03.",
"budgets": {
"kernel": {
"gzip": 39987,
"min": 126142
"gzip": 42285,
"min": 131819
},
"core-min": {
"gzip": 30154,
"min": 97265
"gzip": 32443,
"min": 102905
},
"full-umbrella": {
"gzip": 54587,
"min": 171999
"gzip": 56884,
"min": 177652
}
}
}
5 changes: 4 additions & 1 deletion packages/core/src/internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export type {
ObserverDeps,
} from './reactivity/index.js'

export { QueryEngine, LiveQuery, SparseSetU32, compileQuery } from './query/index.js'
export { QueryEngine, LiveQuery, SparseSetU32, compileQuery, analyzeEachBody } from './query/index.js'
export type {
QueryEngineDeps,
LiveQueryDeps,
Expand All @@ -117,6 +117,9 @@ export type {
RowFilterTerm,
ValueRole,
Word,
EachPlan,
EachViewSpec,
EachAnalyzeDeps,
} from './query/index.js'

// Low-level schema inference helpers re-exported through core (not on the curated public surface; the
Expand Down
Loading
Loading