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
+17
-1
@@ -4,7 +4,17 @@ import { resolveObjects } from '../core/model/instance';
|
||||
import { findObjectHolder } from '../core/model/Project';
|
||||
import type { FrameRect } from '../core/model/texture';
|
||||
import { effectiveScale, gameClock } from '../core/time';
|
||||
import { activeDrag, Button, currentMenu, isRendering, renderCount, requestRender, ui, Widget } from '../ui';
|
||||
import {
|
||||
activeDrag,
|
||||
afterFrames,
|
||||
Button,
|
||||
currentMenu,
|
||||
isRendering,
|
||||
renderCount,
|
||||
requestRender,
|
||||
ui,
|
||||
Widget,
|
||||
} from '../ui';
|
||||
import { particleLibrary } from '../viewport/ParticleLibrary';
|
||||
import { shaderLibrary } from '../viewport/ShaderLibrary';
|
||||
import { soundLibrary } from '../viewport/SoundLibrary';
|
||||
@@ -530,6 +540,12 @@ export function installDebugHook(app: Application, state: EditorState, editor: E
|
||||
renderStats: () => ({ drawn: renderCount(), awake: isRendering() }),
|
||||
/** Draw again, whatever happens next – what a screenshot needs before it is taken. */
|
||||
wake: () => requestRender(),
|
||||
/**
|
||||
* Resolves once the editor has drawn `count` more frames. The checks await this instead of
|
||||
* sleeping a guessed number of milliseconds: a frame is the editor saying it has caught up,
|
||||
* where a deadline is only the harness hoping it has.
|
||||
*/
|
||||
afterFrames: (count = 2, timeoutMs = 2000) => afterFrames(count, timeoutMs),
|
||||
/** Visible nodes carrying a filter – one extra render pass each; since #77 a shader is none. */
|
||||
filterCount: () => {
|
||||
let n = 0;
|
||||
|
||||
+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