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.
74 lines
3.7 KiB
JavaScript
74 lines
3.7 KiB
JavaScript
/**
|
||
* Menus that can grow with the project. Runs last: it fills the scene with throwaway objects
|
||
* to make the parent picker overflow, which the earlier steps' object counts would not survive.
|
||
*/
|
||
export async function menus(t) {
|
||
const { check, sleep, evaluate, json, shot, click, locate, mouse, mutate } = t;
|
||
/** Like locate(), but a row scrolled out of view answers with null instead of throwing. */
|
||
const rowAt = async (text) =>
|
||
(await evaluate(`__gm.locate(${JSON.stringify(text)}, true)`)) ?? { x: -1, y: -1 };
|
||
|
||
await mutate(`
|
||
const scene = __gm.state.activeScene;
|
||
// Any plain stored object will do – not a placed container, whose content the picker
|
||
// would list forty times over as well.
|
||
const source = scene.objects.find(o => o.id === 'otext');
|
||
for (let i = 1; i <= 40; i++) {
|
||
const copy = structuredClone(source);
|
||
copy.id = 'filler' + i;
|
||
copy.name = 'Filler ' + i;
|
||
copy.parentId = '';
|
||
copy.overrides = {};
|
||
scene.objects.push(copy);
|
||
}
|
||
__gm.state.select({ kind: 'object', holderId: 'scene1', objectIds: ['otext'], objectId: 'otext' })`);
|
||
|
||
// 11a. A picker with 40+ entries is capped in height and stays fully on screen instead of
|
||
// running off the bottom edge. The cap is a share of the canvas, not a constant: a tenth is
|
||
// left free at either end, so the number to beat is read off the screen rather than written
|
||
// down here – 40 rows would be well over a thousand pixels without it.
|
||
const parentRow = await json("__gm.inspectorField('Parent')");
|
||
await click(parentRow.x, parentRow.y);
|
||
const rect = await json('__gm.overlayRect()');
|
||
const screenHeight = await json('window.innerHeight');
|
||
const budget = screenHeight - Math.round(screenHeight * 0.1) * 2;
|
||
check(
|
||
'a long parent picker is capped to the canvas budget instead of growing with the object count',
|
||
rect && rect.h <= budget,
|
||
JSON.stringify({ rect, budget, screenHeight }),
|
||
);
|
||
check(
|
||
'the capped picker stays fully on screen',
|
||
rect && rect.y >= 0 && rect.y + rect.h <= screenHeight,
|
||
JSON.stringify({ rect, screenHeight }),
|
||
);
|
||
await shot('31-long-parent-menu');
|
||
|
||
// 11b. The entries scroll under the wheel; the search box above them does not move.
|
||
// Anchored on a filler row read off the open menu: "None" could match an inspector field behind it,
|
||
// and which fillers are drawn depends on what earlier steps left in the scene.
|
||
const visibleFillers = (await json('__gm.overlayTexts()')).filter((e) => e.startsWith('Filler '));
|
||
const anchor = visibleFillers[visibleFillers.length - 1];
|
||
check('the picker lists the filler objects', Boolean(anchor), JSON.stringify(visibleFillers));
|
||
const firstBefore = await rowAt(anchor);
|
||
const searchBefore = await locate('Search…', true);
|
||
// Move onto the list first: a wheel event goes to whatever is hovered, and the pointer last
|
||
// touched the field above the menu.
|
||
await mouse('mouseMoved', rect.x + rect.w / 2, rect.y + rect.h / 2);
|
||
await sleep(50);
|
||
await mouse('mouseWheel', rect.x + rect.w / 2, rect.y + rect.h / 2, 'none', { deltaX: 0, deltaY: 240 });
|
||
// Measured here: the list had not moved 40 ms after the wheel but had at 340 ms. Both
|
||
// readings are reported so an early one cannot look like a mystery failure again.
|
||
await sleep(40);
|
||
const rightAfter = await rowAt(anchor);
|
||
await sleep(300);
|
||
const firstAfter = await rowAt(anchor);
|
||
const searchAfter = await locate('Search…', true);
|
||
check(
|
||
'the wheel scrolls the entries',
|
||
firstAfter.y >= 0 && firstAfter.y < firstBefore.y - 100,
|
||
JSON.stringify({ firstBefore, rightAfter, firstAfter }),
|
||
);
|
||
check('the search box stays put while the entries scroll', searchAfter.y === searchBefore.y);
|
||
}
|