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
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:
1 parent
c990dbcb26
commit
7e5003b590
41 files changed
+362
-732
No files matched your search
+8
-1
@@ -2,7 +2,14 @@ export { Widget, Box, type Size } from './Widget';
|
||||
export { HStack, VStack, Spacer, Stack, type StackOptions } from './layout/Stack';
|
||||
export { Theme, textStyle, type TextStyleToken } from './Theme';
|
||||
export { initUI, ui, type UIContext } from './UIContext';
|
||||
export { initRenderLoop, isRendering, renderCount, requestRender, resetRenderLoop } from './render';
|
||||
export {
|
||||
afterFrames,
|
||||
initRenderLoop,
|
||||
isRendering,
|
||||
renderCount,
|
||||
requestRender,
|
||||
resetRenderLoop,
|
||||
} from './render';
|
||||
export { drawBand, drawControlBox, drawGrid, type ControlBoxOptions, type GridOptions } from './draw';
|
||||
export { clamp, centreY, formatBytes, formatNumber, NUMBER_DECIMALS, snapToStep, zoomFactor } from './math';
|
||||
export { activeDrag, type DragPayload, endDrag, onDragChange, startDrag } from './drag';
|
||||
|
||||
@@ -37,6 +37,39 @@ export function renderCount(): number {
|
||||
return rendered;
|
||||
}
|
||||
|
||||
/** Resolved once the next frame has been drawn; see `afterFrames`. */
|
||||
let frameWaiters: Array<() => void> = [];
|
||||
|
||||
/**
|
||||
* Resolves once `count` frames have actually been drawn – "the editor has caught up with what I
|
||||
* just did", said as a fact instead of as a deadline. The checks wait on this rather than on a
|
||||
* guessed number of milliseconds, which is both faster (a frame is ~16 ms) and truer.
|
||||
*
|
||||
* It wakes the loop itself, because an idle editor draws nothing and a promise waiting for a
|
||||
* frame that nobody asked for never settles. `timeoutMs` is the other half of that promise: a
|
||||
* wedged renderer must fail the check it belongs to, not hang the whole run.
|
||||
*/
|
||||
export function afterFrames(count = 1, timeoutMs = 2000): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
let left = Math.max(1, count);
|
||||
let done = false;
|
||||
const finish = (): void => {
|
||||
if (done) return;
|
||||
done = true;
|
||||
clearTimeout(timer);
|
||||
resolve();
|
||||
};
|
||||
const timer = setTimeout(finish, timeoutMs);
|
||||
const tick = (): void => {
|
||||
if (done) return;
|
||||
if (--left <= 0) finish();
|
||||
else frameWaiters.push(tick);
|
||||
};
|
||||
frameWaiters.push(tick);
|
||||
requestRender();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the renderer off the ticker and drives it from `requestRender()` instead. Added at the
|
||||
* ticker's lowest priority, so everything that may request a frame has run for this tick already.
|
||||
@@ -49,6 +82,10 @@ export function initRenderLoop(app: Application): void {
|
||||
if (!isRendering()) return;
|
||||
rendered += 1;
|
||||
app.render();
|
||||
// Taken first: a waiter that queues itself again must land in the next round, not this one.
|
||||
const waiters = frameWaiters;
|
||||
frameWaiters = [];
|
||||
for (const w of waiters) w();
|
||||
},
|
||||
undefined,
|
||||
UPDATE_PRIORITY.LOW,
|
||||
@@ -60,4 +97,5 @@ export function initRenderLoop(app: Application): void {
|
||||
export function resetRenderLoop(): void {
|
||||
deadline = 0;
|
||||
rendered = 0;
|
||||
frameWaiters = [];
|
||||
}
|
||||
Reference in new issue
Block a user