Files
adminandClaude Opus 5.5 f14c4ecb77
game-maker / gate (push) Successful in 2m35s
game-maker / suite (push) Successful in 11m46s
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>
2026-09-30 11:18:39 +02:00

153 lines
5.0 KiB
TypeScript
Raw Permalink 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.
/**
* The performance overlay (#275): the meter averages each area per frame over half a second, the
* editor's share is what its tick took beyond the systems and the drawing, and the world reports
* each system it ticked – and only those.
*/
import { describe, expect, it } from 'vitest';
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';
/** One frame of 0.1 s: behaviors 2 ms, physics 1 ms, the tick 10 ms in all, 3 ms of it drawing. */
function frame(meter: FrameMeter): boolean {
meter.addSystem('behaviors', 2);
meter.addSystem('physics', 1);
return meter.endFrame(0.1, 10, 3, 0.5);
}
describe('FrameMeter', () => {
it('has nothing to show before half a second has passed', () => {
const meter = new FrameMeter();
for (let i = 0; i < 4; i++) expect(frame(meter)).toBe(false);
expect(meter.average).toBeNull();
});
it('averages every area per frame over the window', () => {
const meter = new FrameMeter();
for (let i = 0; i < 4; i++) frame(meter);
meter.addSystem('behaviors', 7);
meter.addSystem('animation', 0.5);
expect(meter.endFrame(0.1, 12, 3, 0.5)).toBe(true);
expect(meter.average).toEqual({
behaviors: (4 * 2 + 7) / 5,
physics: (4 * 1) / 5,
animation: 0.5 / 5,
drawing: 3.5,
// 10 - 3 - 3 in the first four frames, 12 - 7.5 - 3 in the last.
editor: (4 * 4 + 1.5) / 5,
});
});
it('starts the next window from zero', () => {
const meter = new FrameMeter();
for (let i = 0; i < 5; i++) frame(meter);
for (let i = 0; i < 4; i++) meter.endFrame(0.1, 1, 0, 0);
expect(meter.endFrame(0.1, 1, 0, 0)).toBe(true);
expect(meter.average).toEqual({ behaviors: 0, physics: 0, animation: 0, drawing: 0, editor: 1 });
});
it('never gives the editor less than nothing', () => {
const meter = new FrameMeter();
meter.addSystem('physics', 5);
meter.endFrame(0.5, 4, 1, 0);
expect(meter.average?.editor).toBe(0);
});
it('forgets everything on reset', () => {
const meter = new FrameMeter();
for (let i = 0; i < 5; i++) frame(meter);
meter.addSystem('behaviors', 100);
meter.reset();
expect(meter.average).toBeNull();
for (let i = 0; i < 4; i++) meter.endFrame(0.1, 1, 0, 0);
meter.endFrame(0.1, 1, 0, 0);
expect(meter.average?.behaviors).toBe(0);
});
});
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',
'Animation – ms',
'Drawing – ms',
'Editor – ms',
]);
});
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',
'Animation 0.00 ms',
'Drawing 3.00 ms',
'Editor 0.01 ms',
]);
});
});
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 {
runs: 0,
runsWhilePaused,
update() {
this.runs++;
},
};
}
it('reports every system it ticks, once a tick', () => {
const f = makeSpriteScene();
const world = World.instantiate(f.project, f.sceneId);
const a = counting();
const b = counting();
world.addSystem(a).addSystem(b);
const told: System[] = [];
world.timeSystem = (system, ms) => {
expect(ms).toBeGreaterThanOrEqual(0);
told.push(system);
};
world.tick(1 / 60);
world.tick(1 / 60);
expect(told).toEqual([a, b, a, b]);
expect(a.runs).toBe(2);
});
it('does not report a system a paused frame skips', () => {
const f = makeSpriteScene();
const world = World.instantiate(f.project, f.sceneId);
const still = counting();
const awake = counting(true);
world.addSystem(still).addSystem(awake);
const told: System[] = [];
world.timeSystem = (system) => told.push(system);
world.paused = true;
world.tick(1 / 60);
expect(told).toEqual([awake]);
expect(still.runs).toBe(0);
});
});