From 5ad0dd0004f1b9121774b8d155541e3b37c7ccbf Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Fri, 2 Oct 2026 14:35:33 -0700 Subject: [PATCH] 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. --- src/app/embed.js | 42 +++++++++++++++++++++++++++++++++++++++++- src/app/embed.test.mjs | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/src/app/embed.js b/src/app/embed.js index 4ef83f7fc..0b2fd81a7 100644 --- a/src/app/embed.js +++ b/src/app/embed.js @@ -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?.(); }; } diff --git a/src/app/embed.test.mjs b/src/app/embed.test.mjs index 3010fdb56..f3792e448 100644 --- a/src/app/embed.test.mjs +++ b/src/app/embed.test.mjs @@ -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; + } +});