fix(layers): release bundled GeoJSON entities on toggle-off instead of hiding them

Toggling datacenters, dams, or submarine cables off only set `show = false`
on their data sources. Cesium's DataSourceDisplay still walks every
visualizer of a hidden source each frame, and the ~11.5k hidden entities
(hundreds of MB) stayed resident, so the whole scene rendered ~2x slower
for the rest of the session after a single on/off round trip (owner field
test 2026-09-13).

disable() now removes the data sources from the viewer and drops the built
records; the parsed dataset (NDJSON features / TeleGeography JSON) is cached
so enable() rebuilds without a refetch or re-parse. A disable that lands
while a load is in flight releases the finished build instead of parking it
hidden. destroy() clears the cache.

Measured with a headful Chrome harness (flights+military+satellites on,
camera rotating, 250-frame windows), before -> after:
- entities resident after OFF: 11,537 -> 0
- DataSourceDisplay.update per frame after OFF: 1.09 ms -> 0.02 ms
- scene.render per frame after OFF: 13.9 ms (control 6.6 ms) -> 5.2 ms
  (fresh baseline 5.0 ms in the same run)
- JS heap 30 s after OFF: 1.6 GB and climbing -> 342 MB after GC

Tests: the GeoJSON lifecycle and cables suites assert disable leaves zero
sources/contexts and re-enable rebuilds with fetch called once.
This commit is contained in:
Bilawal Sidhu committed 2026-09-13 17:08:23 -05:00
1 parent ca5f5fff93
commit 499e1168a7
4 files changed
+190 -32

No files matched your search

+42 -1
View File
@@ -743,7 +743,15 @@ test('an abort landing at the post-cable-GeoJson boundary stops the stale load b
env.layer.disable();
env.layer.enable(env.viewer);
await env.settle();
assert.equal(geojson.calls.length, 1, 'stale load A must not start the landing GeoJson build');
// A already cached the fetched JSON, so load B skips the network and goes
// straight to its own CABLE build; the only new GeoJson call must be that
// one — a landing build here would mean stale A ran on past its check.
assert.equal(env.pendingFetches.length, 0, 'load B rebuilds from the cached JSON without refetching');
assert.equal(geojson.calls.length, 2, 'stale load A must not start the landing GeoJson build');
assert.ok(
Cesium.Color.equals(geojson.calls[1][1].stroke, geojson.calls[0][1].stroke),
'the second GeoJson call is load B\'s cable build, not A\'s landing build',
);
env.releaseAll();
await env.settle();
@@ -1254,3 +1262,36 @@ test('an unchanged cohort never republishes, so a parked camera stays governor-i
env.layer.destroy(env.viewer);
});
test('a toggle-off releases every data source and a toggle-on rebuilds them without refetching', async () => {
const env = await createRealCableLayerHarness();
assert.equal(env.dataSources.length, 3, 'cable + landing + reference sources while on');
const before = env.layer.getStats().count;
assert.equal(before, 2);
// Any fetch after this point is a bug: the rebuild must come from the cache.
const originalFetch = globalThis.fetch;
let fetches = 0;
globalThis.fetch = async () => { fetches += 1; throw new Error('unexpected refetch'); };
try {
env.layer.disable();
assert.equal(env.dataSources.length, 0, 'hidden entities are not free; disable must remove the sources');
env.layer.enable(env.viewer);
const deadline = Date.now() + 2_000;
while (Date.now() < deadline && env.dataSources.length < 3) {
await new Promise((resolve) => setTimeout(resolve, 10));
}
assert.equal(fetches, 0, 'the rebuild must not touch the network');
assert.equal(env.layer.getStats().error, null);
assert.equal(env.dataSources.length, 3, 're-enable rebuilds the trio');
assert.equal(env.layer.getStats().count, before);
const referenceSource = env.dataSources.find((ds) => /References/.test(ds.name || ''));
assert.ok(referenceSource?.entities.values.length > 0, 'reference stems are rebuilt');
env.layer.destroy(env.viewer);
assert.equal(env.dataSources.length, 0);
} finally {
globalThis.fetch = originalFetch;
}
});