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.
113 lines
4.3 KiB
JavaScript
113 lines
4.3 KiB
JavaScript
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);
|
||
}
|