orbit-studio / src /modules /util /CesiumCleanupHelper.test.ts
moncefem's picture
Deploy Orbit Studio propagator
9f21d0a
Raw
History Blame Contribute Delete
5.89 kB
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { CesiumCleanupHelper, collectLabelCollections } from "./CesiumCleanupHelper";
// A stand-in for Cesium's BillboardCollection: remove() takes a billboard out and
// destroys it, which is why the pool must be emptied alongside.
//
// A Set rather than an array, because the real `remove` is indexed off the
// billboard (`_index`) rather than a scan — and because a linear one made this
// file quadratic at the 68,000 billboards the regression actually produced.
const billboardCollection = (count: number) => {
const held = new Set(Array.from({ length: count }, (_, index) => ({ id: index })));
return {
get billboards(): { id: number }[] {
return [...held];
},
remove(billboard: unknown): boolean {
return held.delete(billboard as { id: number });
},
};
};
/**
* The shape the drain reaches into, nested the way Cesium nests it: the entity
* cluster's label collection three primitive collections down.
*/
const sceneWith = (spareCount: number, extraBillboards = 1) => {
const glyphs = billboardCollection(spareCount + extraBillboards);
const labelCollection = {
_labels: Array.from({ length: 10 }, () => ({})),
_spareBillboards: glyphs.billboards.slice(0, spareCount) as unknown[],
_glyphBillboardCollection: glyphs,
};
const primitives = { _primitives: [{ _primitives: [{ _primitives: [{ _labelCollection: labelCollection }] }] }] };
const requestRender = vi.fn();
return { viewer: { scene: { primitives, requestRender } }, labelCollection, glyphs, requestRender };
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const drain = (viewer: unknown): number => CesiumCleanupHelper.drain(viewer as any);
describe("collectLabelCollections", () => {
test("finds the collection wherever it is nested", () => {
const { viewer, labelCollection } = sceneWith(200);
expect(collectLabelCollections(viewer.scene.primitives)).toEqual([labelCollection]);
});
test("finds one per data source", () => {
const first = sceneWith(200).labelCollection;
const second = sceneWith(200).labelCollection;
const primitives = { _primitives: [{ _labelCollection: first }, { _primitives: [{ _labelCollection: second }] }] };
expect(collectLabelCollections(primitives)).toHaveLength(2);
});
test("survives a tree with no label collection at all", () => {
expect(collectLabelCollections({ _primitives: [{}, { _primitives: [] }] })).toEqual([]);
expect(collectLabelCollections(undefined)).toEqual([]);
});
});
describe("CesiumCleanupHelper.drain", () => {
beforeEach(() => {
CesiumCleanupHelper.resetReported();
});
afterEach(() => {
vi.restoreAllMocks();
});
test("takes the spare billboards out of the glyph collection and empties the pool", () => {
vi.spyOn(console, "info").mockImplementation(() => {});
const { viewer, labelCollection, glyphs, requestRender } = sceneWith(67_952);
expect(drain(viewer)).toBe(67_952);
// The pool has to be emptied in the same breath: remove() destroys the
// billboard, so a spare left in the pool would be handed to the next glyph.
expect(labelCollection._spareBillboards).toHaveLength(0);
// The one billboard still bound to a live glyph is left alone.
expect(glyphs.billboards).toHaveLength(1);
expect(requestRender).toHaveBeenCalled();
});
test("leaves a small pool alone rather than rebuilding vertex arrays for it", () => {
const { viewer, labelCollection, glyphs, requestRender } = sceneWith(99);
expect(drain(viewer)).toBe(0);
expect(labelCollection._spareBillboards).toHaveLength(99);
expect(glyphs.billboards).toHaveLength(100);
expect(requestRender).not.toHaveBeenCalled();
});
test("the pooled labels themselves are left in place", () => {
vi.spyOn(console, "info").mockImplementation(() => {});
const { viewer, labelCollection } = sceneWith(200);
drain(viewer);
// Cesium reuses those slots, and measured they cost nothing: what costs
// frames is the orphaned billboards, so this is deliberately not a removeAll.
expect(labelCollection._labels).toHaveLength(10);
});
// The regression that made this file necessary twice: `_billboardCollection`
// was renamed to `_glyphBillboardCollection` upstream and the drain became a
// silent no-op. A miss has to be audible.
test("reports a renamed internal instead of skipping quietly", () => {
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const { viewer, labelCollection } = sceneWith(200);
delete (labelCollection as { _glyphBillboardCollection?: unknown })._glyphBillboardCollection;
expect(drain(viewer)).toBe(0);
expect(error).toHaveBeenCalledTimes(1);
expect(error.mock.calls[0]?.[0]).toContain("_glyphBillboardCollection");
});
test("a primitive tree with no label collection is silent, not reported", () => {
// A points-only scene has no label collection at all, and used to log an
// internals-have-moved error on every teardown for it. See CesiumCleanupHelper.drain.
// The test above pins the case that must still report: a collection that
// exists but cannot be read.
const error = vi.spyOn(console, "error").mockImplementation(() => {});
expect(drain({ scene: { primitives: { _primitives: [] }, requestRender: vi.fn() } })).toBe(0);
expect(error).not.toHaveBeenCalled();
});
test("a broken assumption is reported once, not once per reconcile", () => {
const error = vi.spyOn(console, "error").mockImplementation(() => {});
const { viewer, labelCollection } = sceneWith(200);
delete (labelCollection as { _spareBillboards?: unknown })._spareBillboards;
drain(viewer);
drain(viewer);
drain(viewer);
expect(error).toHaveBeenCalledTimes(1);
});
});