knitting 0.1.62 → 0.1.70
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 +525 -335
- package/knitting.browser.js +1 -1
- package/map.md +0 -6
- package/package.json +3 -3
- package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/linux-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64/knitting_windows_shared_memory.dll +0 -0
- package/prebuilds/win32-x64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-127/knitting_shm.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_doorbell.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
- package/scripts/build-native-addons.ts +5 -0
- package/shared-memory.d.ts +3 -0
- package/shared-memory.js +3 -0
- package/src/api.js +109 -42
- package/src/common/with-resolvers.js +2 -5
- package/src/common/worker-runtime.d.ts +7 -0
- package/src/common/worker-runtime.js +7 -0
- package/src/connections/buffer-reference.d.ts +10 -36
- package/src/connections/buffer-reference.js +15 -170
- package/src/connections/node-addons.d.ts +1 -1
- package/src/connections/shared-array-buffer-payload.d.ts +7 -0
- package/src/connections/shared-array-buffer-payload.js +27 -11
- package/src/ipc/transport/shared-memory.d.ts +9 -1
- package/src/ipc/transport/shared-memory.js +13 -1
- package/src/knitting_buffer_pointer.cc +57 -2
- package/src/knitting_doorbell.cc +220 -0
- package/src/memory/knitting-body.d.ts +44 -0
- package/src/memory/knitting-body.js +51 -0
- package/src/memory/knitting-buffer-http.d.ts +116 -0
- package/src/memory/knitting-buffer-http.js +255 -0
- package/src/memory/knitting-buffer.d.ts +250 -0
- package/src/memory/knitting-buffer.js +695 -0
- package/src/memory/lazy-region-registry.d.ts +83 -0
- package/src/memory/lazy-region-registry.js +355 -0
- package/src/memory/lock.d.ts +38 -15
- package/src/memory/lock.js +227 -109
- package/src/memory/payloadCodec.d.ts +18 -2
- package/src/memory/payloadCodec.js +309 -65
- package/src/memory/regionRegistry.d.ts +6 -0
- package/src/memory/regionRegistry.js +125 -240
- package/src/memory/shared-buffer-io.d.ts +7 -0
- package/src/memory/shared-buffer-io.js +34 -8
- package/src/runtime/deno-doorbell.d.ts +26 -0
- package/src/runtime/deno-doorbell.js +117 -0
- package/src/runtime/dispatcher.d.ts +8 -6
- package/src/runtime/dispatcher.js +80 -58
- package/src/runtime/host-arg-arena.d.ts +3 -0
- package/src/runtime/host-arg-arena.js +16 -0
- package/src/runtime/node-doorbell.d.ts +14 -0
- package/src/runtime/node-doorbell.js +84 -0
- package/src/runtime/pool.d.ts +27 -15
- package/src/runtime/pool.js +138 -116
- package/src/runtime/process-worker.d.ts +9 -0
- package/src/runtime/process-worker.js +22 -2
- package/src/runtime/tx-queue.d.ts +2 -5
- package/src/runtime/tx-queue.js +52 -48
- package/src/runtime/worker-common.d.ts +2 -1
- package/src/runtime/worker-common.js +19 -5
- package/src/types.d.ts +36 -70
- package/src/worker/loop.js +95 -62
- package/src/worker/rx-queue.d.ts +2 -3
- package/src/worker/rx-queue.js +34 -40
- package/src/worker/shared-return.d.ts +9 -0
- package/src/worker/shared-return.js +22 -0
- package/src/worker/task-loader.js +1 -2
- package/src/worker/timers.d.ts +2 -6
- package/src/worker/timers.js +14 -19
- package/unsafe.d.ts +2 -1
- package/unsafe.js +2 -1
package/src/runtime/pool.js
CHANGED
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
import { createHostTxQueue } from "./tx-queue.js";
|
|
2
|
-
import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessStealMemoryLayout, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, spawnProcessWorker, toProcessWorkerBootPayload, } from "./process-worker.js";
|
|
2
|
+
import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessStealMemoryLayout, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, processWorkerUsesIpc, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, spawnProcessWorker, toProcessWorkerBootPayload, } from "./process-worker.js";
|
|
3
3
|
import { serializeWorkerBootstrapData, terminateWorkerQuietly, toWorkerCompatExecArgv, toWorkerSafeExecArgv, } from "./worker-common.js";
|
|
4
|
-
import { createSharedMemoryTransport, TRANSPORT_SIGNAL_BYTES, } from "../ipc/transport/shared-memory.js";
|
|
4
|
+
import { createSharedMemoryTransport, TRANSPORT_SIGNAL_BYTES, WORKER_STOP, } from "../ipc/transport/shared-memory.js";
|
|
5
5
|
import { ChannelHandler, hostDispatcherLoop, } from "./dispatcher.js";
|
|
6
|
+
import { createNodeCompletionDoorbell } from "./node-doorbell.js";
|
|
6
7
|
import { HEADER_SLOT_STRIDE_U32, lock2, LOCK_SECTOR_BYTE_LENGTH, LockBound, } from "../memory/lock.js";
|
|
7
|
-
//
|
|
8
|
-
// see lock.ts). Must run before any lock2() call.
|
|
8
|
+
// Registers the payload codec before any lock is built.
|
|
9
9
|
import "../memory/payloadCodec.js";
|
|
10
10
|
import "../worker/loop.js";
|
|
11
11
|
import { createSharedArrayBuffer, createWasmSharedArrayBuffer, IS_BROWSER, RUNTIME, } from "../common/runtime.js";
|
|
12
|
-
import { HAS_NODE_WORKER_THREADS, RUNTIME_WORKER, } from "../common/worker-runtime.js";
|
|
12
|
+
import { HAS_NODE_WORKER_THREADS, isProcessCompletionDoorbell, RUNTIME_WORKER, } from "../common/worker-runtime.js";
|
|
13
13
|
import { probeLockBufferTextCompat } from "../common/shared-buffer-text.js";
|
|
14
14
|
import { signalAbortFactory } from "../shared/abortSignal.js";
|
|
15
15
|
import { createLockControlCarpet } from "../memory/byte-carpet.js";
|
|
16
16
|
import { resolvePayloadBufferOptions, } from "../memory/payload-config.js";
|
|
17
|
-
import { createBufferReferenceReturnReleaseMessage, detachArrayBufferBestEffort, } from "../connections/buffer-reference.js";
|
|
18
17
|
import { spawnCompiledWorkerContext } from "./compiled-worker.js";
|
|
19
18
|
const WORKER_FATAL_MESSAGE_KEY = "__knittingWorkerFatal";
|
|
20
19
|
const isWorkerFatalMessage = (value) => !!value &&
|
|
21
20
|
typeof value === "object" &&
|
|
22
21
|
typeof value[WORKER_FATAL_MESSAGE_KEY] === "string";
|
|
22
|
+
// Bound the wait for a worker to acknowledge loop exit.
|
|
23
|
+
const WORKER_STOP_ACK_TIMEOUT_MS = 50;
|
|
24
|
+
// Node owns returned BufferReference backing stores in a process-global native
|
|
25
|
+
// registry. Give its worker loop a chance to drain deferred producer pins
|
|
26
|
+
// before the host falls back to terminate().
|
|
27
|
+
const WORKER_STOP_NEEDS_ACK = RUNTIME === "deno" || RUNTIME === "node";
|
|
23
28
|
// Keep idle workers self-healing if an Atomics.notify wake is missed.
|
|
24
29
|
const DEFAULT_WORKER_PARK_MS = 1;
|
|
30
|
+
const SINGLE_WORKER_SPIN_US = 50;
|
|
25
31
|
const DEFAULT_ABORT_SIGNAL_CAPACITY = 258;
|
|
26
32
|
const sanitizePositiveInteger = (value) => {
|
|
27
33
|
if (!Number.isFinite(value))
|
|
@@ -31,15 +37,20 @@ const sanitizePositiveInteger = (value) => {
|
|
|
31
37
|
};
|
|
32
38
|
const resolveAbortSignalCapacity = (value) => sanitizePositiveInteger(value) ?? DEFAULT_ABORT_SIGNAL_CAPACITY;
|
|
33
39
|
const abortSignalByteLength = (capacity) => Math.max(1, Math.ceil(capacity / 32)) * Uint32Array.BYTES_PER_ELEMENT;
|
|
34
|
-
const withDefaultWorkerTimers = (options) => {
|
|
40
|
+
const withDefaultWorkerTimers = (options, workerCount) => {
|
|
35
41
|
const parkMs = options?.timers?.parkMs ?? DEFAULT_WORKER_PARK_MS;
|
|
42
|
+
// `??` and not `||`: an explicit `spinMicroseconds: 0` is the whole point of
|
|
43
|
+
// the multi-worker default and must not fall through to it.
|
|
44
|
+
const spinMicroseconds = options?.timers?.spinMicroseconds ??
|
|
45
|
+
(workerCount <= 1 ? SINGLE_WORKER_SPIN_US : 0);
|
|
36
46
|
if (options === undefined)
|
|
37
|
-
return { timers: { parkMs } };
|
|
47
|
+
return { timers: { parkMs, spinMicroseconds } };
|
|
38
48
|
return {
|
|
39
49
|
...options,
|
|
40
50
|
timers: {
|
|
41
51
|
...options.timers,
|
|
42
52
|
parkMs,
|
|
53
|
+
spinMicroseconds,
|
|
43
54
|
},
|
|
44
55
|
};
|
|
45
56
|
};
|
|
@@ -48,16 +59,7 @@ const withFixedPayloadConfig = (config) => ({
|
|
|
48
59
|
mode: "fixed",
|
|
49
60
|
payloadInitialBytes: config.payloadMaxByteLength,
|
|
50
61
|
});
|
|
51
|
-
/**
|
|
52
|
-
* Build the buffers, host-side locks and pending registry that a stealing pool
|
|
53
|
-
* shares. One submit region for every worker to claim from, one private return
|
|
54
|
-
* region per worker, and a single pool-global queue so a response arriving on
|
|
55
|
-
* any lane settles the right promise.
|
|
56
|
-
*
|
|
57
|
-
* Region width follows paper §6.1: the largest power-of-two `g` leaving a spare
|
|
58
|
-
* region for a delayed claimant (`slots / g >= consumers + 1`). At 32 slots that
|
|
59
|
-
* caps how wide a region can be well before it caps the worker count.
|
|
60
|
-
*/
|
|
62
|
+
/** Build the shared submit and private return buffers for a stealing pool. */
|
|
61
63
|
export const resolveStealRegionLanes = (consumers) => {
|
|
62
64
|
for (let lanes = LockBound.slots; lanes >= 1; lanes >>= 1) {
|
|
63
65
|
if (LockBound.slots / lanes >= consumers + 1)
|
|
@@ -65,9 +67,13 @@ export const resolveStealRegionLanes = (consumers) => {
|
|
|
65
67
|
}
|
|
66
68
|
throw new RangeError(`${consumers} stealing workers need more than ${LockBound.slots} lanes`);
|
|
67
69
|
};
|
|
70
|
+
/** Return the widest region allowed by the selected claim discipline. */
|
|
71
|
+
export const resolveMaxStealRegionLanes = (consumers, stealClaim) => stealClaim !== undefined && stealClaim !== "dekker"
|
|
72
|
+
? LockBound.slots
|
|
73
|
+
: resolveStealRegionLanes(consumers);
|
|
68
74
|
/** Maximum claimants that leave the protocol's required spare region. */
|
|
69
75
|
export const MAX_STEAL_CONSUMERS = LockBound.slots - 1;
|
|
70
|
-
export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSignalCapacity, usesAbortSignal, processWorker, }) => {
|
|
76
|
+
export const createStealPoolBuffers = ({ threads, payload, sharedArgs, regionLanes, stealClaim, abortSignalCapacity, usesAbortSignal, processWorker, }) => {
|
|
71
77
|
const basePayloadConfig = resolvePayloadBufferOptions({ options: payload });
|
|
72
78
|
const payloadConfig = processWorker === undefined
|
|
73
79
|
? basePayloadConfig
|
|
@@ -91,9 +97,10 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
|
|
|
91
97
|
payload: payloadSab,
|
|
92
98
|
}),
|
|
93
99
|
});
|
|
94
|
-
|
|
100
|
+
// Keep the default width valid for Dekker; wider CAS-mask regions are explicit.
|
|
101
|
+
const maxLanes = resolveMaxStealRegionLanes(threads, stealClaim);
|
|
95
102
|
const lanes = regionLanes === undefined
|
|
96
|
-
?
|
|
103
|
+
? resolveStealRegionLanes(threads)
|
|
97
104
|
: Math.min(Math.max(1, regionLanes | 0), maxLanes);
|
|
98
105
|
const resolvedAbortSignalCapacity = resolveAbortSignalCapacity(abortSignalCapacity);
|
|
99
106
|
const processMemory = processWorker === undefined ? undefined : createProcessStealMemoryLayout({
|
|
@@ -124,7 +131,11 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
|
|
|
124
131
|
textCompat: submitBuffers.textCompat,
|
|
125
132
|
consumers: threads,
|
|
126
133
|
regionLanes: lanes,
|
|
134
|
+
stealClaim,
|
|
127
135
|
processBoundary: processMemory !== undefined,
|
|
136
|
+
// Shared arguments are opt-in because the receiving task may outlive the
|
|
137
|
+
// borrowed region.
|
|
138
|
+
sharedReturn: sharedArgs === true,
|
|
128
139
|
});
|
|
129
140
|
const returnBuffers = [];
|
|
130
141
|
const hostReturnLocks = [];
|
|
@@ -170,13 +181,14 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
|
|
|
170
181
|
hostReturnLocks,
|
|
171
182
|
sharedQueue,
|
|
172
183
|
regionLanes: lanes,
|
|
184
|
+
stealClaim,
|
|
173
185
|
processMemory,
|
|
174
186
|
abortSignalSAB,
|
|
175
187
|
abortSignalMax: processMemory?.abortSignalMax ??
|
|
176
188
|
(abortSignalSAB === undefined ? undefined : resolvedAbortSignalCapacity),
|
|
177
189
|
};
|
|
178
190
|
};
|
|
179
|
-
export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload,
|
|
191
|
+
export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, permission, host, payload, sharedBytesEnabled, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, denoCompletionDoorbell, stealPool, }) => {
|
|
180
192
|
if (workerOptions?.runtime === "compiled") {
|
|
181
193
|
return spawnCompiledWorkerContext({
|
|
182
194
|
list,
|
|
@@ -189,14 +201,22 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
189
201
|
}
|
|
190
202
|
const tsFileUrl = new URL(import.meta.url);
|
|
191
203
|
const poliWorker = RUNTIME_WORKER;
|
|
192
|
-
const resolvedWorkerOptions = serializeWorkerBootstrapData(withDefaultWorkerTimers(workerOptions));
|
|
204
|
+
const resolvedWorkerOptions = serializeWorkerBootstrapData(withDefaultWorkerTimers(workerOptions, workerCount ?? totalNumberOfThread));
|
|
193
205
|
const useProcessWorkerRuntime = resolvedWorkerOptions.runtime === "process";
|
|
206
|
+
const canUseDenoCompletionDoorbell = !useProcessWorkerRuntime &&
|
|
207
|
+
denoCompletionDoorbell !== undefined;
|
|
194
208
|
const processWorkerRuntime = useProcessWorkerRuntime
|
|
195
209
|
? readProcessWorkerRuntime(resolvedWorkerOptions)
|
|
196
210
|
: undefined;
|
|
197
211
|
const processWorkerCommandPrefix = useProcessWorkerRuntime
|
|
198
212
|
? readProcessWorkerCommandPrefix(resolvedWorkerOptions)
|
|
199
213
|
: undefined;
|
|
214
|
+
const canUseProcessCompletionDoorbell = useProcessWorkerRuntime &&
|
|
215
|
+
host?.doorbell !== false &&
|
|
216
|
+
processWorkerUsesIpc({
|
|
217
|
+
processRuntime: processWorkerRuntime,
|
|
218
|
+
commandPrefix: processWorkerCommandPrefix,
|
|
219
|
+
});
|
|
200
220
|
const processSharedMemorySettings = useProcessWorkerRuntime
|
|
201
221
|
? readProcessSharedMemorySettings(resolvedWorkerOptions)
|
|
202
222
|
: undefined;
|
|
@@ -229,10 +249,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
229
249
|
const abortBytes = stealPool === undefined && usesAbortSignal === true
|
|
230
250
|
? abortSignalByteLength(resolvedAbortSignalCapacity)
|
|
231
251
|
: 0;
|
|
232
|
-
//
|
|
233
|
-
// `thread` must find its slice there. Falling back to a private layout would
|
|
234
|
-
// silently give this lane its own submit region — it would boot and then
|
|
235
|
-
// never see a task the host published to the shared one.
|
|
252
|
+
// Stealing process workers use their slice of the pool-wide mapping.
|
|
236
253
|
const stealProcessMemory = stealPool?.processMemory;
|
|
237
254
|
const processWorkerMemory = !useProcessWorkerRuntime
|
|
238
255
|
? undefined
|
|
@@ -255,18 +272,14 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
255
272
|
createWasmSharedArrayBuffer;
|
|
256
273
|
const createPayloadBuffer = processSharedMemory?.createBuffer;
|
|
257
274
|
const makePayloadBuffer = () => createPayloadBuffer
|
|
258
|
-
//
|
|
259
|
-
// payload ceiling instead of relying on SAB growth.
|
|
275
|
+
// Process-shared buffers are fixed-size.
|
|
260
276
|
? createPayloadBuffer(resolvedPayloadConfig.payloadMaxByteLength)
|
|
261
277
|
: resolvedPayloadConfig.mode === "growable"
|
|
262
278
|
? createSharedArrayBuffer(resolvedPayloadConfig.payloadInitialBytes, resolvedPayloadConfig.payloadMaxByteLength)
|
|
263
279
|
: createSharedArrayBuffer(resolvedPayloadConfig.payloadInitialBytes);
|
|
264
280
|
const makeLockControlLayout = () => {
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
// Request/return headers stay in separate contiguous slabs to preserve
|
|
268
|
-
// sequential batching locality.
|
|
269
|
-
// Abort bitmap stays at the tail.
|
|
281
|
+
// Place hot control words first; keep headers contiguous and the abort bitmap
|
|
282
|
+
// at the tail.
|
|
270
283
|
return createLockControlCarpet({
|
|
271
284
|
signalBytes,
|
|
272
285
|
abortBytes,
|
|
@@ -341,68 +354,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
341
354
|
processRuntime: processWorkerRuntime,
|
|
342
355
|
signal: signalBox.opView,
|
|
343
356
|
});
|
|
344
|
-
const outstandingBorrowedReturns = bufferReferenceReturn === "borrow" && typeof WeakRef === "function"
|
|
345
|
-
? new Map()
|
|
346
|
-
: undefined;
|
|
347
|
-
const releaseBorrowedReturnToken = (token) => {
|
|
348
|
-
outstandingBorrowedReturns?.delete(token);
|
|
349
|
-
worker?.postMessage?.(createBufferReferenceReturnReleaseMessage(token));
|
|
350
|
-
};
|
|
351
|
-
const revokeOutstandingBorrowedReturns = (copyBytes) => {
|
|
352
|
-
if (outstandingBorrowedReturns === undefined)
|
|
353
|
-
return;
|
|
354
|
-
for (const [token, record] of outstandingBorrowedReturns) {
|
|
355
|
-
try {
|
|
356
|
-
const ref = record.ref.deref();
|
|
357
|
-
if (ref !== undefined) {
|
|
358
|
-
ref.revokeBorrow(copyBytes);
|
|
359
|
-
continue;
|
|
360
|
-
}
|
|
361
|
-
const aliasBuffer = record.aliasBuffer?.deref();
|
|
362
|
-
if (aliasBuffer !== undefined &&
|
|
363
|
-
detachArrayBufferBestEffort(record.runtime, aliasBuffer) &&
|
|
364
|
-
copyBytes) {
|
|
365
|
-
releaseBorrowedReturnToken(token);
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
catch {
|
|
369
|
-
// best effort
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
outstandingBorrowedReturns.clear();
|
|
373
|
-
};
|
|
374
|
-
const returnHooks = bufferReferenceReturn === "borrow"
|
|
375
|
-
? {
|
|
376
|
-
release: releaseBorrowedReturnToken,
|
|
377
|
-
track: (ref, token, aliasBuffer) => {
|
|
378
|
-
if (outstandingBorrowedReturns === undefined)
|
|
379
|
-
return;
|
|
380
|
-
const record = outstandingBorrowedReturns.get(token);
|
|
381
|
-
if (record !== undefined) {
|
|
382
|
-
if (aliasBuffer !== undefined) {
|
|
383
|
-
record.aliasBuffer = new WeakRef(aliasBuffer);
|
|
384
|
-
}
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
outstandingBorrowedReturns.set(token, {
|
|
388
|
-
ref: new WeakRef(ref),
|
|
389
|
-
aliasBuffer: aliasBuffer === undefined
|
|
390
|
-
? undefined
|
|
391
|
-
: new WeakRef(aliasBuffer),
|
|
392
|
-
runtime: ref.runtime,
|
|
393
|
-
});
|
|
394
|
-
},
|
|
395
|
-
}
|
|
396
|
-
: undefined;
|
|
397
357
|
const queue = stealPool?.sharedQueue ?? createHostTxQueue({
|
|
398
358
|
lock,
|
|
399
359
|
returnLock,
|
|
400
360
|
abortSignals,
|
|
401
|
-
releaseBufferReferenceReturn: returnHooks,
|
|
402
361
|
});
|
|
403
|
-
if (stealPool !== undefined) {
|
|
404
|
-
stealPool.sharedQueue.setReturnHooks(stealPool.consumerId, returnHooks);
|
|
405
|
-
}
|
|
406
362
|
const { enqueue, rejectAll, txIdle, } = queue;
|
|
407
363
|
const thisSignal = signalBox.opView;
|
|
408
364
|
const a_add = Atomics.add;
|
|
@@ -411,8 +367,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
411
367
|
const canNotifySignal = thisSignal.buffer instanceof SharedArrayBuffer;
|
|
412
368
|
const notifySignal = nativeNotifySignal ??
|
|
413
369
|
(canNotifySignal ? (() => a_notify(thisSignal, 0, 1)) : undefined);
|
|
414
|
-
//
|
|
415
|
-
// (new work just arrived) — the dispatcher drain wakes it the same way.
|
|
370
|
+
// Wake this lane when its worker is parked.
|
|
416
371
|
const laneWake = () => {
|
|
417
372
|
if (a_load(signalBox.rxStatus, 0) === 0) {
|
|
418
373
|
a_add(thisSignal, 0, 1);
|
|
@@ -425,10 +380,16 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
425
380
|
const ownsChannel = sharedChannelHandler === undefined &&
|
|
426
381
|
stealPool === undefined;
|
|
427
382
|
const ownChannel = sharedChannelHandler ?? new ChannelHandler();
|
|
428
|
-
//
|
|
429
|
-
|
|
430
|
-
const
|
|
431
|
-
|
|
383
|
+
// Create the Node doorbell before bootstrapping the worker.
|
|
384
|
+
let nodeCompletionWake;
|
|
385
|
+
const nodeCompletionDoorbell = !useProcessWorkerRuntime &&
|
|
386
|
+
host?.doorbell !== false && host?.nativeDoorbell === true
|
|
387
|
+
? createNodeCompletionDoorbell(() => nodeCompletionWake?.())
|
|
388
|
+
: undefined;
|
|
389
|
+
const canUseNodeCompletionDoorbell = nodeCompletionDoorbell !== undefined;
|
|
390
|
+
// A stealing pool has one dispatcher for its shared queue.
|
|
391
|
+
const { check: dispatcherCheck, wakeCompletion: directCompletionWake, } = stealPool !== undefined
|
|
392
|
+
? { check: undefined, wakeCompletion: undefined }
|
|
432
393
|
: hostDispatcherLoop({
|
|
433
394
|
signalBox,
|
|
434
395
|
queue,
|
|
@@ -436,7 +397,15 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
436
397
|
dispatcherOptions: host,
|
|
437
398
|
notifySignal: nativeNotifySignal,
|
|
438
399
|
crossProcess: useProcessWorkerRuntime,
|
|
400
|
+
nativeCompletionDoorbell: canUseDenoCompletionDoorbell ||
|
|
401
|
+
canUseNodeCompletionDoorbell,
|
|
402
|
+
processCompletionDoorbell: canUseProcessCompletionDoorbell,
|
|
439
403
|
});
|
|
404
|
+
if (canUseDenoCompletionDoorbell && dispatcherCheck !== undefined) {
|
|
405
|
+
denoCompletionDoorbell.listen(thread, directCompletionWake);
|
|
406
|
+
}
|
|
407
|
+
nodeCompletionWake = directCompletionWake;
|
|
408
|
+
let processCompletionWake = directCompletionWake;
|
|
440
409
|
if (ownsChannel && dispatcherCheck !== undefined) {
|
|
441
410
|
ownChannel.open(dispatcherCheck);
|
|
442
411
|
channelHandler = ownChannel;
|
|
@@ -444,7 +413,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
444
413
|
if (dispatcherCheck.isRunning === true)
|
|
445
414
|
return;
|
|
446
415
|
dispatcherCheck.isRunning = true;
|
|
447
|
-
|
|
416
|
+
queueMicrotask(dispatcherCheck);
|
|
448
417
|
laneWake();
|
|
449
418
|
};
|
|
450
419
|
}
|
|
@@ -475,18 +444,27 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
475
444
|
lock: lockBuffers,
|
|
476
445
|
returnLock: returnLockBuffers,
|
|
477
446
|
payloadConfig: resolvedPayloadConfig,
|
|
478
|
-
|
|
447
|
+
sharedReturn: sharedBytesEnabled === true,
|
|
479
448
|
permission,
|
|
480
|
-
//
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
449
|
+
// Select the completion doorbell supported by the worker runtime.
|
|
450
|
+
notifyOnHostPublish: host?.doorbell !== false &&
|
|
451
|
+
(canUseProcessCompletionDoorbell ||
|
|
452
|
+
canUseNodeCompletionDoorbell ||
|
|
453
|
+
(!useProcessWorkerRuntime &&
|
|
454
|
+
(RUNTIME === "bun" || RUNTIME === "node") &&
|
|
455
|
+
typeof Atomics.waitAsync === "function")),
|
|
456
|
+
processCompletionDoorbell: canUseProcessCompletionDoorbell,
|
|
457
|
+
denoCompletionDoorbell: canUseDenoCompletionDoorbell
|
|
458
|
+
? denoCompletionDoorbell.pointer
|
|
459
|
+
: undefined,
|
|
460
|
+
nodeCompletionDoorbell: canUseNodeCompletionDoorbell
|
|
461
|
+
? nodeCompletionDoorbell.pointer
|
|
462
|
+
: undefined,
|
|
486
463
|
steal: stealPool === undefined ? undefined : {
|
|
487
464
|
consumers: stealPool.consumers,
|
|
488
465
|
consumerId: stealPool.consumerId,
|
|
489
466
|
regionLanes: stealPool.regionLanes,
|
|
467
|
+
claim: stealPool.stealClaim,
|
|
490
468
|
},
|
|
491
469
|
};
|
|
492
470
|
const baseWorkerOptions = {
|
|
@@ -556,6 +534,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
556
534
|
worker.postMessage?.(workerDataPayload);
|
|
557
535
|
}
|
|
558
536
|
let closedReason;
|
|
537
|
+
// A worker that exits because the host asked it to stop is a closed thread,
|
|
538
|
+
// not a crash. The stop is acknowledged in shared memory before the worker
|
|
539
|
+
// unwinds, so on a slow host the `exit` event can land ahead of `kills()`
|
|
540
|
+
// and label an orderly shutdown "Worker exited with code 0".
|
|
541
|
+
let stopRequested = false;
|
|
559
542
|
const deactivateStealConsumer = () => {
|
|
560
543
|
stealPool?.hostSubmitLock.deactivateStealConsumer(stealPool.consumerId);
|
|
561
544
|
};
|
|
@@ -570,21 +553,21 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
570
553
|
deactivateStealConsumer();
|
|
571
554
|
}
|
|
572
555
|
};
|
|
573
|
-
|
|
574
|
-
// can be copied out; after a crash/exit they are gone and borrows are
|
|
575
|
-
// detached so stale reads fail loud instead of aliasing freed memory.
|
|
576
|
-
const markWorkerClosed = (reason, workerBytesReadable = false) => {
|
|
556
|
+
const markWorkerClosed = (reason) => {
|
|
577
557
|
if (closedReason)
|
|
578
558
|
return;
|
|
579
559
|
closedReason = reason;
|
|
580
|
-
revokeOutstandingBorrowedReturns(workerBytesReadable);
|
|
581
560
|
rejectAll(reason);
|
|
582
561
|
channelHandler?.close();
|
|
583
562
|
};
|
|
584
563
|
const onWorkerMessage = (message) => {
|
|
564
|
+
if (isProcessCompletionDoorbell(message)) {
|
|
565
|
+
processCompletionWake?.();
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
585
568
|
if (!isWorkerFatalMessage(message))
|
|
586
569
|
return;
|
|
587
|
-
markWorkerClosed(`Worker startup failed: ${message[WORKER_FATAL_MESSAGE_KEY]}
|
|
570
|
+
markWorkerClosed(`Worker startup failed: ${message[WORKER_FATAL_MESSAGE_KEY]}`);
|
|
588
571
|
terminateFailedWorker();
|
|
589
572
|
};
|
|
590
573
|
const onWorkerError = (error) => {
|
|
@@ -597,11 +580,14 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
597
580
|
nodeWorker.on("message", onWorkerMessage);
|
|
598
581
|
nodeWorker.on("error", onWorkerError);
|
|
599
582
|
nodeWorker.on("exit", (code) => {
|
|
600
|
-
// Exit is the point at which this endpoint can no longer write WANT, so
|
|
601
|
-
// survivors may safely ignore any intent it left behind.
|
|
602
583
|
deactivateStealConsumer();
|
|
584
|
+
nodeCompletionDoorbell?.close();
|
|
603
585
|
if (closedReason !== undefined)
|
|
604
586
|
return;
|
|
587
|
+
if (stopRequested) {
|
|
588
|
+
markWorkerClosed("Thread closed");
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
605
591
|
const normalized = typeof code === "number" ? code : -1;
|
|
606
592
|
markWorkerClosed(`Worker exited with code ${normalized}`);
|
|
607
593
|
});
|
|
@@ -637,13 +623,39 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
637
623
|
return pending;
|
|
638
624
|
};
|
|
639
625
|
};
|
|
626
|
+
const requestWorkerStop = async () => {
|
|
627
|
+
stopRequested = true;
|
|
628
|
+
const stopView = signalBox.stopView;
|
|
629
|
+
if (stopView === undefined)
|
|
630
|
+
return true;
|
|
631
|
+
try {
|
|
632
|
+
Atomics.store(stopView, 0, WORKER_STOP.requested);
|
|
633
|
+
laneWake();
|
|
634
|
+
notifySignal?.();
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
if (!WORKER_STOP_NEEDS_ACK)
|
|
640
|
+
return true;
|
|
641
|
+
const deadline = Date.now() + WORKER_STOP_ACK_TIMEOUT_MS;
|
|
642
|
+
while (Atomics.load(stopView, 0) !== WORKER_STOP.acknowledged) {
|
|
643
|
+
if (Date.now() >= deadline)
|
|
644
|
+
return false;
|
|
645
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
646
|
+
}
|
|
647
|
+
return true;
|
|
648
|
+
};
|
|
640
649
|
const context = {
|
|
641
650
|
txIdle,
|
|
642
651
|
call,
|
|
652
|
+
requestStop: requestWorkerStop,
|
|
643
653
|
kills: async () => {
|
|
644
|
-
markWorkerClosed("Thread closed"
|
|
645
|
-
|
|
646
|
-
|
|
654
|
+
markWorkerClosed("Thread closed");
|
|
655
|
+
// Process workers must exit before their shared memory is unmapped.
|
|
656
|
+
const awaitExit = processWorkerMemory !== undefined;
|
|
657
|
+
const termination = terminateWorkerQuietly(worker, awaitExit);
|
|
658
|
+
if (awaitExit)
|
|
647
659
|
await termination;
|
|
648
660
|
cleanupProcessWorkerMemoryQuietly(processWorkerMemory);
|
|
649
661
|
},
|
|
@@ -656,6 +668,16 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
656
668
|
bindSend: sharedChannelHandler !== undefined || stealPool !== undefined
|
|
657
669
|
? ((fn) => void (dispatchSend = fn))
|
|
658
670
|
: undefined,
|
|
671
|
+
bindCompletionWake: (canUseProcessCompletionDoorbell ||
|
|
672
|
+
canUseNodeCompletionDoorbell) &&
|
|
673
|
+
stealPool !== undefined
|
|
674
|
+
? ((fn) => {
|
|
675
|
+
processCompletionWake = fn;
|
|
676
|
+
nodeCompletionWake = fn;
|
|
677
|
+
})
|
|
678
|
+
: undefined,
|
|
679
|
+
processCompletionDoorbell: canUseProcessCompletionDoorbell,
|
|
680
|
+
nodeCompletionDoorbell: canUseNodeCompletionDoorbell,
|
|
659
681
|
};
|
|
660
682
|
return context;
|
|
661
683
|
};
|
|
@@ -92,6 +92,15 @@ export declare const createProcessWorkerNativeSignalNotifier: ({ processRuntime,
|
|
|
92
92
|
processRuntime: ProcessWorkerRuntime | undefined;
|
|
93
93
|
signal: Int32Array;
|
|
94
94
|
}) => (() => void) | undefined;
|
|
95
|
+
/**
|
|
96
|
+
* Whether this host/child pair has the Node-compatible IPC channel that the
|
|
97
|
+
* process completion-doorbell prototype uses. Deno process workers currently
|
|
98
|
+
* boot through environment data and do not have that channel.
|
|
99
|
+
*/
|
|
100
|
+
export declare const processWorkerUsesIpc: ({ processRuntime, commandPrefix, }: {
|
|
101
|
+
processRuntime: ProcessWorkerRuntime | undefined;
|
|
102
|
+
commandPrefix?: ProcessWorkerCommandPrefix;
|
|
103
|
+
}) => boolean;
|
|
95
104
|
export declare const spawnProcessWorker: (options: {
|
|
96
105
|
workerUrl: string | URL;
|
|
97
106
|
bootPayload: ProcessWorkerBootPayload;
|
|
@@ -109,6 +109,13 @@ const isWindowsRuntimeHost = () => {
|
|
|
109
109
|
return denoOs === "windows";
|
|
110
110
|
return globalThis.process?.platform === "win32";
|
|
111
111
|
};
|
|
112
|
+
const processWorkerNeedsNamedMemory = (sharedMemory) =>
|
|
113
|
+
// `Deno.Command` does not reliably carry an anonymous memfd/shm fd into a
|
|
114
|
+
// child as stdin: Linux can hand the child a non-mappable descriptor and
|
|
115
|
+
// macOS cannot always reopen the host fd through `/dev/fd`. Named mappings
|
|
116
|
+
// are reopened by the worker instead, which works across Deno's supported
|
|
117
|
+
// hosts and process-worker runtimes.
|
|
118
|
+
sharedMemory.mode === "named" || isWindowsRuntimeHost() || RUNTIME === "deno";
|
|
112
119
|
let processWorkerMemoryNameCounter = 0;
|
|
113
120
|
const makeProcessWorkerMemoryName = (thread, prefix = "kpw") => {
|
|
114
121
|
const processId = globalThis.process?.pid ??
|
|
@@ -142,7 +149,7 @@ export const createProcessWorkerMemoryLayout = ({ signalBytes, abortBytes, paylo
|
|
|
142
149
|
const requestPayloadSlice = carpet.take("requestPayload", payloadBytes);
|
|
143
150
|
const returnPayloadSlice = carpet.take("returnPayload", payloadBytes);
|
|
144
151
|
const primitives = getProcessWorkerSharedMemoryPrimitives();
|
|
145
|
-
const forceNamed = sharedMemory
|
|
152
|
+
const forceNamed = processWorkerNeedsNamedMemory(sharedMemory);
|
|
146
153
|
const mapping = primitives.createSharedMemory(forceNamed
|
|
147
154
|
? {
|
|
148
155
|
size: carpet.byteLength(),
|
|
@@ -215,7 +222,7 @@ export const createProcessStealMemoryLayout = ({ threads, signalBytes, abortByte
|
|
|
215
222
|
returnPayload: carpet.take(`returnPayload-${thread}`, payloadBytes),
|
|
216
223
|
}));
|
|
217
224
|
const primitives = getProcessWorkerSharedMemoryPrimitives();
|
|
218
|
-
const forceNamed = sharedMemory
|
|
225
|
+
const forceNamed = processWorkerNeedsNamedMemory(sharedMemory);
|
|
219
226
|
const mapping = primitives.createSharedMemory(forceNamed
|
|
220
227
|
? {
|
|
221
228
|
size: carpet.byteLength(),
|
|
@@ -546,6 +553,19 @@ export const createProcessWorkerNativeSignalNotifier = ({ processRuntime, signal
|
|
|
546
553
|
return undefined;
|
|
547
554
|
}
|
|
548
555
|
};
|
|
556
|
+
/**
|
|
557
|
+
* Whether this host/child pair has the Node-compatible IPC channel that the
|
|
558
|
+
* process completion-doorbell prototype uses. Deno process workers currently
|
|
559
|
+
* boot through environment data and do not have that channel.
|
|
560
|
+
*/
|
|
561
|
+
export const processWorkerUsesIpc = ({ processRuntime, commandPrefix, }) => {
|
|
562
|
+
if (processRuntime === undefined || commandPrefix !== undefined)
|
|
563
|
+
return false;
|
|
564
|
+
if (RUNTIME === "node") {
|
|
565
|
+
return processRuntime === "node" || processRuntime === "bun";
|
|
566
|
+
}
|
|
567
|
+
return RUNTIME === "bun" && processRuntime === "bun";
|
|
568
|
+
};
|
|
549
569
|
const createProcessWorkerEventHub = () => {
|
|
550
570
|
const messageHandlers = [];
|
|
551
571
|
const errorHandlers = [];
|
|
@@ -2,7 +2,6 @@ import "../memory/payloadCodec.js";
|
|
|
2
2
|
import { type Lock2, type Task } from "../memory/lock.js";
|
|
3
3
|
import type { AbortSignalOption, TaskTimeout } from "../types.js";
|
|
4
4
|
import { type SignalAbortStore } from "../shared/abortSignal.js";
|
|
5
|
-
import { type BufferReferenceReturnHooks } from "../connections/buffer-reference.js";
|
|
6
5
|
type RawArguments = unknown;
|
|
7
6
|
type FunctionID = number;
|
|
8
7
|
type QueueTask = Task;
|
|
@@ -23,19 +22,17 @@ type CreateHostTxQueueArgs = {
|
|
|
23
22
|
* When omitted, only `returnLock` is drained (the classic one-lane shape).
|
|
24
23
|
*/
|
|
25
24
|
extraReturnLocks?: readonly Lock2[];
|
|
26
|
-
releaseBufferReferenceReturn?: ((token: bigint) => void) | BufferReferenceReturnHooks;
|
|
27
25
|
abortSignals?: Pick<SignalAbortStore, "getSignal" | "setSignal" | "resetSignal" | "closeNow">;
|
|
28
26
|
now?: () => number;
|
|
29
27
|
};
|
|
30
|
-
|
|
31
|
-
export declare function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, releaseBufferReferenceReturn, abortSignals, now, }: CreateHostTxQueueArgs): {
|
|
28
|
+
export declare function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, abortSignals, now, }: CreateHostTxQueueArgs): {
|
|
32
29
|
rejectAll: (reason: string) => void;
|
|
33
30
|
hasPendingFrames: () => boolean;
|
|
34
31
|
txIdle: () => boolean;
|
|
35
32
|
completeFrame: () => number;
|
|
36
33
|
waitForCompletion: (onWake: () => void, timeoutMs?: number) => boolean;
|
|
34
|
+
armCompletionNotifier: () => boolean;
|
|
37
35
|
setCompletionWaiterArmed: (armed: boolean) => void;
|
|
38
|
-
setReturnHooks: (lane: number, hooks: ReturnHooks | undefined) => void;
|
|
39
36
|
enqueue: (functionID: FunctionID, timeout?: TaskTimeout, abortSignal?: AbortSignalOption) => (rawArgs: RawArguments) => Promise<never> | import("../common/with-resolvers.js").PromiseWithMaybeReject<unknown>;
|
|
40
37
|
flushToWorker: () => boolean;
|
|
41
38
|
enqueueKnown: (task: QueueTask) => boolean;
|