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);
  });
});