@navaramap/worker 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -0
- package/README.md +3 -0
- package/dist/helpers/transferConstructedPolygonGeometry.d.ts +6 -0
- package/dist/helpers/transferConstructedPolylineGeometry.d.ts +6 -0
- package/dist/helpers/transferReturnedConstructedTerrainMesh.d.ts +6 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3203 -0
- package/dist/index.umd.cjs +11 -0
- package/dist/manager.d.ts +19 -0
- package/dist/pool.d.ts +17 -0
- package/dist/recyclingPool.d.ts +86 -0
- package/dist/tasks/constructPolygonBatchedFeature.d.ts +2 -0
- package/dist/tasks/constructPolylineBatchedFeature.d.ts +2 -0
- package/dist/tasks/constructQuantizedMeshTerrainMesh.d.ts +4 -0
- package/dist/tasks/constructTerrainMesh.d.ts +4 -0
- package/dist/tasks/getImageDataFromBlob.d.ts +12 -0
- package/dist/tasks/getImageDataFromImageBitmap.d.ts +1 -0
- package/dist/tasks/index.d.ts +26 -0
- package/dist/tasks/parseMvtTile.d.ts +15 -0
- package/dist/tasks/upsampleQuantizedMeshTerrainMesh.d.ts +2 -0
- package/dist/tasks/upsampleTerrainMesh.d.ts +2 -0
- package/dist/tasks/waitWasm.d.ts +8 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/toExtentRadianF32Like.d.ts +3 -0
- package/dist/utils/toPolygonMaterialLike.d.ts +3 -0
- package/dist/utils/toPolylineMaterialLike.d.ts +3 -0
- package/dist/utils/toTransferableMartiniLike.d.ts +3 -0
- package/dist/utils/toTransferablePolygonBatchedFeatureLike.d.ts +3 -0
- package/dist/utils/toTransferablePolylineBatchedFeatureLike.d.ts +3 -0
- package/dist/utils/toTransferableRasterDEMDataLike.d.ts +3 -0
- package/dist/utils/toTransferableTileLike.d.ts +5 -0
- package/dist/utils/toUpsamplableTerrainGeometryLike.d.ts +3 -0
- package/dist/utils/toVec3Like.d.ts +3 -0
- package/dist/worker/index.d.ts +4 -0
- package/dist/worker/transfer.d.ts +1 -0
- package/package.json +37 -0
- package/src/helpers/transferConstructedPolygonGeometry.ts +25 -0
- package/src/helpers/transferConstructedPolylineGeometry.ts +28 -0
- package/src/helpers/transferReturnedConstructedTerrainMesh.ts +21 -0
- package/src/index.ts +4 -0
- package/src/manager.test.ts +45 -0
- package/src/manager.ts +43 -0
- package/src/pool.test.ts +87 -0
- package/src/pool.ts +150 -0
- package/src/recyclingPool.test.ts +404 -0
- package/src/recyclingPool.ts +385 -0
- package/src/tasks/constructPolygonBatchedFeature.ts +40 -0
- package/src/tasks/constructPolylineBatchedFeature.ts +36 -0
- package/src/tasks/constructQuantizedMeshTerrainMesh.ts +36 -0
- package/src/tasks/constructTerrainMesh.ts +44 -0
- package/src/tasks/getImageDataFromBlob.ts +30 -0
- package/src/tasks/getImageDataFromImageBitmap.ts +19 -0
- package/src/tasks/index.ts +31 -0
- package/src/tasks/parseMvtTile.ts +67 -0
- package/src/tasks/upsampleQuantizedMeshTerrainMesh.ts +38 -0
- package/src/tasks/upsampleTerrainMesh.ts +40 -0
- package/src/tasks/waitWasm.ts +19 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/toExtentRadianF32Like.ts +6 -0
- package/src/utils/toPolygonMaterialLike.ts +20 -0
- package/src/utils/toPolylineMaterialLike.ts +17 -0
- package/src/utils/toTransferableMartiniLike.ts +6 -0
- package/src/utils/toTransferablePolygonBatchedFeatureLike.ts +41 -0
- package/src/utils/toTransferablePolylineBatchedFeatureLike.ts +21 -0
- package/src/utils/toTransferableRasterDEMDataLike.ts +22 -0
- package/src/utils/toTransferableTileLike.ts +34 -0
- package/src/utils/toUpsamplableTerrainGeometryLike.ts +14 -0
- package/src/utils/toVec3Like.ts +6 -0
- package/src/vite-env.d.ts +1 -0
- package/src/worker/index.ts +44 -0
- package/src/worker/transfer.ts +6 -0
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
2
|
+
import workerpool from "workerpool";
|
|
3
|
+
import type Pool from "workerpool/types/Pool";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
RECYCLE_AFTER_TASKS_BACKSTOP,
|
|
7
|
+
RECYCLE_IDLE_MS,
|
|
8
|
+
RecyclingWorkerPool,
|
|
9
|
+
} from "./recyclingPool";
|
|
10
|
+
|
|
11
|
+
type MockExecution = {
|
|
12
|
+
method: string;
|
|
13
|
+
settled: boolean;
|
|
14
|
+
resolve: (value?: unknown) => void;
|
|
15
|
+
reject: (reason?: unknown) => void;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type MockPool = {
|
|
19
|
+
executions: MockExecution[];
|
|
20
|
+
exec: ReturnType<typeof vi.fn>;
|
|
21
|
+
terminate: ReturnType<typeof vi.fn>;
|
|
22
|
+
stats: () => ReturnType<Pool["stats"]>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Every workerpool.pool() call yields a fresh mock, recorded in order so
|
|
26
|
+
// tests can distinguish original slot pools from recycled replacements.
|
|
27
|
+
let pools: MockPool[] = [];
|
|
28
|
+
|
|
29
|
+
vi.spyOn(workerpool, "pool").mockImplementation(() => {
|
|
30
|
+
const mock: MockPool = {
|
|
31
|
+
executions: [],
|
|
32
|
+
exec: vi.fn(
|
|
33
|
+
(method: string) =>
|
|
34
|
+
new workerpool.Promise(
|
|
35
|
+
(
|
|
36
|
+
resolve: (value?: unknown) => void,
|
|
37
|
+
reject: (e?: unknown) => void,
|
|
38
|
+
) => {
|
|
39
|
+
const execution: MockExecution = {
|
|
40
|
+
method,
|
|
41
|
+
settled: false,
|
|
42
|
+
resolve: (value?: unknown) => {
|
|
43
|
+
execution.settled = true;
|
|
44
|
+
resolve(value);
|
|
45
|
+
},
|
|
46
|
+
reject: (e?: unknown) => {
|
|
47
|
+
execution.settled = true;
|
|
48
|
+
reject(e);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
mock.executions.push(execution);
|
|
52
|
+
},
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
terminate: vi.fn(),
|
|
56
|
+
stats: () => ({
|
|
57
|
+
totalWorkers: 1,
|
|
58
|
+
busyWorkers: 0,
|
|
59
|
+
idleWorkers: 1,
|
|
60
|
+
pendingTasks: 0,
|
|
61
|
+
activeTasks: 0,
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
pools.push(mock);
|
|
65
|
+
return mock as unknown as Pool;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
beforeEach(() => {
|
|
69
|
+
pools = [];
|
|
70
|
+
vi.useFakeTimers();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
vi.useRealTimers();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const PROBE = "getWasmMemoryUsage";
|
|
78
|
+
|
|
79
|
+
const nextPending = (pool: MockPool, method: string) =>
|
|
80
|
+
pool.executions.find((e) => e.method === method && !e.settled);
|
|
81
|
+
|
|
82
|
+
/** Settles the oldest pending "test" task. */
|
|
83
|
+
const settleTask = (pool: MockPool) => {
|
|
84
|
+
nextPending(pool, "test")?.resolve();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** Answers the pending heap probe with the given byte count. */
|
|
88
|
+
const respondToProbe = (pool: MockPool, bytes: number) => {
|
|
89
|
+
nextPending(pool, PROBE)?.resolve(bytes);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
it("creates one single-worker pool per slot", () => {
|
|
93
|
+
new RecyclingWorkerPool("https://example.com", 3);
|
|
94
|
+
expect(pools).toHaveLength(3);
|
|
95
|
+
expect(workerpool.pool).toHaveBeenLastCalledWith(
|
|
96
|
+
"https://example.com",
|
|
97
|
+
expect.objectContaining({
|
|
98
|
+
maxWorkers: 1,
|
|
99
|
+
minWorkers: 0,
|
|
100
|
+
// Must outlive the longest synchronous WASM task, or a cancel while
|
|
101
|
+
// running force-terminates the warm worker despite the worker-side
|
|
102
|
+
// abort listener (workerpool's default is 1s).
|
|
103
|
+
workerTerminateTimeout: expect.any(Number),
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("dispatches tasks to the least-loaded slot", () => {
|
|
109
|
+
const pool = new RecyclingWorkerPool("https://example.com", 2);
|
|
110
|
+
|
|
111
|
+
pool.exec("test");
|
|
112
|
+
pool.exec("test");
|
|
113
|
+
|
|
114
|
+
expect(pools[0].exec).toHaveBeenCalledTimes(1);
|
|
115
|
+
expect(pools[1].exec).toHaveBeenCalledTimes(1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("probes heap usage after a settled task and recycles once over budget", async () => {
|
|
119
|
+
const pool = new RecyclingWorkerPool("https://example.com", 1, {
|
|
120
|
+
maxWorkerHeapBytes: 100,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const task = pool.exec("test");
|
|
124
|
+
settleTask(pools[0]);
|
|
125
|
+
await task;
|
|
126
|
+
expect(nextPending(pools[0], PROBE)).toBeDefined();
|
|
127
|
+
|
|
128
|
+
// Under budget: the slot keeps its worker.
|
|
129
|
+
respondToProbe(pools[0], 99);
|
|
130
|
+
expect(pools[0].terminate).not.toHaveBeenCalled();
|
|
131
|
+
|
|
132
|
+
// Over budget: the slot recycles and pre-warms the replacement.
|
|
133
|
+
const task2 = pool.exec("test");
|
|
134
|
+
settleTask(pools[0]);
|
|
135
|
+
await task2;
|
|
136
|
+
respondToProbe(pools[0], 100);
|
|
137
|
+
expect(pools[0].terminate).toHaveBeenCalledTimes(1);
|
|
138
|
+
expect(pools).toHaveLength(2);
|
|
139
|
+
expect(pools[1].executions[0]?.method).toBe("warmUp");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("coalesces heap probes", async () => {
|
|
143
|
+
const pool = new RecyclingWorkerPool("https://example.com", 1);
|
|
144
|
+
|
|
145
|
+
const task1 = pool.exec("test");
|
|
146
|
+
const task2 = pool.exec("test");
|
|
147
|
+
settleTask(pools[0]);
|
|
148
|
+
await task1;
|
|
149
|
+
settleTask(pools[0]);
|
|
150
|
+
await task2;
|
|
151
|
+
|
|
152
|
+
expect(pools[0].executions.filter((e) => e.method === PROBE)).toHaveLength(1);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("recycles conservatively when the heap probe fails", async () => {
|
|
156
|
+
const pool = new RecyclingWorkerPool("https://example.com", 1);
|
|
157
|
+
|
|
158
|
+
const task = pool.exec("test");
|
|
159
|
+
settleTask(pools[0]);
|
|
160
|
+
await task;
|
|
161
|
+
|
|
162
|
+
nextPending(pools[0], PROBE)?.reject(new Error("worker died"));
|
|
163
|
+
expect(pools[0].terminate).toHaveBeenCalledTimes(1);
|
|
164
|
+
expect(pools[1].executions[0]?.method).toBe("warmUp");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("stops dispatching to a draining slot until it recycles", async () => {
|
|
168
|
+
const pool = new RecyclingWorkerPool("https://example.com", 2, {
|
|
169
|
+
maxWorkerHeapBytes: 100,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Two overlapping tasks on the first slot (plus one pinned on the second
|
|
173
|
+
// so in-flight ties keep resolving to the first).
|
|
174
|
+
const first = pool.exec("test");
|
|
175
|
+
pool.exec("test");
|
|
176
|
+
const second = pool.exec("test");
|
|
177
|
+
|
|
178
|
+
// Settling one task triggers a probe reporting over budget; the remaining
|
|
179
|
+
// task keeps the slot from recycling immediately.
|
|
180
|
+
settleTask(pools[0]);
|
|
181
|
+
await first;
|
|
182
|
+
respondToProbe(pools[0], 100);
|
|
183
|
+
expect(pools[0].terminate).not.toHaveBeenCalled();
|
|
184
|
+
|
|
185
|
+
// The draining slot must not receive new tasks even though it ties the
|
|
186
|
+
// other slot on in-flight count.
|
|
187
|
+
pool.exec("test");
|
|
188
|
+
expect(pools[1].exec).toHaveBeenCalledTimes(2);
|
|
189
|
+
|
|
190
|
+
// Once the remaining task settles, the slot recycles and gets warmed.
|
|
191
|
+
settleTask(pools[0]);
|
|
192
|
+
await second;
|
|
193
|
+
expect(pools[0].terminate).toHaveBeenCalledTimes(1);
|
|
194
|
+
expect(pools[2].executions[0]?.method).toBe("warmUp");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it("recycles after the task-count backstop even when probes stay under budget", async () => {
|
|
198
|
+
const pool = new RecyclingWorkerPool("https://example.com", 1);
|
|
199
|
+
|
|
200
|
+
for (let i = 0; i < RECYCLE_AFTER_TASKS_BACKSTOP; i++) {
|
|
201
|
+
const task = pool.exec("test");
|
|
202
|
+
settleTask(pools[0]);
|
|
203
|
+
await task;
|
|
204
|
+
respondToProbe(pools[0], 1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
expect(pools[0].terminate).toHaveBeenCalledTimes(1);
|
|
208
|
+
expect(pools[1].executions[0]?.method).toBe("warmUp");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it("recycles once a late probe settles on a slot drained by the backstop", async () => {
|
|
212
|
+
const pool = new RecyclingWorkerPool("https://example.com", 1);
|
|
213
|
+
|
|
214
|
+
// The probe from the first settle stays pending for the whole loop, so
|
|
215
|
+
// when the backstop drains the slot, the probe is the only in-flight work.
|
|
216
|
+
for (let i = 0; i < RECYCLE_AFTER_TASKS_BACKSTOP; i++) {
|
|
217
|
+
const task = pool.exec("test");
|
|
218
|
+
settleTask(pools[0]);
|
|
219
|
+
await task;
|
|
220
|
+
}
|
|
221
|
+
expect(pools[0].terminate).not.toHaveBeenCalled();
|
|
222
|
+
|
|
223
|
+
// Even an under-budget probe result must trigger the deferred recycle.
|
|
224
|
+
respondToProbe(pools[0], 1);
|
|
225
|
+
expect(pools[0].terminate).toHaveBeenCalledTimes(1);
|
|
226
|
+
expect(pools[1].executions[0]?.method).toBe("warmUp");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("recycles used slots after the pool goes idle", async () => {
|
|
230
|
+
const pool = new RecyclingWorkerPool("https://example.com", 2);
|
|
231
|
+
|
|
232
|
+
const task = pool.exec("test");
|
|
233
|
+
settleTask(pools[0]);
|
|
234
|
+
await task;
|
|
235
|
+
respondToProbe(pools[0], 1);
|
|
236
|
+
|
|
237
|
+
vi.advanceTimersByTime(RECYCLE_IDLE_MS);
|
|
238
|
+
|
|
239
|
+
// Only the used slot is recycled; the untouched slot keeps its pool.
|
|
240
|
+
expect(pools[0].terminate).toHaveBeenCalledTimes(1);
|
|
241
|
+
expect(pools[1].terminate).not.toHaveBeenCalled();
|
|
242
|
+
expect(pools[2].executions[0]?.method).toBe("warmUp");
|
|
243
|
+
// The warm-up settlement must not re-arm the sweep against fresh slots.
|
|
244
|
+
nextPending(pools[2], "warmUp")?.resolve();
|
|
245
|
+
vi.advanceTimersByTime(RECYCLE_IDLE_MS);
|
|
246
|
+
expect(pools[2].terminate).not.toHaveBeenCalled();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("warmUp() warms a single worker first, then the rest in the background", async () => {
|
|
250
|
+
const pool = new RecyclingWorkerPool("https://example.com", 2);
|
|
251
|
+
|
|
252
|
+
const warmed = pool.warmUp();
|
|
253
|
+
|
|
254
|
+
// Only one worker fetches WASM up front, keeping startup bandwidth free.
|
|
255
|
+
expect(pools[0].executions[0]?.method).toBe("warmUp");
|
|
256
|
+
expect(pools[1].executions).toHaveLength(0);
|
|
257
|
+
|
|
258
|
+
nextPending(pools[0], "warmUp")?.resolve();
|
|
259
|
+
await warmed;
|
|
260
|
+
|
|
261
|
+
// The remaining slot warms in the background once the first is ready.
|
|
262
|
+
expect(nextPending(pools[1], "warmUp")).toBeDefined();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("warmUp() falls back to the remaining workers when the first fails", async () => {
|
|
266
|
+
const pool = new RecyclingWorkerPool("https://example.com", 2);
|
|
267
|
+
|
|
268
|
+
const warmed = pool.warmUp();
|
|
269
|
+
|
|
270
|
+
nextPending(pools[0], "warmUp")?.reject(new Error("boom"));
|
|
271
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
272
|
+
nextPending(pools[1], "warmUp")?.resolve();
|
|
273
|
+
await warmed;
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("warmUp() rejects only when every worker fails to warm", async () => {
|
|
277
|
+
const pool = new RecyclingWorkerPool("https://example.com", 2);
|
|
278
|
+
|
|
279
|
+
const warmed = pool.warmUp();
|
|
280
|
+
|
|
281
|
+
nextPending(pools[0], "warmUp")?.reject(new Error("boom0"));
|
|
282
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
283
|
+
nextPending(pools[1], "warmUp")?.reject(new Error("boom1"));
|
|
284
|
+
await expect(warmed).rejects.toBeInstanceOf(AggregateError);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("warmUp() rejects with the original error for a single-worker pool", async () => {
|
|
288
|
+
const pool = new RecyclingWorkerPool("https://example.com", 1);
|
|
289
|
+
|
|
290
|
+
const warmed = pool.warmUp();
|
|
291
|
+
|
|
292
|
+
nextPending(pools[0], "warmUp")?.reject(new Error("boom"));
|
|
293
|
+
await expect(warmed).rejects.toThrow("boom");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("reports no capacity while every slot is draining", async () => {
|
|
297
|
+
const pool = new RecyclingWorkerPool("https://example.com", 1, {
|
|
298
|
+
maxWorkerHeapBytes: 100,
|
|
299
|
+
});
|
|
300
|
+
expect(pool.canAcceptTasks()).toBe(true);
|
|
301
|
+
|
|
302
|
+
const task1 = pool.exec("test");
|
|
303
|
+
const task2 = pool.exec("test");
|
|
304
|
+
settleTask(pools[0]);
|
|
305
|
+
await task1;
|
|
306
|
+
respondToProbe(pools[0], 100);
|
|
307
|
+
expect(pool.canAcceptTasks()).toBe(false);
|
|
308
|
+
|
|
309
|
+
// Recycling restores capacity.
|
|
310
|
+
settleTask(pools[0]);
|
|
311
|
+
await task2;
|
|
312
|
+
expect(pool.canAcceptTasks()).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("terminate() terminates all slots and cancels the idle sweep", async () => {
|
|
316
|
+
const pool = new RecyclingWorkerPool("https://example.com", 2);
|
|
317
|
+
|
|
318
|
+
const task = pool.exec("test");
|
|
319
|
+
settleTask(pools[0]);
|
|
320
|
+
await task;
|
|
321
|
+
|
|
322
|
+
pool.terminate();
|
|
323
|
+
expect(pools[0].terminate).toHaveBeenCalledTimes(1);
|
|
324
|
+
expect(pools[1].terminate).toHaveBeenCalledTimes(1);
|
|
325
|
+
|
|
326
|
+
vi.advanceTimersByTime(RECYCLE_IDLE_MS);
|
|
327
|
+
expect(pools).toHaveLength(2);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("aggregates stats across slots", async () => {
|
|
331
|
+
const pool = new RecyclingWorkerPool("https://example.com", 2);
|
|
332
|
+
|
|
333
|
+
const task = pool.exec("test");
|
|
334
|
+
settleTask(pools[0]);
|
|
335
|
+
await task;
|
|
336
|
+
|
|
337
|
+
expect(pool.stats()).toEqual({
|
|
338
|
+
totalWorkers: 2,
|
|
339
|
+
busyWorkers: 0,
|
|
340
|
+
idleWorkers: 2,
|
|
341
|
+
pendingTasks: 0,
|
|
342
|
+
activeTasks: 0,
|
|
343
|
+
tasksSinceSpawn: 1,
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("stores probed heap bytes and aggregates them in heapStats()", async () => {
|
|
348
|
+
const pool = new RecyclingWorkerPool("https://example.com", 2, {
|
|
349
|
+
maxWorkerHeapBytes: 1000,
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
expect(pool.heapStats()).toEqual({
|
|
353
|
+
perSlot: [undefined, undefined],
|
|
354
|
+
totalBytes: 0,
|
|
355
|
+
maxWorkerHeapBytes: 1000,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// A settled task triggers a probe on its slot.
|
|
359
|
+
const task = pool.exec("test");
|
|
360
|
+
settleTask(pools[0]);
|
|
361
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
362
|
+
respondToProbe(pools[0], 300);
|
|
363
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
364
|
+
await task;
|
|
365
|
+
|
|
366
|
+
const stats = pool.heapStats();
|
|
367
|
+
expect(stats.perSlot[0]).toBe(300);
|
|
368
|
+
expect(stats.totalBytes).toBe(300);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("probeHeap() probes idle slots and coalesces with in-flight probes", async () => {
|
|
372
|
+
const pool = new RecyclingWorkerPool("https://example.com", 2);
|
|
373
|
+
|
|
374
|
+
pool.probeHeap();
|
|
375
|
+
// Calling again while probes are in flight must not enqueue duplicates.
|
|
376
|
+
pool.probeHeap();
|
|
377
|
+
expect(pools[0].executions.filter((e) => e.method === PROBE)).toHaveLength(1);
|
|
378
|
+
expect(pools[1].executions.filter((e) => e.method === PROBE)).toHaveLength(1);
|
|
379
|
+
|
|
380
|
+
respondToProbe(pools[0], 111);
|
|
381
|
+
respondToProbe(pools[1], 222);
|
|
382
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
383
|
+
|
|
384
|
+
expect(pool.heapStats().totalBytes).toBe(333);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it("clears a slot's heap sample when it is recycled", async () => {
|
|
388
|
+
const pool = new RecyclingWorkerPool("https://example.com", 1, {
|
|
389
|
+
maxWorkerHeapBytes: 1000,
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
const task = pool.exec("test");
|
|
393
|
+
settleTask(pools[0]);
|
|
394
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
395
|
+
// Over budget: the probe both records the sample and drains the slot,
|
|
396
|
+
// which recycles immediately (inflight is 0) — the sample must not
|
|
397
|
+
// survive into the fresh worker.
|
|
398
|
+
respondToProbe(pools[0], 2000);
|
|
399
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
400
|
+
await task;
|
|
401
|
+
|
|
402
|
+
expect(pool.heapStats().perSlot[0]).toBeUndefined();
|
|
403
|
+
expect(pool.heapStats().totalBytes).toBe(0);
|
|
404
|
+
});
|