mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(player): perf harness waits for assets-ready before play instead of a fixed 5s (#4137)
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { waitForPlayerAssetsReady } from "./runner.ts";
|
||||
|
||||
type Pred = () => boolean;
|
||||
|
||||
/** A page whose `__playerAssetsReady` flips after `gateMs`, polled like puppeteer's waitForFunction. */
|
||||
function pageWithSlowGate(gateMs: number) {
|
||||
const win: { __playerAssetsReady?: boolean } = {};
|
||||
setTimeout(() => (win.__playerAssetsReady = true), gateMs);
|
||||
return {
|
||||
waitForFunction: (pred: Pred, opts: { timeout: number }) =>
|
||||
new Promise<void>((resolve, reject) => {
|
||||
const started = Date.now();
|
||||
const poll = setInterval(() => {
|
||||
(globalThis as { window?: unknown }).window = win;
|
||||
if (pred()) {
|
||||
clearInterval(poll);
|
||||
resolve();
|
||||
} else if (Date.now() - started >= opts.timeout) {
|
||||
clearInterval(poll);
|
||||
reject(new Error(`Waiting failed: ${opts.timeout}ms exceeded`));
|
||||
}
|
||||
}, 5);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
describe("waitForPlayerAssetsReady", () => {
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
it("outlasts an asset gate slower than the old fixed 5s play-confirm window", async () => {
|
||||
vi.useFakeTimers();
|
||||
const page = pageWithSlowGate(7_000);
|
||||
const done = waitForPlayerAssetsReady(page as never);
|
||||
await vi.advanceTimersByTimeAsync(7_100);
|
||||
await expect(done).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("names the queued asset gate when it never opens", async () => {
|
||||
vi.useFakeTimers();
|
||||
const page = pageWithSlowGate(60_000);
|
||||
const done = waitForPlayerAssetsReady(page as never, 1_000);
|
||||
const assertion = expect(done).rejects.toThrow(/never emitted assetsready.*queued behind/);
|
||||
await vi.advanceTimersByTimeAsync(1_100);
|
||||
await assertion;
|
||||
});
|
||||
});
|
||||
@@ -41,6 +41,7 @@ declare global {
|
||||
interface Window {
|
||||
__playerReady?: boolean;
|
||||
__playerReadyAt?: number;
|
||||
__playerAssetsReady?: boolean;
|
||||
__playerNavStart?: number;
|
||||
__playerDuration?: number;
|
||||
__playerError?: string;
|
||||
@@ -126,6 +127,23 @@ export async function loadHostPage(
|
||||
return { loadMs, duration };
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the player's own `assetsready` signal. `play()` queues until it fires, so a
|
||||
* scenario that plays right after `ready` and waits a fixed window for `isPlaying` races the gate.
|
||||
*/
|
||||
export async function waitForPlayerAssetsReady(
|
||||
page: Pick<Page, "waitForFunction">,
|
||||
timeoutMs = 30_000,
|
||||
): Promise<void> {
|
||||
try {
|
||||
await page.waitForFunction(() => window.__playerAssetsReady === true, { timeout: timeoutMs });
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`[player-perf] player never emitted assetsready within ${timeoutMs}ms; play() stays queued behind the asset gate (${e instanceof Error ? e.message : String(e)})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function percentile(samples: number[], pct: number): number {
|
||||
if (samples.length === 0) return 0;
|
||||
const sorted = [...samples].sort((a, b) => a - b);
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
*/
|
||||
|
||||
import type { Browser, Frame, Page } from "puppeteer-core";
|
||||
import { loadHostPage } from "../runner.ts";
|
||||
import { loadHostPage, waitForPlayerAssetsReady } from "../runner.ts";
|
||||
import type { Metric } from "../perf-gate.ts";
|
||||
|
||||
export type FpsScenarioOpts = {
|
||||
@@ -120,6 +120,7 @@ async function runOnce(
|
||||
try {
|
||||
const page = await ctx.newPage();
|
||||
const { duration } = await loadHostPage(page, opts.origin, { fixture });
|
||||
await waitForPlayerAssetsReady(page);
|
||||
const frame = await getFixtureFrame(page, fixture);
|
||||
|
||||
// Install the wall-clock sampler in the iframe context. We use setInterval
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
*/
|
||||
|
||||
import type { Browser, Frame, Page } from "puppeteer-core";
|
||||
import { loadHostPage, percentile } from "../runner.ts";
|
||||
import { loadHostPage, waitForPlayerAssetsReady, percentile } from "../runner.ts";
|
||||
import type { Metric } from "../perf-gate.ts";
|
||||
|
||||
export type DriftScenarioOpts = {
|
||||
@@ -126,6 +126,7 @@ async function runOnce(
|
||||
try {
|
||||
const page = await ctx.newPage();
|
||||
const { duration } = await loadHostPage(page, opts.origin, { fixture });
|
||||
await waitForPlayerAssetsReady(page);
|
||||
const requiredDurationSec = PLAYBACK_DURATION_MS / 1000;
|
||||
if (duration < requiredDurationSec) {
|
||||
throw new Error(
|
||||
|
||||
@@ -73,7 +73,7 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { Browser, Frame, Page } from "puppeteer-core";
|
||||
import { loadHostPage } from "../runner.ts";
|
||||
import { loadHostPage, waitForPlayerAssetsReady } from "../runner.ts";
|
||||
import type { Metric } from "../perf-gate.ts";
|
||||
|
||||
export type ParityScenarioOpts = {
|
||||
@@ -261,6 +261,7 @@ async function runOnce(
|
||||
try {
|
||||
const page = await ctx.newPage();
|
||||
const { duration } = await loadHostPage(page, opts.origin, { fixture });
|
||||
await waitForPlayerAssetsReady(page);
|
||||
if (duration < TARGET_TIME_S + 0.1) {
|
||||
throw new Error(
|
||||
`[scenario:parity] fixture composition is ${duration.toFixed(2)}s but parity target needs >= ${(TARGET_TIME_S + 0.1).toFixed(2)}s`,
|
||||
|
||||
@@ -100,6 +100,7 @@ function buildHostHtml(fixtureName: string, width: number, height: number): stri
|
||||
></hyperframes-player>
|
||||
<script>
|
||||
window.__playerReady = false;
|
||||
window.__playerAssetsReady = false;
|
||||
window.__playerReadyAt = null;
|
||||
window.__playerNavStart = performance.timeOrigin + performance.now();
|
||||
window.__sandboxProbeResults = [];
|
||||
@@ -114,6 +115,9 @@ function buildHostHtml(fixtureName: string, width: number, height: number): stri
|
||||
window.__playerReadyAt = performance.timeOrigin + performance.now();
|
||||
window.__playerDuration = (event.detail && event.detail.duration) || 0;
|
||||
});
|
||||
player.addEventListener("assetsready", function () {
|
||||
window.__playerAssetsReady = true;
|
||||
});
|
||||
player.addEventListener("error", function (event) {
|
||||
window.__playerError = (event.detail && event.detail.message) || "unknown";
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user