From 39d522c9a99a46f2851351f9a34b943e2337cda2 Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Fri, 16 Oct 2026 19:33:46 +0400 Subject: [PATCH] perf(globe): less churn in movers and lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of Tasks 2–3: - the mover store's instance checks no longer build arrays or closures per set() (orientation and colour checked field by field), and dirty() returns one reused range object; - Cesium models get their matrix and colour through scratch objects (Model.modelMatrix and Model.color copy what they are given) instead of a new Matrix4 and Color per placement; - a destroyed MoverSet releases its store (count 0, empty instance arrays, no pick ids) so a set object a layer still holds keeps no memory, and the Cesium parts drop their billboard, point and model lists; - (with the overlay commit) the Navara line viewport is updated in place. Node micro-measure, 10 000 instances set + packed per frame: 0.27-0.29 ms, about 1.7 KB per frame (0.17 B per instance), as before. Contract: Cesium 70/70, Navara 76/76. --- src/globe/cesium/movers.js | 16 +++++++--- src/globe/movers.js | 63 ++++++++++++++++++++++++++++++-------- src/globe/movers.test.mjs | 22 ++++++++++++- 3 files changed, 84 insertions(+), 17 deletions(-) diff --git a/src/globe/cesium/movers.js b/src/globe/cesium/movers.js index 2411b7c85..dcd382a15 100644 --- a/src/globe/cesium/movers.js +++ b/src/globe/cesium/movers.js @@ -12,6 +12,8 @@ const scratchPosition = new Cesium.Cartesian3(); const scratchColor = new Cesium.Color(); const scratchQuaternion = new Cesium.Quaternion(); const scratchRotation = new Cesium.Matrix3(); +// Model.modelMatrix and Model.color copy what they are given. +const scratchModelMatrix = new Cesium.Matrix4(); /** The atlas cells as canvases, by key (a DOM canvas or image source). */ async function cropAtlasCells(atlas, keys) { @@ -143,6 +145,9 @@ function spritePart(scene, options, store, factories, requestRender, owners) { }, destroy() { destroyed = true; + billboards.length = 0; + shownCell.length = 0; + cells = null; }, }; } @@ -180,7 +185,9 @@ function pointPart(options, store, factories, requestRender, owners) { } requestRender('movers'); }, - destroy() {}, + destroy() { + points.length = 0; + }, }; } @@ -203,13 +210,12 @@ function modelPart(options, store, factories, requestRender, owners) { item.modelMatrix = Cesium.Matrix4.fromRotationTranslation( Cesium.Matrix3.fromQuaternion(scratchQuaternion, scratchRotation), readPosition(store, index), - new Cesium.Matrix4(), + scratchModelMatrix, ); item.scale = store.scales[index]; item.show = index < store.count && store.shows[index] === 1; item.id = store.pickIds[index]; - if (model.colorBlend) - item.color = Cesium.Color.clone(readColor(store, index)); + if (model.colorBlend) item.color = readColor(store, index); }; const ensure = (index) => { if (models[index] || loading[index]) return; @@ -258,6 +264,8 @@ function modelPart(options, store, factories, requestRender, owners) { }, destroy() { destroyed = true; + models.length = 0; + loading.length = 0; }, }; } diff --git a/src/globe/movers.js b/src/globe/movers.js index 5cbc9b45e..468276326 100644 --- a/src/globe/movers.js +++ b/src/globe/movers.js @@ -150,17 +150,18 @@ export function normalizeMoverSetOptions(options) { export function createMoverStore(options) { const { capacity, cellKeys, kind } = options; const cellIndex = new Map(cellKeys.map((key, index) => [key, index])); - const positions = new Float64Array(capacity * 3); - const orientations = new Float64Array(capacity * 4); - const colors = new Float32Array(capacity * 4).fill(1); - const rotations = new Float32Array(capacity); - const scales = new Float32Array(capacity).fill(1); - const cells = new Uint16Array(capacity); - const shows = new Uint8Array(capacity).fill(1); - const pickIds = new Array(capacity); + let positions = new Float64Array(capacity * 3); + let orientations = new Float64Array(capacity * 4); + let colors = new Float32Array(capacity * 4).fill(1); + let rotations = new Float32Array(capacity); + let scales = new Float32Array(capacity).fill(1); + let cells = new Uint16Array(capacity); + let shows = new Uint8Array(capacity).fill(1); + let pickIds = new Array(capacity); for (let index = 0; index < capacity; index++) orientations[index * 4 + 3] = 1; let count = 0; + const range = { from: 0, to: 0 }; let dirtyFrom = Infinity; let dirtyTo = -Infinity; const touch = (from, to) => { @@ -189,7 +190,10 @@ export function createMoverStore(options) { orientation !== undefined && !( isObject(orientation) && - ['x', 'y', 'z', 'w'].every((axis) => isFiniteNumber(orientation[axis])) + isFiniteNumber(orientation.x) && + isFiniteNumber(orientation.y) && + isFiniteNumber(orientation.z) && + isFiniteNumber(orientation.w) ) ) throw new TypeError('Mover orientation must be a quaternion'); @@ -198,7 +202,10 @@ export function createMoverStore(options) { !( (Array.isArray(color) || ArrayBuffer.isView(color)) && color.length === 4 && - Array.prototype.every.call(color, isFiniteNumber) + isFiniteNumber(color[0]) && + isFiniteNumber(color[1]) && + isFiniteNumber(color[2]) && + isFiniteNumber(color[3]) ) ) throw new TypeError('Mover color must be [r, g, b, a]'); @@ -256,9 +263,40 @@ export function createMoverStore(options) { count = next; }, positionsView: () => positions.subarray(0, count * 3), + /** + * Drops the instance arrays (a destroyed set): the count goes to 0 and + * the arrays to empty ones, so a set object still held keeps no memory. + */ + release() { + count = 0; + positions = new Float64Array(0); + orientations = new Float64Array(0); + colors = new Float32Array(0); + rotations = new Float32Array(0); + scales = new Float32Array(0); + cells = new Uint16Array(0); + shows = new Uint8Array(0); + pickIds = []; + Object.assign(this, { + positions, + orientations, + colors, + rotations, + scales, + cells, + shows, + pickIds, + }); + this.clean(); + }, /** Indices changed since clean(), as [from, to), or null. */ - dirty: () => - dirtyFrom === Infinity ? null : { from: dirtyFrom, to: dirtyTo }, + /** The changed range (one object, reused: read it at once), or null. */ + dirty() { + if (dirtyFrom === Infinity) return null; + range.from = dirtyFrom; + range.to = dirtyTo; + return range; + }, clean() { dirtyFrom = Infinity; dirtyTo = -Infinity; @@ -296,6 +334,7 @@ export function createMoverSet(store, { commit, destroy }) { if (destroyed) return; destroyed = true; destroy(); + store.release?.(); }, isDestroyed: () => destroyed, }; diff --git a/src/globe/movers.test.mjs b/src/globe/movers.test.mjs index 0f1f76f3a..aeb083ddf 100644 --- a/src/globe/movers.test.mjs +++ b/src/globe/movers.test.mjs @@ -170,7 +170,7 @@ test('the set facade forwards to the store and an engine commit, once destroyed ); const calls = []; const set = createMoverSet(store, { - commit: () => calls.push(['commit', store.dirty()]), + commit: () => calls.push(['commit', { ...store.dirty() }]), destroy: () => calls.push(['destroy']), }); set.set(0, { position: { x: 1, y: 2, z: 3 } }); @@ -187,3 +187,23 @@ test('the set facade forwards to the store and an engine commit, once destroyed assert.throws(() => set.set(0, {}), /destroyed/); assert.throws(() => set.commit(), /destroyed/); }); + +test('a destroyed set releases its instance arrays', () => { + const store = createMoverStore( + normalizeMoverSetOptions({ + id: 'r', + kind: 'point', + capacity: 1000, + point: { sizePx: 2 }, + }), + ); + const set = createMoverSet(store, { commit() {}, destroy() {} }); + set.set(0, { position: { x: 1, y: 2, z: 3 }, pickId: 'p' }); + set.setCount(1); + set.destroy(); + assert.equal(store.count, 0); + assert.equal(store.positions.length, 0); + assert.equal(store.pickIds.length, 0); + assert.equal(set.positionsView().length, 0); + assert.equal(store.dirty(), null); +});