A test that waits for a deadline somebody guessed is waiting for the wrong thing, so it waits for the editor (#93)
game-maker / suite (push) Successful in 6m48s

The suite spent 88% of its step time asleep: 564 s of steps, 496 s of
them in sleep(n) - fixed spans estimated by hand and then copied from
step to step. That is why it was slow, and why it flickered.

The signal is afterFrames(count, timeout) in ui/render.ts, exposed as
__gm.afterFrames(). It wakes the loop itself, because an editor that
draws nothing would never redeem a promise of a frame, and it gives up
after a timeout instead of hanging the run. settle() in the harness
waits two drawn frames (~32 ms) instead of 150-300 ms and falls back to
sleep(150) when the hook is missing (an old build): silently not
waiting would make every later check a coin toss.

Every action goes through it now - click, doubleClick, wheel, drag,
dragAsset, press, typeText, select, mutate, undo, redo - and with that
about 550 sleep() calls behind those actions became redundant, helpers
that are nothing but a mutate in a coat included. What a frame cannot
answer got its own condition instead: waitUntil(expr), play()/stop()
waiting on __gm.state.mode rather than on 300 ms, saved() for the
autosave (the two hand-written polling loops sampled at 300 ms for
something that arrives after a second), and showTab() as a real tool -
47 places called evaluate("__gm.showTab(...)") raw, and 24 of them
waited for nothing at all and read the old panel.

564 s -> 243 s of step time, the waiting share 88% -> 76%. Three kinds
of wait were kept, and each one bit when it was taken away: loading
bytes (textures, fonts), game time that a repaint cannot stand in for
(time scale, shader and particle clocks, the live values a running
graph carries), and the inertia the scroll step measures on purpose.

Two things the new speed uncovered, both real: 25-animation read the
frame menu unfiltered and had frozen an incomplete list (the CI failure
on c990dbc), and 33-render-on-change measured idleness while the
autosave of the step before was still painting - it waits for
saveStatus to leave dirty/saving now, so it measures that nothing
happens any more instead of that it waited long enough.
This commit is contained in:
admin committed 2026-09-16 08:08:10 +02:00
1 parent c990dbcb26
commit 7e5003b590
41 files changed
+362 -732

No files matched your search

+3 -33
View File
@@ -87,7 +87,6 @@ export async function nestedBehavior(t) {
${MAKE_OBJECT}
scene.objects.push(make('nb-box', 'crate', '', 700, 'Nested box'), make('nb-logic', 'nb-caller', 'nb-box', 700, 'Caller'));
`);
await sleep(400);
// 19b. The call really runs, and the value really comes back: the box moves at dx × scale, and
// dx is nothing the caller holds – it is what the nested graph returned. Without the result
@@ -95,7 +94,6 @@ export async function nestedBehavior(t) {
const boxX = async () => (await object('nb-box')).position.x;
const idleX = await boxX();
await t.play();
await sleep(300);
const before = (await entity('nb-box')).x;
await sleep(1000);
const after = (await entity('nb-box')).x;
@@ -106,7 +104,6 @@ export async function nestedBehavior(t) {
`moved ${speed.toFixed(1)} px in ~1 s – 2 × ${SCALE} means the returned value arrived, 0 would mean nothing came back`,
);
await stop();
await sleep(300);
check(
'the stored object is untouched by the run, as in any play session',
(await boxX()) === idleX,
@@ -115,10 +112,7 @@ export async function nestedBehavior(t) {
// 19c. The node in the graph carries the *other* behavior's ports and name – neither is part of
// its own type.
await evaluate(
`__gm.state.select({ kind: 'node', assetId: 'nb-caller', nodeId: 'nb-call', nodeIds: ['nb-call'] })`,
);
await sleep(300);
await t.select({ kind: 'node', assetId: 'nb-caller', nodeId: 'nb-call', nodeIds: ['nb-call'] });
const inPort = await portAt('nb-call', 'in1', false);
const outPort = await portAt('nb-call', 'out1', true);
check(
@@ -137,27 +131,19 @@ export async function nestedBehavior(t) {
// 19d. Declaring a port on one behavior reaches the node in the other one, live. This is the
// check that a port list derived from another graph is really derived and not copied.
check('the calling node has no second input yet', !(await portAt('nb-call', 'in2', false)));
await evaluate(
`__gm.state.select({ kind: 'node', assetId: 'nb-double', nodeId: 'nb-start', nodeIds: ['nb-start'] })`,
);
await sleep(300);
await t.select({ kind: 'node', assetId: 'nb-double', nodeId: 'nb-start', nodeIds: ['nb-start'] });
const addButton = await locate('+ Input');
check('the start node offers its inputs in the inspector', Boolean(addButton), JSON.stringify(addButton));
await shot('38-behavior-ports');
await click(addButton.x, addButton.y);
await sleep(200);
await clickText('Number');
await sleep(300);
const declared = await startPorts();
check(
'adding an input declares it on the start node',
declared.length === 2 && declared[1].key === 'in2' && declared[1].kind === 'number',
JSON.stringify(declared),
);
await evaluate(
`__gm.state.select({ kind: 'node', assetId: 'nb-caller', nodeId: 'nb-call', nodeIds: ['nb-call'] })`,
);
await sleep(300);
await t.select({ kind: 'node', assetId: 'nb-caller', nodeId: 'nb-call', nodeIds: ['nb-call'] });
check(
'and the node in the other graph grew that input without being touched',
Boolean(await portAt('nb-call', 'in2', false)),
@@ -165,7 +151,6 @@ export async function nestedBehavior(t) {
// 19e. Undo takes the port back, and with it the port on the calling node.
await undo();
await sleep(300);
const afterUndo = await startPorts();
check(
'undo removes it in both graphs',
@@ -180,14 +165,11 @@ export async function nestedBehavior(t) {
const idsBefore = await nodeIds();
const bare = await json('__gm.graph.emptySpot()');
await click(bare.x, bare.y, 'right');
await sleep(150);
check('node palette opens', Boolean(await locate('Search nodes…')));
await typeText('Double');
await sleep(200);
// Enter takes the first match. NOT a click on the label: "Double" also names the asset in the
// list on the left, and that is what a click by text would find first.
await press('Enter');
await sleep(300);
const added = (await asset('nb-caller')).graph.nodes.find((n) => !idsBefore.includes(n.id));
check(
'picking a behavior in the palette drops in a node already set to it',
@@ -197,7 +179,6 @@ export async function nestedBehavior(t) {
JSON.stringify(added && { type: added.type, params: added.params }),
);
await undo();
await sleep(200);
// 19g. Dragging a behavior out of the asset panel drops a call into the graph, where the
// pointer let go of it.
@@ -213,7 +194,6 @@ export async function nestedBehavior(t) {
});
const dropAt = spot(0.25, 0.75);
await t.dragAsset('nb-double', dropAt.x, dropAt.y);
await sleep(300);
const dropped = await newNodeIn(beforeDrag);
check(
'a behavior dragged from the asset panel becomes a call node',
@@ -231,7 +211,6 @@ export async function nestedBehavior(t) {
const beforeEmpty = await nodeIds();
const emptyRow = await json(`__gm.assetRow('nb-empty')`);
await t.dragAsset('nb-empty', spot(0.6, 0.75).x, spot(0.6, 0.75).y);
await sleep(300);
const emptyCall = await newNodeIn(beforeEmpty);
check(
'a behavior without a start node can be added just the same',
@@ -255,7 +234,6 @@ export async function nestedBehavior(t) {
const beforeSelf = await nodeIds();
const selfRow = await json(`__gm.assetRow('nb-caller')`);
await t.dragAsset('nb-caller', spot(0.45, 0.5).x, spot(0.45, 0.5).y);
await sleep(300);
const afterSelf = await nodeIds();
const { notice } = await t.state();
// `dragging` tells a refusal apart from a drag that never arrived – both leave the graph alone.
@@ -277,7 +255,6 @@ export async function nestedBehavior(t) {
check('the graph shown is still the caller', (await evaluate('__gm.graph.current()')) === 'nb-caller');
check('no breadcrumb on a graph opened from the outside', (await json('__gm.graph.crumbs()')).length === 0);
await t.doubleClick(callNode.x, callNode.y);
await sleep(400);
const opened = await evaluate('__gm.graph.current()');
check(
'double-clicking a call node opens the behavior it runs',
@@ -307,7 +284,6 @@ export async function nestedBehavior(t) {
await trail(),
);
await t.doubleClick(inner.x, inner.y);
await sleep(400);
check(
'each call opened further down adds a crumb',
(await evaluate('__gm.graph.current()')) === 'nb-empty' && (await trail()) === 'Caller › Double › Empty*',
@@ -330,7 +306,6 @@ export async function nestedBehavior(t) {
const clickCrumb = async (name) => {
const crumb = (await crumbs()).find((c) => c.name === name);
await click(crumb.x, crumb.y);
await sleep(300);
};
await clickCrumb('Double');
await shot('39c-breadcrumb-back');
@@ -350,12 +325,10 @@ export async function nestedBehavior(t) {
const enterDouble = async () => {
const call = await json(`__gm.graph.nodeAt('nb-call')`);
await t.doubleClick(call.x, call.y);
await sleep(400);
};
await enterDouble();
const emptyAgain = await json(`__gm.assetRow('nb-empty')`);
await click(emptyAgain.x, emptyAgain.y);
await sleep(300);
check(
'choosing another behavior in the asset panel ends the trail',
(await evaluate('__gm.graph.current()')) === 'nb-empty' && (await crumbs()).length === 0,
@@ -366,10 +339,8 @@ export async function nestedBehavior(t) {
await enterDouble();
const picker = await json('__gm.graph.pickerAt()');
await click(picker.x, picker.y);
await sleep(150);
const emptyItem = await locate('Empty', true);
await click(emptyItem.x, emptyItem.y);
await sleep(300);
check(
'choosing another behavior in the picker ends the trail',
(await evaluate('__gm.graph.current()')) === 'nb-empty' && (await crumbs()).length === 0,
@@ -385,7 +356,6 @@ export async function nestedBehavior(t) {
p.assets = p.assets.filter((a) => !mine.includes(a.id));
scene.objects = scene.objects.filter((o) => o.id !== 'nb-box' && o.id !== 'nb-logic');
`);
await sleep(300);
const left = await json(`{
objects: __gm.sceneObjects().filter((o) => o.id.startsWith('nb-')).length,
assets: __gm.state.project.assets.filter((a) => a.id.startsWith('nb-')).length,