Report a panel's render errors in full and restart drawing

Cesium stops drawing after a render error and prints worker failures as
[object Object]. An inline panel now logs the error's details and starts
drawing again, up to three times.
This commit is contained in:
Sameh Khamis committed 2026-10-02 18:29:42 -07:00
1 parent 47742f335d
commit 5ad0dd0004
2 files changed
+82 -1

No files matched your search

+41 -1
View File
@@ -49,6 +49,19 @@ export function isEmbeddedInline() {
// frames; see keepPanelRendering.
const PANEL_FRAME_MS = 33;
const MISSED_FRAMES_MS = 250;
const RENDER_RESTARTS = 3;
const RENDER_RESTART_MS = 1000;
/** A render error's details, including the plain objects workers report. */
function describeError(error) {
if (error instanceof Error)
return `${error.name}: ${error.message}\n${error.stack}`;
try {
return JSON.stringify(error, Object.getOwnPropertyNames(error ?? {}));
} catch {
return String(error);
}
}
/**
* Keep the globe drawing in an inline panel. Some hosts report a panel on
@@ -67,14 +80,41 @@ export function keepPanelRendering(
frameRequest = windowRef.requestAnimationFrame(onFrame);
};
frameRequest = windowRef.requestAnimationFrame(onFrame);
// Cesium stops drawing after a render error. Report it in full, and
// start again a few times in case it does not repeat.
let failed = false;
let restarts = 0;
let restart = null;
const removeErrorListener = viewer.scene?.renderError?.addEventListener(
(_scene, error) => {
failed = true;
console.error(
"[God's Eye View panel] render error:",
describeError(error),
);
if (restarts >= RENDER_RESTARTS) return;
restarts += 1;
restart = windowRef.setTimeout(() => {
failed = false;
viewer.useDefaultRenderLoop = true;
}, RENDER_RESTART_MS);
},
);
const timer = windowRef.setInterval(() => {
if (now() - lastFrame < MISSED_FRAMES_MS || viewer.isDestroyed?.()) return;
if (
failed ||
now() - lastFrame < MISSED_FRAMES_MS ||
viewer.isDestroyed?.()
)
return;
viewer.resize();
viewer.render();
}, PANEL_FRAME_MS);
return () => {
windowRef.clearInterval(timer);
windowRef.clearTimeout?.(restart);
windowRef.cancelAnimationFrame(frameRequest);
removeErrorListener?.();
};
}
+41
View File
@@ -246,3 +246,44 @@ test('a panel keeps drawing from a timer while animation frames stop', () => {
assert.equal(tick, null);
assert.equal(frame, null);
});
test('a panel reports a render error in full and starts drawing again', () => {
let listener = null;
const timeouts = [];
const windowRef = {
requestAnimationFrame: () => 1,
cancelAnimationFrame() {},
setInterval: () => 1,
clearInterval() {},
setTimeout: (fn) => (timeouts.push(fn), timeouts.length),
clearTimeout() {},
};
const viewer = {
useDefaultRenderLoop: false,
scene: {
renderError: {
addEventListener: (fn) => ((listener = fn), () => (listener = null)),
},
},
};
const logged = [];
const original = console.error;
console.error = (...args) => logged.push(args.join(' '));
try {
const stop = keepPanelRendering(viewer, { windowRef, now: () => 0 });
for (let attempt = 0; attempt < 5; attempt++)
listener(viewer.scene, {
name: 'RuntimeError',
message: 'worker failed',
});
assert.match(logged[0], /render error: .*"message":"worker failed"/);
// Only a few restarts, so an error that repeats does not spin.
assert.equal(timeouts.length, 3);
timeouts[0]();
assert.equal(viewer.useDefaultRenderLoop, true);
stop();
assert.equal(listener, null);
} finally {
console.error = original;
}
});