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>
153 lines
5.0 KiB
TypeScript
153 lines
5.0 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
});
|