Spaces:
Configuration error
Configuration error
File size: 5,889 Bytes
9f21d0a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | 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);
});
});
|