mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
fix(qa-cctv-v2): keep long headful waits under puppeteer's protocolTimeout
The headful drain budget is uncapped (camCount * 800 + 30000 ms, ~50 min at N≈3,700), but each page.waitForFunction is one CDP call that puppeteer aborts after its 180 s default protocolTimeout. The .catch then recorded "timed out waiting for drain on the real-GPU sign-off path" after ~3 min. - Poll the drain in <=120 s slices (waitForFunctionSliced) so no single CDP call outlives protocolTimeout, whatever the catalog size. - Launch with protocolTimeout 240 s, headroom above the largest slice and the 60 s fixed waits. - Bound the sibling-owner 3-frame postRender wait at 10 s and report the frame count, so a renderer that stops producing frames fails with evidence instead of hanging the evaluate. - List --disable-backgrounding-occluded-windows explicitly. Puppeteer 25.10 already passes it by default, so this documents the headful contract rather than changing behaviour. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
132b9e96dc
commit
1a0b9c8b0f
1 file changed
+60
-16
+60
-16
@@ -139,6 +139,41 @@ function record(name, ok, detail) {
|
|||||||
|
|
||||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
// Every page.waitForFunction / page.evaluate is a single CDP call that
|
||||||
|
// puppeteer aborts after `protocolTimeout` (default 180s). The headful drain
|
||||||
|
// budget is uncapped and reaches ~50 minutes on the full ~3,900-camera
|
||||||
|
// catalog, so long waits are polled in slices of at most WAIT_SLICE_MS and
|
||||||
|
// protocolTimeout is set with headroom above the largest slice.
|
||||||
|
const WAIT_SLICE_MS = 120000;
|
||||||
|
const PROTOCOL_TIMEOUT_MS = 2 * WAIT_SLICE_MS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* page.waitForFunction with an overall budget that may exceed
|
||||||
|
* protocolTimeout: waits in WAIT_SLICE_MS slices until the predicate holds or
|
||||||
|
* the budget runs out.
|
||||||
|
* @param {import('puppeteer').Page} page
|
||||||
|
* @param {Function} predicate - In-page predicate.
|
||||||
|
* @param {number} budgetMs - Overall budget.
|
||||||
|
* @returns {Promise<boolean>} True if the predicate held within the budget.
|
||||||
|
*/
|
||||||
|
async function waitForFunctionSliced(page, predicate, budgetMs) {
|
||||||
|
const deadline = Date.now() + budgetMs;
|
||||||
|
for (;;) {
|
||||||
|
const remaining = deadline - Date.now();
|
||||||
|
if (remaining <= 0) return false;
|
||||||
|
const ok = await page
|
||||||
|
.waitForFunction(predicate, {
|
||||||
|
timeout: Math.min(remaining, WAIT_SLICE_MS),
|
||||||
|
})
|
||||||
|
.then(() => true)
|
||||||
|
.catch((err) => {
|
||||||
|
if (err?.name === 'TimeoutError') return false;
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
if (ok) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// SwiftShader can spend tens of seconds servicing the hover drillPick emitted
|
// SwiftShader can spend tens of seconds servicing the hover drillPick emitted
|
||||||
// by page.mouse.move(), before the LEFT_DOWN under test even runs. Position the
|
// by page.mouse.move(), before the LEFT_DOWN under test even runs. Position the
|
||||||
// pointer while ADJUST is disabled, then re-enable it and explicitly render a
|
// pointer while ADJUST is disabled, then re-enable it and explicitly render a
|
||||||
@@ -364,6 +399,7 @@ async function main() {
|
|||||||
const chromeExecutable = findChromeExecutable();
|
const chromeExecutable = findChromeExecutable();
|
||||||
const browser = await puppeteer.launch({
|
const browser = await puppeteer.launch({
|
||||||
headless: HEADFUL ? false : 'new',
|
headless: HEADFUL ? false : 'new',
|
||||||
|
protocolTimeout: PROTOCOL_TIMEOUT_MS,
|
||||||
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
|
...(chromeExecutable ? { executablePath: chromeExecutable } : {}),
|
||||||
args: [
|
args: [
|
||||||
'--no-sandbox',
|
'--no-sandbox',
|
||||||
@@ -373,6 +409,9 @@ async function main() {
|
|||||||
'--disable-web-security',
|
'--disable-web-security',
|
||||||
'--disable-background-timer-throttling',
|
'--disable-background-timer-throttling',
|
||||||
'--disable-renderer-backgrounding',
|
'--disable-renderer-backgrounding',
|
||||||
|
// Puppeteer's defaults already include this; listed so the headful
|
||||||
|
// run keeps rendering when covered even if those defaults change.
|
||||||
|
'--disable-backgrounding-occluded-windows',
|
||||||
'--window-size=1280,800',
|
'--window-size=1280,800',
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -475,18 +514,15 @@ async function main() {
|
|||||||
console.log(
|
console.log(
|
||||||
`Waiting for geometry-load queue to drain (N=${camCount}, budget ${Math.round(drainBudgetMs / 1000)}s)...`,
|
`Waiting for geometry-load queue to drain (N=${camCount}, budget ${Math.round(drainBudgetMs / 1000)}s)...`,
|
||||||
);
|
);
|
||||||
const drained = await page
|
const drained = await waitForFunctionSliced(
|
||||||
.waitForFunction(
|
page,
|
||||||
() => {
|
() => {
|
||||||
const mod =
|
const mod = window.__godsEyeView.dataManager.layers.get('cctv').module;
|
||||||
window.__godsEyeView.dataManager.layers.get('cctv').module;
|
const ui = mod.getUIState();
|
||||||
const ui = mod.getUIState();
|
return ui.loading && ui.loading.active === false;
|
||||||
return ui.loading && ui.loading.active === false;
|
},
|
||||||
},
|
drainBudgetMs,
|
||||||
{ timeout: drainBudgetMs },
|
);
|
||||||
)
|
|
||||||
.then(() => true)
|
|
||||||
.catch(() => false);
|
|
||||||
record(
|
record(
|
||||||
`geometry-load queue drains within ${Math.round(drainBudgetMs / 1000)}s (N=${camCount})`,
|
`geometry-load queue drains within ${Math.round(drainBudgetMs / 1000)}s (N=${camCount})`,
|
||||||
drained ? true : HEADFUL ? false : null,
|
drained ? true : HEADFUL ? false : null,
|
||||||
@@ -1358,16 +1394,23 @@ async function main() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
window.__qaCctvSiblingOwner = owner;
|
window.__qaCctvSiblingOwner = owner;
|
||||||
await new Promise((resolve) => {
|
// Bounded: if the browser stops producing frames (headful window
|
||||||
|
// asleep or hidden), report it instead of hanging the CDP call.
|
||||||
|
const framesRendered = await new Promise((resolve) => {
|
||||||
let frames = 0;
|
let frames = 0;
|
||||||
|
const finish = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
remove();
|
||||||
|
resolve(frames);
|
||||||
|
};
|
||||||
const remove = scene.postRender.addEventListener(() => {
|
const remove = scene.postRender.addEventListener(() => {
|
||||||
if (++frames < 3) {
|
if (++frames < 3) {
|
||||||
scene.requestRender();
|
scene.requestRender();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
remove();
|
finish();
|
||||||
resolve();
|
|
||||||
});
|
});
|
||||||
|
const timer = setTimeout(finish, 10000);
|
||||||
scene.requestRender();
|
scene.requestRender();
|
||||||
});
|
});
|
||||||
const rect = scene.canvas.getBoundingClientRect();
|
const rect = scene.canvas.getBoundingClientRect();
|
||||||
@@ -1382,6 +1425,7 @@ async function main() {
|
|||||||
typeof pickedId === 'string' ? pickedId : (pickedId?.id ?? null),
|
typeof pickedId === 'string' ? pickedId : (pickedId?.id ?? null),
|
||||||
ownerId: owner.id,
|
ownerId: owner.id,
|
||||||
ownsExactObject: picked?.id === owner,
|
ownsExactObject: picked?.id === owner,
|
||||||
|
framesRendered,
|
||||||
projectionDelta: Math.hypot(
|
projectionDelta: Math.hypot(
|
||||||
projected.x - canvasPoint.x,
|
projected.x - canvasPoint.x,
|
||||||
projected.y - canvasPoint.y,
|
projected.y - canvasPoint.y,
|
||||||
@@ -1395,7 +1439,7 @@ async function main() {
|
|||||||
siblingTarget.ownsExactObject
|
siblingTarget.ownsExactObject
|
||||||
: null,
|
: null,
|
||||||
siblingTarget
|
siblingTarget
|
||||||
? `picked=${siblingTarget.pickedId} exactOwner=${siblingTarget.ownsExactObject} projectionΔ=${siblingTarget.projectionDelta}`
|
? `picked=${siblingTarget.pickedId} exactOwner=${siblingTarget.ownsExactObject} projectionΔ=${siblingTarget.projectionDelta} frames=${siblingTarget.framesRendered}/3`
|
||||||
: 'no previously verified empty canvas ray was available',
|
: 'no previously verified empty canvas ray was available',
|
||||||
);
|
);
|
||||||
if (
|
if (
|
||||||
|
|||||||
Reference in new issue
Block a user