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
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.
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
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
+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.
const MAX_MOSAICS = 6;
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
* tile scheduling, projection and texture disposal; the application clock is never touched. */
@@ -126,6 +128,7 @@ function createGlobeRendering({
frame.offRender?.();
frame.offCamera?.();
clearTimeout(frame.timeout);
clearTimeout(frame.wake);
for (const request of frame.requests) request.cancel?.();
frame.requests.clear();
frame.retries.clear();
@@ -277,6 +280,8 @@ function createGlobeRendering({
frame.offCamera?.();
frame.offCamera = null;
clearTimeout(frame.timeout);
clearTimeout(frame.wake);
frame.wake = null;
frame.offAbort?.();
if (ok) {
lastError = null;
@@ -450,8 +455,20 @@ function createGlobeRendering({
if (++settled >= 2) finish(true);
else viewer.scene.requestRender();
} else settled = 0;
if (incoming === frame && frame.pending === 0 && frame.loaded > 0)
viewer.scene.requestRender();
// Waiting out the quiet interval needs another readiness check
// 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();
onChange();
+45 -1
View File
@@ -1,6 +1,6 @@
import test from 'node:test';
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 { createWeatherClock } from './clock.js';
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();
});
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 () => {
let now = 0;
const h = renderingHarness({ now: () => now });