239 lines
11 KiB
JavaScript
239 lines
11 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* qa-cables-overlay — submarine-cable label path measurement + contract gate.
|
|
*
|
|
* Two jobs:
|
|
* 1. MEASURE (both builds, `--legacy` on the pre-migration build): with the
|
|
* cables layer ON at a fixed mid-Atlantic camera, capture
|
|
* a. per-frame `scene.render` cost over a ~10 s driven-orbit window
|
|
* (mean / p50 / p95 / max — relative numbers, SwiftShader surface),
|
|
* b. toggle-ON → first-labels-visible latency,
|
|
* c. entity / native-label / host-entry counts,
|
|
* d. parked-idle postRender fires over 5 s (render-governor honesty).
|
|
* 2. GATE (unified build only): assert the migration contract —
|
|
* zero native `LabelGraphics` on cable reference entities, host entries
|
|
* present for the source, idle stays near-zero, and a clean OFF→ON cycle
|
|
* (no orphan host entries after disable).
|
|
*
|
|
* Usage: node scripts/qa-cables-overlay.mjs [--url http://localhost:4214] [--legacy]
|
|
* Requires a running dev server. Headless; rAF throttling disabled so the
|
|
* frame clock is honest (hidden-pane gotcha).
|
|
*/
|
|
import puppeteer from 'puppeteer';
|
|
|
|
const argv = process.argv;
|
|
const url = argv.includes('--url') ? argv[argv.indexOf('--url') + 1] : 'http://localhost:4214';
|
|
const legacy = argv.includes('--legacy');
|
|
// --control: never enable the layer; measure the empty-scene orbit cost so the
|
|
// cables layer's share of frame time is attributable.
|
|
const control = argv.includes('--control');
|
|
const LAYER_ID = 'telegeography-submarine-cables';
|
|
|
|
const results = [];
|
|
function check(name, pass, detail) {
|
|
results.push({ name, pass });
|
|
const tag = pass ? 'PASS' : 'FAIL';
|
|
console.log(` [${tag}] ${name}${detail !== undefined ? ` — ${JSON.stringify(detail)}` : ''}`);
|
|
}
|
|
function report(name, detail) {
|
|
console.log(` [MEAS] ${name} — ${JSON.stringify(detail)}`);
|
|
}
|
|
|
|
const browser = await puppeteer.launch({
|
|
headless: 'new',
|
|
protocolTimeout: 300_000,
|
|
args: [
|
|
'--no-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
'--window-size=1440,900',
|
|
'--disable-backgrounding-occluded-windows',
|
|
'--disable-renderer-backgrounding',
|
|
'--disable-background-timer-throttling',
|
|
],
|
|
});
|
|
|
|
try {
|
|
const page = await browser.newPage();
|
|
await page.setViewport({ width: 1440, height: 860 });
|
|
await page.goto(url, { waitUntil: 'domcontentloaded' });
|
|
await page.waitForFunction(() => !!window.__godsEyeView?.viewer, { timeout: 90_000 });
|
|
await new Promise((r) => setTimeout(r, 12_000)); // boot flyTo + deferred init
|
|
|
|
// Park mid-Atlantic (many cables + both coasts' landings in range) and
|
|
// disable every layer so the cables layer is measured in isolation.
|
|
await page.evaluate(async () => {
|
|
const gev = window.__godsEyeView;
|
|
const v = gev.viewer;
|
|
v.camera.cancelFlight();
|
|
const ell = v.scene.globe.ellipsoid;
|
|
v.camera.setView({
|
|
destination: ell.cartographicToCartesian({
|
|
longitude: -40 * Math.PI / 180, latitude: 35 * Math.PI / 180, height: 4_500_000,
|
|
}),
|
|
orientation: { heading: 0, pitch: -Math.PI / 2, roll: 0 },
|
|
});
|
|
for (const [id, entry] of gev.dataManager.layers) {
|
|
if (entry.enabled) { try { await gev.dataManager.setEnabled(id, false, { origin: 'user' }); } catch { /* measured via counts */ } }
|
|
}
|
|
});
|
|
await new Promise((r) => setTimeout(r, 6_000)); // tiles + fades settle
|
|
|
|
// ── b. toggle-ON → labels visible ─────────────────────────────────────
|
|
if (!control) {
|
|
const labelLatency = await page.evaluate(async (layerId, isLegacy) => {
|
|
const gev = window.__godsEyeView;
|
|
const t0 = performance.now();
|
|
await gev.dataManager.setEnabled(layerId, true, { origin: 'user' });
|
|
const deadline = t0 + 60_000;
|
|
const labelsUp = () => {
|
|
if (isLegacy) {
|
|
const stats = gev.dataManager.layers.get(layerId)?.module?.getStats?.() || {};
|
|
return (stats.referenceLabelCount || 0) > 0;
|
|
}
|
|
const diag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
|
|
return (diag.paintedBySource?.[layerId] || 0) > 0;
|
|
};
|
|
while (performance.now() < deadline) {
|
|
if (labelsUp()) return { ms: Math.round(performance.now() - t0), timedOut: false };
|
|
gev.viewer.scene.requestRender?.();
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
}
|
|
return { ms: 60_000, timedOut: true };
|
|
}, LAYER_ID, legacy);
|
|
report('toggle-ON -> labels visible (ms)', labelLatency);
|
|
check('labels became visible after enable', labelLatency.timedOut === false, labelLatency);
|
|
await new Promise((r) => setTimeout(r, 3_000)); // let load/labels settle
|
|
}
|
|
|
|
// ── c. entity / label / host counts ───────────────────────────────────
|
|
const counts = await page.evaluate((layerId) => {
|
|
const gev = window.__godsEyeView;
|
|
const v = gev.viewer;
|
|
const out = {
|
|
dataSources: {}, nativeLabels: 0, dynamicPositionEntities: 0,
|
|
referenceEntities: 0, hostEntries: 0, hostPainted: 0,
|
|
};
|
|
for (let i = 0; i < v.dataSources.length; i++) {
|
|
const ds = v.dataSources.get(i);
|
|
if (!/TeleGeography/.test(ds.name || '')) continue;
|
|
const list = ds.entities.values;
|
|
out.dataSources[ds.name] = list.length;
|
|
for (const e of list) {
|
|
if (e.label !== undefined) out.nativeLabels++;
|
|
if (e.position && e.position.isConstant === false) out.dynamicPositionEntities++;
|
|
if (e.polyline?.positions && e.polyline.positions.isConstant === false) {
|
|
out.dynamicPositionEntities++;
|
|
}
|
|
}
|
|
if (/References/.test(ds.name)) out.referenceEntities = list.length;
|
|
}
|
|
const diag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
|
|
out.hostEntries = diag.entriesBySource?.[layerId] || 0;
|
|
out.hostPainted = diag.paintedBySource?.[layerId] || 0;
|
|
const stats = gev.dataManager.layers.get(layerId)?.module?.getStats?.() || {};
|
|
out.referenceLabelCount = stats.referenceLabelCount ?? null;
|
|
return out;
|
|
}, LAYER_ID);
|
|
report(control ? 'counts (control, cables OFF)' : 'counts with cables ON', counts);
|
|
|
|
// ── a. per-frame scene.render cost over a ~10 s driven orbit ─────────
|
|
const frameCost = await page.evaluate(() => new Promise((resolve) => {
|
|
const v = window.__godsEyeView.viewer;
|
|
const scene = v.scene;
|
|
const durations = [];
|
|
const originalRender = scene.render;
|
|
scene.render = function patchedRender(...args) {
|
|
const started = performance.now();
|
|
const result = originalRender.apply(this, args);
|
|
durations.push(performance.now() - started);
|
|
return result;
|
|
};
|
|
const t0 = performance.now();
|
|
const tick = () => {
|
|
// Slow orbit: every frame is a camera change, so Cesium renders every
|
|
// rAF — the honest "user is interacting" cost of the layer.
|
|
v.camera.rotateRight(0.0004);
|
|
if (performance.now() - t0 < 10_000) requestAnimationFrame(tick);
|
|
else {
|
|
scene.render = originalRender;
|
|
durations.sort((a, b) => a - b);
|
|
const n = durations.length;
|
|
const sum = durations.reduce((s, d) => s + d, 0);
|
|
resolve({
|
|
frames: n,
|
|
meanMs: +(sum / Math.max(1, n)).toFixed(2),
|
|
p50Ms: +(durations[Math.floor(n * 0.5)] || 0).toFixed(2),
|
|
p95Ms: +(durations[Math.floor(n * 0.95)] || 0).toFixed(2),
|
|
maxMs: +(durations[n - 1] || 0).toFixed(2),
|
|
effectiveFps: +(n / 10).toFixed(1),
|
|
});
|
|
}
|
|
};
|
|
requestAnimationFrame(tick);
|
|
}));
|
|
report(control
|
|
? 'scene.render cost, control (cables OFF), 10s orbit'
|
|
: 'scene.render cost, cables ON, 10s orbit', frameCost);
|
|
|
|
// ── d. parked idle honesty (labels must not force continuous render) ──
|
|
await new Promise((r) => setTimeout(r, 4_000)); // orbit stop + fades settle
|
|
const idle = await page.evaluate(() => new Promise((resolve) => {
|
|
const scene = window.__godsEyeView.viewer.scene;
|
|
let renders = 0;
|
|
const remove = scene.postRender.addEventListener(() => { renders += 1; });
|
|
setTimeout(() => { remove(); resolve({ renders }); }, 5_000);
|
|
}));
|
|
report(control
|
|
? 'parked idle, control (postRender fires / 5s)'
|
|
: 'parked idle with cables ON (postRender fires / 5s)', idle);
|
|
|
|
if (!legacy && !control) {
|
|
// ── unified-build contract gates ────────────────────────────────────
|
|
check('zero native LabelGraphics on cable entities', counts.nativeLabels === 0, counts.nativeLabels);
|
|
check('zero per-frame CallbackProperty cable entities', counts.dynamicPositionEntities === 0, counts.dynamicPositionEntities);
|
|
check('host has cable entries', counts.hostEntries > 0, counts.hostEntries);
|
|
check('host painted cable labels', counts.hostPainted > 0, counts.hostPainted);
|
|
// The cables source never republishes an unchanged cohort (unit pin), and
|
|
// since the 2026-08-18 host fix the occluder observer is scoped (body
|
|
// childList filtered to inventory chrome; attribute observation
|
|
// element-only) and the right-rail allocator writes-if-changed — so a
|
|
// parked camera measures 0 postRender fires / 5 s with cables ON, equal
|
|
// to the empty-scene control (pre-fix: ~56-61). The headroom below covers
|
|
// an occasional genuine chrome transition (loading-chip flip + its 100 ms
|
|
// occluder-refresh echo) landing inside the window; ticking churn (~10)
|
|
// or the old observer leak (~56+) must fail.
|
|
check('parked idle stays near zero (≤6 / 5s)', idle.renders <= 6, idle);
|
|
|
|
// OFF must clear the host source (no orphan labels), ON must restore.
|
|
const cycle = await page.evaluate(async (layerId) => {
|
|
const gev = window.__godsEyeView;
|
|
await gev.dataManager.setEnabled(layerId, false, { origin: 'user' });
|
|
gev.viewer.scene.requestRender?.();
|
|
await new Promise((r) => setTimeout(r, 1_200));
|
|
const offDiag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
|
|
const offEntries = offDiag.entriesBySource?.[layerId] || 0;
|
|
const offPainted = offDiag.paintedBySource?.[layerId] || 0;
|
|
await gev.dataManager.setEnabled(layerId, true, { origin: 'user' });
|
|
const t0 = performance.now();
|
|
let onPainted = 0;
|
|
while (performance.now() - t0 < 20_000) {
|
|
const diag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
|
|
onPainted = diag.paintedBySource?.[layerId] || 0;
|
|
if (onPainted > 0) break;
|
|
gev.viewer.scene.requestRender?.();
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
}
|
|
return { offEntries, offPainted, onPainted };
|
|
}, LAYER_ID);
|
|
check('disable clears host entries (no orphans)', cycle.offEntries === 0 && cycle.offPainted === 0, cycle);
|
|
check('re-enable restores painted labels', cycle.onPainted > 0, cycle);
|
|
}
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
|
|
const passed = results.filter((r) => r.pass).length;
|
|
console.log(`\nqa-cables-overlay: ${passed}/${results.length} passed${legacy ? ' (legacy measurement mode)' : ''}${control ? ' (control mode)' : ''}`);
|
|
console.log(`RESULT: ${passed} passed, ${results.length - passed} failed, 0 skipped`);
|
|
process.exit(passed === results.length ? 0 : 1);
|