mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 00:56:23 +00:00
pollSubCompositionTimelines waits for every [data-composition-id] host to
register window.__timelines[id]. When the script carrying that registration
fails to load (404 / request failure), the registration can never arrive —
but the poll still burned the full playerReadyTimeout (45s), then warned and
shipped a silently animation-less render. Wild scale: the capture-setup
histogram over 30 days of local renders decays smoothly (402/503/364/282/191
per 5s bucket) then spikes to 705 at the 45s bucket — ~1,000 renders/month
across 402 distinct users, ~15 user-hours of pure waiting.
- Sessions now record failed SCRIPT resources (requestfailed + HTTP>=400
response, listeners that already existed for diagnostics) in
session.scriptLoadFailures.
- pollSubCompositionTimelines takes a failure getter and cuts the wait to a
2s grace once any script failed, with a loud warning naming the URL(s).
Late-registering fetch-async comps are unaffected: no script failure means
the full timeout still applies, and a registration landing inside the
grace window still wins (tested).
- Outcome telemetry: session.subTimelineWaitOutcome ("ready" | "timeout" |
"script_failure") -> CapturePerfSummary -> RenderPerfSummary.subTimelineWait
(worst across sessions) -> render_complete sub_timeline_wait, so the wild
rate becomes directly trackable instead of setup-histogram forensics.
Validation: the discovery comp (0768f038, its animations.js unreachable)
drops from ~72s to 23.1s total — poll cut at 2.1s with the script named;
healthy comp reports "ready". Canary suite 7/7 (PSNRs identical). 4 new
poll unit tests; engine suite 907 passed (14 failures are PRE-EXISTING on
main at v0.7.42 — 18 fail on a clean checkout, stash A/B verified).
tsc/oxlint/oxfmt clean.
Corpus note: 258/1,762 corpus comps (14%) reference local scripts missing
from the corpus fetch — their historical eval INIT timings measured this
timeout, not the engine. Capture-stage ratios remain valid (both paths paid
it equally).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
/**
|
|
* pollSubCompositionTimelines fail-fast contract: a script resource that
|
|
* failed to load can never register its `window.__timelines[id]`, so the
|
|
* poll must cut to the short grace window instead of burning the full
|
|
* playerReadyTimeout (measured wild: a 705-render spike at the 45s setup
|
|
* bucket over 30 days — ~1% of local renders).
|
|
*/
|
|
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import type { Page } from "puppeteer-core";
|
|
import { pollSubCompositionTimelines } from "./frameCapture.js";
|
|
|
|
function makeMockPage(evaluateResults: (expr: string) => unknown): Page {
|
|
return {
|
|
evaluate: vi.fn(async (expr: string) => evaluateResults(expr)),
|
|
} as unknown as Page;
|
|
}
|
|
|
|
describe("pollSubCompositionTimelines fail-fast", () => {
|
|
it("returns ready and forces a timeline rebind when timelines register", async () => {
|
|
const page = makeMockPage(() => true);
|
|
const outcome = await pollSubCompositionTimelines(page, 1_000, 10);
|
|
expect(outcome).toBe("ready");
|
|
// Second evaluate is the __hfForceTimelineRebind call.
|
|
expect((page.evaluate as ReturnType<typeof vi.fn>).mock.calls.length).toBe(2);
|
|
});
|
|
|
|
it("bails after the grace window when a script resource failed to load", async () => {
|
|
const page = makeMockPage((expr) =>
|
|
expr.includes("__hfForceTimelineRebind") ? undefined : false,
|
|
);
|
|
const started = Date.now();
|
|
const outcome = await pollSubCompositionTimelines(
|
|
page,
|
|
60_000, // full timeout must NOT be waited
|
|
10,
|
|
() => ["http://localhost/animations.js"],
|
|
50, // grace
|
|
);
|
|
expect(outcome).toBe("script_failure");
|
|
expect(Date.now() - started).toBeLessThan(5_000);
|
|
});
|
|
|
|
it("waits the full timeout when timelines are missing but no script failed", async () => {
|
|
const page = makeMockPage(() => false);
|
|
const outcome = await pollSubCompositionTimelines(page, 120, 10, () => []);
|
|
expect(outcome).toBe("timeout");
|
|
});
|
|
|
|
it("keeps waiting through the grace window when failures appear but timelines register late", async () => {
|
|
let calls = 0;
|
|
const page = makeMockPage((expr) => {
|
|
if (expr.includes("__hfForceTimelineRebind")) return undefined;
|
|
calls++;
|
|
return calls >= 3; // registers on the 3rd poll tick, inside the grace window
|
|
});
|
|
const outcome = await pollSubCompositionTimelines(
|
|
page,
|
|
60_000,
|
|
10,
|
|
() => ["http://localhost/late.js"],
|
|
10_000, // generous grace — registration lands first
|
|
);
|
|
expect(outcome).toBe("ready");
|
|
});
|
|
});
|