mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
fix(perf): free a turned-off layer's entities that Cesium keeps (#1367)
Release stale Cesium command references after disabling a layer while preserving current-frame commands. Contribution by @quanticsoul4772.
This commit is contained in:
1 parent
405c7f0d20
commit
69fba2ebbe
6 files changed
+237
-1
No files matched your search
@@ -0,0 +1,4 @@
|
||||
- Turning a layer off frees the memory its entities held. Cesium kept
|
||||
removed layers' draw commands in reused per-pass arrays, so Data Centers
|
||||
kept 116 MiB after it was switched off, submarine cables 74 MiB and dams
|
||||
30 MiB; they now keep 13, 9 and 9 MiB (quanticsoul4772, #1367).
|
||||
@@ -2159,6 +2159,16 @@ fetching, trailing-24-hour filtering and partial-success caching are unchanged.
|
||||
> always exits non-zero, `1` meaning the control is healthy and `2` meaning it
|
||||
> is not. Plus the unit pins above.
|
||||
|
||||
> **2026-10-24 — a turned-off layer's memory is released:** Cesium 1.138
|
||||
> keeps the draw commands past each frame's count in the opaque and other
|
||||
> per-pass arrays (`View.frustumCommandsList`) and in the offscreen pick view,
|
||||
> so a turned-off layer's entities stayed reachable (Data Centers kept 116
|
||||
> MiB). `src/sceneCommandRelease.js`, installed in `src/app/tools.js`,
|
||||
> requests one frame through `governorRequestRender` after every layer
|
||||
> disable and then trims those arrays. It reads private Cesium fields;
|
||||
> `src/sceneCommandRelease.test.mjs` pins them and fails if Cesium renames
|
||||
> them or changes which passes it truncates.
|
||||
|
||||
> **2026-08-08 — performance waves 1+2:** the app idles via an explicit render
|
||||
> governor (`src/renderGovernor.js` — hold/release from every per-frame
|
||||
> animator; discrete mutators call `governorRequestRender`). Any NEW
|
||||
|
||||
@@ -2574,7 +2574,8 @@
|
||||
"src/voice/geminiSession.js",
|
||||
"src/voice/geminiAudio.js",
|
||||
"src/voice/geminiCaptureWorklet.js",
|
||||
"src/ui/statusBadge.js"
|
||||
"src/ui/statusBadge.js",
|
||||
"src/sceneCommandRelease.js"
|
||||
],
|
||||
"external": [
|
||||
"hls.js",
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createRecentImageryPanel } from '../ui/recentImagery.js';
|
||||
import { initGevVoiceCommands } from '../voice/gevRealtime.js';
|
||||
import { installViews, isEmbeddedInline } from './embed.js';
|
||||
import { installScopeMask, destroyScopeMask } from '../scopeMask.js';
|
||||
import { installLayerReleaseTrim } from '../sceneCommandRelease.js';
|
||||
import {
|
||||
installRenderGovernor,
|
||||
getRenderGovernorDiagnostics,
|
||||
@@ -110,6 +111,15 @@ export function createApplicationTools({
|
||||
// nothing animates per frame. Installed AFTER every module above has had
|
||||
// its chance to register pre-install holds. (perf wave 2)
|
||||
installRenderGovernor(viewer);
|
||||
// Cesium keeps a turned-off layer's draw commands (and so all its entities)
|
||||
// alive in reused per-pass arrays; trim them after each disable.
|
||||
defer(
|
||||
installLayerReleaseTrim({
|
||||
scene: viewer.scene,
|
||||
dataManager,
|
||||
requestRender: governorRequestRender,
|
||||
}),
|
||||
);
|
||||
|
||||
// Install the explicit scope mask used by the DISPLAY controls.
|
||||
installScopeMask(viewer);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Let a turned-off layer's memory go.
|
||||
*
|
||||
* Cesium (engine 22.3, `Scene/View.js`) reuses each view's per-pass command
|
||||
* arrays: every frame resets `frustumCommands.indices[pass]` to 0 and
|
||||
* overwrites `commands[pass][i]` from the start. `Scene.js` truncates the
|
||||
* voxel, Gaussian-splat and translucent passes to their counts; the opaque
|
||||
* and other passes keep the slots past the new count. When a layer is turned
|
||||
* off the next frame has fewer commands, so that tail keeps the destroyed
|
||||
* primitives' DrawCommands, and through `_owner` → primitive → entity →
|
||||
* EntityCollection the whole data source stays reachable. The offscreen view
|
||||
* `scene.sampleHeight` / `pickFromRay` render into keeps its last list the
|
||||
* same way, until the next height sample.
|
||||
*
|
||||
* Measured (production build, heap after GC, Data Centers on → off): 116 MiB
|
||||
* kept on main, 13 MiB with this module; heap snapshots traced the entities
|
||||
* through `View.frustumCommandsList` and `Picking._pickOffscreenView`.
|
||||
*
|
||||
* After a layer finishes turning off, this renders one frame, trims the main
|
||||
* view's arrays to that frame's counts, and empties the offscreen pick view's
|
||||
* lists (it rebuilds them in full on every pick). It reads private Cesium
|
||||
* fields: `src/sceneCommandRelease.test.mjs` fails if any of them is renamed
|
||||
* or if Cesium changes which passes it truncates, and once Cesium clears the
|
||||
* tail itself this module can go.
|
||||
*/
|
||||
|
||||
/** Drop command references a view no longer draws. */
|
||||
function trimView(view, { keepCurrent }) {
|
||||
for (const frustum of view?.frustumCommandsList || []) {
|
||||
const { commands, indices } = frustum;
|
||||
for (let pass = 0; pass < commands.length; pass += 1) {
|
||||
const keep = keepCurrent ? indices[pass] : 0;
|
||||
if (commands[pass].length > keep) commands[pass].length = keep;
|
||||
if (!keepCurrent) indices[pass] = 0;
|
||||
}
|
||||
}
|
||||
if (!keepCurrent)
|
||||
for (const extent of view?._commandExtents || [])
|
||||
extent.command = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release stale command references in the scene's main view (keeping what
|
||||
* the last frame drew) and in its offscreen pick view (all of it).
|
||||
* @param {object} scene Cesium Scene, right after a render.
|
||||
*/
|
||||
export function trimStaleSceneCommands(scene) {
|
||||
trimView(scene._defaultView, { keepCurrent: true });
|
||||
trimView(scene._picking?._pickOffscreenView, { keepCurrent: false });
|
||||
}
|
||||
|
||||
/**
|
||||
* After every layer disable, render one frame and trim once it is drawn.
|
||||
* @param {{scene: object, dataManager: object, requestRender: (reason: string) => void}} deps
|
||||
* @returns {() => void} uninstall
|
||||
*/
|
||||
export function installLayerReleaseTrim({ scene, dataManager, requestRender }) {
|
||||
let removeOneShot = null;
|
||||
const unsubscribe = dataManager.subscribe((change) => {
|
||||
if (change?.type !== 'visibility' || change.enabled !== false) return;
|
||||
if (removeOneShot) return; // a trim is already waiting for its frame
|
||||
removeOneShot = scene.postRender.addEventListener(() => {
|
||||
removeOneShot();
|
||||
removeOneShot = null;
|
||||
trimStaleSceneCommands(scene);
|
||||
});
|
||||
requestRender('layer-release-trim');
|
||||
});
|
||||
return () => {
|
||||
unsubscribe();
|
||||
removeOneShot?.();
|
||||
removeOneShot = null;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
// A turned-off layer's draw commands must not stay reachable through Cesium's
|
||||
// reused per-pass command arrays (src/sceneCommandRelease.js).
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { createRequire } from 'node:module';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import {
|
||||
installLayerReleaseTrim,
|
||||
trimStaleSceneCommands,
|
||||
} from './sceneCommandRelease.js';
|
||||
|
||||
const frustum = (lists, counts) => ({
|
||||
commands: lists.map((list) => [...list]),
|
||||
indices: [...counts],
|
||||
});
|
||||
|
||||
test('the main view keeps what the last frame drew and drops the stale tail', () => {
|
||||
const view = {
|
||||
frustumCommandsList: [frustum([['a', 'b', 'stale'], ['c'], []], [2, 1, 0])],
|
||||
};
|
||||
trimStaleSceneCommands({ _defaultView: view });
|
||||
assert.deepEqual(view.frustumCommandsList[0].commands, [
|
||||
['a', 'b'],
|
||||
['c'],
|
||||
[],
|
||||
]);
|
||||
assert.deepEqual(view.frustumCommandsList[0].indices, [2, 1, 0]);
|
||||
});
|
||||
|
||||
test('the offscreen pick view is emptied, since every pick rebuilds it', () => {
|
||||
const extents = [{ command: 'x' }, { command: 'y' }];
|
||||
const pick = {
|
||||
frustumCommandsList: [frustum([['p', 'q'], ['r']], [2, 1])],
|
||||
_commandExtents: extents,
|
||||
};
|
||||
trimStaleSceneCommands({
|
||||
_defaultView: { frustumCommandsList: [] },
|
||||
_picking: { _pickOffscreenView: pick },
|
||||
});
|
||||
assert.deepEqual(pick.frustumCommandsList[0].commands, [[], []]);
|
||||
assert.deepEqual(pick.frustumCommandsList[0].indices, [0, 0]);
|
||||
assert.deepEqual(
|
||||
extents.map((e) => e.command),
|
||||
[undefined, undefined],
|
||||
);
|
||||
});
|
||||
|
||||
test('a layer disable renders one frame and trims after it', () => {
|
||||
const listeners = new Set();
|
||||
const postRender = new Set();
|
||||
const requests = [];
|
||||
const view = { frustumCommandsList: [frustum([['a', 'stale']], [1])] };
|
||||
const scene = {
|
||||
_defaultView: view,
|
||||
postRender: {
|
||||
addEventListener(fn) {
|
||||
postRender.add(fn);
|
||||
return () => postRender.delete(fn);
|
||||
},
|
||||
},
|
||||
};
|
||||
const uninstall = installLayerReleaseTrim({
|
||||
scene,
|
||||
dataManager: {
|
||||
subscribe(fn) {
|
||||
listeners.add(fn);
|
||||
return () => listeners.delete(fn);
|
||||
},
|
||||
},
|
||||
requestRender: (reason) => requests.push(reason),
|
||||
});
|
||||
const emit = (change) => listeners.forEach((fn) => fn(change));
|
||||
|
||||
emit({ type: 'visibility', layerId: 'x', enabled: true });
|
||||
assert.equal(requests.length, 0, 'enabling trims nothing');
|
||||
emit({ type: 'visibility', layerId: 'x', enabled: false });
|
||||
emit({ type: 'visibility', layerId: 'y', enabled: false });
|
||||
assert.deepEqual(
|
||||
requests,
|
||||
['layer-release-trim'],
|
||||
'one frame for back-to-back disables',
|
||||
);
|
||||
assert.equal(
|
||||
view.frustumCommandsList[0].commands[0].length,
|
||||
2,
|
||||
'not before the frame',
|
||||
);
|
||||
postRender.forEach((fn) => fn());
|
||||
assert.deepEqual(view.frustumCommandsList[0].commands, [['a']]);
|
||||
assert.equal(postRender.size, 0, 'the frame listener is one-shot');
|
||||
uninstall();
|
||||
assert.equal(listeners.size, 0);
|
||||
});
|
||||
|
||||
// The workaround reads private Cesium fields. Pin the shape it relies on, and
|
||||
// fail when Cesium starts clearing the tail itself so the module can go.
|
||||
test('Cesium still reuses per-pass command arrays without clearing their tail', () => {
|
||||
const require = createRequire(import.meta.url);
|
||||
const enginePkg = require.resolve('@cesium/engine/package.json');
|
||||
const read = (rel) =>
|
||||
readFileSync(new URL(rel, pathToFileURL(enginePkg)), 'utf8');
|
||||
const scene = read('./Source/Scene/Scene.js');
|
||||
const view = read('./Source/Scene/View.js');
|
||||
const frustumCommands = read('./Source/Scene/FrustumCommands.js');
|
||||
const picking = read('./Source/Scene/Picking.js');
|
||||
// Every private field the trim reads. A rename would otherwise turn the
|
||||
// trim into a silent no-op (it reaches them through `?.` and `|| []`).
|
||||
assert.match(scene, /this\._picking = new Picking\(this\);/);
|
||||
assert.match(scene, /this\._defaultView = new View\(/);
|
||||
assert.match(picking, /this\._pickOffscreenView = new View\(/);
|
||||
assert.match(view, /this\.frustumCommandsList = \[\];/);
|
||||
assert.match(view, /frustumCommandsList\[n\]\.indices\[p\] = 0;/);
|
||||
assert.match(view, /this\._commandExtents = \[\];/);
|
||||
assert.match(
|
||||
frustumCommands,
|
||||
/this\.commands = commands;\s*this\.indices = indices;/,
|
||||
);
|
||||
// Cesium truncates only the voxel, Gaussian-splat and translucent passes,
|
||||
// each in its own draw step; the opaque and other passes keep their tail.
|
||||
// A change here means Cesium's handling moved: re-check whether this
|
||||
// module is still needed.
|
||||
assert.doesNotMatch(
|
||||
view,
|
||||
/\.length\s*=\s*[^;]*indices\[/,
|
||||
'View.js now truncates the per-pass arrays: remove src/sceneCommandRelease.js',
|
||||
);
|
||||
assert.deepEqual(
|
||||
[
|
||||
...scene.matchAll(
|
||||
/commands\.length = frustumCommands\.indices\[Pass\.(\w+)\];/g,
|
||||
),
|
||||
].map((match) => match[1]),
|
||||
['VOXELS', 'GAUSSIAN_SPLATS', 'TRANSLUCENT'],
|
||||
'Scene.js truncates a different set of passes: re-check src/sceneCommandRelease.js',
|
||||
);
|
||||
});
|
||||
Reference in new issue
Block a user