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

-16
View File
@@ -42,7 +42,6 @@ export async function structVariable(t) {
});
__gm.state.select({ kind: 'asset', assetId: 'structy' });
`);
await sleep(150);
// 22a. The declaration is what the inspector draws: a head that can add fields, a row per field.
check('a struct variable carries the add-a-field control', (await find('+ Field')) !== null);
@@ -62,7 +61,6 @@ export async function structVariable(t) {
await click(xField.x, xField.y);
await typeText('42');
await press('Enter');
await sleep(200);
check('typing in a field writes that field', (await piece()).x === 42, JSON.stringify(await piece()));
const tagField = await json("__gm.inspectorField('tag')");
@@ -70,7 +68,6 @@ export async function structVariable(t) {
await press('a', { ctrl: true });
await typeText('ell');
await press('Enter');
await sleep(200);
check(
'and the next field edited keeps the one before it',
JSON.stringify(await piece()) === '{"x":42,"tag":"ell"}',
@@ -85,7 +82,6 @@ export async function structVariable(t) {
// 22d. Adding a field reaches the declaration and the value at once.
await clickText('+ Field');
await sleep(150);
await pickKind('Boolean');
await sleep(250);
const grown = await piece();
@@ -93,7 +89,6 @@ export async function structVariable(t) {
check('and the new field has a row', (await find('field1')) !== null);
await shot('02-field-added');
await undo();
await sleep(200);
check('undo takes it out of the value too', !('field1' in (await piece())), JSON.stringify(await piece()));
// 22e. Renaming a field carries its value along.
@@ -102,7 +97,6 @@ export async function structVariable(t) {
await press('a', { ctrl: true });
await typeText('column');
await press('Enter');
await sleep(250);
check(
'renaming a field keeps its value',
JSON.stringify(await piece()) === '{"column":30,"tag":"tee"}',
@@ -112,11 +106,9 @@ export async function structVariable(t) {
// 22f. A field may itself be a list. Its row is the same "n items" button a list property has,
// and it opens the List tab one level down – on the field, not on the whole bundle.
await clickText('+ Field');
await sleep(150);
// "List" is also the tab bar's own label, so the menu entry is reached as the last drawn one.
const listOption = await json(`__gm.locate('List', true)`);
await click(listOption.x, listOption.y);
await sleep(250);
check(
'+ Field ▸ List gives the bundle an empty list',
Array.isArray((await piece()).field1),
@@ -126,7 +118,6 @@ export async function structVariable(t) {
check('and that field reads as an item count', itemsButton !== null);
await click(itemsButton.x, itemsButton.y);
await sleep(250);
check('clicking it opens the List tab', (await json('__gm.tab()')) === 'list');
const info = await json('__gm.list.info()');
check('whose title names the field inside the bundle', info.title.endsWith('· field1'), info.title);
@@ -134,7 +125,6 @@ export async function structVariable(t) {
// The depth the stored value goes ends here: this list sits in a struct field, so a struct item
// would be dropped by the next write. It is left out of the menu rather than doing nothing.
await clickText('+ Item');
await sleep(150);
check(
'the menu of a list in a struct field leaves the struct kind out',
(await find('Number')) !== null && (await find('Struct')) === null,
@@ -150,7 +140,6 @@ export async function structVariable(t) {
await shot('03-list-field');
await undo();
await undo();
await sleep(200);
check(
'undo takes the list field back out of the bundle',
!('field1' in (await piece())),
@@ -158,7 +147,6 @@ export async function structVariable(t) {
);
await mutate('__gm.state.openList(null)');
await sleep(200);
// 22g. The same bundle on a placed object. An override is sparse – it starts out unset – while a
// field edit rebuilds the whole bundle, so the untouched fields have to come from what the row
@@ -177,7 +165,6 @@ export async function structVariable(t) {
});
`);
await select({ kind: 'object', holderId: 'scene1', objectId: 'ostruct' });
await sleep(200);
check('a placed behavior shows its bundle, filled from the asset', (await find('tee')) !== null);
// 22h. It reads exactly like the asset does: a head carrying the struct mark, then one row per
@@ -236,7 +223,6 @@ export async function structVariable(t) {
await press('a', { ctrl: true });
await typeText('7');
await press('Enter');
await sleep(250);
check(
'the first override of one field keeps the asset values of the others',
JSON.stringify(await placed()) === '{"column":7,"tag":"tee"}',
@@ -245,7 +231,6 @@ export async function structVariable(t) {
await shot('04-object-override');
await undo();
await sleep(200);
check(
'and undo takes the override away again, not just the one field',
(await json("__gm.state.activeScene.objects.find((o) => o.id === 'ostruct').overrides.piece")) === null,
@@ -260,7 +245,6 @@ export async function structVariable(t) {
__gm.state.select(null);
__gm.state.preview();
`);
await sleep(200);
const left = await json(
'__gm.state.project.assets.filter((a) => a.id === "structy").length +' +
' __gm.state.activeScene.objects.filter((o) => o.id === "ostruct").length',