perf(globe): less churn in movers and lines

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.
This commit is contained in:
German Khokhlov committed 2026-10-16 19:33:46 +04:00
1 parent d483e5897e
commit 39d522c9a9
3 files changed
+84 -17

No files matched your search

+12 -4
View File
@@ -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;
},
};
}
+51 -12
View File
@@ -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,
};
+21 -1
View File
@@ -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);
});