Fixes#3012
## What
Fixes `HeadlessExperimental.beginFrame` hanging at 60fps and other high frame rates.
## Why
Warmup always advances Chrome in 33ms steps, but the first capture timestamp was calculated from the output fps.
At 60fps, the last warmup tick is 1947ms, while the next commit tick used to jump back to about 1067ms. Chrome sees time moving backwards and `beginFrame` can stall.
## How
Keep the old timestamps when they are already safe. If the commit tick would go backwards, move the capture baseline past warmup first.
The init commit and both producer probes now use the same timestamp helpers, so they cannot calculate different values.
## Test plan
- [x] Added tests for 24, 30, 31, 32, 33, 60, 120, 240, and 59.94fps
- [x] Added coverage for the actual session baseline, commit parameters, and both producer probe paths
- [x] Engine tests: 1383 passed, 3 skipped
- [x] Producer unit tests passed
- [x] Engine and producer typechecks and builds passed
- [ ] Manual render test
* fix(engine): commit render-frame siblings with a visual BeginFrame at init
Chunk-lambda renders drop a periodic near-black frame — one every
chunk_frames/worker_count frames (every 60 on a 4-worker single-video chunk),
YAVG ~22 against YMAX ~240 in signalstats. Local single-process renders don't
show it because they don't run under BeginFrame.
It's the isNewImage branch in injectVideoFramesBatch: the first time a session
paints a given videoId there's no __render_frame__ sibling yet, so it creates
the <img> on the spot (createElement + insertBefore) right before capture.
Under HeadlessExperimental.BeginFrame the compositor doesn't have that fresh
layer in the immediately-next frame, so the first captured frame per session
paints only body background + already-composited overlays. Each lambda worker
is its own session, hence the worker-boundary periodicity.
Pre-create the hidden sibling at the end of initializeSession, then drive one
non-capture visual BeginFrame (noDisplayUpdates: false) to composite the new
layers before the first real capture. The warmup ticks are noDisplayUpdates:
true (they advance the clock but don't paint) and the per-frame seek doesn't
tick, so this explicit visual frame is what actually commits the layers; its
tick sits in the gap between warmup and frame 0 so ticks stay monotonic and no
render frame is consumed. Every subsequent inject then takes the hasImg=true
(src-update) path; the isNewImage branch stays as a fallback for callers that
don't go through initializeSession.
* fix(engine): place the render-frame commit tick before the liveness probe
The commit tick at init sends its BeginFrame at `beginFrameTimeTicks - 1·interval`.
The producer's liveness probe then fires right after init at
`beginFrameTimeTicks - 5·interval` — an earlier tick. Per-session BeginFrame time
has to be monotonic, so the probe running backwards past the commit tick stalls
chrome-headless-shell indefinitely; the engine reads that timeout as a SwiftShader
heavy-layer stall and routes the render to screenshot capture, which then dies
relaunching and hangs the shard to the job timeout.
Reproduced on a native x86 SwiftShader host and bisected: with the commit tick
present the probe times out even with zero render-frame siblings created, so it's
the tick ordering, not layer count. Moving the commit tick to `-6·interval` (below
the probe, above the warmup ticks) keeps warmup < commit < probe < capture
monotonic and clears the stall on every affected comp — sub-composition-video,
chat, style-5-prod — while a healthy comp (style-18-prod) is unchanged. The commit
tick itself is untouched, so the black-frame fix it exists for still holds.