@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,385 @@
|
|
|
1
|
+
import invariant from "tiny-invariant";
|
|
2
|
+
import workerpool from "workerpool";
|
|
3
|
+
import type { Promise as WorkerpoolPromise } from "workerpool";
|
|
4
|
+
import type Pool from "workerpool/types/Pool";
|
|
5
|
+
import type { ExecOptions } from "workerpool/types/types";
|
|
6
|
+
|
|
7
|
+
// WASM linear memory only grows, so each worker's heap ratchets up to its
|
|
8
|
+
// peak decode working set and never returns memory to the OS. The only way
|
|
9
|
+
// to release it is to terminate the worker thread (the wasm-bindgen glue
|
|
10
|
+
// caches its instance and skips re-initialization within a worker), so this
|
|
11
|
+
// pool recycles worker threads.
|
|
12
|
+
//
|
|
13
|
+
// Each worker gets its own single-worker workerpool (a "slot") so it can be
|
|
14
|
+
// recycled independently at its own task boundaries: after each settled task
|
|
15
|
+
// the slot's actual heap size is probed ("getWasmMemoryUsage"); once it
|
|
16
|
+
// exceeds the budget the slot stops receiving new tasks, and as soon as its
|
|
17
|
+
// in-flight tasks drain it is terminated and replaced. This keeps memory
|
|
18
|
+
// bounded even under sustained load (no pool-wide idle moment is required)
|
|
19
|
+
// and never discards more than one warm WASM instance at a time. The
|
|
20
|
+
// trade-off versus a single shared queue is possible head-of-line blocking
|
|
21
|
+
// within a slot, but upstream backpressure (ConcurrencyManager) keeps
|
|
22
|
+
// per-slot queues shallow.
|
|
23
|
+
//
|
|
24
|
+
// A burst that stays under the heap budget would otherwise keep its heaps
|
|
25
|
+
// forever, so an idle sweep additionally recycles every used slot once the
|
|
26
|
+
// pool has been quiet for RECYCLE_IDLE_MS.
|
|
27
|
+
//
|
|
28
|
+
// Recycled slots are pre-warmed immediately with a fire-and-forget "warmUp"
|
|
29
|
+
// task so the next real task doesn't pay WASM initialization latency.
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* WASM linear memory budget per worker; a worker measured over this budget
|
|
33
|
+
* is recycled. Must be comfortably above the post-init baseline heap, or
|
|
34
|
+
* recycling would thrash.
|
|
35
|
+
*/
|
|
36
|
+
export const MAX_WORKER_HEAP_BYTES_DEFAULT = 256 * 1024 * 1024;
|
|
37
|
+
/**
|
|
38
|
+
* Backstop for when heap probes cannot run (a failed probe already recycles
|
|
39
|
+
* conservatively, so this only matters if probing silently stalls).
|
|
40
|
+
*/
|
|
41
|
+
export const RECYCLE_AFTER_TASKS_BACKSTOP = 128;
|
|
42
|
+
export const RECYCLE_IDLE_MS = 5_000;
|
|
43
|
+
/**
|
|
44
|
+
* How long workerpool waits for a worker to acknowledge a cleanup message
|
|
45
|
+
* (sent on cancel-while-running) before force-terminating it. A worker busy
|
|
46
|
+
* in a synchronous WASM task cannot respond until the task finishes, so the
|
|
47
|
+
* default (1s) would terminate exactly the long-running tasks the worker-side
|
|
48
|
+
* abort listener is meant to keep alive (see `keepWorkerAliveOnAbort`). Must
|
|
49
|
+
* comfortably exceed the longest expected task; a genuinely wedged worker
|
|
50
|
+
* still gets terminated once this elapses.
|
|
51
|
+
*/
|
|
52
|
+
export const WORKER_TERMINATE_TIMEOUT_MS = 30_000;
|
|
53
|
+
|
|
54
|
+
export type RecyclingWorkerPoolOptions = {
|
|
55
|
+
/** Overrides {@link MAX_WORKER_HEAP_BYTES_DEFAULT}. */
|
|
56
|
+
maxWorkerHeapBytes?: number;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type Slot = {
|
|
60
|
+
pool: Pool;
|
|
61
|
+
/** Settled tasks since this slot's pool was (re)created. */
|
|
62
|
+
tasksSinceSpawn: number;
|
|
63
|
+
/** Dispatched but not yet settled tasks, including warm-ups and probes. */
|
|
64
|
+
inflight: number;
|
|
65
|
+
/** Set once the slot exceeds its heap budget; drained slots recycle. */
|
|
66
|
+
draining: boolean;
|
|
67
|
+
/** Whether a heap probe is already in flight (probes are coalesced). */
|
|
68
|
+
probing: boolean;
|
|
69
|
+
/** WASM heap size from the most recent probe; undefined until the first
|
|
70
|
+
* probe settles and after a recycle. Point-in-time — see {@link heapStats}. */
|
|
71
|
+
lastHeapBytes: number | undefined;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type WorkerPoolStats = ReturnType<Pool["stats"]> & {
|
|
75
|
+
tasksSinceSpawn: number;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type WorkerPoolHeapStats = {
|
|
79
|
+
/** Last probed WASM heap per slot (undefined = not probed yet). */
|
|
80
|
+
perSlot: (number | undefined)[];
|
|
81
|
+
/** Sum of the probed heaps. */
|
|
82
|
+
totalBytes: number;
|
|
83
|
+
/** The per-worker budget slots are recycled against. */
|
|
84
|
+
maxWorkerHeapBytes: number;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const createPool = (url: string) =>
|
|
88
|
+
workerpool.pool(url, {
|
|
89
|
+
// One worker per pool so each worker can be recycled independently.
|
|
90
|
+
maxWorkers: 1,
|
|
91
|
+
// Avoid oversubscribing CPU when combined with other systems (e.g., DRACO loader).
|
|
92
|
+
minWorkers: 0,
|
|
93
|
+
workerTerminateTimeout: WORKER_TERMINATE_TIMEOUT_MS,
|
|
94
|
+
workerOpts: {
|
|
95
|
+
type: import.meta.env.PROD ? undefined : "module",
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export class RecyclingWorkerPool {
|
|
100
|
+
private readonly url: string;
|
|
101
|
+
private readonly maxWorkerHeapBytes: number;
|
|
102
|
+
private readonly slots: Slot[];
|
|
103
|
+
private idleSweepTimer: ReturnType<typeof setTimeout> | undefined;
|
|
104
|
+
private terminated = false;
|
|
105
|
+
|
|
106
|
+
constructor(
|
|
107
|
+
url: string,
|
|
108
|
+
maxWorkers: number,
|
|
109
|
+
options?: RecyclingWorkerPoolOptions,
|
|
110
|
+
) {
|
|
111
|
+
invariant(maxWorkers > 0, "maxWorkers must be a positive number.");
|
|
112
|
+
this.url = url;
|
|
113
|
+
this.maxWorkerHeapBytes =
|
|
114
|
+
options?.maxWorkerHeapBytes ?? MAX_WORKER_HEAP_BYTES_DEFAULT;
|
|
115
|
+
this.slots = Array.from({ length: maxWorkers }, () => ({
|
|
116
|
+
pool: createPool(url),
|
|
117
|
+
tasksSinceSpawn: 0,
|
|
118
|
+
inflight: 0,
|
|
119
|
+
draining: false,
|
|
120
|
+
probing: false,
|
|
121
|
+
lastHeapBytes: undefined,
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
exec<T extends (...args: any[]) => any>(
|
|
126
|
+
method: string | T,
|
|
127
|
+
params?: Parameters<T> | null,
|
|
128
|
+
options?: ExecOptions,
|
|
129
|
+
): WorkerpoolPromise<ReturnType<T>> {
|
|
130
|
+
const slot = this.pickSlot();
|
|
131
|
+
slot.inflight++;
|
|
132
|
+
try {
|
|
133
|
+
return (
|
|
134
|
+
slot.pool
|
|
135
|
+
.exec(method, params, options)
|
|
136
|
+
// `finally` doesn't work, so use `then()` and `catch()` to ensure that `onSettled()` is invoked.
|
|
137
|
+
.then((value) => {
|
|
138
|
+
this.onSettled(slot);
|
|
139
|
+
return value;
|
|
140
|
+
})
|
|
141
|
+
.catch((e) => {
|
|
142
|
+
this.onSettled(slot);
|
|
143
|
+
throw e;
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
this.onSettled(slot);
|
|
148
|
+
throw e;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Pre-initializes WASM on the workers so that upcoming tasks don't pay
|
|
154
|
+
* the initialization latency. Warms a single worker and resolves once it
|
|
155
|
+
* is ready; the rest warm in the background. Correctness doesn't depend
|
|
156
|
+
* on warm workers (every task awaits WASM internally), and a slot whose
|
|
157
|
+
* warm-up fails surfaces the error on its next real task — the same
|
|
158
|
+
* policy as a recycled slot. Rejects only when every worker fails to
|
|
159
|
+
* warm, which signals a broken worker setup.
|
|
160
|
+
*/
|
|
161
|
+
warmUp(): Promise<void> {
|
|
162
|
+
// Warming every slot at once would fetch and compile the worker WASM N
|
|
163
|
+
// times concurrently, competing over the same HTTP/2 connection with
|
|
164
|
+
// the main-thread WASM and other startup fetches. The first warm-up
|
|
165
|
+
// populates the HTTP cache, so the background ones load from cache.
|
|
166
|
+
const [first, ...rest] = this.slots;
|
|
167
|
+
return this.warmSlot(first).then(
|
|
168
|
+
() => {
|
|
169
|
+
for (const slot of rest) {
|
|
170
|
+
// Fire-and-forget; a failure surfaces on the slot's next real task.
|
|
171
|
+
this.warmSlot(slot).catch(() => undefined);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
(e) => {
|
|
175
|
+
// A single failed spawn doesn't prove the setup is broken; fall
|
|
176
|
+
// back to the remaining workers and reject only when all fail.
|
|
177
|
+
if (rest.length === 0) throw e;
|
|
178
|
+
return Promise.any(rest.map((slot) => this.warmSlot(slot))).then(
|
|
179
|
+
() => undefined,
|
|
180
|
+
);
|
|
181
|
+
},
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Whether at least one worker is still under its heap budget. When false,
|
|
187
|
+
* every slot is draining and upstream intake should pause until a recycled
|
|
188
|
+
* worker comes back (tasks dispatched anyway still run, on the
|
|
189
|
+
* least-loaded draining slot).
|
|
190
|
+
*/
|
|
191
|
+
canAcceptTasks(): boolean {
|
|
192
|
+
return this.slots.some((slot) => !slot.draining);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
stats(): WorkerPoolStats {
|
|
196
|
+
const aggregated = {
|
|
197
|
+
totalWorkers: 0,
|
|
198
|
+
busyWorkers: 0,
|
|
199
|
+
idleWorkers: 0,
|
|
200
|
+
pendingTasks: 0,
|
|
201
|
+
activeTasks: 0,
|
|
202
|
+
tasksSinceSpawn: 0,
|
|
203
|
+
};
|
|
204
|
+
for (const slot of this.slots) {
|
|
205
|
+
const stats = slot.pool.stats();
|
|
206
|
+
aggregated.totalWorkers += stats.totalWorkers;
|
|
207
|
+
aggregated.busyWorkers += stats.busyWorkers;
|
|
208
|
+
aggregated.idleWorkers += stats.idleWorkers;
|
|
209
|
+
aggregated.pendingTasks += stats.pendingTasks;
|
|
210
|
+
aggregated.activeTasks += stats.activeTasks;
|
|
211
|
+
aggregated.tasksSinceSpawn += slot.tasksSinceSpawn;
|
|
212
|
+
}
|
|
213
|
+
return aggregated;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Aggregated WASM heap sizes from the most recent probes. Values are
|
|
218
|
+
* point-in-time samples taken after task settlements (or {@link probeHeap});
|
|
219
|
+
* under load they lag by the tasks queued ahead of the probe.
|
|
220
|
+
*/
|
|
221
|
+
heapStats(): WorkerPoolHeapStats {
|
|
222
|
+
const perSlot = this.slots.map((slot) => slot.lastHeapBytes);
|
|
223
|
+
return {
|
|
224
|
+
perSlot,
|
|
225
|
+
totalBytes: perSlot.reduce<number>((sum, b) => sum + (b ?? 0), 0),
|
|
226
|
+
maxWorkerHeapBytes: this.maxWorkerHeapBytes,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Requests a fresh heap probe on every non-draining slot (draining slots
|
|
232
|
+
* are about to be recycled anyway). Probes coalesce per slot, so calling
|
|
233
|
+
* this at display frequency is safe.
|
|
234
|
+
*/
|
|
235
|
+
probeHeap(): void {
|
|
236
|
+
if (this.terminated) return;
|
|
237
|
+
for (const slot of this.slots) {
|
|
238
|
+
if (!slot.draining) {
|
|
239
|
+
this.probeSlot(slot);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
terminate(): void {
|
|
245
|
+
if (this.idleSweepTimer !== undefined) {
|
|
246
|
+
clearTimeout(this.idleSweepTimer);
|
|
247
|
+
this.idleSweepTimer = undefined;
|
|
248
|
+
}
|
|
249
|
+
this.terminated = true;
|
|
250
|
+
for (const slot of this.slots) {
|
|
251
|
+
slot.pool.terminate();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private pickSlot(): Slot {
|
|
256
|
+
// Prefer slots still under their heap budget; fall back to a draining
|
|
257
|
+
// slot only when every slot is draining at once (it recycles once it
|
|
258
|
+
// finally drains, so this cannot starve the recycling).
|
|
259
|
+
const candidates = this.slots.filter((slot) => !slot.draining);
|
|
260
|
+
const eligible = candidates.length > 0 ? candidates : this.slots;
|
|
261
|
+
return eligible.reduce((least, slot) =>
|
|
262
|
+
slot.inflight < least.inflight ? slot : least,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private onSettled(slot: Slot): void {
|
|
267
|
+
slot.inflight--;
|
|
268
|
+
if (this.terminated) return;
|
|
269
|
+
slot.tasksSinceSpawn++;
|
|
270
|
+
if (slot.tasksSinceSpawn >= RECYCLE_AFTER_TASKS_BACKSTOP) {
|
|
271
|
+
slot.draining = true;
|
|
272
|
+
}
|
|
273
|
+
// Terminating the worker must not kill in-flight tasks, so a slot over
|
|
274
|
+
// its budget stops receiving tasks (pickSlot skips it) and recycles at
|
|
275
|
+
// its next task boundary instead of immediately.
|
|
276
|
+
if (slot.draining && slot.inflight === 0) {
|
|
277
|
+
this.recycleSlot(slot);
|
|
278
|
+
} else if (!slot.draining) {
|
|
279
|
+
this.probeSlot(slot);
|
|
280
|
+
}
|
|
281
|
+
this.armIdleSweep();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private recycleSlot(slot: Slot): void {
|
|
285
|
+
slot.pool.terminate();
|
|
286
|
+
slot.pool = createPool(this.url);
|
|
287
|
+
slot.tasksSinceSpawn = 0;
|
|
288
|
+
slot.draining = false;
|
|
289
|
+
slot.lastHeapBytes = undefined;
|
|
290
|
+
// Fire-and-forget; a warm-up failure surfaces on the next real task.
|
|
291
|
+
this.warmSlot(slot).catch(() => undefined);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Warm-ups and probes are internal traffic: they count into `inflight` (to
|
|
295
|
+
// steer dispatch away from a busy worker) but not into `tasksSinceSpawn`,
|
|
296
|
+
// and their settlement doesn't re-arm the idle sweep, so a sweep can't
|
|
297
|
+
// perpetuate itself through the work it triggers.
|
|
298
|
+
|
|
299
|
+
private warmSlot(slot: Slot): Promise<void> {
|
|
300
|
+
slot.inflight++;
|
|
301
|
+
const settle = () => {
|
|
302
|
+
slot.inflight--;
|
|
303
|
+
// Same as the probe: if the slot drained while this warm-up was in
|
|
304
|
+
// flight, its settlement is the last task boundary, so recycle here.
|
|
305
|
+
if (!this.terminated && slot.draining && slot.inflight === 0) {
|
|
306
|
+
this.recycleSlot(slot);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
// Wrapped in a native promise so that a synchronous exec failure rejects
|
|
310
|
+
// instead of throwing through the recycle path.
|
|
311
|
+
return new Promise((resolve, reject) => {
|
|
312
|
+
try {
|
|
313
|
+
slot.pool
|
|
314
|
+
.exec("warmUp")
|
|
315
|
+
.then(() => {
|
|
316
|
+
settle();
|
|
317
|
+
resolve();
|
|
318
|
+
})
|
|
319
|
+
.catch((e) => {
|
|
320
|
+
settle();
|
|
321
|
+
reject(e);
|
|
322
|
+
});
|
|
323
|
+
} catch (e) {
|
|
324
|
+
settle();
|
|
325
|
+
reject(e);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// The heap probe is the recycle trigger: after each settled task the
|
|
331
|
+
// worker reports its actual linear memory size, which task-count or
|
|
332
|
+
// input-size heuristics could only approximate. A probe rides the slot's
|
|
333
|
+
// queue, so under load the measurement lags by the tasks queued ahead of
|
|
334
|
+
// it — acceptable since upstream backpressure keeps queues shallow.
|
|
335
|
+
private probeSlot(slot: Slot): void {
|
|
336
|
+
if (slot.probing) return;
|
|
337
|
+
slot.probing = true;
|
|
338
|
+
slot.inflight++;
|
|
339
|
+
const settle = (overBudget: boolean) => {
|
|
340
|
+
slot.probing = false;
|
|
341
|
+
slot.inflight--;
|
|
342
|
+
if (this.terminated) return;
|
|
343
|
+
if (overBudget) {
|
|
344
|
+
slot.draining = true;
|
|
345
|
+
}
|
|
346
|
+
// Also covers a slot drained by the backstop while this probe was in
|
|
347
|
+
// flight: the probe is then the last thing keeping the slot alive, so
|
|
348
|
+
// its settlement must trigger the recycle even when under budget.
|
|
349
|
+
if (slot.draining && slot.inflight === 0) {
|
|
350
|
+
this.recycleSlot(slot);
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
try {
|
|
354
|
+
slot.pool
|
|
355
|
+
.exec("getWasmMemoryUsage")
|
|
356
|
+
.then((bytes) => {
|
|
357
|
+
if (typeof bytes === "number") {
|
|
358
|
+
slot.lastHeapBytes = bytes;
|
|
359
|
+
}
|
|
360
|
+
settle(typeof bytes === "number" && bytes >= this.maxWorkerHeapBytes);
|
|
361
|
+
})
|
|
362
|
+
.catch(() => {
|
|
363
|
+
// A worker that cannot even report its heap is recycled conservatively.
|
|
364
|
+
settle(true);
|
|
365
|
+
});
|
|
366
|
+
} catch {
|
|
367
|
+
settle(true);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private armIdleSweep(): void {
|
|
372
|
+
// Re-armed on every settled task, so it fires RECYCLE_IDLE_MS after the
|
|
373
|
+
// last one settles; slots that are still busy at that point are swept
|
|
374
|
+
// when their remaining work settles and re-arms it.
|
|
375
|
+
if (this.idleSweepTimer !== undefined) clearTimeout(this.idleSweepTimer);
|
|
376
|
+
this.idleSweepTimer = setTimeout(() => {
|
|
377
|
+
this.idleSweepTimer = undefined;
|
|
378
|
+
for (const slot of this.slots) {
|
|
379
|
+
if (slot.tasksSinceSpawn > 0 && slot.inflight === 0) {
|
|
380
|
+
this.recycleSlot(slot);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}, RECYCLE_IDLE_MS);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PolygonMaterialLike,
|
|
3
|
+
TransferablePolygonBatchedFeatureLike,
|
|
4
|
+
ConstructedPolygonGeometryLike,
|
|
5
|
+
ExtentRadianF32Like,
|
|
6
|
+
} from "@navaramap/core";
|
|
7
|
+
import { constructPolygonBatchedFeature as constructPolygonBatchedFeatureImpl } from "@navaramap/engine-worker";
|
|
8
|
+
|
|
9
|
+
import { transfer } from "..";
|
|
10
|
+
import { transferConstructedPolygonGeometry } from "../helpers/transferConstructedPolygonGeometry";
|
|
11
|
+
import { toExtentRadianF32Like } from "../utils";
|
|
12
|
+
import { toPolygonMaterialLike } from "../utils/toPolygonMaterialLike";
|
|
13
|
+
import { toTransferablePolygonBatchedFeatureLike } from "../utils/toTransferablePolygonBatchedFeatureLike";
|
|
14
|
+
|
|
15
|
+
import { waitWasm } from "./waitWasm";
|
|
16
|
+
|
|
17
|
+
export async function constructPolygonBatchedFeature(
|
|
18
|
+
transferableBatchedFeatureLike: TransferablePolygonBatchedFeatureLike,
|
|
19
|
+
materialLike: PolygonMaterialLike,
|
|
20
|
+
flat: boolean,
|
|
21
|
+
tile_extent: ExtentRadianF32Like | undefined,
|
|
22
|
+
): Promise<ConstructedPolygonGeometryLike | undefined> {
|
|
23
|
+
await waitWasm();
|
|
24
|
+
|
|
25
|
+
const geometry = constructPolygonBatchedFeatureImpl(
|
|
26
|
+
toTransferablePolygonBatchedFeatureLike(transferableBatchedFeatureLike),
|
|
27
|
+
toPolygonMaterialLike(materialLike),
|
|
28
|
+
flat,
|
|
29
|
+
tile_extent ? toExtentRadianF32Like(tile_extent) : undefined,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (!geometry) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const { result, transfers } = transferConstructedPolygonGeometry(geometry);
|
|
37
|
+
geometry.free();
|
|
38
|
+
|
|
39
|
+
return transfer(result, transfers);
|
|
40
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TransferablePolylineBatchedFeatureLike,
|
|
3
|
+
PolylineMaterialLike,
|
|
4
|
+
ConstructedPolylineGeometryLike,
|
|
5
|
+
} from "@navaramap/core";
|
|
6
|
+
import { constructPolylineBatchedFeature as constructPolylineBatchedFeatureImpl } from "@navaramap/engine-worker";
|
|
7
|
+
|
|
8
|
+
import { transfer } from "..";
|
|
9
|
+
import { transferConstructedPolylineGeometry } from "../helpers/transferConstructedPolylineGeometry";
|
|
10
|
+
import { toPolylineMaterialLike } from "../utils/toPolylineMaterialLike";
|
|
11
|
+
import { toTransferablePolylineBatchedFeatureLike } from "../utils/toTransferablePolylineBatchedFeatureLike";
|
|
12
|
+
|
|
13
|
+
import { waitWasm } from "./waitWasm";
|
|
14
|
+
|
|
15
|
+
export async function constructPolylineBatchedFeature(
|
|
16
|
+
transferableBatchedFeatureLike: TransferablePolylineBatchedFeatureLike,
|
|
17
|
+
materialLike: PolylineMaterialLike,
|
|
18
|
+
flat: boolean,
|
|
19
|
+
): Promise<ConstructedPolylineGeometryLike | undefined> {
|
|
20
|
+
await waitWasm();
|
|
21
|
+
|
|
22
|
+
const geometry = constructPolylineBatchedFeatureImpl(
|
|
23
|
+
toTransferablePolylineBatchedFeatureLike(transferableBatchedFeatureLike),
|
|
24
|
+
toPolylineMaterialLike(materialLike),
|
|
25
|
+
flat,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (!geometry) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { result, transfers } = transferConstructedPolylineGeometry(geometry);
|
|
33
|
+
geometry.free();
|
|
34
|
+
|
|
35
|
+
return transfer(result, transfers);
|
|
36
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ReturnedConstructedTerrainMeshLike,
|
|
3
|
+
TransferableTileLike,
|
|
4
|
+
} from "@navaramap/core";
|
|
5
|
+
import { constructQuantizedMeshTerrainMesh as constructQuantizedMeshTerrainMeshImpl } from "@navaramap/engine-worker";
|
|
6
|
+
|
|
7
|
+
import { transfer } from "..";
|
|
8
|
+
import { transferReturnedConstructedTerrainMesh } from "../helpers/transferReturnedConstructedTerrainMesh";
|
|
9
|
+
import { toTransferableTile } from "../utils";
|
|
10
|
+
|
|
11
|
+
import { waitWasm } from "./waitWasm";
|
|
12
|
+
|
|
13
|
+
export async function constructQuantizedMeshTerrainMesh(
|
|
14
|
+
bytes: Uint8Array,
|
|
15
|
+
tile: TransferableTileLike,
|
|
16
|
+
skirt: boolean,
|
|
17
|
+
skirtExaggeration: number,
|
|
18
|
+
geographic: boolean,
|
|
19
|
+
tms: boolean,
|
|
20
|
+
): Promise<{
|
|
21
|
+
result: ReturnedConstructedTerrainMeshLike;
|
|
22
|
+
}> {
|
|
23
|
+
await waitWasm();
|
|
24
|
+
|
|
25
|
+
const mesh = constructQuantizedMeshTerrainMeshImpl(
|
|
26
|
+
bytes,
|
|
27
|
+
toTransferableTile(tile),
|
|
28
|
+
skirt,
|
|
29
|
+
skirtExaggeration,
|
|
30
|
+
geographic,
|
|
31
|
+
tms,
|
|
32
|
+
);
|
|
33
|
+
const { result, transfers } = transferReturnedConstructedTerrainMesh(mesh);
|
|
34
|
+
mesh.free();
|
|
35
|
+
return transfer({ result }, [...transfers]);
|
|
36
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
TransferableTileLike,
|
|
3
|
+
TransferableRasterDEMDataLike,
|
|
4
|
+
ReturnedConstructedTerrainMeshLike,
|
|
5
|
+
} from "@navaramap/core";
|
|
6
|
+
import {
|
|
7
|
+
constructTerrainMesh as constructTerrainMeshImpl,
|
|
8
|
+
TransferableMartini,
|
|
9
|
+
} from "@navaramap/engine-worker";
|
|
10
|
+
|
|
11
|
+
import { transfer } from "..";
|
|
12
|
+
import { transferReturnedConstructedTerrainMesh } from "../helpers/transferReturnedConstructedTerrainMesh";
|
|
13
|
+
import { toTransferableTile } from "../utils";
|
|
14
|
+
import { toTransferableRasterDEMDataLike } from "../utils/toTransferableRasterDEMDataLike";
|
|
15
|
+
|
|
16
|
+
import { waitWasm } from "./waitWasm";
|
|
17
|
+
|
|
18
|
+
export async function constructTerrainMesh(
|
|
19
|
+
bytes: Uint8Array,
|
|
20
|
+
tile: TransferableTileLike,
|
|
21
|
+
rasterDEMData: TransferableRasterDEMDataLike,
|
|
22
|
+
size: number,
|
|
23
|
+
skirt: boolean,
|
|
24
|
+
skirtExaggeration: number,
|
|
25
|
+
): Promise<{
|
|
26
|
+
result: ReturnedConstructedTerrainMeshLike;
|
|
27
|
+
}> {
|
|
28
|
+
await waitWasm();
|
|
29
|
+
|
|
30
|
+
const tileSizeWithBoundary = size + 1;
|
|
31
|
+
const martini = TransferableMartini.fromSize(tileSizeWithBoundary);
|
|
32
|
+
|
|
33
|
+
const mesh = constructTerrainMeshImpl(
|
|
34
|
+
bytes,
|
|
35
|
+
toTransferableTile(tile),
|
|
36
|
+
toTransferableRasterDEMDataLike(rasterDEMData),
|
|
37
|
+
martini,
|
|
38
|
+
skirt,
|
|
39
|
+
skirtExaggeration,
|
|
40
|
+
);
|
|
41
|
+
const { result, transfers } = transferReturnedConstructedTerrainMesh(mesh);
|
|
42
|
+
mesh.free();
|
|
43
|
+
return transfer({ result }, [...transfers]);
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { transfer } from "..";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Decode a compressed image Blob into raw RGBA pixels entirely off the main
|
|
5
|
+
* thread. Running `createImageBitmap` inside the worker keeps the (potentially
|
|
6
|
+
* expensive) image decode off the main thread, which matters for DEM /
|
|
7
|
+
* elevation tiles that stream in large numbers while panning and zooming.
|
|
8
|
+
*
|
|
9
|
+
* The pixel buffer is transferred back to the caller (not structured-cloned),
|
|
10
|
+
* so the worker→main hand-off moves ownership instead of copying width*height*4
|
|
11
|
+
* bytes. The Blob argument is already cheap to pass in: its structured clone
|
|
12
|
+
* shares the immutable backing bytes by reference, and Blob is not transferable.
|
|
13
|
+
*/
|
|
14
|
+
export async function getImageDataFromBlob(
|
|
15
|
+
blob: Blob,
|
|
16
|
+
): Promise<Uint8ClampedArray> {
|
|
17
|
+
const bitmap = await createImageBitmap(blob);
|
|
18
|
+
try {
|
|
19
|
+
const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
20
|
+
const context = canvas.getContext("2d");
|
|
21
|
+
if (context === null) {
|
|
22
|
+
throw new Error("failed to get context of canvas");
|
|
23
|
+
}
|
|
24
|
+
context.drawImage(bitmap, 0, 0);
|
|
25
|
+
const data = context.getImageData(0, 0, bitmap.width, bitmap.height).data;
|
|
26
|
+
return transfer(data, [data.buffer]);
|
|
27
|
+
} finally {
|
|
28
|
+
bitmap.close();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export async function getImageDataFromImageBitmap(
|
|
2
|
+
img: ImageBitmap,
|
|
3
|
+
canvas: OffscreenCanvas,
|
|
4
|
+
): Promise<Uint8ClampedArray> {
|
|
5
|
+
try {
|
|
6
|
+
const context = canvas.getContext("2d");
|
|
7
|
+
if (context === null) {
|
|
8
|
+
throw new Error("failed to get context of canvas");
|
|
9
|
+
} else {
|
|
10
|
+
context.drawImage(img, 0, 0);
|
|
11
|
+
}
|
|
12
|
+
const data = context.getImageData(0, 0, img.width, img.height).data;
|
|
13
|
+
return data;
|
|
14
|
+
} finally {
|
|
15
|
+
// The bitmap is transferred into this worker; close it so its backing
|
|
16
|
+
// store is released immediately instead of waiting for GC.
|
|
17
|
+
img.close();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { constructPolygonBatchedFeature } from "./constructPolygonBatchedFeature";
|
|
2
|
+
import { constructPolylineBatchedFeature } from "./constructPolylineBatchedFeature";
|
|
3
|
+
import { constructQuantizedMeshTerrainMesh } from "./constructQuantizedMeshTerrainMesh";
|
|
4
|
+
import { constructTerrainMesh } from "./constructTerrainMesh";
|
|
5
|
+
import { getImageDataFromBlob } from "./getImageDataFromBlob";
|
|
6
|
+
import { getImageDataFromImageBitmap } from "./getImageDataFromImageBitmap";
|
|
7
|
+
import { parseMvtTile } from "./parseMvtTile";
|
|
8
|
+
import { upsampleQuantizedMeshTerrainMesh } from "./upsampleQuantizedMeshTerrainMesh";
|
|
9
|
+
import { upsampleTerrainMesh } from "./upsampleTerrainMesh";
|
|
10
|
+
import { getWasmMemoryUsage, waitWasm } from "./waitWasm";
|
|
11
|
+
|
|
12
|
+
/** Pre-warm worker by initializing WASM module */
|
|
13
|
+
export async function warmUp(): Promise<void> {
|
|
14
|
+
await waitWasm();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const commonTasks = {
|
|
18
|
+
constructTerrainMesh,
|
|
19
|
+
constructQuantizedMeshTerrainMesh,
|
|
20
|
+
upsampleTerrainMesh,
|
|
21
|
+
upsampleQuantizedMeshTerrainMesh,
|
|
22
|
+
getImageDataFromImageBitmap,
|
|
23
|
+
getImageDataFromBlob,
|
|
24
|
+
constructPolygonBatchedFeature,
|
|
25
|
+
constructPolylineBatchedFeature,
|
|
26
|
+
parseMvtTile,
|
|
27
|
+
warmUp,
|
|
28
|
+
getWasmMemoryUsage,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type CommonTasks = typeof commonTasks;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ExtentRadianF32Like } from "@navaramap/core";
|
|
2
|
+
import { parseMvtTile as parseMvtTileImpl } from "@navaramap/engine-worker";
|
|
3
|
+
|
|
4
|
+
import { transfer } from "..";
|
|
5
|
+
import { toExtentRadianF32Like } from "../utils";
|
|
6
|
+
|
|
7
|
+
import { waitWasm } from "./waitWasm";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A whole tile's parsed groups packed into four per-type streams (see
|
|
11
|
+
* `navara_parser::mvt::pack_parsed_mvt_groups` for the segment order). The
|
|
12
|
+
* tile meta (per-group headers + per-layer property tables) rides along as a
|
|
13
|
+
* structured clone; only the four stream buffers are transferred.
|
|
14
|
+
*/
|
|
15
|
+
export type ParsedMvtTileResultLike = {
|
|
16
|
+
f64_stream: Float64Array;
|
|
17
|
+
f32_stream: Float32Array;
|
|
18
|
+
u32_stream: Uint32Array;
|
|
19
|
+
u8_stream: Uint8Array;
|
|
20
|
+
meta: unknown;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export async function parseMvtTile(
|
|
24
|
+
bytes: Uint8Array,
|
|
25
|
+
x: number,
|
|
26
|
+
y: number,
|
|
27
|
+
z: number,
|
|
28
|
+
tileExtent: ExtentRadianF32Like | undefined,
|
|
29
|
+
compression: number,
|
|
30
|
+
configsJson: string,
|
|
31
|
+
): Promise<ParsedMvtTileResultLike> {
|
|
32
|
+
await waitWasm();
|
|
33
|
+
|
|
34
|
+
const packed = parseMvtTileImpl(
|
|
35
|
+
bytes,
|
|
36
|
+
x,
|
|
37
|
+
y,
|
|
38
|
+
z,
|
|
39
|
+
tileExtent ? toExtentRadianF32Like(tileExtent) : undefined,
|
|
40
|
+
compression,
|
|
41
|
+
configsJson,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// Free the wasm-bindgen result even when a stream getter throws (e.g. an
|
|
45
|
+
// allocation failure), or its Rust-side Vecs would leak in worker WASM memory.
|
|
46
|
+
let f64_stream: Float64Array;
|
|
47
|
+
let f32_stream: Float32Array;
|
|
48
|
+
let u32_stream: Uint32Array;
|
|
49
|
+
let u8_stream: Uint8Array;
|
|
50
|
+
let meta: unknown;
|
|
51
|
+
try {
|
|
52
|
+
f64_stream = packed.f64_stream();
|
|
53
|
+
f32_stream = packed.f32_stream();
|
|
54
|
+
u32_stream = packed.u32_stream();
|
|
55
|
+
u8_stream = packed.u8_stream();
|
|
56
|
+
meta = packed.meta();
|
|
57
|
+
} finally {
|
|
58
|
+
packed.free();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return transfer({ f64_stream, f32_stream, u32_stream, u8_stream, meta }, [
|
|
62
|
+
f64_stream.buffer,
|
|
63
|
+
f32_stream.buffer,
|
|
64
|
+
u32_stream.buffer,
|
|
65
|
+
u8_stream.buffer,
|
|
66
|
+
]);
|
|
67
|
+
}
|