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
102 changes: 101 additions & 1 deletion packages/exojs-bench/baselines/structural.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"recorded": {
"at": "2026-09-08T17:20:19.719Z",
"at": "2026-09-11T04:46:48.402Z",
"engineVersion": "0.17.0",
"adapter": "ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (Subzero) (0x0000C0DE)), SwiftShader driver)"
},
Expand Down Expand Up @@ -35,6 +35,16 @@
"textureBinds": 0,
"bufferUploads": 1
},
{
"engine": "exojs",
"config": "current",
"backend": "webgl2",
"archetype": "dynamic-all",
"nodeCount": 200,
"drawCalls": 1,
"textureBinds": 0,
"bufferUploads": 1
},
{
"engine": "exojs",
"config": "current",
Expand Down Expand Up @@ -165,6 +175,26 @@
"textureBinds": 0,
"bufferUploads": 0
},
{
"engine": "exojs",
"config": "current",
"backend": "webgl2",
"archetype": "particles-draw",
"nodeCount": 200,
"drawCalls": 1,
"textureBinds": 0,
"bufferUploads": 1
},
{
"engine": "exojs",
"config": "current",
"backend": "webgl2",
"archetype": "particles-lifecycle",
"nodeCount": 200,
"drawCalls": 1,
"textureBinds": 0,
"bufferUploads": 1
},
{
"engine": "exojs",
"config": "current",
Expand Down Expand Up @@ -205,6 +235,26 @@
"textureBinds": 0,
"bufferUploads": 0
},
{
"engine": "exojs",
"config": "current",
"backend": "webgl2",
"archetype": "tilemap-edit",
"nodeCount": 200,
"drawCalls": 1,
"textureBinds": 0,
"bufferUploads": 1
},
{
"engine": "exojs",
"config": "current",
"backend": "webgl2",
"archetype": "tilemap-scroll",
"nodeCount": 200,
"drawCalls": 1,
"textureBinds": 0,
"bufferUploads": 0
},
{
"engine": "exojs",
"config": "retained",
Expand Down Expand Up @@ -235,6 +285,16 @@
"textureBinds": 0,
"bufferUploads": 19
},
{
"engine": "exojs",
"config": "retained",
"backend": "webgl2",
"archetype": "dynamic-all",
"nodeCount": 200,
"drawCalls": 4,
"textureBinds": 0,
"bufferUploads": 7
},
{
"engine": "exojs",
"config": "retained",
Expand Down Expand Up @@ -365,6 +425,26 @@
"textureBinds": 0,
"bufferUploads": 0
},
{
"engine": "exojs",
"config": "retained",
"backend": "webgl2",
"archetype": "particles-draw",
"nodeCount": 200,
"drawCalls": 1,
"textureBinds": 0,
"bufferUploads": 1
},
{
"engine": "exojs",
"config": "retained",
"backend": "webgl2",
"archetype": "particles-lifecycle",
"nodeCount": 200,
"drawCalls": 1,
"textureBinds": 0,
"bufferUploads": 1
},
{
"engine": "exojs",
"config": "retained",
Expand Down Expand Up @@ -404,6 +484,26 @@
"drawCalls": 4,
"textureBinds": 0,
"bufferUploads": 12
},
{
"engine": "exojs",
"config": "retained",
"backend": "webgl2",
"archetype": "tilemap-edit",
"nodeCount": 200,
"drawCalls": 1,
"textureBinds": 0,
"bufferUploads": 1
},
{
"engine": "exojs",
"config": "retained",
"backend": "webgl2",
"archetype": "tilemap-scroll",
"nodeCount": 200,
"drawCalls": 1,
"textureBinds": 0,
"bufferUploads": 0
}
]
}
1 change: 1 addition & 0 deletions packages/exojs-bench/competitors/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"description": "Pinned exact-version competitor libraries for @codexo/exojs-bench (Pixi, Phaser, Excalibur, matter-js, planck, nape-js, rapier2d-compat arms). Deliberately NOT a member of the repository workspace: a plain root `pnpm install` never resolves or downloads anything here, so a normal contributor pays zero weight for competitor libraries. Only `pnpm --filter @codexo/exojs-bench bench:setup` installs this folder - as its own workspace root (see pnpm-workspace.yaml beside this file), which applies the same minimumReleaseAge supply-chain quarantine as the repository workspace - and links the results into ../node_modules so the adapters' plain `import 'pixi.js'` (etc.) resolve unmodified.",
"dependencies": {
"pixi.js": "8.19.0",
"@pixi/tilemap": "5.0.2",
"phaser": "4.2.1",
"excalibur": "0.32.0",
"matter-js": "0.20.0",
Expand Down
12 changes: 12 additions & 0 deletions packages/exojs-bench/competitors/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions packages/exojs-bench/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"scripts": {
"bench:setup": "pnpm install --dir competitors --frozen-lockfile && node competitors/link.ts",
"bench": "node --conditions=@codexo/exojs-source --import ../../scripts/glsl-register.ts --import tsx/esm src/run.ts",
"bench:reference": "node --conditions=@codexo/exojs-source --import ../../scripts/glsl-register.ts --import tsx/esm src/run.ts --suite=reference --domain=all",
"perf:baseline": "node --conditions=@codexo/exojs-source --import ../../scripts/glsl-register.ts --import tsx/esm src/run.ts",
"gate:timing": "node --conditions=@codexo/exojs-source --import ../../scripts/glsl-register.ts --import tsx/esm src/runTimingGate.ts",
"gate:structural": "node --conditions=@codexo/exojs-source --import ../../scripts/glsl-register.ts --import tsx/esm src/runStructuralGate.ts",
Expand All @@ -20,7 +21,9 @@
"devDependencies": {
"@codexo/exojs": "workspace:*",
"@codexo/exojs-config": "workspace:*",
"@codexo/exojs-particles": "workspace:*",
"@codexo/exojs-physics": "workspace:*",
"@codexo/exojs-tilemap": "workspace:*",
"@types/node": "^26.4.0",
"@webgpu/types": "^0.1.69",
"playwright": "^1.62.1",
Expand Down
41 changes: 31 additions & 10 deletions packages/exojs-bench/src/comparison/aggregate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Provenance } from '../rendering/driver';
import type { ReportData } from '../rendering/report';
import { exceedsFrameBudget } from '../shared/frameBudget';
import type { LibraryProvenance, PlatformVersionStamp, PrereleaseStamp } from '../shared/provenance';
import { mergeTimerChecks } from '../shared/timerCheck';
import { median } from '../shared/timing';
import type { BackendComparison, ComparisonCell, ComparisonRow, ComparisonSection, ExcludedRow } from './build';
import { buildPhysicsComparison, buildRenderingComparison } from './build';
Expand Down Expand Up @@ -242,6 +243,12 @@ const NO_SPREAD: RunSpread = { minMs: Number.NaN, maxMs: Number.NaN, ratio: Numb

const measured = (value: number | null): value is number => value !== null && Number.isFinite(value);

const pooledOptional = (values: ReadonlyArray<number | null | undefined>): number | null => {
const measuredValues = values.filter((value): value is number => value !== null && value !== undefined && Number.isFinite(value));

return measuredValues.length > 0 ? median(measuredValues) : null;
};

/**
* Pool one arm pair across the runs that produced it.
*
Expand Down Expand Up @@ -272,15 +279,22 @@ const aggregateCell = (perRun: readonly ComparisonCell[], runCount: number): Agg
// pooled across runs, not a tail across the pooled samples, which no
// published artifact retains.
referenceP95Ms: referenceP95s.length > 0 ? median(referenceP95s) : null,
referenceGpuMs: pooledOptional(perRun.map(cell => cell.referenceGpuMs)),
// Recomputed from the POOLED median rather than carried over from a run: the
// published number is the one the mark has to describe, and a run's own mark
// can disagree with it near the line.
referenceOverFrameBudget: exceedsFrameBudget(referenceMs),
competitorMs,
competitorP95Ms: competitorP95s.length > 0 ? median(competitorP95s) : null,
competitorGpuMs: pooledOptional(perRun.map(cell => cell.competitorGpuMs)),
competitorOverFrameBudget: exceedsFrameBudget(competitorMs),
verdict: stable ? pooled : UNSTABLE_VERDICT,
mechanism: first.mechanism,
// Merged from the per-run checks rather than recomputed from the pooled
// medians: a limitation any run established stands for the pooled figure,
// and pooling the durations first would let a well-resolved repetition carry
// a limited one past the threshold.
timer: mergeTimerChecks(perRun.map(cell => cell.timer)),
aggregate: {
runs: perRun.length,
reference: references.length > 0 ? spreadOf(references) : NO_SPREAD,
Expand Down Expand Up @@ -310,13 +324,11 @@ const groupBy = <T>(values: readonly T[], key: (value: T) => string): Map<string
};

/**
* Pool one archetype's row across the runs that produced it.
* Pool one archetype's row at one load across the runs that produced it.
*
* The runs have to agree on the row's count. Each run picks it from the
* archetype's ladder and lowers it only when some arm failed to produce a valid
* cell, so a disagreement means the runs measured different scenes - and pooling
* their medians would publish a number belonging to neither at a size only some
* of them used.
* Rows are grouped by archetype AND load, so two loads of one archetype never
* merge. The count is still checked, because the load id is derived from it and
* a disagreement would mean the runs measured different scenes under one id.
*/
const aggregateRow = (perRun: readonly ComparisonRow[], runCount: number, domain: string): AggregatedRow => {
const first = perRun[0]!;
Expand All @@ -325,7 +337,7 @@ const aggregateRow = (perRun: readonly ComparisonRow[], runCount: number, domain
if (counts.length > 1) {
throw new IncomparableRunsError(
domain,
`${domain} runs measured '${first.archetype}' at different counts (${counts.map(String).join(', ')}), so their medians describe different scenes. Re-measure: a row's count only moves when an arm failed to produce a valid cell.`,
`${domain} runs measured '${first.archetype}' at different counts under one load id (${counts.map(String).join(', ')}), so their medians describe different scenes.`,
);
}

Expand All @@ -334,11 +346,20 @@ const aggregateRow = (perRun: readonly ComparisonRow[], runCount: number, domain
cell => cell.competitor,
);

return { archetype: first.archetype, category: first.category, count: first.count, cells: [...cells.values()].map(group => aggregateCell(group, runCount)) };
return {
archetype: first.archetype,
category: first.category,
count: first.count,
loadId: first.loadId,
unit: first.unit,
primary: first.primary,
...(first.label !== undefined && { label: first.label }),
cells: [...cells.values()].map(group => aggregateCell(group, runCount)),
};
};

const aggregateRows = (perRun: ReadonlyArray<readonly ComparisonRow[]>, runCount: number, domain: string): readonly AggregatedRow[] =>
[...groupBy(perRun.flat(), row => row.archetype).values()].map(group => aggregateRow(group, runCount, domain));
[...groupBy(perRun.flat(), row => `${row.archetype}|${row.loadId}`).values()].map(group => aggregateRow(group, runCount, domain));

const aggregateSection = (perRun: readonly ComparisonSection[], runCount: number, domain: string): AggregatedSection => ({
title: perRun[0]!.title,
Expand Down Expand Up @@ -470,7 +491,7 @@ export const aggregatePhysicsRuns = (runs: readonly PhysicsReportData[]): Aggreg
runs: runs.map(run => run.provenance),
libraries: runs[0]!.libraries,
section: aggregateSection(
runs.map(run => buildPhysicsComparison(run.results)),
runs.map(run => buildPhysicsComparison(run.results, run.provenance.clock)),
runs.length,
'physics',
),
Expand Down
Loading
Loading