perf(weather): wake a staged frame on a 150 ms timer, not every render (#1345)

Wake staged weather imagery on a bounded timer instead of requesting every render.

Contribution by @nu-stats.
This commit is contained in:
nu-stats authored and GitHub committed 2026-10-25 19:07:12 -07:00
1 parent 2b2dad5bbe
commit 06c51d3438
4 files changed
+71 -3

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
- Wake staged weather imagery on a 150 ms timer while waiting for readiness,
avoiding a continuous render loop and cancelling the wake when work ends
(@nu-stats, #1345).
+4
View File
@@ -306,6 +306,10 @@ catalog construction owns each instance.
Wind animates one forecast without advancing forecast time. Separate Weather Wind animates one forecast without advancing forecast time. Separate Weather
observation layers provide radar and satellite history; none claims measured cloud volume. observation layers provide radar and satellite history; none claims measured cloud volume.
Weather imagery releases its camera move listener on disable and registers it once on re-enable. Disabling also clears active imagery work. Weather imagery releases its camera move listener on disable and registers it once on re-enable. Disabling also clears active imagery work.
A staged weather frame with loaded tiles and no pending requests rechecks
readiness on a single 150 ms wake timer instead of requesting every render.
The existing readiness conditions and two settled frames still govern promotion;
completion, cancellation, replacement and teardown clear the wake timer.
Weather imagery drapes onto the globe basemap. On photorealistic 3D Tiles each Weather imagery drapes onto the globe basemap. On photorealistic 3D Tiles each
observed product is instead a raised, translucent shell: one rectangle over the observed product is instead a raised, translucent shell: one rectangle over the
product bounds at a fixed height (global infrared 5.5 km, regional infrared product bounds at a fixed height (global infrared 5.5 km, regional infrared
+19 -2
View File
@@ -14,6 +14,8 @@ const GLOBAL_TILE_MAXIMUM_LEVEL = 3;
// Six decoded 2048×1024 canvases (8 MiB each) bound the cache below 50 MiB. // Six decoded 2048×1024 canvases (8 MiB each) bound the cache below 50 MiB.
const MAX_MOSAICS = 6; const MAX_MOSAICS = 6;
const MAX_PREFETCH_TILES = 8; const MAX_PREFETCH_TILES = 8;
// How often a staged frame waiting out its quiet interval rechecks readiness.
export const READY_WAKE_MS = 150;
/** Globe imagery: own at most a displayed and a staging frame. Use native Cesium /** Globe imagery: own at most a displayed and a staging frame. Use native Cesium
* tile scheduling, projection and texture disposal; the application clock is never touched. */ * tile scheduling, projection and texture disposal; the application clock is never touched. */
@@ -126,6 +128,7 @@ function createGlobeRendering({
frame.offRender?.(); frame.offRender?.();
frame.offCamera?.(); frame.offCamera?.();
clearTimeout(frame.timeout); clearTimeout(frame.timeout);
clearTimeout(frame.wake);
for (const request of frame.requests) request.cancel?.(); for (const request of frame.requests) request.cancel?.();
frame.requests.clear(); frame.requests.clear();
frame.retries.clear(); frame.retries.clear();
@@ -277,6 +280,8 @@ function createGlobeRendering({
frame.offCamera?.(); frame.offCamera?.();
frame.offCamera = null; frame.offCamera = null;
clearTimeout(frame.timeout); clearTimeout(frame.timeout);
clearTimeout(frame.wake);
frame.wake = null;
frame.offAbort?.(); frame.offAbort?.();
if (ok) { if (ok) {
lastError = null; lastError = null;
@@ -450,8 +455,20 @@ function createGlobeRendering({
if (++settled >= 2) finish(true); if (++settled >= 2) finish(true);
else viewer.scene.requestRender(); else viewer.scene.requestRender();
} else settled = 0; } else settled = 0;
if (incoming === frame && frame.pending === 0 && frame.loaded > 0) // Waiting out the quiet interval needs another readiness check
viewer.scene.requestRender(); // later, not a render every frame: wake on a timer so the render
// governor's frame cap still holds while a frame is staged.
if (
incoming === frame &&
frame.pending === 0 &&
frame.loaded > 0 &&
!frame.wake
)
frame.wake = setTimeout(() => {
frame.wake = null;
if (!frame.closed && incoming === frame)
viewer.scene.requestRender();
}, READY_WAKE_MS);
}); });
viewer.scene.requestRender(); viewer.scene.requestRender();
onChange(); onChange();
+45 -1
View File
@@ -1,6 +1,6 @@
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import { createWeatherRendering } from './rendering.js'; import { createWeatherRendering, READY_WAKE_MS } from './rendering.js';
import { createWeatherLayer } from './index.js'; import { createWeatherLayer } from './index.js';
import { createWeatherClock } from './clock.js'; import { createWeatherClock } from './clock.js';
import * as Cesium from 'cesium'; import * as Cesium from 'cesium';
@@ -242,6 +242,50 @@ test('a ready weather frame does not wait for unrelated terrain, but requires su
h.rendering.clear(); h.rendering.clear();
}); });
test('a staged frame waiting out its quiet interval wakes on a timer, not every render', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
let now = 0;
const h = renderingHarness({ now: () => now });
h.viewer.scene.globe.tilesLoaded = false;
const loaded = h.rendering.setFrame(snapshot, times[0]);
const tile = deferred();
h.providers[0].response = tile;
const request = h.providers[0].requestImage(0, 0, 0, {});
tile.resolve({});
await request;
const before = h.renders();
for (let i = 0; i < 10; i++) h.postRender.emit();
assert.equal(
h.renders(),
before,
'renders while waiting do not request further renders',
);
t.mock.timers.tick(READY_WAKE_MS - 1);
assert.equal(h.renders(), before);
t.mock.timers.tick(1);
assert.equal(h.renders(), before + 1, 'one wake per interval');
now = 200;
h.postRender.emit();
h.postRender.emit();
assert.equal(await loaded, true);
const settledRenders = h.renders();
t.mock.timers.tick(READY_WAKE_MS * 4);
assert.equal(h.renders(), settledRenders, 'commit clears the wake timer');
const abandoned = h.rendering.setFrame(snapshot, times[1]);
const second = deferred();
h.providers.at(-1).response = second;
const secondRequest = h.providers.at(-1).requestImage(0, 0, 0, {});
second.resolve({});
await secondRequest;
h.postRender.emit();
h.rendering.clear();
assert.equal(await abandoned, false);
const clearedRenders = h.renders();
t.mock.timers.tick(READY_WAKE_MS * 4);
assert.equal(h.renders(), clearedRenders, 'clear cancels the wake timer');
});
test('camera movement drops abandoned deferred tiles but still waits for admitted requests', async () => { test('camera movement drops abandoned deferred tiles but still waits for admitted requests', async () => {
let now = 0; let now = 0;
const h = renderingHarness({ now: () => now }); const h = renderingHarness({ now: () => now });