Files
feuerwerk/tests/editor/frame-meter.test.ts
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

136 lines
4.4 KiB
TypeScript
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.
/**
* 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 } 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([
'– 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([
'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('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);
});
});