mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-08 12:23:59 +00:00
Release Navara overlay targets and sprite images on repeated globe switches
This commit is contained in:
1 parent
160e445677
commit
14f7b32537
4 files changed
+118
-1
No files matched your search
@@ -4,6 +4,7 @@ import { stations, installRadioFixture } from './fixtures/w2/radio.mjs';
|
||||
import { blockPaidQaMaps } from './qa-map-policy.mjs';
|
||||
import { switchGlobe } from './qa-switch-globe.mjs';
|
||||
const url = new URL(process.argv[2]);
|
||||
const soak = process.argv.includes('--soak');
|
||||
if (
|
||||
!['localhost', '127.0.0.1'].includes(url.hostname) ||
|
||||
['', '4173', '4185', '4186', '4187', '4188', '4189', '4194'].includes(
|
||||
@@ -23,6 +24,20 @@ try {
|
||||
const errors = [];
|
||||
page.on('pageerror', (e) => errors.push(e.message));
|
||||
await installRadioFixture(page);
|
||||
if (soak) await page.evaluateOnNewDocument(() => {
|
||||
const fetch = window.fetch.bind(window);
|
||||
window.qaQuakeReads = 0;
|
||||
window.fetch = (input, init) => {
|
||||
const url = new URL(input?.url ?? input, location.href);
|
||||
if (url.hostname !== 'earthquake.usgs.gov') return fetch(input, init);
|
||||
window.qaQuakeReads++;
|
||||
return Promise.resolve(Response.json({
|
||||
type: 'FeatureCollection',
|
||||
features: [{ type: 'Feature', id: 'qa-austin', properties: { mag: 4.1, time: Date.now(), place: 'Austin' },
|
||||
geometry: { type: 'Point', coordinates: [-97.74, 30.27, 8] } }],
|
||||
}));
|
||||
};
|
||||
});
|
||||
await page.goto(url.href);
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
@@ -56,8 +71,53 @@ try {
|
||||
window.qaSnapshot = window.qaLayer.getAcceptedCatalogSnapshot();
|
||||
return window.__qaRadio.reads;
|
||||
});
|
||||
for (const kind of ['cesium', 'navara']) {
|
||||
const client = soak ? await page.createCDPSession() : null;
|
||||
if (soak) await page.evaluate(async () => {
|
||||
const { dataManager, engine } = window.__godsEyeView;
|
||||
await dataManager.setEnabled('earthquakes', true, { origin: 'user' });
|
||||
window.qaModels = new Map([...dataManager.layers].map(([id, entry]) => [id, entry.module]));
|
||||
window.qaEngines = new Map([[engine.kind, engine]]);
|
||||
});
|
||||
const swaps = soak ? 50 : 2, measurements = [], memory = [];
|
||||
let contextListeners;
|
||||
for (let i = 1; i <= swaps; i++) {
|
||||
const kind = i % 2 ? 'cesium' : 'navara';
|
||||
const start = await page.evaluate(() => performance.now());
|
||||
await switchGlobe(page, kind);
|
||||
const elapsed = await page.evaluate((start) => performance.now() - start, start);
|
||||
measurements.push({ kind, ms: Math.round(elapsed) });
|
||||
if (soak) {
|
||||
assert.ok(await page.evaluate(() => {
|
||||
const { dataManager, engine } = window.__godsEyeView;
|
||||
const previous = window.qaEngines.get(engine.kind);
|
||||
window.qaEngines.set(engine.kind, engine);
|
||||
return (!previous || previous === engine) && window.qaEngines.size === 2 &&
|
||||
[...window.qaModels].every(([id, model]) => dataManager.layers.get(id)?.module === model) &&
|
||||
window.qaModels.size === 33 && window.qaQuakeReads >= 1 &&
|
||||
dataManager.getEnabledLayerIds().has('earthquakes');
|
||||
}));
|
||||
if ([2, 10, 50].includes(i)) {
|
||||
const { result } = await client.send('Runtime.evaluate', {
|
||||
includeCommandLineAPI: true,
|
||||
returnByValue: true,
|
||||
expression: `(() => {
|
||||
const events = getEventListeners(window.__godsEyeView.engine.host.canvas);
|
||||
return [events.webglcontextlost.length, events.webglcontextrestored.length];
|
||||
})()`,
|
||||
});
|
||||
assert.ok(
|
||||
Array.isArray(result.value) && result.value.length === 2 &&
|
||||
result.value.every((count) => Number.isInteger(count) && count > 0),
|
||||
'the active canvas must expose both WebGL event subscriptions',
|
||||
);
|
||||
contextListeners ??= result.value;
|
||||
assert.deepEqual(result.value, contextListeners, 'warm Navara must release old context listeners');
|
||||
await client.send('HeapProfiler.collectGarbage');
|
||||
const heap = await client.send('Runtime.getHeapUsage');
|
||||
const dom = await client.send('Memory.getDOMCounters');
|
||||
memory.push({ swap: i, contextListeners, jsMiB: Math.round(heap.usedSize / 1048576), ...dom });
|
||||
}
|
||||
}
|
||||
const state = await page.evaluate(() => ({
|
||||
same:
|
||||
window.qaLayer ===
|
||||
@@ -86,6 +146,14 @@ try {
|
||||
assert.equal(state.state.selected.id, stations[10].id);
|
||||
assert.equal(state.state.volume, 0.36);
|
||||
}
|
||||
if (soak) {
|
||||
const warm = {};
|
||||
for (const kind of ['navara', 'cesium']) {
|
||||
const ms = measurements.slice(2).filter(x => x.kind === kind).map(x => x.ms).sort((a, b) => a - b);
|
||||
warm[kind] = { p50: ms[Math.ceil(ms.length * 0.50) - 1], p95: ms[Math.ceil(ms.length * 0.95) - 1] };
|
||||
}
|
||||
console.log('SOAK', JSON.stringify({ swaps, first: measurements.slice(0, 2), warm, memory }));
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(
|
||||
'PASS one live radio player, one station directory and the same selection',
|
||||
|
||||
@@ -217,7 +217,14 @@ function atlasTexture(atlas, encoded, onLoad) {
|
||||
const texture = cesiumSampling(new Texture(), encoded);
|
||||
const image = new Image();
|
||||
image.crossOrigin = 'anonymous';
|
||||
const release = () => {
|
||||
image.onload = null;
|
||||
texture.image = null;
|
||||
texture.removeEventListener('dispose', release);
|
||||
};
|
||||
texture.addEventListener('dispose', release);
|
||||
image.onload = () => {
|
||||
image.onload = null;
|
||||
texture.image = image;
|
||||
texture.needsUpdate = true;
|
||||
onLoad();
|
||||
|
||||
@@ -23,6 +23,37 @@ const spriteOptions = normalizeMoverSetOptions({
|
||||
},
|
||||
});
|
||||
|
||||
test('disposing a sprite atlas drops decoded images and detaches pending load callbacks', (t) => {
|
||||
const previous = globalThis.Image;
|
||||
let image;
|
||||
globalThis.Image = class {
|
||||
constructor() { image = this; }
|
||||
};
|
||||
t.after(() => {
|
||||
if (previous) globalThis.Image = previous;
|
||||
else delete globalThis.Image;
|
||||
});
|
||||
const options = normalizeMoverSetOptions({
|
||||
...spriteOptions, sprite: {
|
||||
sizePx: 24, atlas: { url: '/bracket.svg', cells: { a: [0, 0, 1, 1] } },
|
||||
},
|
||||
});
|
||||
for (const loaded of [false, true]) {
|
||||
let frames = 0;
|
||||
const mover = createMoverMesh(options, createMoverStore(options), {
|
||||
onTextureLoad: () => frames++,
|
||||
});
|
||||
if (loaded) {
|
||||
image.onload();
|
||||
assert.equal(mover.mesh.material.map.image, image);
|
||||
}
|
||||
mover.dispose();
|
||||
assert.equal(image.onload, null);
|
||||
assert.equal(mover.mesh.material.map.image, null);
|
||||
assert.equal(frames, Number(loaded));
|
||||
}
|
||||
});
|
||||
|
||||
const basicShader = () => ({
|
||||
uniforms: UniformsUtils.clone(ShaderLib.basic.uniforms),
|
||||
vertexShader: ShaderLib.basic.vertexShader,
|
||||
|
||||
@@ -43,6 +43,17 @@ class GevOverlayEffectDesc extends EffectDesc {
|
||||
);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
// Navara removes the pass from its composer; our pass owns its GPU target
|
||||
// and context event handlers until it is explicitly disposed.
|
||||
const pass = this.raw?.rawPass;
|
||||
try {
|
||||
super.onDestroy();
|
||||
} finally {
|
||||
pass?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Each drawn frame: the scene depth the overlays test against, then each
|
||||
// overlay's own per-frame work (pixel footprint, origin, viewport).
|
||||
update(time) {
|
||||
|
||||
Reference in new issue
Block a user