@czap/worker 0.1.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 +21 -0
- package/README.md +19 -0
- package/dist/compositor-script.d.ts +19 -0
- package/dist/compositor-script.d.ts.map +1 -0
- package/dist/compositor-script.js +374 -0
- package/dist/compositor-script.js.map +1 -0
- package/dist/compositor-startup.d.ts +200 -0
- package/dist/compositor-startup.d.ts.map +1 -0
- package/dist/compositor-startup.js +490 -0
- package/dist/compositor-startup.js.map +1 -0
- package/dist/compositor-types.d.ts +135 -0
- package/dist/compositor-types.d.ts.map +1 -0
- package/dist/compositor-types.js +7 -0
- package/dist/compositor-types.js.map +1 -0
- package/dist/compositor-worker.d.ts +65 -0
- package/dist/compositor-worker.d.ts.map +1 -0
- package/dist/compositor-worker.js +454 -0
- package/dist/compositor-worker.js.map +1 -0
- package/dist/evaluate-inline.d.ts +22 -0
- package/dist/evaluate-inline.d.ts.map +1 -0
- package/dist/evaluate-inline.js +42 -0
- package/dist/evaluate-inline.js.map +1 -0
- package/dist/host.d.ts +97 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +115 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/messages.d.ts +254 -0
- package/dist/messages.d.ts.map +1 -0
- package/dist/messages.js +55 -0
- package/dist/messages.js.map +1 -0
- package/dist/render-worker.d.ts +77 -0
- package/dist/render-worker.d.ts.map +1 -0
- package/dist/render-worker.js +396 -0
- package/dist/render-worker.js.map +1 -0
- package/dist/spsc-ring.d.ts +171 -0
- package/dist/spsc-ring.d.ts.map +1 -0
- package/dist/spsc-ring.js +240 -0
- package/dist/spsc-ring.js.map +1 -0
- package/package.json +51 -0
- package/src/compositor-script.ts +374 -0
- package/src/compositor-startup.ts +666 -0
- package/src/compositor-types.ts +175 -0
- package/src/compositor-worker.ts +613 -0
- package/src/evaluate-inline.ts +42 -0
- package/src/host.ts +189 -0
- package/src/index.ts +49 -0
- package/src/messages.ts +343 -0
- package/src/render-worker.ts +454 -0
- package/src/spsc-ring.ts +309 -0
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup packet building, startup mode management, and compositor lease
|
|
3
|
+
* lifecycle helpers for the CompositorWorker module.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { RuntimeCoordinator } from '@czap/core';
|
|
9
|
+
import type {
|
|
10
|
+
ToWorkerMessage,
|
|
11
|
+
WorkerUpdate,
|
|
12
|
+
BootstrapQuantizerRegistration,
|
|
13
|
+
StartupComputePacket,
|
|
14
|
+
ResolvedStateEntry,
|
|
15
|
+
} from './messages.js';
|
|
16
|
+
import type {
|
|
17
|
+
CompositorWorkerStartupTelemetry,
|
|
18
|
+
CompositorWorkerStartupDiagnosticStage,
|
|
19
|
+
ResolvedStateAckPayload,
|
|
20
|
+
StandbyCompositorLease,
|
|
21
|
+
StartupPacketState,
|
|
22
|
+
} from './compositor-types.js';
|
|
23
|
+
import { COMPOSITOR_WORKER_SCRIPT } from './compositor-script.js';
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Module-level cached state
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
let cachedCompositorWorkerUrl: string | null = null;
|
|
30
|
+
let cachedCreateObjectUrl: typeof URL.createObjectURL | null = null;
|
|
31
|
+
let cleanupRegistered = false;
|
|
32
|
+
let standbyCompositorLease: StandbyCompositorLease | null = null;
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Timing helpers
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Return the current high-resolution wall-clock time in nanoseconds.
|
|
40
|
+
*
|
|
41
|
+
* Uses `performance.now()` when available; falls back to `Date.now()`
|
|
42
|
+
* in environments without the performance timeline.
|
|
43
|
+
*/
|
|
44
|
+
export function currentTimeNs(): number {
|
|
45
|
+
const currentTimeMs = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
46
|
+
return currentTimeMs * 1e6;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Forward a fine-grained startup-diagnostic duration sample to a
|
|
51
|
+
* telemetry sink (if the sink opts into diagnostic stages).
|
|
52
|
+
*
|
|
53
|
+
* Safe to call when `telemetry` is undefined or does not implement
|
|
54
|
+
* `recordDiagnosticStage` -- the call becomes a no-op.
|
|
55
|
+
*/
|
|
56
|
+
export function recordStartupDiagnosticStage(
|
|
57
|
+
telemetry: CompositorWorkerStartupTelemetry | undefined,
|
|
58
|
+
stage: CompositorWorkerStartupDiagnosticStage,
|
|
59
|
+
durationNs: number,
|
|
60
|
+
): void {
|
|
61
|
+
const recordDiagnosticStage = (
|
|
62
|
+
telemetry as
|
|
63
|
+
| (CompositorWorkerStartupTelemetry & {
|
|
64
|
+
readonly recordDiagnosticStage?: (
|
|
65
|
+
diagnosticStage: CompositorWorkerStartupDiagnosticStage,
|
|
66
|
+
diagnosticDurationNs: number,
|
|
67
|
+
) => void;
|
|
68
|
+
})
|
|
69
|
+
| undefined
|
|
70
|
+
)?.recordDiagnosticStage;
|
|
71
|
+
|
|
72
|
+
recordDiagnosticStage?.(stage, durationNs);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Notify a telemetry sink that the worker acknowledged a resolved-state
|
|
77
|
+
* hydration. Safe to call when the sink does not implement
|
|
78
|
+
* `onResolvedStateSettled`.
|
|
79
|
+
*/
|
|
80
|
+
export function notifyResolvedStateSettled(
|
|
81
|
+
telemetry: CompositorWorkerStartupTelemetry | undefined,
|
|
82
|
+
states: readonly ResolvedStateEntry[],
|
|
83
|
+
): void {
|
|
84
|
+
const onResolvedStateSettled = (
|
|
85
|
+
telemetry as
|
|
86
|
+
| (CompositorWorkerStartupTelemetry & {
|
|
87
|
+
readonly onResolvedStateSettled?: (settledStates: readonly ResolvedStateEntry[]) => void;
|
|
88
|
+
})
|
|
89
|
+
| undefined
|
|
90
|
+
)?.onResolvedStateSettled;
|
|
91
|
+
|
|
92
|
+
onResolvedStateSettled?.(states);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// Startup packet state management
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Project a set of bootstrap registrations down to the minimal
|
|
101
|
+
* `{ name, states }` shape the runtime coordinator needs to seed its
|
|
102
|
+
* quantizer registry.
|
|
103
|
+
*/
|
|
104
|
+
export function registrationsToRuntimeSeed(registrations: readonly BootstrapQuantizerRegistration[]): readonly {
|
|
105
|
+
readonly name: string;
|
|
106
|
+
readonly states: readonly string[];
|
|
107
|
+
}[] {
|
|
108
|
+
return registrations.map((registration) => ({
|
|
109
|
+
name: registration.name,
|
|
110
|
+
states: registration.states,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Build a fresh {@link StartupPacketState} seeded with an initial
|
|
116
|
+
* bootstrap mode and registration list. Used by the compositor worker to
|
|
117
|
+
* stage messages before flushing them in a single `startup-compute` post.
|
|
118
|
+
*/
|
|
119
|
+
export function createStartupPacketState(
|
|
120
|
+
bootstrapMode: StartupComputePacket['bootstrapMode'],
|
|
121
|
+
initialRegistrations: readonly BootstrapQuantizerRegistration[] = [],
|
|
122
|
+
): StartupPacketState {
|
|
123
|
+
return {
|
|
124
|
+
bootstrapMode,
|
|
125
|
+
registrations: new Map(initialRegistrations.map((registration) => [registration.name, registration] as const)),
|
|
126
|
+
registrationList: initialRegistrations.length > 0 ? [...initialRegistrations] : [],
|
|
127
|
+
runtimeSeedList: initialRegistrations.length > 0 ? registrationsToRuntimeSeed(initialRegistrations) : [],
|
|
128
|
+
updates: [],
|
|
129
|
+
runtimeSeedDirty: false,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Snapshot a {@link StartupPacketState} into an immutable
|
|
135
|
+
* {@link StartupComputePacket} suitable for `postMessage`.
|
|
136
|
+
*/
|
|
137
|
+
export function buildStartupComputePacket(packet: StartupPacketState): StartupComputePacket {
|
|
138
|
+
const builtPacket = {
|
|
139
|
+
bootstrapMode: packet.bootstrapMode,
|
|
140
|
+
registrations: getStartupPacketRegistrations(packet),
|
|
141
|
+
updates: packet.updates,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return builtPacket;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Return the ordered list of registrations in the startup packet,
|
|
149
|
+
* caching the result so repeated reads are O(1).
|
|
150
|
+
*/
|
|
151
|
+
export function getStartupPacketRegistrations(packet: StartupPacketState): readonly BootstrapQuantizerRegistration[] {
|
|
152
|
+
if (packet.registrationList !== null) {
|
|
153
|
+
return packet.registrationList;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
packet.registrationList = Array.from(packet.registrations.values());
|
|
157
|
+
return packet.registrationList;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Return the runtime-seed projection of the startup packet's
|
|
162
|
+
* registrations, recomputing on demand if invalidated.
|
|
163
|
+
*/
|
|
164
|
+
export function getStartupPacketRuntimeSeed(packet: StartupPacketState): readonly {
|
|
165
|
+
readonly name: string;
|
|
166
|
+
readonly states: readonly string[];
|
|
167
|
+
}[] {
|
|
168
|
+
if (packet.runtimeSeedList !== null && !packet.runtimeSeedDirty) {
|
|
169
|
+
return packet.runtimeSeedList;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
packet.runtimeSeedList = registrationsToRuntimeSeed(getStartupPacketRegistrations(packet));
|
|
173
|
+
packet.runtimeSeedDirty = false;
|
|
174
|
+
return packet.runtimeSeedList;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Return `true` when the given runtime coordinator already has every
|
|
179
|
+
* quantizer referenced by the runtime seed registered (by name).
|
|
180
|
+
*
|
|
181
|
+
* Used to decide whether a pre-warmed lease's runtime can be reused
|
|
182
|
+
* as-is or must be reset before replay.
|
|
183
|
+
*/
|
|
184
|
+
export function runtimeMatchesStartupSeed(
|
|
185
|
+
runtime: RuntimeCoordinator.Shape,
|
|
186
|
+
runtimeSeed: readonly {
|
|
187
|
+
readonly name: string;
|
|
188
|
+
readonly states: readonly string[];
|
|
189
|
+
}[],
|
|
190
|
+
): boolean {
|
|
191
|
+
const registeredNames = runtime.registeredNames();
|
|
192
|
+
if (
|
|
193
|
+
!sameArray(
|
|
194
|
+
registeredNames,
|
|
195
|
+
runtimeSeed.map((registration) => registration.name),
|
|
196
|
+
)
|
|
197
|
+
) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const registration of runtimeSeed) {
|
|
202
|
+
if (!runtime.hasQuantizer(registration.name)) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Insert or overwrite a registration in the startup packet, invalidating
|
|
212
|
+
* derived caches. Pass `invalidateRuntimeSeed: false` when the caller
|
|
213
|
+
* already knows the runtime seed is structurally unchanged (e.g. only
|
|
214
|
+
* initial state or blend weights changed).
|
|
215
|
+
*/
|
|
216
|
+
export function setStartupPacketRegistration(
|
|
217
|
+
packet: StartupPacketState,
|
|
218
|
+
registration: BootstrapQuantizerRegistration,
|
|
219
|
+
invalidateRuntimeSeed = true,
|
|
220
|
+
): void {
|
|
221
|
+
packet.registrations.set(registration.name, registration);
|
|
222
|
+
packet.registrationList = null;
|
|
223
|
+
if (invalidateRuntimeSeed) {
|
|
224
|
+
packet.runtimeSeedList = null;
|
|
225
|
+
packet.runtimeSeedDirty = true;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Drop a registration by name from the startup packet and invalidate
|
|
231
|
+
* derived caches.
|
|
232
|
+
*/
|
|
233
|
+
export function removeStartupPacketRegistration(packet: StartupPacketState, name: string): void {
|
|
234
|
+
packet.registrations.delete(name);
|
|
235
|
+
packet.registrationList = null;
|
|
236
|
+
packet.runtimeSeedList = null;
|
|
237
|
+
packet.runtimeSeedDirty = true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Queue a {@link WorkerUpdate} to be replayed after bootstrap. Order is
|
|
242
|
+
* preserved to match main-thread issue order.
|
|
243
|
+
*/
|
|
244
|
+
export function pushStartupPacketUpdate(packet: StartupPacketState, update: WorkerUpdate): void {
|
|
245
|
+
packet.updates.push(update);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Filter the packet's pending update queue in-place. Typically used to
|
|
250
|
+
* drop redundant updates (e.g. newer `set-blend` supersedes older ones).
|
|
251
|
+
*/
|
|
252
|
+
export function filterStartupPacketUpdates(packet: StartupPacketState, keep: (update: WorkerUpdate) => boolean): void {
|
|
253
|
+
if (packet.updates.length === 0) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const filtered = packet.updates.filter(keep);
|
|
257
|
+
if (filtered.length !== packet.updates.length) {
|
|
258
|
+
packet.updates = filtered;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Structural equality check for `Record<string, number>` blend-weight
|
|
264
|
+
* maps. `undefined === undefined` is true; mismatched presence is false.
|
|
265
|
+
*/
|
|
266
|
+
export function sameNumericRecord(
|
|
267
|
+
left: Record<string, number> | undefined,
|
|
268
|
+
right: Record<string, number> | undefined,
|
|
269
|
+
): boolean {
|
|
270
|
+
if (left === right) {
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!left || !right) {
|
|
275
|
+
return left === right;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const leftKeys = Object.keys(left);
|
|
279
|
+
const rightKeys = Object.keys(right);
|
|
280
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
for (const key of leftKeys) {
|
|
285
|
+
if (left[key] !== right[key]) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Merge an updated initial-state assignment into an existing registration.
|
|
295
|
+
* Also scrubs any queued `evaluate` update targeting the same quantizer,
|
|
296
|
+
* since the new initial state supersedes it.
|
|
297
|
+
*/
|
|
298
|
+
export function setStartupPacketInitialState(
|
|
299
|
+
packet: StartupPacketState,
|
|
300
|
+
registration: BootstrapQuantizerRegistration,
|
|
301
|
+
state: string,
|
|
302
|
+
): void {
|
|
303
|
+
const currentRegistration = packet.registrations.get(registration.name)!;
|
|
304
|
+
|
|
305
|
+
const defaultState = currentRegistration.states[0];
|
|
306
|
+
const nextRegistration =
|
|
307
|
+
state === defaultState
|
|
308
|
+
? (() => {
|
|
309
|
+
const { initialState: _initialState, ...withoutInitialState } = currentRegistration;
|
|
310
|
+
return withoutInitialState;
|
|
311
|
+
})()
|
|
312
|
+
: { ...currentRegistration, initialState: state };
|
|
313
|
+
const nextInitialState = 'initialState' in nextRegistration ? nextRegistration.initialState : undefined;
|
|
314
|
+
if (
|
|
315
|
+
currentRegistration.boundaryId !== nextRegistration.boundaryId ||
|
|
316
|
+
!sameArray(currentRegistration.states, nextRegistration.states) ||
|
|
317
|
+
!sameArray(currentRegistration.thresholds, nextRegistration.thresholds) ||
|
|
318
|
+
currentRegistration.initialState !== nextInitialState ||
|
|
319
|
+
!sameNumericRecord(currentRegistration.blendWeights, nextRegistration.blendWeights)
|
|
320
|
+
) {
|
|
321
|
+
setStartupPacketRegistration(packet, nextRegistration, false);
|
|
322
|
+
}
|
|
323
|
+
filterStartupPacketUpdates(packet, (update) => !(update.type === 'evaluate' && update.name === registration.name));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Merge updated blend weights for an existing registration. Returns
|
|
328
|
+
* `false` when no registration with that name is present (the update is
|
|
329
|
+
* ignored). Scrubs superseded `set-blend` updates from the queue.
|
|
330
|
+
*/
|
|
331
|
+
export function setStartupPacketBlendWeights(
|
|
332
|
+
packet: StartupPacketState,
|
|
333
|
+
name: string,
|
|
334
|
+
weights: Record<string, number>,
|
|
335
|
+
): boolean {
|
|
336
|
+
const registration = packet.registrations.get(name);
|
|
337
|
+
if (!registration) {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!sameNumericRecord(registration.blendWeights, weights)) {
|
|
342
|
+
setStartupPacketRegistration(
|
|
343
|
+
packet,
|
|
344
|
+
{
|
|
345
|
+
...registration,
|
|
346
|
+
blendWeights: weights,
|
|
347
|
+
},
|
|
348
|
+
false,
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
filterStartupPacketUpdates(packet, (update) => !(update.type === 'set-blend' && update.name === name));
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Remove a registration and every pending update targeting it.
|
|
357
|
+
* Equivalent to undoing `add-quantizer` + any in-flight mutations.
|
|
358
|
+
*/
|
|
359
|
+
export function removeStartupPacketEntries(packet: StartupPacketState, name: string): void {
|
|
360
|
+
removeStartupPacketRegistration(packet, name);
|
|
361
|
+
if (packet.updates.length === 0) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const filtered = packet.updates.filter((update) => update.name !== name);
|
|
366
|
+
if (filtered.length !== packet.updates.length) {
|
|
367
|
+
packet.updates = filtered;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Clear all transient state on a startup packet, leaving only the
|
|
373
|
+
* `bootstrapMode` in place. Used when the lease is recycled and the
|
|
374
|
+
* caller wants to start accumulating fresh messages.
|
|
375
|
+
*/
|
|
376
|
+
export function resetStartupPacketTransientState(packet: StartupPacketState): void {
|
|
377
|
+
packet.registrations.clear();
|
|
378
|
+
packet.registrationList = [];
|
|
379
|
+
packet.runtimeSeedList = [];
|
|
380
|
+
packet.updates = [];
|
|
381
|
+
packet.runtimeSeedDirty = false;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
// Compositor worker URL and blob management
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
function revokeCachedCompositorWorkerUrl(): void {
|
|
389
|
+
if (!cachedCompositorWorkerUrl) {
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
URL.revokeObjectURL(cachedCompositorWorkerUrl);
|
|
394
|
+
cachedCompositorWorkerUrl = null;
|
|
395
|
+
cachedCreateObjectUrl = null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function disposeStandbyCompositorLease(): void {
|
|
399
|
+
standbyCompositorLease?.worker.terminate();
|
|
400
|
+
standbyCompositorLease = null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Typed helper that extracts globalThis.process without casting at call sites. */
|
|
404
|
+
function getNodeProcess(): { once?: (event: string, fn: () => void) => void } | null {
|
|
405
|
+
/* v8 ignore next — `globalThis` is available in every ES2020+ host (Node, browsers,
|
|
406
|
+
workers). The guard is defense-in-depth in case the module is ever loaded in a
|
|
407
|
+
pre-ES2020 sandbox where `globalThis` is missing. */
|
|
408
|
+
if (typeof globalThis === 'undefined' || !('process' in globalThis)) return null;
|
|
409
|
+
const p = (globalThis as unknown as { process?: unknown }).process;
|
|
410
|
+
/* v8 ignore next — Node's `process` is always the NodeJS.Process object; this guard
|
|
411
|
+
covers hosts that define `process` as a non-object (e.g. a compatibility shim that
|
|
412
|
+
sets it to `undefined` while still keeping the property slot). */
|
|
413
|
+
if (typeof p !== 'object' || p === null) return null;
|
|
414
|
+
return p as { once?: (event: string, fn: () => void) => void };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function registerCachedWorkerCleanup(): void {
|
|
418
|
+
if (cleanupRegistered) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
cleanupRegistered = true;
|
|
423
|
+
const cleanup = (): void => {
|
|
424
|
+
disposeStandbyCompositorLease();
|
|
425
|
+
revokeCachedCompositorWorkerUrl();
|
|
426
|
+
};
|
|
427
|
+
if (typeof globalThis.addEventListener === 'function') {
|
|
428
|
+
globalThis.addEventListener('pagehide', cleanup, { once: true });
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const proc = getNodeProcess();
|
|
433
|
+
if (proc !== null && typeof proc.once === 'function') proc.once('exit', cleanup);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function getCompositorWorkerUrl(): string {
|
|
437
|
+
if (cachedCompositorWorkerUrl && cachedCreateObjectUrl === URL.createObjectURL) {
|
|
438
|
+
return cachedCompositorWorkerUrl;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (cachedCompositorWorkerUrl) {
|
|
442
|
+
revokeCachedCompositorWorkerUrl();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
cachedCompositorWorkerUrl = URL.createObjectURL(
|
|
446
|
+
new Blob([COMPOSITOR_WORKER_SCRIPT], { type: 'application/javascript' }),
|
|
447
|
+
);
|
|
448
|
+
cachedCreateObjectUrl = URL.createObjectURL;
|
|
449
|
+
registerCachedWorkerCleanup();
|
|
450
|
+
return cachedCompositorWorkerUrl;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function createRawCompositorWorker(): Worker {
|
|
454
|
+
const url = getCompositorWorkerUrl();
|
|
455
|
+
return new Worker(url, { type: 'classic', name: 'czap-compositor' });
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function createRuntimeCoordinator(capacity: number): RuntimeCoordinator.Shape {
|
|
459
|
+
return RuntimeCoordinator.create({
|
|
460
|
+
capacity,
|
|
461
|
+
name: 'czap-worker-runtime',
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// ---------------------------------------------------------------------------
|
|
466
|
+
// Compositor lease lifecycle
|
|
467
|
+
// ---------------------------------------------------------------------------
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Claim a compositor lease: either hand back the standby pre-warmed
|
|
471
|
+
* worker (if one is parked and matches the requested capacity) or mint a
|
|
472
|
+
* fresh `Worker` + {@link RuntimeCoordinator}. Emits
|
|
473
|
+
* `claim-or-create` and `coordinator-reset-or-create` stage samples to
|
|
474
|
+
* the optional telemetry sink.
|
|
475
|
+
*
|
|
476
|
+
* @param capacity - Runtime coordinator capacity to request.
|
|
477
|
+
* @param startupTelemetry - Optional sink for stage timings.
|
|
478
|
+
* @returns The worker, its coordinator, and any bootstrap snapshot the
|
|
479
|
+
* parked lease brought with it.
|
|
480
|
+
*/
|
|
481
|
+
export function claimCompositorLease(
|
|
482
|
+
capacity: number,
|
|
483
|
+
startupTelemetry?: CompositorWorkerStartupTelemetry,
|
|
484
|
+
): {
|
|
485
|
+
readonly worker: Worker;
|
|
486
|
+
readonly runtime: RuntimeCoordinator.Shape;
|
|
487
|
+
readonly bootstrapSnapshot: readonly BootstrapQuantizerRegistration[];
|
|
488
|
+
} {
|
|
489
|
+
if (
|
|
490
|
+
standbyCompositorLease &&
|
|
491
|
+
(standbyCompositorLease.workerConstructor !== Worker ||
|
|
492
|
+
standbyCompositorLease.createObjectUrl !== URL.createObjectURL ||
|
|
493
|
+
standbyCompositorLease.capacity !== capacity)
|
|
494
|
+
) {
|
|
495
|
+
disposeStandbyCompositorLease();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const claimStartNs = currentTimeNs();
|
|
499
|
+
const claimedLease = standbyCompositorLease;
|
|
500
|
+
standbyCompositorLease = null;
|
|
501
|
+
const worker = claimedLease?.worker ?? createRawCompositorWorker();
|
|
502
|
+
startupTelemetry?.recordStage('claim-or-create', currentTimeNs() - claimStartNs);
|
|
503
|
+
|
|
504
|
+
const coordinatorStartNs = currentTimeNs();
|
|
505
|
+
const runtime = claimedLease?.runtime ?? createRuntimeCoordinator(capacity);
|
|
506
|
+
const bootstrapSnapshot = claimedLease?.bootstrapSnapshot ?? [];
|
|
507
|
+
if (claimedLease) {
|
|
508
|
+
const runtimeResetStartNs = currentTimeNs();
|
|
509
|
+
runtime.reset();
|
|
510
|
+
recordStartupDiagnosticStage(
|
|
511
|
+
startupTelemetry,
|
|
512
|
+
'coordinator-reset-or-create:runtime-reset-reuse',
|
|
513
|
+
currentTimeNs() - runtimeResetStartNs,
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
startupTelemetry?.recordStage('coordinator-reset-or-create', currentTimeNs() - coordinatorStartNs);
|
|
517
|
+
|
|
518
|
+
return {
|
|
519
|
+
worker,
|
|
520
|
+
runtime,
|
|
521
|
+
bootstrapSnapshot,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Park a compositor lease in the module-level standby slot so a future
|
|
527
|
+
* {@link claimCompositorLease} can reuse it. If the standby slot is
|
|
528
|
+
* already occupied, the incoming lease is disposed (`dispose` message +
|
|
529
|
+
* `terminate()`) instead.
|
|
530
|
+
*/
|
|
531
|
+
export function parkOrDisposeCompositorLease(lease: {
|
|
532
|
+
readonly worker: Worker;
|
|
533
|
+
readonly runtime: RuntimeCoordinator.Shape;
|
|
534
|
+
readonly capacity: number;
|
|
535
|
+
readonly bootstrapSnapshot: readonly BootstrapQuantizerRegistration[];
|
|
536
|
+
}): void {
|
|
537
|
+
if (
|
|
538
|
+
!standbyCompositorLease &&
|
|
539
|
+
typeof Worker !== 'undefined' &&
|
|
540
|
+
Worker === lease.worker.constructor &&
|
|
541
|
+
typeof URL.createObjectURL === 'function' &&
|
|
542
|
+
URL.createObjectURL === cachedCreateObjectUrl
|
|
543
|
+
) {
|
|
544
|
+
standbyCompositorLease = {
|
|
545
|
+
...lease,
|
|
546
|
+
workerConstructor: Worker,
|
|
547
|
+
createObjectUrl: URL.createObjectURL,
|
|
548
|
+
};
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
_send(lease.worker, { type: 'dispose' });
|
|
553
|
+
lease.worker.terminate();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// ---------------------------------------------------------------------------
|
|
557
|
+
// Utility helpers
|
|
558
|
+
// ---------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Internal `postMessage` helper with an explicit transfer-list default.
|
|
562
|
+
* Named with a leading underscore to signal that host code should use
|
|
563
|
+
* the typed methods on {@link CompositorWorkerShape} instead.
|
|
564
|
+
*/
|
|
565
|
+
export function _send(worker: Worker, msg: ToWorkerMessage, transfer?: Transferable[]): void {
|
|
566
|
+
worker.postMessage(msg, transfer ?? []);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Convert a registration's thresholds to a Float64Array for transfer.
|
|
571
|
+
* Returns a new registration object with the typed array and the ArrayBuffer to transfer.
|
|
572
|
+
*/
|
|
573
|
+
export function prepareRegistrationForTransfer(registration: BootstrapQuantizerRegistration): {
|
|
574
|
+
registration: BootstrapQuantizerRegistration;
|
|
575
|
+
buffer: ArrayBuffer;
|
|
576
|
+
} {
|
|
577
|
+
const f64 = new Float64Array(registration.thresholds);
|
|
578
|
+
return {
|
|
579
|
+
registration: { ...registration, thresholds: f64 },
|
|
580
|
+
buffer: f64.buffer,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Prepare a list of registrations for transfer, returning new registrations
|
|
586
|
+
* and the collected ArrayBuffers to include in the transfer list.
|
|
587
|
+
*/
|
|
588
|
+
export function prepareRegistrationsForTransfer(registrations: readonly BootstrapQuantizerRegistration[]): {
|
|
589
|
+
registrations: readonly BootstrapQuantizerRegistration[];
|
|
590
|
+
buffers: ArrayBuffer[];
|
|
591
|
+
} {
|
|
592
|
+
const buffers: ArrayBuffer[] = [];
|
|
593
|
+
const prepared = registrations.map((reg) => {
|
|
594
|
+
const { registration, buffer } = prepareRegistrationForTransfer(reg);
|
|
595
|
+
buffers.push(buffer);
|
|
596
|
+
return registration;
|
|
597
|
+
});
|
|
598
|
+
return { registrations: prepared, buffers };
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Structural equality for two `ArrayLike` sequences (same length, same
|
|
603
|
+
* `===` elements at every index). Works for both plain arrays and typed
|
|
604
|
+
* arrays.
|
|
605
|
+
*/
|
|
606
|
+
export function sameArray<T>(left: ArrayLike<T>, right: ArrayLike<T>): boolean {
|
|
607
|
+
if (left.length !== right.length) {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
for (let index = 0; index < left.length; index++) {
|
|
612
|
+
if (left[index] !== right[index]) {
|
|
613
|
+
return false;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Return `true` when two bootstrap registrations share the same
|
|
622
|
+
* `boundaryId`, state list, and threshold list. Used to elide redundant
|
|
623
|
+
* `add-quantizer` messages during bootstrap coalescing.
|
|
624
|
+
*/
|
|
625
|
+
export function sameBootstrapRegistration(
|
|
626
|
+
left: BootstrapQuantizerRegistration | undefined,
|
|
627
|
+
right: BootstrapQuantizerRegistration,
|
|
628
|
+
): boolean {
|
|
629
|
+
if (!left) {
|
|
630
|
+
return false;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return (
|
|
634
|
+
left.boundaryId === right.boundaryId &&
|
|
635
|
+
sameArray(left.states, right.states) &&
|
|
636
|
+
sameArray(left.thresholds, right.thresholds)
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Quantize a numeric value against a registration's thresholds and
|
|
642
|
+
* return the corresponding state label. Falls back to `states[0]` if the
|
|
643
|
+
* value lies below every threshold.
|
|
644
|
+
*/
|
|
645
|
+
export function evaluateRegistrationState(registration: BootstrapQuantizerRegistration, value: number): string {
|
|
646
|
+
for (let index = registration.thresholds.length - 1; index >= 0; index--) {
|
|
647
|
+
if (value >= registration.thresholds[index]!) {
|
|
648
|
+
return registration.states[index] ?? registration.states[0]!;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return registration.states[0]!;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Re-shape a {@link ResolvedStateAckPayload} into the flat
|
|
657
|
+
* {@link ResolvedStateEntry} form that the main-thread state store
|
|
658
|
+
* consumes. Propagates `ack.generation` into each entry.
|
|
659
|
+
*/
|
|
660
|
+
export function toResolvedStateEntriesFromAck(ack: ResolvedStateAckPayload): readonly ResolvedStateEntry[] {
|
|
661
|
+
return ack.states.map((state) => ({
|
|
662
|
+
name: state.name,
|
|
663
|
+
state: state.state,
|
|
664
|
+
generation: ack.generation,
|
|
665
|
+
}));
|
|
666
|
+
}
|