/** * The frame governor, fed frames by hand. A second of slow frames should * cost one rung and not four; a second of fast ones should not immediately * give it back, because a resolution that flaps is worse than a low one. */ import { expect, test } from "bun:test"; import { FAST, makeBudget, measure, PATIENCE, SLOW, WINDOW } from "../../static/engine/budget.js"; /** `seconds` of frames, each `ms` long, and how many times the level moved. */ function run(budget: ReturnType, ms: number, seconds: number, from = 0) { let at = from; let moves = 0; const end = from + seconds; while (at < end) { at += ms / 1000; if (measure(budget, ms, at)) moves++; } return { moves, at }; } test("a second over budget costs exactly one rung, not four", () => { const budget = makeBudget(0); const { moves } = run(budget, SLOW + 8, WINDOW * 1.5); expect(moves).toBe(1); expect(budget.level).toBe(1); }); test("frames inside the budget leave the level alone", () => { const budget = makeBudget(2); run(budget, (SLOW + FAST) / 2, 10); expect(budget.level).toBe(2); }); test("headroom gives a rung back, but not before two windows of it", () => { const budget = makeBudget(0); let at = run(budget, SLOW + 8, WINDOW * 1.2).at; expect(budget.level).toBe(1); at = run(budget, 8, WINDOW * 1.2, at).at; expect(budget.level).toBe(1); // one good window is not enough run(budget, 8, WINDOW * 1.2, at); expect(budget.level).toBe(0); }); test("it never refines past the rung the window started on", () => { const budget = makeBudget(2); run(budget, 4, 20); expect(budget.level).toBe(2); expect(budget.good).toBeLessThanOrEqual(PATIENCE); }); test("it stops at the coarsest rung rather than walking off the ladder", () => { const budget = makeBudget(0, 3); run(budget, 60, 40); expect(budget.level).toBe(3); }); test("one slow window, then one fast, does not oscillate", () => { const budget = makeBudget(0); let at = 0; const levels: number[] = []; for (let i = 0; i < 8; i++) { at = run(budget, i % 2 === 0 ? SLOW + 8 : 6, WINDOW * 1.05, at).at; levels.push(budget.level); } // Alternating windows never hand a rung back — it takes two good ones — // so the level climbs slowly and stays put rather than flickering. expect(levels.filter((l, i) => i > 0 && l < levels[i - 1]!).length).toBe(0); }); test("the window reports the frame rate and the mean it decided on", () => { const budget = makeBudget(0); run(budget, 20, WINDOW * 1.1); expect(budget.mean).toBeCloseTo(20, 1); expect(budget.fps).toBeGreaterThan(45); expect(budget.fps).toBeLessThan(55); });