knitting 0.1.61 → 0.1.63
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/README.md +10 -9
- package/knitting.browser.js +1 -1
- package/package.json +1 -1
- package/src/api.js +26 -16
- package/src/common/task-source.d.ts +9 -0
- package/src/common/task-source.js +15 -0
- package/src/ipc/transport/shared-memory.d.ts +9 -1
- package/src/ipc/transport/shared-memory.js +13 -1
- package/src/memory/lock.d.ts +12 -1
- package/src/memory/lock.js +82 -41
- package/src/runtime/dispatcher.d.ts +11 -8
- package/src/runtime/dispatcher.js +159 -26
- package/src/runtime/inline-executor.js +6 -1
- package/src/runtime/pool.d.ts +22 -4
- package/src/runtime/pool.js +104 -27
- package/src/runtime/process-worker.d.ts +22 -0
- package/src/runtime/process-worker.js +116 -2
- package/src/runtime/tx-queue.d.ts +2 -0
- package/src/runtime/tx-queue.js +68 -0
- package/src/runtime/worker-common.d.ts +2 -1
- package/src/runtime/worker-common.js +20 -5
- package/src/types.d.ts +44 -9
- package/src/worker/loop.js +24 -6
|
@@ -191,6 +191,106 @@ export const createProcessWorkerMemoryLayout = ({ signalBytes, abortBytes, paylo
|
|
|
191
191
|
},
|
|
192
192
|
};
|
|
193
193
|
};
|
|
194
|
+
export const createProcessStealMemoryLayout = ({ threads, signalBytes, abortBytes, abortSignalMax, payloadBytes, sharedMemory, }) => {
|
|
195
|
+
if (!Number.isInteger(threads) || threads < 2) {
|
|
196
|
+
throw new RangeError("process stealing needs at least two workers");
|
|
197
|
+
}
|
|
198
|
+
const carpet = createByteCarpet();
|
|
199
|
+
const requestLockSlice = carpet.take("requestLockSector", LOCK_SECTOR_BYTE_LENGTH);
|
|
200
|
+
const requestHeadersSlice = carpet.take("requestHeaders", getHeaderBlockByteLength({
|
|
201
|
+
slotCount: LockBound.slots,
|
|
202
|
+
slotStrideU32: HEADER_SLOT_STRIDE_U32,
|
|
203
|
+
alignTo: 64,
|
|
204
|
+
}));
|
|
205
|
+
const abortSignalsSlice = carpet.take("abortSignals", abortBytes);
|
|
206
|
+
const requestPayloadSlice = carpet.take("requestPayload", payloadBytes);
|
|
207
|
+
const workerSlices = Array.from({ length: threads }, (_, thread) => ({
|
|
208
|
+
signal: carpet.take(`signals-${thread}`, signalBytes),
|
|
209
|
+
returnLock: carpet.take(`returnLockSector-${thread}`, LOCK_SECTOR_BYTE_LENGTH),
|
|
210
|
+
returnHeaders: carpet.take(`returnHeaders-${thread}`, getHeaderBlockByteLength({
|
|
211
|
+
slotCount: LockBound.slots,
|
|
212
|
+
slotStrideU32: HEADER_SLOT_STRIDE_U32,
|
|
213
|
+
alignTo: 64,
|
|
214
|
+
})),
|
|
215
|
+
returnPayload: carpet.take(`returnPayload-${thread}`, payloadBytes),
|
|
216
|
+
}));
|
|
217
|
+
const primitives = getProcessWorkerSharedMemoryPrimitives();
|
|
218
|
+
const forceNamed = sharedMemory.mode === "named" || isWindowsRuntimeHost();
|
|
219
|
+
const mapping = primitives.createSharedMemory(forceNamed
|
|
220
|
+
? {
|
|
221
|
+
size: carpet.byteLength(),
|
|
222
|
+
mode: "create",
|
|
223
|
+
name: makeProcessWorkerMemoryName(0, sharedMemory.namePrefix),
|
|
224
|
+
}
|
|
225
|
+
: { size: carpet.byteLength() });
|
|
226
|
+
const descriptor = FileDescriptor.fromMapping(mapping);
|
|
227
|
+
if (isWindowsRuntimeHost() && descriptor.name === undefined) {
|
|
228
|
+
throw new Error("Windows process worker shared memory must use a named mapping");
|
|
229
|
+
}
|
|
230
|
+
if (sharedMemory.mode === "named" && descriptor.name === undefined) {
|
|
231
|
+
throw new Error("processSharedMemory mode named needs a named shared-memory backend");
|
|
232
|
+
}
|
|
233
|
+
const buffer = mapping.buffer;
|
|
234
|
+
const bind = (slice) => makeSharedBufferRegion(buffer, slice.byteOffset, slice.byteLength);
|
|
235
|
+
const abortSignals = bind(abortSignalsSlice);
|
|
236
|
+
const requestLock = {
|
|
237
|
+
headers: bind(requestHeadersSlice),
|
|
238
|
+
headerSlotStrideU32: HEADER_SLOT_STRIDE_U32,
|
|
239
|
+
lockSector: bind(requestLockSlice),
|
|
240
|
+
payloadSector: bind(requestLockSlice),
|
|
241
|
+
};
|
|
242
|
+
// Each worker calls cleanup, so named-memory unlink happens only after every
|
|
243
|
+
// endpoint has released its view. The per-worker mapping wrappers below do
|
|
244
|
+
// not expose close(); the host views remain attached until host exit because
|
|
245
|
+
// Bun's external ArrayBuffer finalizer can race an explicit munmap.
|
|
246
|
+
let remaining = threads;
|
|
247
|
+
let cleaned = false;
|
|
248
|
+
const cleanupSharedMapping = () => {
|
|
249
|
+
if (cleaned)
|
|
250
|
+
return;
|
|
251
|
+
remaining--;
|
|
252
|
+
if (remaining > 0)
|
|
253
|
+
return;
|
|
254
|
+
cleaned = true;
|
|
255
|
+
const name = descriptor.name;
|
|
256
|
+
if (name !== undefined && sharedMemory.unlinkOnShutdown) {
|
|
257
|
+
primitives.unlinkSharedMemory?.(name);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
const workerMapping = {
|
|
261
|
+
...mapping,
|
|
262
|
+
close: undefined,
|
|
263
|
+
};
|
|
264
|
+
const workers = workerSlices.map((slices) => {
|
|
265
|
+
const returnLock = {
|
|
266
|
+
headers: bind(slices.returnHeaders),
|
|
267
|
+
headerSlotStrideU32: HEADER_SLOT_STRIDE_U32,
|
|
268
|
+
lockSector: bind(slices.returnLock),
|
|
269
|
+
payloadSector: bind(slices.returnLock),
|
|
270
|
+
};
|
|
271
|
+
return {
|
|
272
|
+
mapping: workerMapping,
|
|
273
|
+
descriptor,
|
|
274
|
+
controlLayout: {
|
|
275
|
+
controlSAB: buffer,
|
|
276
|
+
signals: bind(slices.signal),
|
|
277
|
+
abortSignals,
|
|
278
|
+
lock: requestLock,
|
|
279
|
+
returnLock,
|
|
280
|
+
slices: carpet.slices,
|
|
281
|
+
},
|
|
282
|
+
lockPayload: bind(requestPayloadSlice),
|
|
283
|
+
returnPayload: bind(slices.returnPayload),
|
|
284
|
+
cleanup: cleanupSharedMapping,
|
|
285
|
+
};
|
|
286
|
+
});
|
|
287
|
+
return {
|
|
288
|
+
mapping,
|
|
289
|
+
descriptor,
|
|
290
|
+
workers,
|
|
291
|
+
abortSignalMax,
|
|
292
|
+
};
|
|
293
|
+
};
|
|
194
294
|
const toChildProcessSharedBufferMetadata = (source, descriptor) => {
|
|
195
295
|
const region = toSharedBufferRegion(source);
|
|
196
296
|
const metadata = descriptor.toMetadata();
|
|
@@ -562,9 +662,23 @@ const spawnNodeHostedProcessWorker = ({ workerUrl, bootPayload, memory, processR
|
|
|
562
662
|
queueMicrotask(() => child.send?.(bootPayload));
|
|
563
663
|
}
|
|
564
664
|
child.on("error", events.emitError);
|
|
565
|
-
|
|
665
|
+
let resolveExit = () => { };
|
|
666
|
+
const exited = new Promise((resolve) => {
|
|
667
|
+
resolveExit = resolve;
|
|
668
|
+
});
|
|
669
|
+
child.on("exit", (code) => {
|
|
670
|
+
resolveExit();
|
|
671
|
+
events.emitExit(code ?? -1);
|
|
672
|
+
});
|
|
566
673
|
return {
|
|
567
|
-
terminate: () =>
|
|
674
|
+
terminate: () => {
|
|
675
|
+
try {
|
|
676
|
+
child.kill();
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
}
|
|
680
|
+
return exited;
|
|
681
|
+
},
|
|
568
682
|
unref: () => child.unref?.(),
|
|
569
683
|
on: events.on,
|
|
570
684
|
};
|
|
@@ -33,6 +33,8 @@ export declare function createHostTxQueue({ max, lock, returnLock, extraReturnLo
|
|
|
33
33
|
hasPendingFrames: () => boolean;
|
|
34
34
|
txIdle: () => boolean;
|
|
35
35
|
completeFrame: () => number;
|
|
36
|
+
waitForCompletion: (onWake: () => void, timeoutMs?: number) => boolean;
|
|
37
|
+
setCompletionWaiterArmed: (armed: boolean) => void;
|
|
36
38
|
setReturnHooks: (lane: number, hooks: ReturnHooks | undefined) => void;
|
|
37
39
|
enqueue: (functionID: FunctionID, timeout?: TaskTimeout, abortSignal?: AbortSignalOption) => (rawArgs: RawArguments) => Promise<never> | import("../common/with-resolvers.js").PromiseWithMaybeReject<unknown>;
|
|
38
40
|
flushToWorker: () => boolean;
|
package/src/runtime/tx-queue.js
CHANGED
|
@@ -54,6 +54,18 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
|
|
|
54
54
|
activeRejectPlaceholder: PLACE_HOLDER,
|
|
55
55
|
onResolved: onReturnResolved,
|
|
56
56
|
}));
|
|
57
|
+
const returnWaiters = [
|
|
58
|
+
returnLock,
|
|
59
|
+
...(extraReturnLocks ?? []),
|
|
60
|
+
].map((each) => typeof each.waitForHostChange === "function"
|
|
61
|
+
? each.waitForHostChange
|
|
62
|
+
: () => undefined);
|
|
63
|
+
const returnArmers = [
|
|
64
|
+
returnLock,
|
|
65
|
+
...(extraReturnLocks ?? []),
|
|
66
|
+
].map((each) => typeof each.setHostWaiterArmed === "function"
|
|
67
|
+
? each.setHostWaiterArmed
|
|
68
|
+
: (_armed) => { });
|
|
57
69
|
// A stealing queue drains one private return lock per worker. Borrowed
|
|
58
70
|
// BufferReferences must therefore be claimed with the hooks belonging to the
|
|
59
71
|
// worker that produced that particular return. The hooks are bound after the
|
|
@@ -84,6 +96,60 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
|
|
|
84
96
|
}
|
|
85
97
|
return resolved;
|
|
86
98
|
};
|
|
99
|
+
// A stealing queue has one private return lock per worker. Keep at most one
|
|
100
|
+
// waitAsync waiter per lane: after lane A rings, lane B's unresolved waiter
|
|
101
|
+
// must survive the next arm or Atomics.notify(..., 1) could wake an obsolete
|
|
102
|
+
// waiter and starve the live one until the watchdog timeout.
|
|
103
|
+
const completionArmed = new Uint8Array(returnWaiters.length);
|
|
104
|
+
let completionWake;
|
|
105
|
+
let completionGeneration = 0 | 0;
|
|
106
|
+
const setCompletionWaiterArmed = (armed) => {
|
|
107
|
+
for (const setArmed of returnArmers)
|
|
108
|
+
setArmed(armed);
|
|
109
|
+
};
|
|
110
|
+
const waitForCompletion = (onWake, timeoutMs) => {
|
|
111
|
+
completionWake = onWake;
|
|
112
|
+
// A persistent waiter may still be pending after a send preempted the
|
|
113
|
+
// dispatcher. Re-arm its shared gate before relying on that waiter again.
|
|
114
|
+
setCompletionWaiterArmed(true);
|
|
115
|
+
let supported = true;
|
|
116
|
+
for (let index = 0; index < returnWaiters.length; index++) {
|
|
117
|
+
if (completionArmed[index] !== 0)
|
|
118
|
+
continue;
|
|
119
|
+
let wait;
|
|
120
|
+
try {
|
|
121
|
+
wait = returnWaiters[index](timeoutMs);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
wait = undefined;
|
|
125
|
+
}
|
|
126
|
+
if (wait === undefined) {
|
|
127
|
+
supported = false;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
completionArmed[index] = 1;
|
|
131
|
+
const generation = completionGeneration;
|
|
132
|
+
const wakeLane = () => {
|
|
133
|
+
if (completionArmed[index] === 0)
|
|
134
|
+
return;
|
|
135
|
+
completionArmed[index] = 0;
|
|
136
|
+
returnArmers[index](false);
|
|
137
|
+
if (generation !== completionGeneration)
|
|
138
|
+
return;
|
|
139
|
+
completionWake?.();
|
|
140
|
+
};
|
|
141
|
+
if (!wait.async)
|
|
142
|
+
wakeLane();
|
|
143
|
+
else
|
|
144
|
+
Promise.resolve(wait.value).then(wakeLane, wakeLane);
|
|
145
|
+
}
|
|
146
|
+
if (!supported) {
|
|
147
|
+
completionWake = undefined;
|
|
148
|
+
setCompletionWaiterArmed(false);
|
|
149
|
+
completionGeneration = (completionGeneration + 1) | 0;
|
|
150
|
+
}
|
|
151
|
+
return supported;
|
|
152
|
+
};
|
|
87
153
|
const hasActiveTasks = () => {
|
|
88
154
|
const count = (inUsed - getPendingPromiseCount()) | 0;
|
|
89
155
|
return count > 0;
|
|
@@ -116,6 +182,8 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
|
|
|
116
182
|
hasPendingFrames,
|
|
117
183
|
txIdle,
|
|
118
184
|
completeFrame,
|
|
185
|
+
waitForCompletion,
|
|
186
|
+
setCompletionWaiterArmed,
|
|
119
187
|
setReturnHooks: (lane, hooks) => {
|
|
120
188
|
if (!Number.isInteger(lane) || lane < 0 || lane >= returnHooks.length) {
|
|
121
189
|
throw new RangeError(`return lane ${lane} out of range`);
|
|
@@ -10,4 +10,5 @@ export type NodeWorkerLike = {
|
|
|
10
10
|
export declare const toWorkerSafeExecArgv: (flags: string[] | undefined) => string[] | undefined;
|
|
11
11
|
export declare const toWorkerCompatExecArgv: (flags: string[] | undefined) => string[] | undefined;
|
|
12
12
|
export declare const serializeWorkerBootstrapData: (options: WorkerSettings) => WorkerSettings;
|
|
13
|
-
|
|
13
|
+
/** Terminate a worker, optionally waiting for it to exit. */
|
|
14
|
+
export declare const terminateWorkerQuietly: (worker: SpawnedWorker, awaitExit?: boolean) => Promise<void>;
|
|
@@ -90,13 +90,28 @@ export const serializeWorkerBootstrapData = (options) => {
|
|
|
90
90
|
},
|
|
91
91
|
};
|
|
92
92
|
};
|
|
93
|
-
|
|
93
|
+
// Bound the wait for process-worker exit.
|
|
94
|
+
const WORKER_EXIT_WAIT_MS = 2_000;
|
|
95
|
+
/** Terminate a worker, optionally waiting for it to exit. */
|
|
96
|
+
export const terminateWorkerQuietly = (worker, awaitExit = false) => {
|
|
94
97
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
worker.
|
|
98
|
-
|
|
98
|
+
if (!awaitExit)
|
|
99
|
+
worker.unref?.();
|
|
100
|
+
const exited = Promise.resolve(worker.terminate()).then(() => { }, () => { });
|
|
101
|
+
if (!awaitExit)
|
|
102
|
+
return exited;
|
|
103
|
+
return new Promise((resolve) => {
|
|
104
|
+
const timer = setTimeout(() => {
|
|
105
|
+
worker.unref?.();
|
|
106
|
+
resolve();
|
|
107
|
+
}, WORKER_EXIT_WAIT_MS);
|
|
108
|
+
void exited.then(() => {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
resolve();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
99
113
|
}
|
|
100
114
|
catch {
|
|
115
|
+
return Promise.resolve();
|
|
101
116
|
}
|
|
102
117
|
};
|
package/src/types.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ type WorkerInvoke = (args: Uint8Array) => Promise<unknown>;
|
|
|
13
13
|
interface WorkerContext {
|
|
14
14
|
txIdle(): boolean;
|
|
15
15
|
call(descriptor: WorkerCall): WorkerInvoke;
|
|
16
|
+
/** Ask the worker to leave its dispatch loop before termination. */
|
|
17
|
+
requestStop?(): Promise<void>;
|
|
16
18
|
kills(): Promise<void>;
|
|
17
19
|
}
|
|
18
20
|
type CreateContext = WorkerContext;
|
|
@@ -34,6 +36,8 @@ type WorkerData = {
|
|
|
34
36
|
payloadConfig?: PayloadBufferOptions;
|
|
35
37
|
bufferReferenceReturn?: "copy" | "borrow";
|
|
36
38
|
permission?: ResolvedPermissionProtocol;
|
|
39
|
+
/** Whether this host can arm an async completion waiter on the return lock. */
|
|
40
|
+
notifyOnHostPublish?: boolean;
|
|
37
41
|
/**
|
|
38
42
|
* Work stealing. When present, `lock` is a submit region shared by every
|
|
39
43
|
* worker and this worker claims from it as consumer `consumerId` of
|
|
@@ -341,13 +345,46 @@ type WorkerTimers = {
|
|
|
341
345
|
};
|
|
342
346
|
type DispatcherSettings = {
|
|
343
347
|
/**
|
|
344
|
-
* How many immediate notify loops before
|
|
348
|
+
* How many immediate notify loops before the dispatcher stops re-arming the
|
|
349
|
+
* pump for free.
|
|
350
|
+
*
|
|
351
|
+
* The default depends on what the dispatcher escalates *to*. Polling
|
|
352
|
+
* escalates to a `setTimeout` ladder costing ~1.1ms even at delay 0, so it
|
|
353
|
+
* defaults to 128 — escalating is expensive and the wide window also batches
|
|
354
|
+
* completions. A doorbell escalates to `Atomics.waitAsync` at roughly the
|
|
355
|
+
* price of one hop, so pools that have one default to 1. Pools without a
|
|
356
|
+
* doorbell — Deno, or `doorbell: false` — keep 128.
|
|
357
|
+
*
|
|
358
|
+
* Setting this explicitly opts out of that coupling for every pool shape.
|
|
345
359
|
*/
|
|
346
360
|
stallFreeLoops?: number;
|
|
347
361
|
/**
|
|
348
362
|
* Max backoff delay (milliseconds).
|
|
349
363
|
*/
|
|
350
364
|
maxBackoffMs?: number;
|
|
365
|
+
/**
|
|
366
|
+
* Replace idle completion polling with an `Atomics.waitAsync` doorbell when
|
|
367
|
+
* the host runtime supports it.
|
|
368
|
+
*
|
|
369
|
+
* Defaults to enabled on Node and Bun at any worker count, where it costs
|
|
370
|
+
* 1.6-3.9x less host CPU per completed call. Under HTTP load, where the host
|
|
371
|
+
* has real work of its own, that converts to +13% to +36% throughput.
|
|
372
|
+
*
|
|
373
|
+
* Turn it off for a pool that oversubscribes its machine. The doorbell only
|
|
374
|
+
* progresses when the host gets scheduled, so once workers occupy every core
|
|
375
|
+
* a wake must preempt one: measured +5% to +12.6% rps while workers+host fit
|
|
376
|
+
* within the cores, -22% to -32% once they do not. That is not gated
|
|
377
|
+
* automatically because the core count cannot be probed portably.
|
|
378
|
+
*
|
|
379
|
+
* Forced off, overriding an explicit `true`, where a doorbell cannot work:
|
|
380
|
+
* on Deno, whose `waitAsync` does not wake an idle event loop, and for
|
|
381
|
+
* process workers, which live in another process and so cannot ring a host
|
|
382
|
+
* waiter at all — V8 keeps its Atomics waiter list per isolate, which is why
|
|
383
|
+
* they wake through a native futex addon instead. Compiled (Porffor) workers
|
|
384
|
+
* never reach this path: they reject `host` outright and use pipes rather
|
|
385
|
+
* than shared memory.
|
|
386
|
+
*/
|
|
387
|
+
doorbell?: boolean;
|
|
351
388
|
/**
|
|
352
389
|
* Host dispatcher topology.
|
|
353
390
|
* - `"per-thread"`: each worker owns its dispatcher and macro channel.
|
|
@@ -366,14 +403,12 @@ type DispatcherSettings = {
|
|
|
366
403
|
* claim from, private return lanes, and a pool-global pending registry. The
|
|
367
404
|
* endpoint that claims a task owns its response.
|
|
368
405
|
*
|
|
369
|
-
* Enabled by default for multi-worker thread
|
|
370
|
-
* private-lane dispatcher was explicitly selected.
|
|
371
|
-
* inliners, compiled/Porffor workers,
|
|
372
|
-
* current 31-claimant protocol limit retain their existing transport.
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
* (or `KNITTING_STEAL=0`) to opt out; `KNITTING_STEAL=1` explicitly opts in
|
|
376
|
-
* and overrides a balancer/dispatcher selection.
|
|
406
|
+
* Enabled by default for compatible multi-worker thread and process pools
|
|
407
|
+
* unless a balancer or private-lane dispatcher was explicitly selected.
|
|
408
|
+
* One-worker pools, inliners, compiled/Porffor workers, and pools above the
|
|
409
|
+
* current 31-claimant protocol limit retain their existing transport. Set
|
|
410
|
+
* `false` (or `KNITTING_STEAL=0`) to opt out; `KNITTING_STEAL=1` explicitly
|
|
411
|
+
* opts in and overrides a balancer/dispatcher selection.
|
|
377
412
|
*/
|
|
378
413
|
steal?: boolean;
|
|
379
414
|
/**
|
package/src/worker/loop.js
CHANGED
|
@@ -2,7 +2,7 @@ import { addRuntimeDataListener, createRuntimeMessageChannel, RUNTIME_IS_MAIN_TH
|
|
|
2
2
|
import { isSharedBufferSource } from "../common/shared-buffer-region.js";
|
|
3
3
|
import { isLockBufferTextCompat } from "../common/shared-buffer-text.js";
|
|
4
4
|
import { createWorkerRxQueue } from "./rx-queue.js";
|
|
5
|
-
import { createSharedMemoryTransport } from "../ipc/transport/shared-memory.js";
|
|
5
|
+
import { createSharedMemoryTransport, WORKER_STOP, } from "../ipc/transport/shared-memory.js";
|
|
6
6
|
import { lock2 } from "../memory/lock.js";
|
|
7
7
|
// Side-effect import: registers the payload codec (cycle break for Andromeda;
|
|
8
8
|
// see lock.ts). Must run before any lock2() call.
|
|
@@ -73,7 +73,7 @@ export const workerMainLoop = async (startupData) => {
|
|
|
73
73
|
installTerminationGuard();
|
|
74
74
|
installUnhandledRejectionSilencer();
|
|
75
75
|
installPerformanceNowGuard();
|
|
76
|
-
const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, bufferReferenceReturn, permission, totalNumberOfThread, list, ids, names, at, steal, } = startupData;
|
|
76
|
+
const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, bufferReferenceReturn, permission, notifyOnHostPublish, totalNumberOfThread, list, ids, names, at, steal, } = startupData;
|
|
77
77
|
scrubWorkerDataSensitiveBuffers(startupData);
|
|
78
78
|
assertWorkerSharedMemoryBootData({ sab, lock, returnLock });
|
|
79
79
|
const debugNamespaces = resolveDebugNamespaces(debug);
|
|
@@ -120,21 +120,23 @@ export const workerMainLoop = async (startupData) => {
|
|
|
120
120
|
payloadConfig,
|
|
121
121
|
textCompat: returnLock.textCompat,
|
|
122
122
|
processBoundary: RUNTIME_IS_PROCESS_WORKER,
|
|
123
|
+
// The host parks on this lock's publication word when it has no work to
|
|
124
|
+
// flush. Request locks are host-produced and must not wake that waiter.
|
|
125
|
+
notifyOnHostPublish,
|
|
123
126
|
});
|
|
124
127
|
const timers = workerOptions?.timers;
|
|
125
128
|
const spinMicroseconds = timers?.spinMicroseconds ??
|
|
126
|
-
|
|
129
|
+
(totalNumberOfThread <= 1 ? 50 : 0);
|
|
127
130
|
const parkMs = dbg !== undefined
|
|
128
131
|
? Number.POSITIVE_INFINITY
|
|
129
|
-
: (timers?.parkMs ??
|
|
130
|
-
Math.max(1, totalNumberOfThread) * 50);
|
|
132
|
+
: (timers?.parkMs ?? 1);
|
|
131
133
|
const pauseSpin = (() => {
|
|
132
134
|
const fn = typeof timers?.pauseNanoseconds === "number"
|
|
133
135
|
? whilePausing({ pauseInNanoseconds: timers.pauseNanoseconds })
|
|
134
136
|
: pauseGeneric;
|
|
135
137
|
return () => fn(); // always a closure wrapper
|
|
136
138
|
})();
|
|
137
|
-
const { opView, rxStatus, txStatus } = signals;
|
|
139
|
+
const { opView, rxStatus, txStatus, stopView } = signals;
|
|
138
140
|
const a_store = Atomics.store;
|
|
139
141
|
const a_load = Atomics.load;
|
|
140
142
|
const nativeWaitU32 = getProcessWorkerNativeWaitU32();
|
|
@@ -258,17 +260,33 @@ export const workerMainLoop = async (startupData) => {
|
|
|
258
260
|
}
|
|
259
261
|
: pauseUntil;
|
|
260
262
|
const flushBeforeClaim = steal !== undefined;
|
|
263
|
+
/** Leave the dispatch loop and acknowledge shutdown. */
|
|
264
|
+
const stopLoop = () => {
|
|
265
|
+
a_store(stopView, 0, WORKER_STOP.acknowledged);
|
|
266
|
+
a_store(rxStatus, 0, 0);
|
|
267
|
+
try {
|
|
268
|
+
port1.close?.();
|
|
269
|
+
port2.close?.();
|
|
270
|
+
}
|
|
271
|
+
catch { }
|
|
272
|
+
};
|
|
261
273
|
const loop = () => {
|
|
262
274
|
isInMacro = false;
|
|
263
275
|
let progressed = true;
|
|
264
276
|
let awaiting = 0;
|
|
265
277
|
while (true) {
|
|
278
|
+
if (stopView[0] !== WORKER_STOP.running)
|
|
279
|
+
return stopLoop();
|
|
266
280
|
if (flushBeforeClaim) {
|
|
267
281
|
// Stealing only. Flush finished work before taking more on: a computed
|
|
268
282
|
// response should not wait behind a claim, and that claim can block on
|
|
269
283
|
// a peer withdrawing its intent. Measured to hurt the per-lane path,
|
|
270
284
|
// where a claim is a cheap private decode and picking work up promptly
|
|
271
285
|
// matters more, so the classic order is kept below.
|
|
286
|
+
// Reordering must not make `progressed` sticky: it answers "did this
|
|
287
|
+
// iteration move anything", so it has to start false every pass or the
|
|
288
|
+
// park below is unreachable and the worker spins a core forever.
|
|
289
|
+
progressed = false;
|
|
272
290
|
if (_hasCompleted()) {
|
|
273
291
|
if (_writeBatch(WRITE_MAX) > 0)
|
|
274
292
|
progressed = true;
|