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.
135 lines
5.3 KiB
JavaScript
135 lines
5.3 KiB
JavaScript
import { edge, node } from '../lib/builders.mjs';
|
|
import { LANES, scaffoldBehavior } from './lib/scaffold.mjs';
|
|
|
|
/**
|
|
* The auto-repeat of `On key down`. A plain and a repeating node on the same key are measured in one
|
|
* hold, so "it repeated" cannot be confused with "the key arrived more often than expected".
|
|
*/
|
|
|
|
const PLAIN = 'plain hit';
|
|
const REPEATING = 'repeating hit';
|
|
const DELAY = 300;
|
|
const INTERVAL = 50;
|
|
|
|
const repeatGraph = {
|
|
nodes: [
|
|
node('r-plain', 'OnKeyDown', 0, 0, { key: 'ArrowRight' }),
|
|
node('r-plain-log', 'Log', 340, 0, { message: PLAIN }),
|
|
node('r-repeat', 'OnKeyDown', 0, 220, {
|
|
key: 'ArrowRight',
|
|
repeatAfter: DELAY,
|
|
repeatInterval: INTERVAL,
|
|
}),
|
|
node('r-repeat-log', 'Log', 340, 220, { message: REPEATING }),
|
|
],
|
|
edges: [
|
|
edge('r-e1', 'r-plain', 'exec', 'r-plain-log', 'exec'),
|
|
edge('r-e2', 'r-repeat', 'exec', 'r-repeat-log', 'exec'),
|
|
],
|
|
};
|
|
|
|
export async function keyRepeat(t) {
|
|
const { check, sleep, json, shot, click, holdKey, world, stop, select, typeText } = t;
|
|
/** Both counts out of one read of the world, so the two are always of the same moment. */
|
|
const hits = async () => {
|
|
const log = (await world()).log;
|
|
const count = (message) => log.filter((line) => line.includes(message)).length;
|
|
return { plain: count(PLAIN), repeating: count(REPEATING) };
|
|
};
|
|
|
|
// 20a. A box with the two-node behavior below it, written into the model directly: what is under
|
|
// test is the repeat, not another way of building a graph.
|
|
const rig = await scaffoldBehavior(t, {
|
|
assetId: 'repeater',
|
|
name: 'Repeater',
|
|
graph: repeatGraph,
|
|
boxId: 'r-box',
|
|
boxName: 'Repeat box',
|
|
logicId: 'r-logic',
|
|
y: LANES.keyRepeat,
|
|
});
|
|
|
|
await t.play();
|
|
|
|
// 20b. A hold shorter than the delay is still just a press. Without this the check below would
|
|
// also pass on a node that simply fires every frame the key is down.
|
|
const beforeShort = await hits();
|
|
await holdKey('ArrowRight', 120);
|
|
await sleep(200);
|
|
const afterShort = await hits();
|
|
check(
|
|
'a hold shorter than the delay fires once, repeat or not',
|
|
afterShort.plain === beforeShort.plain + 1 && afterShort.repeating === beforeShort.repeating + 1,
|
|
`plain ${beforeShort.plain} → ${afterShort.plain}, repeating ${beforeShort.repeating} → ${afterShort.repeating} over 120 ms with a ${DELAY} ms delay`,
|
|
);
|
|
|
|
// 20c. Hold it past the delay: the plain node still counts one press, the repeating one runs.
|
|
await holdKey('ArrowRight', 800);
|
|
await sleep(200);
|
|
const afterLong = await hits();
|
|
const gained = afterLong.repeating - afterShort.repeating;
|
|
check(
|
|
'holding past the delay keeps the repeating node firing, while the plain one counts one press',
|
|
afterLong.plain === afterShort.plain + 1 && gained > 4 && gained < 25,
|
|
`plain +${afterLong.plain - afterShort.plain}, repeating +${gained} over 800 ms (press + ~${Math.round((800 - DELAY) / INTERVAL)} repeats expected)`,
|
|
);
|
|
await stop();
|
|
|
|
// 20d. Both settings are on the node itself, so a graph can be tuned where it is built.
|
|
await select({ kind: 'node', assetId: 'repeater', nodeId: 'r-repeat' });
|
|
const afterField = await json(`__gm.inspectorField('Repeat after (ms)')`);
|
|
const intervalField = await json(`__gm.inspectorField('Repeat interval (ms)')`);
|
|
check(
|
|
'the node inspector offers the delay and the interval',
|
|
afterField !== null && intervalField !== null,
|
|
JSON.stringify({ afterField, intervalField }),
|
|
);
|
|
|
|
// A switched-off repeat is worth no rows: two lines of "0" on every key node would make each
|
|
// one taller for nothing. The repeating node next to it shows both, so this is not just hiding.
|
|
const plainRows = await json(`__gm.graph.nodeSettings('r-plain')`);
|
|
const repeatRows = await json(`__gm.graph.nodeSettings('r-repeat')`);
|
|
check(
|
|
'a node with no repeat set carries no repeat rows, the repeating one carries both',
|
|
plainRows.length === 1 &&
|
|
repeatRows.length === 3 &&
|
|
repeatRows.some((r) => r.startsWith('Repeat after')) &&
|
|
repeatRows.some((r) => r.startsWith('Repeat interval')),
|
|
JSON.stringify({ plainRows, repeatRows }),
|
|
);
|
|
await shot('40-key-repeat-node');
|
|
|
|
await click(afterField.x, afterField.y);
|
|
await typeText('750');
|
|
await click(intervalField.x, intervalField.y);
|
|
const params = await json(
|
|
`__gm.state.project.assets.find((a) => a.id === 'repeater').graph.nodes.find((n) => n.id === 'r-repeat').params`,
|
|
);
|
|
check(
|
|
'typing a new delay lands on the node',
|
|
params.repeatAfter === 750 && params.repeatInterval === INTERVAL,
|
|
JSON.stringify(params),
|
|
);
|
|
|
|
// 20e. The typed value is what the next run obeys: 750 ms is longer than this hold, so the
|
|
// repeating node falls silent although nothing else about the graph changed.
|
|
await t.play();
|
|
const beforeTuned = await hits();
|
|
await holdKey('ArrowRight', 500);
|
|
await sleep(200);
|
|
const afterTuned = await hits();
|
|
check(
|
|
'the delay typed into the inspector is the one the run uses',
|
|
afterTuned.repeating === beforeTuned.repeating + 1,
|
|
`repeating ${beforeTuned.repeating} → ${afterTuned.repeating} over 500 ms with the delay raised to 750 ms`,
|
|
);
|
|
await stop();
|
|
|
|
const left = await rig.remove();
|
|
check(
|
|
'the step leaves nothing of its own behind',
|
|
left.objects === 0 && left.assets === 0,
|
|
JSON.stringify(left),
|
|
);
|
|
}
|