Files
feuerwerk/tools/e2e/59-while-playing.mjs
T
adminandClaude Opus 5.5 77f4c2c1a3
game-maker / gate (push) Successful in 2m35s
game-maker / suite (push) Successful in 11m44s
Show entities and time per area while playing in the Game View (#275)
A game that stutters gave no hint where the frame goes. The Game tab
now has a Performance switch. It shows a small overlay in the top left
corner of the Game View while a game runs: fps, the number of entities
and the mean milliseconds per frame for each area, averaged over half
a second like the fps counter.

- src/editor/frameMeter.ts holds the pure rule. FrameMeter sums the
  areas per frame, closes a window every 0.5 s and formats the lines.
- World.timeSystem times every system the world ticks. PlaySession
  maps the systems to Behaviors, Physics and Animation and sets the
  hook again after Load scene.
- Animation has its own line. Counting the AnimationSystem under
  Behaviors would have skewed that number.
- Drawing is the build of the game picture plus app.render(). It is
  main thread time only. The GPU work after it is not in the number,
  and app.render() draws the whole canvas including the panels.
- Editor is the rest of the editor tick after the systems and the game
  picture: mirroring into the model, panels and console.
- The overlay lets clicks through to the game. It shows dashes until
  the first window closes. The switch is off by default and is not
  saved. The released game (#266) runs without a meter.

Tests:
- Unit frame-meter: 9 passed.
- Gate on melektaus-atx: 197 files, 2853 tests, CHECK EXIT 0.
- e2e 59 on melektaus-atx: ALL PASSED, 18 checks (13 before, +5 in
  59e with a real mouse on the switch). Run twice.
- Three mutations were caught: the editor share not subtracting the
  systems (2 unit tests), systems running in pause not reported
  (1 unit test), the overlay getting no times (59e).
- The e2e total of 1601 is computed (1596 + 5), not measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 09:44:53 +02:00

197 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* While playing (#265): how closely the panels follow a running game. The preset is picked in the
* Project settings with the mouse; Live brings the panels up to date every frame, Balanced four
* times a second, and neither holds the game back. On Balanced the scene view stops drawing behind
* the game tab and catches up when it comes back; on Minimal the hierarchy waits for a pause. A
* crate spawned at the top level is listed in a group named "Crate".
*/
export async function whilePlaying(t) {
const {
check,
json,
evaluate,
click,
clickText,
clickToolbar,
press,
play,
stop,
shot,
showTab,
undo,
sleep,
} = t;
const playInfo = () => json('__gm.playInfo()');
/** Panel updates so far and the page's clock, so a rate is measured in the page, not over CDP. */
const sample = () => json('({ at: performance.now(), ...__gm.playInfo().panelUpdates })');
const valuesPerSecond = async (ms) => {
const a = await sample();
await sleep(ms);
const b = await sample();
return { rate: ((b.values - a.values) * 1000) / (b.at - a.at), a, b };
};
const setWhilePlaying = async (label) => {
await clickToolbar('menu');
await clickText('Project settings…');
const before = (await json('__gm.projectSettings()')).whilePlaying;
const at = await json('__gm.projectSettingsWhilePlayingAt()');
await click(at.x, at.y);
await clickText(label);
const chosen = (await json('__gm.projectSettings()')).whilePlaying;
await clickText('Apply');
return { before, chosen };
};
const undoSteps = () => json('__gm.state.commands.undoStack.length');
/** Whether the hierarchy *shows* the row – `hierarchyOrder` reads the model, not the list on screen. */
const listed = (rowId) => json(`__gm.hierarchyRow(${JSON.stringify(rowId)}) !== null`);
const crateGroups = () => json(`(__gm.state.activeScene.groups ?? []).filter((g) => g.name === 'Crate')`);
await press('Escape');
await evaluate('__gm.state.select(null)');
await showTab('scene');
// 59a. The fixture was written without the setting, so it reads as Balanced; Live is picked with
// the mouse and is one undo step.
check(
'a project written before there was a choice plays Balanced',
(await json('__gm.state.project.whilePlaying')) === 'balanced',
);
const steps = await undoSteps();
const picked = await setWhilePlaying('Live');
check(
'the Project settings show Balanced and take Live from the menu',
picked.before === 'balanced' &&
picked.chosen === 'live' &&
(await json('__gm.state.project.whilePlaying')) === 'live' &&
(await undoSteps()) === steps + 1,
JSON.stringify({ picked, steps: await undoSteps(), before: steps }),
);
// 59b. Live follows every frame; Balanced four times a second.
await play();
const live = await valuesPerSecond(1500);
await stop();
await undo();
check('undo sets it back to Balanced', (await json('__gm.state.project.whilePlaying')) === 'balanced');
await play();
const balanced = await valuesPerSecond(1500);
check(
'Live brings the panels up to date every frame, Balanced at most four times a second',
live.rate > 20 && balanced.rate > 2 && balanced.rate <= 4.5,
JSON.stringify({ live: live.rate, balanced: balanced.rate }),
);
// 59c. Behind the game tab the scene view does not draw on Balanced – three crates spawned, not
// one redraw – and catches up when its tab comes back. The crates are in a group "Crate".
const hidden = (await json('__gm.viewportInfo()')).redraws;
const entitiesBefore = (await json('__gm.world()')).entities.length;
for (let i = 0; i < 3; i++) await press('Space');
await sleep(600);
const stillHidden = (await json('__gm.viewportInfo()')).redraws;
const groups = await crateGroups();
const members = await json(
`__gm.state.activeScene.objects.filter((o) => o.groupId === ${JSON.stringify(groups[0]?.id ?? '')}).length`,
);
check(
'the game ran on – three crates spawned – and they are listed in one group named Crate',
(await json('__gm.world()')).entities.length === entitiesBefore + 3 &&
groups.length === 1 &&
members === 3,
JSON.stringify({ entitiesBefore, groups, members }),
);
check('the hierarchy shows the group after the next update', await listed(`group:${groups[0]?.id}`));
check(
'behind the game tab the scene view did not draw once',
stillHidden === hidden,
JSON.stringify({ hidden, stillHidden }),
);
await showTab('scene');
const shown = (await json('__gm.viewportInfo()')).redraws;
check(
'and it draws again when its tab comes back',
shown > stillHidden,
JSON.stringify({ stillHidden, shown }),
);
await shot('59a-balanced-group');
await stop();
// 59d. Minimal: the hierarchy waits while the game runs, and a pause brings it up to date at once.
await showTab('scene');
await setWhilePlaying('Minimal');
await play();
await press('Space');
await sleep(600);
const spawnedGroups = await crateGroups();
const groupRow = `group:${spawnedGroups[0]?.id}`;
const listedRunning = await listed(groupRow);
check(
'on Minimal a spawn reaches the scene, but the hierarchy waits',
spawnedGroups.length === 1 && !listedRunning,
JSON.stringify({ spawnedGroups, listedRunning }),
);
await clickToolbar('pause');
check('and a pause lists it at once', (await playInfo()).paused && (await listed(groupRow)));
await shot('59b-minimal-paused');
await stop();
await undo();
check(
'the fixture is left on Balanced with the undo stack as found',
(await json('__gm.state.project.whilePlaying')) === 'balanced' && (await undoSteps()) === steps,
JSON.stringify({ steps: await undoSteps(), before: steps }),
);
// 59e. The performance overlay (#275): switched on in the game tab's header, it shows over a
// running game only, top left – fps, the entities the world has, and a time per area.
const performance = () => json('__gm.gamePerformance()');
await showTab('game');
await click((await performance()).button.x, (await performance()).button.y);
const waiting = await performance();
check(
'switched on in the header, the overlay waits for a game to run',
waiting.button.lit && !waiting.shown,
JSON.stringify(waiting),
);
await play();
await sleep(1200);
const running = await json(
'({ overlay: __gm.gamePerformance(), measured: __gm.playInfo().performance, entities: __gm.world().entities.length })',
);
const { overlay, measured } = running;
const times = Object.values(measured?.times ?? {});
check(
'while playing it shows in the top left corner of the game view',
overlay.shown &&
overlay.box.x - overlay.view.x >= 0 &&
overlay.box.x - overlay.view.x < 20 &&
overlay.box.y - overlay.view.y >= 0 &&
overlay.box.y - overlay.view.y < 20 &&
overlay.box.w > 100,
JSON.stringify(overlay),
);
check(
'it counts the entities the running world has',
measured?.entities === running.entities && overlay.lines[0]?.includes(`· ${running.entities} entities`),
JSON.stringify({ measured, entities: running.entities, line: overlay.lines[0] }),
);
check(
'and shows the fps and a time for each of the five areas, drawing included',
/^\d+ fps/.test(overlay.lines[0] ?? '') &&
overlay.lines.length === 6 &&
overlay.lines.slice(1).every((line) => / \d+\.\d\d ms$/.test(line)) &&
times.length === 5 &&
times.every((ms) => Number.isFinite(ms) && ms >= 0) &&
measured.times.drawing > 0,
JSON.stringify(overlay.lines),
);
await shot('59c-performance');
await click(overlay.button.x, overlay.button.y);
const switchedOff = await performance();
await stop();
check(
'a second click switches it off again',
!switchedOff.button.lit && !switchedOff.shown,
JSON.stringify(switchedOff),
);
}