From f14c4ecb77d08dab3cdef403a61afbdd37814334 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Wed, 30 Sep 2026 11:18:39 +0200 Subject: [PATCH] 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 --- README.md | 2 +- src/editor/frameMeter.ts | 11 +++++++++++ tests/editor/frame-meter.test.ts | 19 ++++++++++++++++++- tools/e2e/59-while-playing.mjs | 19 +++++++++++++------ 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5e7e199e..0d455975 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/editor/frameMeter.ts b/src/editor/frameMeter.ts index 8b670cb7..34791105 100644 --- a/src/editor/frameMeter.ts +++ b/src/editor/frameMeter.ts @@ -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'; diff --git a/tests/editor/frame-meter.test.ts b/tests/editor/frame-meter.test.ts index 8498d0f5..9e033f33 100644 --- a/tests/editor/frame-meter.test.ts +++ b/tests/editor/frame-meter.test.ts @@ -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 { diff --git a/tools/e2e/59-while-playing.mjs b/tools/e2e/59-while-playing.mjs index d66f110d..b0268489 100644 --- a/tools/e2e/59-while-playing.mjs +++ b/tools/e2e/59-while-playing.mjs @@ -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();