mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
Part of Phase 2 of the distributed rendering plan (determinism hardening).
See DISTRIBUTED-RENDERING-PLAN.md §5.2 (warmupTicks row) and §17.2 (gating
table).
The BeginFrame warmup loop in `initializeSession` is driven by wall-clock
during page load — different hosts accumulate different tick counts before
page-readiness completes. That shifts `session.beginFrameTimeTicks` and
yields non-byte-identical captures on distributed workers.
This change adds `lockWarmupTicks: boolean` (default false) to
`CaptureOptions`. When false, behavior is unchanged. When true, the loop
runs exactly `LOCKED_WARMUP_TICKS = 60` iterations regardless of page-load
wall clock, and `session.beginFrameTimeTicks` is computed from the
constant — pinning the baseline across hosts.
Refactoring:
- Extract `driveWarmupTicks(options, state)` as a pure helper. Tests
drive it with a stub `tick` callback and an injected `sleep`, so the
iteration-count contract is unit-testable without real Chrome.
- `initializeSession`'s warmup body is now a thin adapter that calls
`driveWarmupTicks` with a CDP-backed tick.
Producer regression baselines remain byte-identical: the in-process
renderer never passes `lockWarmupTicks: true`. Phase 3 distributed
primitives will flip it true when launching chunk workers.
11 new unit tests at packages/engine/src/services/
frameCapture-warmupTicks.test.ts pin both branches (unlocked drifts with
simulated load time; locked produces identical counts).
This is part of a stack of 10 PRs; this is PR 3 of 10.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
175 lines
5.7 KiB
TypeScript
175 lines
5.7 KiB
TypeScript
/**
|
|
* Tests for the `driveWarmupTicks` BeginFrame warmup driver.
|
|
*
|
|
* The wall-clock warmup loop in `initializeSession` accumulates a different
|
|
* number of ticks per host CPU speed, which shifts `beginFrameTimeTicks`
|
|
* and breaks byte-identical captures across distributed workers.
|
|
*
|
|
* `lockWarmupTicks=true` clamps the loop to a fixed iteration count
|
|
* (`LOCKED_WARMUP_TICKS = 60`) so the baseline becomes host-independent.
|
|
*/
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
LOCKED_WARMUP_TICKS,
|
|
driveWarmupTicks,
|
|
warmupFrameTimeTicks,
|
|
type WarmupTickState,
|
|
} from "./frameCapture.js";
|
|
|
|
function makeState(): WarmupTickState {
|
|
return { running: true, ticks: 0 };
|
|
}
|
|
|
|
// Run the warmup driver "concurrently" with a simulated page-load that
|
|
// flips `state.running` to false after `pageLoadIterations` of the
|
|
// `tick` callback. Returns the final state.
|
|
async function runWithSimulatedPageLoad(
|
|
lockWarmupTicks: boolean,
|
|
pageLoadIterations: number,
|
|
tickDelay: () => Promise<void> = () => Promise.resolve(),
|
|
): Promise<WarmupTickState> {
|
|
const state = makeState();
|
|
const intervalMs = 33;
|
|
|
|
const tick = async (): Promise<void> => {
|
|
if (state.ticks + 1 === pageLoadIterations) {
|
|
state.running = false;
|
|
}
|
|
await tickDelay();
|
|
};
|
|
|
|
await driveWarmupTicks(
|
|
{
|
|
intervalMs,
|
|
lockWarmupTicks,
|
|
tick,
|
|
sleep: () => Promise.resolve(),
|
|
},
|
|
state,
|
|
);
|
|
return state;
|
|
}
|
|
|
|
describe("driveWarmupTicks — unlocked", () => {
|
|
it("stops when state.running flips false", async () => {
|
|
const state = await runWithSimulatedPageLoad(false, 10);
|
|
expect(state.ticks).toBe(10);
|
|
});
|
|
|
|
it("yields different tick counts for different simulated page-load lengths", async () => {
|
|
const fast = await runWithSimulatedPageLoad(false, 5);
|
|
const slow = await runWithSimulatedPageLoad(false, 50);
|
|
expect(fast.ticks).toBe(5);
|
|
expect(slow.ticks).toBe(50);
|
|
expect(fast.ticks).not.toBe(slow.ticks);
|
|
});
|
|
|
|
it("frame time derived as ticks * intervalMs", async () => {
|
|
const state = await runWithSimulatedPageLoad(false, 7);
|
|
expect(warmupFrameTimeTicks(state, 33)).toBe(7 * 33);
|
|
});
|
|
|
|
it("does not iterate when state.running starts false", async () => {
|
|
const state: WarmupTickState = { running: false, ticks: 0 };
|
|
await driveWarmupTicks(
|
|
{
|
|
intervalMs: 33,
|
|
lockWarmupTicks: false,
|
|
tick: async () => {},
|
|
sleep: () => Promise.resolve(),
|
|
},
|
|
state,
|
|
);
|
|
expect(state.ticks).toBe(0);
|
|
});
|
|
|
|
it("counts ticks even when tick callback throws (page not ready yet)", async () => {
|
|
// Pre-acquisition errors are swallowed — the loop must keep spinning so
|
|
// it can drive the page once CDP comes up.
|
|
let stopAt = 5;
|
|
const state = makeState();
|
|
await driveWarmupTicks(
|
|
{
|
|
intervalMs: 33,
|
|
lockWarmupTicks: false,
|
|
tick: async () => {
|
|
if (--stopAt <= 0) state.running = false;
|
|
throw new Error("CDP not ready");
|
|
},
|
|
sleep: () => Promise.resolve(),
|
|
},
|
|
state,
|
|
);
|
|
// We don't count ticks on throw, but the loop kept running until
|
|
// state.running flipped — so it exited cleanly.
|
|
expect(state.running).toBe(false);
|
|
expect(state.ticks).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("driveWarmupTicks — locked", () => {
|
|
it("runs exactly LOCKED_WARMUP_TICKS iterations regardless of state.running", async () => {
|
|
// Simulate a fast page load: state.running flips false after just 5 ticks.
|
|
// The locked loop must still run to LOCKED_WARMUP_TICKS.
|
|
const state = await runWithSimulatedPageLoad(true, 5);
|
|
expect(state.ticks).toBe(LOCKED_WARMUP_TICKS);
|
|
});
|
|
|
|
it("runs exactly LOCKED_WARMUP_TICKS iterations on slow simulated load", async () => {
|
|
// state.running stays true past the locked count — loop still stops at
|
|
// LOCKED_WARMUP_TICKS.
|
|
const state = await runWithSimulatedPageLoad(true, 9999);
|
|
expect(state.ticks).toBe(LOCKED_WARMUP_TICKS);
|
|
});
|
|
|
|
it("yields IDENTICAL tick counts across simulated fast and slow loads", async () => {
|
|
// THE determinism property the locked mode exists for.
|
|
const fast = await runWithSimulatedPageLoad(true, 5);
|
|
const slow = await runWithSimulatedPageLoad(true, 200);
|
|
expect(fast.ticks).toBe(slow.ticks);
|
|
expect(fast.ticks).toBe(LOCKED_WARMUP_TICKS);
|
|
expect(warmupFrameTimeTicks(fast, 33)).toBe(warmupFrameTimeTicks(slow, 33));
|
|
});
|
|
|
|
it("yields LOCKED_WARMUP_TICKS even when state.running starts false", async () => {
|
|
// Guards the locked contract independently of the caller's running flag.
|
|
const state: WarmupTickState = { running: false, ticks: 0 };
|
|
await driveWarmupTicks(
|
|
{
|
|
intervalMs: 33,
|
|
lockWarmupTicks: true,
|
|
tick: async () => {},
|
|
sleep: () => Promise.resolve(),
|
|
},
|
|
state,
|
|
);
|
|
expect(state.ticks).toBe(LOCKED_WARMUP_TICKS);
|
|
});
|
|
|
|
it("does not exceed LOCKED_WARMUP_TICKS even if the caller never stops it", async () => {
|
|
const state = makeState();
|
|
let observedMax = 0;
|
|
await driveWarmupTicks(
|
|
{
|
|
intervalMs: 33,
|
|
lockWarmupTicks: true,
|
|
tick: async () => {
|
|
observedMax = Math.max(observedMax, state.ticks + 1);
|
|
},
|
|
sleep: () => Promise.resolve(),
|
|
},
|
|
state,
|
|
);
|
|
expect(observedMax).toBe(LOCKED_WARMUP_TICKS);
|
|
expect(state.ticks).toBe(LOCKED_WARMUP_TICKS);
|
|
});
|
|
|
|
it("baseline frame time matches (LOCKED_WARMUP_TICKS * intervalMs)", async () => {
|
|
// initializeSession derives session.beginFrameTimeTicks from the final
|
|
// tick count — locked mode pins this to a host-independent value.
|
|
const state = await runWithSimulatedPageLoad(true, 5);
|
|
expect(warmupFrameTimeTicks(state, 33)).toBe(LOCKED_WARMUP_TICKS * 33);
|
|
});
|
|
});
|