Show the frame rate the measured times allow in the overlay (#275)
The fps line of the performance overlay stops at the display's rate, 60 on most screens. It does not tell how much room a game has left. The new top line answers that from the times the overlay measures. - possibleFps in src/editor/frameMeter.ts is 1000 ms over the sum of the five areas, rounded down. It has no number while nothing took any time, and the overlay shows a dash then. - The editor's share is in the sum, because it costs time while playing in the editor. A released game can do a little better. - It is an upper bound. The GPU's time is not measured and not in it. - e2e 59e reads the lines one further down and checks the new line against the times it measured itself. Tests: - Unit frame-meter: 11 passed (+2 for possibleFps) - Gate: 197 files, 2855 tests, CHECK EXIT 0 (2853 + 2) - e2e 59: ALL PASSED, 19 checks (+1), on melektaus-atx - Counter-check: possibleFps without the editor's share fails the unit test - The e2e total of 1602 is counted (1601 + 1), not measured Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
77f4c2c1a3
commit
f14c4ecb77
4 files changed
+43
-8
No files matched your search
@@ -45,7 +45,7 @@ npm run build # writes dist/ and regenerates index.html; the folder is wh
|
||||
The browser suite and the three example games need a machine with a GPU:
|
||||
|
||||
```bash
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1601 checks (GM_STEPS=20 for one step)
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # the editor, 1602 checks (GM_STEPS=20 for one step)
|
||||
# every tool signs in first – see the Accounts page
|
||||
node tools/seed-jumprun.mjs && node tools/smoke-jumprun.mjs
|
||||
node tools/seed-tetris.mjs && node tools/smoke-tetris.mjs
|
||||
|
||||
@@ -24,10 +24,21 @@ export interface PerformanceReading {
|
||||
times: FrameTimes | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The frames a second the measured work would allow if nothing waited for the display: a second
|
||||
* over what a frame takes in all areas. Only the main thread counts, the GPU's share is not in it.
|
||||
*/
|
||||
export function possibleFps(times: FrameTimes): number | null {
|
||||
const total = FRAME_AREAS.reduce((sum, area) => sum + times[area], 0);
|
||||
return total > 0 ? Math.floor(1000 / total) : null;
|
||||
}
|
||||
|
||||
/** What the overlay shows, one area a line; a dash stands for what has not been measured yet. */
|
||||
export function performanceLines({ fps, entities, times }: PerformanceReading): string[] {
|
||||
const width = Math.max(...FRAME_AREAS.map((area) => LABELS[area].length));
|
||||
const possible = `${(times && possibleFps(times)) ?? '–'} fps`;
|
||||
return [
|
||||
`${'Possible'.padEnd(width)} ${possible.padStart(9)}`,
|
||||
`${fps ?? '–'} fps · ${entities} ${entities === 1 ? 'entity' : 'entities'}`,
|
||||
...FRAME_AREAS.map((area) => {
|
||||
const ms = times ? `${times[area].toFixed(2)} ms` : '– ms';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* each system it ticked – and only those.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { FrameMeter, performanceLines } from '../../src/editor/frameMeter';
|
||||
import { FrameMeter, performanceLines, possibleFps } from '../../src/editor/frameMeter';
|
||||
import type { System } from '../../src/runtime/systems/System';
|
||||
import { World } from '../../src/runtime/World';
|
||||
import { makeSpriteScene } from '../helpers';
|
||||
@@ -69,6 +69,7 @@ describe('FrameMeter', () => {
|
||||
describe('performanceLines', () => {
|
||||
it('shows dashes until there is something measured', () => {
|
||||
expect(performanceLines({ fps: null, entities: 1, times: null })).toEqual([
|
||||
'Possible – fps',
|
||||
'– fps · 1 entity',
|
||||
'Behaviors – ms',
|
||||
'Physics – ms',
|
||||
@@ -81,6 +82,7 @@ describe('performanceLines', () => {
|
||||
it('lines the numbers up in milliseconds with two decimals', () => {
|
||||
const times = { behaviors: 1.234, physics: 12.5, animation: 0, drawing: 3, editor: 0.006 };
|
||||
expect(performanceLines({ fps: 60, entities: 128, times })).toEqual([
|
||||
'Possible 59 fps',
|
||||
'60 fps · 128 entities',
|
||||
'Behaviors 1.23 ms',
|
||||
'Physics 12.50 ms',
|
||||
@@ -91,6 +93,21 @@ describe('performanceLines', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('possibleFps', () => {
|
||||
it('is a second over what a frame takes in all areas, rounded down', () => {
|
||||
expect(possibleFps({ behaviors: 0.5, physics: 0.25, animation: 0, drawing: 0.75, editor: 0.5 })).toBe(
|
||||
500,
|
||||
);
|
||||
expect(possibleFps({ behaviors: 0.1, physics: 0.1, animation: 0.1, drawing: 0.1, editor: 0.3 })).toBe(
|
||||
1428,
|
||||
);
|
||||
});
|
||||
|
||||
it('has no number while nothing took any time', () => {
|
||||
expect(possibleFps({ behaviors: 0, physics: 0, animation: 0, drawing: 0, editor: 0 })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('World.timeSystem', () => {
|
||||
function counting(runsWhilePaused = false): System & { runs: number } {
|
||||
return {
|
||||
|
||||
@@ -142,7 +142,8 @@ export async function whilePlaying(t) {
|
||||
);
|
||||
|
||||
// 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.
|
||||
// running game only, top left – the fps the times allow, 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);
|
||||
@@ -171,19 +172,25 @@ export async function whilePlaying(t) {
|
||||
);
|
||||
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] }),
|
||||
measured?.entities === running.entities && overlay.lines[1]?.includes(`· ${running.entities} entities`),
|
||||
JSON.stringify({ measured, entities: running.entities, line: overlay.lines[1] }),
|
||||
);
|
||||
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)) &&
|
||||
/^\d+ fps/.test(overlay.lines[1] ?? '') &&
|
||||
overlay.lines.length === 7 &&
|
||||
overlay.lines.slice(2).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),
|
||||
);
|
||||
const possible = Math.floor(1000 / times.reduce((sum, ms) => sum + ms, 0));
|
||||
check(
|
||||
'on top it shows the fps the measured times would allow, a second over their sum',
|
||||
new RegExp(`^Possible +${possible} fps$`).test(overlay.lines[0] ?? '') && possible > 0,
|
||||
JSON.stringify({ line: overlay.lines[0], possible }),
|
||||
);
|
||||
await shot('59c-performance');
|
||||
await click(overlay.button.x, overlay.button.y);
|
||||
const switchedOff = await performance();
|
||||
|
||||
Reference in new issue
Block a user