mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
fix(vessels): pin the idle-render contract and move the entry to a fragment
- The trailing visibility frame uses the layer's injectable runtime timer and is cancelled by resetState as well as disable and destroy. - An empty-feed regular pass records the camera pose, so later in-cadence frames do not keep scheduling trailing frames. - New renderGovernor.test.mjs: a parked camera schedules nothing, a stopped camera gets exactly one trailing frame and no self-wake, a forced pass requests its frame, destroy cancels the timer. - The CHANGELOG entry moves to changelog.d/871-vessels-idle-render.md; docs say the hold also covers vessels dimmed under a live target.
This commit is contained in:
1 parent
87133e8dbf
commit
e6d2babea0
6 files changed
+175
-5
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
- Let the scene idle with AIS vessels enabled. Vessels no longer hold the
|
||||||
|
continuous render loop; they request frames for new data and selection, one
|
||||||
|
trailing frame after the camera stops, and hold the loop only while focus
|
||||||
|
emphasis animates or a vessel stays dimmed under a tracked target. A parked
|
||||||
|
vessels-only scene went from continuous rendering to 0 frames in 5 s. While
|
||||||
|
anything is tracked, the vessel focus pass skips hidden sprites at rest:
|
||||||
|
about 4 ms to 1.2 ms per pass over 12,000 vessels (waar19, #871).
|
||||||
@@ -2262,7 +2262,10 @@ fetching, trailing-24-hour filtering and partial-success caching are unchanged.
|
|||||||
> only when a poll lands, so the layer requests a frame for every forced pass
|
> only when a poll lands, so the layer requests a frame for every forced pass
|
||||||
> (new data, selection), leaves one trailing frame when the camera moved inside
|
> (new data, selection), leaves one trailing frame when the camera moved inside
|
||||||
> the 800 ms visibility cadence, and holds `ais-vessels` only while focus
|
> the 800 ms visibility cadence, and holds `ais-vessels` only while focus
|
||||||
> emphasis is animating. The 80 ms focus pass also skips hidden sprites whose
|
> emphasis animates or any vessel stays dimmed under a live focus target. The
|
||||||
|
> trailing frame goes through the layer's injectable runtime clock and is
|
||||||
|
> cancelled on disable, destroy and reset (`renderGovernor.test.mjs` pins the
|
||||||
|
> no-self-wake invariant). The 80 ms focus pass also skips hidden sprites whose
|
||||||
> focus state is at rest (`spriteFocusAtRest`), since advancing them changes
|
> focus state is at rest (`spriteFocusAtRest`), since advancing them changes
|
||||||
> nothing; with a 12k worldwide feed that is most of the fleet.
|
> nothing; with a 12k worldwide feed that is most of the fleet.
|
||||||
> The scope's OUTSIDE terminus is **altitude-adaptive** (2026-08-17): 0.94
|
> The scope's OUTSIDE terminus is **altitude-adaptive** (2026-08-17): 0.94
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ export function createLifecycle({
|
|||||||
state.interactionHandlerFactory = null;
|
state.interactionHandlerFactory = null;
|
||||||
state.interactionKeyTarget = null;
|
state.interactionKeyTarget = null;
|
||||||
state.preRenderRemover = null;
|
state.preRenderRemover = null;
|
||||||
state.trailingVisibilityTimer = null;
|
components.rendering.cancelTrailingVisibilityPass();
|
||||||
state.lastVisibilityUpdate = 0;
|
state.lastVisibilityUpdate = 0;
|
||||||
state.lastFocusUpdate = 0;
|
state.lastFocusUpdate = 0;
|
||||||
state.activeFocusCount = 0;
|
state.activeFocusCount = 0;
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createVesselLayer } from './index.js';
|
||||||
|
import { VISIBILITY_UPDATE_MS } from './policy.js';
|
||||||
|
|
||||||
|
// Vessels move only when a poll lands, so the layer must let the scene idle:
|
||||||
|
// it asks for frames for forced passes and one trailing frame after the camera
|
||||||
|
// stops, and never schedules a frame from a pass the camera did not cause.
|
||||||
|
|
||||||
|
const noop = () => {};
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
let clock = 10_000;
|
||||||
|
const requests = [];
|
||||||
|
const holds = new Set();
|
||||||
|
const timers = new Map();
|
||||||
|
let nextTimer = 1;
|
||||||
|
const layer = createVesselLayer({
|
||||||
|
source: { getSnapshot: async () => ({ records: [] }) },
|
||||||
|
services: {
|
||||||
|
context: {
|
||||||
|
clearSelectedEntityContextForLayer: noop,
|
||||||
|
registerEntityContext: () => null,
|
||||||
|
},
|
||||||
|
trails: {
|
||||||
|
createTrail: () => ({ setPositions: noop, clear: noop, destroy: noop }),
|
||||||
|
},
|
||||||
|
labels: {},
|
||||||
|
picking: { unregisterPickOwner: noop },
|
||||||
|
overlay: {
|
||||||
|
setOverlayEntries: noop,
|
||||||
|
setOverlaySourceVisible: noop,
|
||||||
|
clearOverlaySource: noop,
|
||||||
|
},
|
||||||
|
geoid: {},
|
||||||
|
sprites: {},
|
||||||
|
focus: {
|
||||||
|
focusNowMs: () => clock,
|
||||||
|
getFocusTarget: () => null,
|
||||||
|
focusPassIsNeeded: () => false,
|
||||||
|
forgetSpriteFocus: noop,
|
||||||
|
},
|
||||||
|
worldFocus: {},
|
||||||
|
render: {
|
||||||
|
holdContinuousRender: (id) => holds.add(id),
|
||||||
|
releaseContinuousRender: (id) => holds.delete(id),
|
||||||
|
governorRequestRender: (reason) => requests.push(reason),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const camera = {
|
||||||
|
positionWC: { x: 0, y: 0, z: 7_000_000 },
|
||||||
|
heading: 0,
|
||||||
|
pitch: -1.2,
|
||||||
|
roll: 0,
|
||||||
|
};
|
||||||
|
layer.testing._setVesselStateForTest({ viewer: { camera, scene: {} } });
|
||||||
|
layer.testing._setAisRuntimeForTest({
|
||||||
|
now: () => clock,
|
||||||
|
setTimeout(fn, ms) {
|
||||||
|
const id = nextTimer++;
|
||||||
|
timers.set(id, { fn, at: clock + ms });
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
clearTimeout: (id) => timers.delete(id),
|
||||||
|
});
|
||||||
|
const advance = (ms) => {
|
||||||
|
clock += ms;
|
||||||
|
for (const [id, timer] of [...timers]) {
|
||||||
|
if (timer.at > clock) continue;
|
||||||
|
timers.delete(id);
|
||||||
|
timer.fn();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const frame = () => layer.testing._runVisibilityPassForTest();
|
||||||
|
return { layer, camera, requests, holds, timers, advance, frame };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('a parked camera schedules no frames after the cadence pass', () => {
|
||||||
|
const { requests, holds, timers, advance, frame } = setup();
|
||||||
|
frame();
|
||||||
|
for (let i = 0; i < 20; i += 1) {
|
||||||
|
advance(100);
|
||||||
|
frame();
|
||||||
|
}
|
||||||
|
assert.deepEqual(requests, []);
|
||||||
|
assert.equal(timers.size, 0);
|
||||||
|
assert.equal(holds.has('ais-vessels'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a camera that stops inside the cadence gets exactly one trailing frame', () => {
|
||||||
|
const { layer, camera, requests, timers, advance, frame } = setup();
|
||||||
|
frame();
|
||||||
|
advance(100);
|
||||||
|
camera.heading = 0.5;
|
||||||
|
frame();
|
||||||
|
advance(100);
|
||||||
|
frame();
|
||||||
|
assert.equal(timers.size, 1, 'one trailing frame, not one per skipped pass');
|
||||||
|
assert.equal(layer.testing._hasTrailingVisibilityPassForTest(), true);
|
||||||
|
|
||||||
|
advance(VISIBILITY_UPDATE_MS);
|
||||||
|
assert.deepEqual(requests, ['ais-vessels-visibility']);
|
||||||
|
// The trailing frame runs the regular pass on the pose the camera stopped
|
||||||
|
// on. Its own frames must not schedule another one.
|
||||||
|
frame();
|
||||||
|
for (let i = 0; i < 20; i += 1) {
|
||||||
|
advance(100);
|
||||||
|
frame();
|
||||||
|
}
|
||||||
|
assert.deepEqual(requests, ['ais-vessels-visibility']);
|
||||||
|
assert.equal(timers.size, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a forced pass requests its own frame', () => {
|
||||||
|
const { layer, requests } = setup();
|
||||||
|
layer.testing._runVisibilityPassForTest(true);
|
||||||
|
assert.deepEqual(requests, ['ais-vessels-update']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy cancels a pending trailing frame', (t) => {
|
||||||
|
const originalDocument = globalThis.document;
|
||||||
|
globalThis.document = { getElementById: () => null };
|
||||||
|
t.after(() => {
|
||||||
|
if (originalDocument === undefined) delete globalThis.document;
|
||||||
|
else globalThis.document = originalDocument;
|
||||||
|
});
|
||||||
|
const { layer, camera, requests, timers, advance, frame } = setup();
|
||||||
|
frame();
|
||||||
|
advance(100);
|
||||||
|
camera.heading = 0.5;
|
||||||
|
frame();
|
||||||
|
assert.equal(timers.size, 1);
|
||||||
|
layer.destroy();
|
||||||
|
assert.equal(timers.size, 0);
|
||||||
|
advance(VISIBILITY_UPDATE_MS);
|
||||||
|
assert.deepEqual(requests, []);
|
||||||
|
});
|
||||||
@@ -223,7 +223,7 @@ export function createRendering({
|
|||||||
0,
|
0,
|
||||||
VISIBILITY_UPDATE_MS - (now - state.lastVisibilityUpdate),
|
VISIBILITY_UPDATE_MS - (now - state.lastVisibilityUpdate),
|
||||||
);
|
);
|
||||||
state.trailingVisibilityTimer = setTimeout(() => {
|
state.trailingVisibilityTimer = vesselState._aisRuntime.setTimeout(() => {
|
||||||
state.trailingVisibilityTimer = null;
|
state.trailingVisibilityTimer = null;
|
||||||
if (state.feed.enabled) governorRequestRender('ais-vessels-visibility');
|
if (state.feed.enabled) governorRequestRender('ais-vessels-visibility');
|
||||||
}, waitMs);
|
}, waitMs);
|
||||||
@@ -231,7 +231,7 @@ export function createRendering({
|
|||||||
|
|
||||||
function cancelTrailingVisibilityPass() {
|
function cancelTrailingVisibilityPass() {
|
||||||
if (!state.trailingVisibilityTimer) return;
|
if (!state.trailingVisibilityTimer) return;
|
||||||
clearTimeout(state.trailingVisibilityTimer);
|
vesselState._aisRuntime.clearTimeout(state.trailingVisibilityTimer);
|
||||||
state.trailingVisibilityTimer = null;
|
state.trailingVisibilityTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +256,15 @@ export function createRendering({
|
|||||||
if (focusPass) state.lastFocusUpdate = now;
|
if (focusPass) state.lastFocusUpdate = now;
|
||||||
if (!state.records.all.length) {
|
if (!state.records.all.length) {
|
||||||
// No records — flush any lingering card entries (vanished-feed case).
|
// No records — flush any lingering card entries (vanished-feed case).
|
||||||
if (regularPass) updateClusteredLabels([]);
|
// Still record the pose this pass evaluated, or every later frame that
|
||||||
|
// falls inside the cadence would schedule another trailing frame.
|
||||||
|
if (regularPass) {
|
||||||
|
if (state.viewer?.camera)
|
||||||
|
vesselState._lastCamPoseSig = cameraPoseSignature(
|
||||||
|
state.viewer.camera,
|
||||||
|
);
|
||||||
|
updateClusteredLabels([]);
|
||||||
|
}
|
||||||
if (focusPass) {
|
if (focusPass) {
|
||||||
state.activeFocusCount = 0;
|
state.activeFocusCount = 0;
|
||||||
releaseContinuousRender('ais-vessels');
|
releaseContinuousRender('ais-vessels');
|
||||||
|
|||||||
@@ -151,6 +151,18 @@ export function createTesting({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Run one preRender visibility/focus pass. Test-only seam. */
|
||||||
|
|
||||||
|
function _runVisibilityPassForTest(force = false) {
|
||||||
|
components.rendering.updateVisibility(force);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True while a trailing visibility frame is scheduled. Test-only seam. */
|
||||||
|
|
||||||
|
function _hasTrailingVisibilityPassForTest() {
|
||||||
|
return state.trailingVisibilityTimer !== null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read lifecycle ownership state without exposing the mutable state object.
|
* Read lifecycle ownership state without exposing the mutable state object.
|
||||||
* Test-only seam.
|
* Test-only seam.
|
||||||
@@ -176,5 +188,7 @@ export function createTesting({
|
|||||||
_setAisRuntimeForTest,
|
_setAisRuntimeForTest,
|
||||||
_getVesselFeedStateForTest,
|
_getVesselFeedStateForTest,
|
||||||
_getVesselStateForTest,
|
_getVesselStateForTest,
|
||||||
|
_runVisibilityPassForTest,
|
||||||
|
_hasTrailingVisibilityPassForTest,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user