Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 89 additions & 3 deletions packages/engine/src/services/browserManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,65 @@ let pooledCaptureMode: CaptureMode = "screenshot";
// Preserve the producer-era export so re-export shims keep the same public API.
export const ENABLE_BROWSER_POOL = DEFAULT_CONFIG.enableBrowserPool;

// Flags only meaningful when Chrome's compositor is driven by
// HeadlessExperimental.beginFrame. If we fall back to screenshot mode they
// must be stripped — `--enable-begin-frame-control` in particular makes the
// compositor wait for frames we'll never send, producing blank screenshots.
const BEGINFRAME_ONLY_FLAGS = new Set([
"--deterministic-mode",
"--enable-begin-frame-control",
"--disable-new-content-rendering-timeout",
"--run-all-compositor-stages-before-draw",
"--disable-threaded-animation",
"--disable-threaded-scrolling",
"--disable-checker-imaging",
"--disable-image-animation-resync",
"--enable-surface-synchronization",
]);

function stripBeginFrameFlags(args: string[]): string[] {
return args.filter((a) => !BEGINFRAME_ONLY_FLAGS.has(a));
}

/**
* Probe whether the browser still speaks HeadlessExperimental.beginFrame.
*
* Recent chrome-headless-shell builds (observed on 147) expose the domain
* well enough that HeadlessExperimental.enable succeeds but drop the
* beginFrame method itself — the capture loop then dies on first frame with
* `'HeadlessExperimental.beginFrame' wasn't found`. So we probe BOTH: enable
* + one cheap beginFrame raced against a 2s timeout. In beginframe-control
* mode the command completes as soon as the compositor acks, so a real
* supported browser returns well under the timeout.
*
* Any failure (method missing, timeout, protocol error) is treated as
* unsupported. Real errors after launch would surface in the warmup loop and
* fall out through the caller's try/catch.
*/
async function probeBeginFrameSupport(browser: Browser): Promise<boolean> {
let page;
try {
page = await browser.newPage();
const client = await page.createCDPSession();
await client.send("HeadlessExperimental.enable");
const beginFrame = client.send("HeadlessExperimental.beginFrame", {
frameTimeTicks: 0,
interval: 33,
noDisplayUpdates: true,
});
const timeout = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("beginFrame probe timeout")), 2000),
);
await Promise.race([beginFrame, timeout]);
await client.detach().catch(() => {});
return true;
} catch {
return false;
} finally {
await page?.close().catch(() => {});
}
}

export async function acquireBrowser(
chromeArgs: string[],
config?: Partial<
Expand Down Expand Up @@ -112,14 +171,41 @@ export async function acquireBrowser(
}

const ppt = await getPuppeteer();
const browser = await ppt.launch({
const browserTimeout = config?.browserTimeout ?? DEFAULT_CONFIG.browserTimeout;
const protocolTimeout = config?.protocolTimeout ?? DEFAULT_CONFIG.protocolTimeout;
let browser = await ppt.launch({
headless: true,
args: chromeArgs,
defaultViewport: null,
executablePath,
timeout: config?.browserTimeout ?? DEFAULT_CONFIG.browserTimeout,
protocolTimeout: config?.protocolTimeout ?? DEFAULT_CONFIG.protocolTimeout,
timeout: browserTimeout,
protocolTimeout,
});

// Probe HeadlessExperimental.beginFrame — recent chrome-headless-shell
// builds (observed on 147) dropped the method while keeping the flags
// valid, so `--enable-begin-frame-control` leaves the compositor waiting
// for beginFrames the engine can no longer send. Auto-fall back to
// screenshot mode with the appropriate flags.
if (captureMode === "beginframe") {
const supported = await probeBeginFrameSupport(browser).catch(() => true);
if (!supported) {
await browser.close().catch(() => {});
console.warn(
"[BrowserManager] HeadlessExperimental.beginFrame unavailable in this Chromium build; falling back to screenshot mode.",
);
captureMode = "screenshot";
browser = await ppt.launch({
headless: true,
args: stripBeginFrameFlags(chromeArgs),
defaultViewport: null,
executablePath,
timeout: browserTimeout,
protocolTimeout,
});
}
}

if (enablePool) {
pooledBrowser = browser;
pooledBrowserRefCount = 1;
Expand Down
Loading