Files
feuerwerk/tools/e2e/33-render-on-change.mjs
T
admin 7e5003b590
game-maker / suite (push) Successful in 6m48s
A test that waits for a deadline somebody guessed is waiting for the wrong thing, so it waits for the editor (#93)
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.
2026-09-16 08:08:10 +02:00

113 lines
4.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { openTab } from './lib/scaffold.mjs';
/**
* Render on change: the editor draws what changed, not sixty pictures a second.
*
* This is the one thing the rest of the suite cannot see. `pixelAt` re-renders the stage on
* demand, so every colour check would pass just as well on a screen that froze a minute ago –
* only the drawn-frame counter tells the difference between "drawn" and "merely correct".
*
* The step runs last and leaves the editor as it found it.
*/
export async function renderOnChange(t) {
const { check, sleep, evaluate, select, click, locate, play, stop, shot, waitUntil } = t;
const stats = () => evaluate('__gm.renderStats()');
/** Frames drawn over `ms` of doing nothing at all. */
const drawnDuring = async (ms) => {
const before = await stats();
await sleep(ms);
const after = await stats();
return after.drawn - before.drawn;
};
/**
* Waits for the trailing window of the last event to run out. Measuring without this counts the
* frames the previous click is still entitled to and calls them a leak.
*
* The autosave is waited out first. It fires ~1 s after the last model change and paints the
* status twice – `saving`, then `saved` – so an earlier step's last edit lands in this step's
* quiet window and looks like a renderer that will not go to sleep. Those frames are earned:
* the status text really did change. The step has to stop measuring before them, not call them
* a leak.
*/
const settle = async (timeoutMs = 5000) => {
await waitUntil("!['dirty', 'saving'].includes(__gm.state.saveStatus)", {
timeout: timeoutMs,
label: 'the autosave finished',
});
for (let waited = 0; waited < timeoutMs; waited += 100) {
if (!(await stats()).awake) return true;
await sleep(100);
}
return false;
};
// 33a. A tab whose picture stands still: the scene viewport may wear a Time shader, and the
// particles and animation previews are moving pictures by definition.
await openTab(t, 'graph');
await select(null);
const quiet = await settle();
const idle = await drawnDuring(700);
check(
'an editor nobody touches draws no frames at all',
quiet && idle === 0,
`settled=${quiet}, ${idle} frames drawn`,
);
// 33b. A model change that arrives without any input – no click, no key – still reaches the
// screen. This is the wake-up that a strict dirty flag on the widgets would have missed.
const beforeChange = (await stats()).drawn;
await evaluate(`__gm.state.setNotice('render check', 'info')`);
await sleep(150);
const afterChange = (await stats()).drawn;
check(
'a change nobody clicked for is drawn anyway',
afterChange > beforeChange,
`${beforeChange} → ${afterChange}`,
);
// 33c. Input wakes it, and it goes back to sleep on its own afterwards.
await settle();
const beforeClick = (await stats()).drawn;
const spot = await locate('Behavior');
await click(spot.x, spot.y);
const afterClick = (await stats()).drawn;
check('a click draws again', afterClick > beforeClick, `${beforeClick} → ${afterClick}`);
const settled = await settle();
const backToSleep = await drawnDuring(700);
check(
'and it stops on its own once the click is dealt with',
settled && backToSleep === 0,
`settled=${settled}, ${backToSleep} frames`,
);
// 33d. While the game runs, the frame cannot come from an event: the world moves by itself.
await play();
const playing = await drawnDuring(600);
const fps = await evaluate('__gm.fps()');
check(
'a running game keeps getting frames without being touched',
playing > 10,
`${playing} frames in 600 ms`,
);
check('and the fps readout keeps measuring them', typeof fps === 'number' && fps > 0, `fps=${fps}`);
await shot('01-playing');
await stop();
// 33e. Stopping ends the reason to draw. Back off the scene tab first: a Time shader left on
// screen by an earlier step is a legitimate reason to keep drawing, and would mask this check.
await openTab(t, 'graph');
const stopped = await settle();
const afterStop = await drawnDuring(700);
check(
'stopping puts it back to sleep',
stopped && afterStop === 0,
`settled=${stopped}, ${afterStop} frames`,
);
await evaluate(`__gm.state.setNotice('', 'info')`);
await sleep(200);
}