knitting 0.1.61 → 0.1.62
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 +21 -15
- package/src/common/task-source.d.ts +9 -0
- package/src/common/task-source.js +15 -0
- package/src/memory/lock.d.ts +12 -1
- package/src/memory/lock.js +51 -2
- 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 +15 -3
- package/src/runtime/pool.js +65 -22
- 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 +1 -1
- package/src/runtime/worker-common.js +2 -1
- package/src/types.d.ts +42 -9
- package/src/worker/loop.js +8 -1
package/src/runtime/pool.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHostTxQueue } from "./tx-queue.js";
|
|
2
|
-
import { type ProcessSharedMemoryBacking } from "./process-worker.js";
|
|
2
|
+
import { type ProcessSharedMemoryBacking, type ProcessStealMemoryLayout, type ResolvedProcessSharedMemorySettings } from "./process-worker.js";
|
|
3
3
|
import { type Sab } from "../ipc/transport/shared-memory.js";
|
|
4
4
|
import { ChannelHandler, type DispatcherCheck } from "./dispatcher.js";
|
|
5
5
|
import { lock2, type Task } from "../memory/lock.js";
|
|
@@ -20,12 +20,16 @@ import { type PayloadBufferOptions } from "../memory/payload-config.js";
|
|
|
20
20
|
export declare const resolveStealRegionLanes: (consumers: number) => number;
|
|
21
21
|
/** Maximum claimants that leave the protocol's required spare region. */
|
|
22
22
|
export declare const MAX_STEAL_CONSUMERS: number;
|
|
23
|
-
export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, abortSignalCapacity, usesAbortSignal, }: {
|
|
23
|
+
export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, abortSignalCapacity, usesAbortSignal, processWorker, }: {
|
|
24
24
|
threads: number;
|
|
25
25
|
payload?: PayloadBufferOptions;
|
|
26
26
|
regionLanes?: number;
|
|
27
27
|
abortSignalCapacity?: number;
|
|
28
28
|
usesAbortSignal?: boolean;
|
|
29
|
+
processWorker?: {
|
|
30
|
+
signalBytes: number;
|
|
31
|
+
sharedMemory: ResolvedProcessSharedMemorySettings;
|
|
32
|
+
};
|
|
29
33
|
}) => {
|
|
30
34
|
submitBuffers: LockBuffers;
|
|
31
35
|
returnBuffers: LockBuffers[];
|
|
@@ -48,6 +52,8 @@ export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, a
|
|
|
48
52
|
shouldSettle?: (task: Task) => boolean;
|
|
49
53
|
activeRejectPlaceholder?: Task["reject"];
|
|
50
54
|
}) => () => number;
|
|
55
|
+
waitForHostChange: (timeoutMs?: number) => import("../memory/lock.js").WaitAsyncResult | undefined;
|
|
56
|
+
setHostWaiterArmed: (armed: boolean) => void;
|
|
51
57
|
hasPendingFrames: () => boolean;
|
|
52
58
|
getPendingFrameCount: () => number;
|
|
53
59
|
getPendingPromiseCount: () => number;
|
|
@@ -75,6 +81,8 @@ export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, a
|
|
|
75
81
|
shouldSettle?: (task: Task) => boolean;
|
|
76
82
|
activeRejectPlaceholder?: Task["reject"];
|
|
77
83
|
}) => () => number;
|
|
84
|
+
waitForHostChange: (timeoutMs?: number) => import("../memory/lock.js").WaitAsyncResult | undefined;
|
|
85
|
+
setHostWaiterArmed: (armed: boolean) => void;
|
|
78
86
|
hasPendingFrames: () => boolean;
|
|
79
87
|
getPendingFrameCount: () => number;
|
|
80
88
|
getPendingPromiseCount: () => number;
|
|
@@ -88,6 +96,8 @@ export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, a
|
|
|
88
96
|
hasPendingFrames: () => boolean;
|
|
89
97
|
txIdle: () => boolean;
|
|
90
98
|
completeFrame: () => number;
|
|
99
|
+
waitForCompletion: (onWake: () => void, timeoutMs?: number) => boolean;
|
|
100
|
+
setCompletionWaiterArmed: (armed: boolean) => void;
|
|
91
101
|
setReturnHooks: (lane: number, hooks: (import("../connections/buffer-reference.js").BufferReferenceReturnHooks | ((token: bigint) => void)) | undefined) => void;
|
|
92
102
|
enqueue: (functionID: number, timeout?: import("../types.js").TaskTimeout, abortSignal?: import("../types.js").AbortSignalOption) => (rawArgs: unknown) => Promise<never> | import("../common/with-resolvers.js").PromiseWithMaybeReject<unknown>;
|
|
93
103
|
flushToWorker: () => boolean;
|
|
@@ -95,7 +105,8 @@ export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, a
|
|
|
95
105
|
settlePromisePayload: (task: Task, isRejected: boolean, value: unknown) => boolean;
|
|
96
106
|
};
|
|
97
107
|
regionLanes: number;
|
|
98
|
-
|
|
108
|
+
processMemory: ProcessStealMemoryLayout;
|
|
109
|
+
abortSignalSAB: SharedArrayBuffer | import("../types.js").SharedBufferRegion;
|
|
99
110
|
abortSignalMax: number;
|
|
100
111
|
};
|
|
101
112
|
export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, stealPool, }: {
|
|
@@ -140,6 +151,7 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
|
|
|
140
151
|
regionLanes: number;
|
|
141
152
|
abortSignalSAB?: LockBuffers["headers"];
|
|
142
153
|
abortSignalMax?: number;
|
|
154
|
+
processMemory?: ProcessStealMemoryLayout;
|
|
143
155
|
};
|
|
144
156
|
}) => (WorkerContext & {
|
|
145
157
|
dispatcherCheck?: never;
|
package/src/runtime/pool.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHostTxQueue } from "./tx-queue.js";
|
|
2
|
-
import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, spawnProcessWorker, toProcessWorkerBootPayload, } from "./process-worker.js";
|
|
2
|
+
import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessStealMemoryLayout, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, spawnProcessWorker, toProcessWorkerBootPayload, } from "./process-worker.js";
|
|
3
3
|
import { serializeWorkerBootstrapData, terminateWorkerQuietly, toWorkerCompatExecArgv, toWorkerSafeExecArgv, } from "./worker-common.js";
|
|
4
4
|
import { createSharedMemoryTransport, TRANSPORT_SIGNAL_BYTES, } from "../ipc/transport/shared-memory.js";
|
|
5
5
|
import { ChannelHandler, hostDispatcherLoop, } from "./dispatcher.js";
|
|
@@ -8,7 +8,7 @@ import { HEADER_SLOT_STRIDE_U32, lock2, LOCK_SECTOR_BYTE_LENGTH, LockBound, } fr
|
|
|
8
8
|
// see lock.ts). Must run before any lock2() call.
|
|
9
9
|
import "../memory/payloadCodec.js";
|
|
10
10
|
import "../worker/loop.js";
|
|
11
|
-
import { createSharedArrayBuffer, createWasmSharedArrayBuffer, IS_BROWSER, } from "../common/runtime.js";
|
|
11
|
+
import { createSharedArrayBuffer, createWasmSharedArrayBuffer, IS_BROWSER, RUNTIME, } from "../common/runtime.js";
|
|
12
12
|
import { HAS_NODE_WORKER_THREADS, 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";
|
|
@@ -67,8 +67,11 @@ export const resolveStealRegionLanes = (consumers) => {
|
|
|
67
67
|
};
|
|
68
68
|
/** Maximum claimants that leave the protocol's required spare region. */
|
|
69
69
|
export const MAX_STEAL_CONSUMERS = LockBound.slots - 1;
|
|
70
|
-
export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSignalCapacity, usesAbortSignal, }) => {
|
|
71
|
-
const
|
|
70
|
+
export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSignalCapacity, usesAbortSignal, processWorker, }) => {
|
|
71
|
+
const basePayloadConfig = resolvePayloadBufferOptions({ options: payload });
|
|
72
|
+
const payloadConfig = processWorker === undefined
|
|
73
|
+
? basePayloadConfig
|
|
74
|
+
: withFixedPayloadConfig(basePayloadConfig);
|
|
72
75
|
const makePayload = () => payloadConfig.mode === "growable"
|
|
73
76
|
? createSharedArrayBuffer(payloadConfig.payloadInitialBytes, payloadConfig.payloadMaxByteLength)
|
|
74
77
|
: createSharedArrayBuffer(payloadConfig.payloadInitialBytes);
|
|
@@ -92,8 +95,25 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
|
|
|
92
95
|
const lanes = regionLanes === undefined
|
|
93
96
|
? maxLanes
|
|
94
97
|
: Math.min(Math.max(1, regionLanes | 0), maxLanes);
|
|
95
|
-
const
|
|
96
|
-
const
|
|
98
|
+
const resolvedAbortSignalCapacity = resolveAbortSignalCapacity(abortSignalCapacity);
|
|
99
|
+
const processMemory = processWorker === undefined ? undefined : createProcessStealMemoryLayout({
|
|
100
|
+
threads,
|
|
101
|
+
signalBytes: processWorker.signalBytes,
|
|
102
|
+
abortBytes: usesAbortSignal === true
|
|
103
|
+
? abortSignalByteLength(resolvedAbortSignalCapacity)
|
|
104
|
+
: 0,
|
|
105
|
+
abortSignalMax: usesAbortSignal === true
|
|
106
|
+
? resolvedAbortSignalCapacity
|
|
107
|
+
: undefined,
|
|
108
|
+
payloadBytes: payloadConfig.payloadMaxByteLength,
|
|
109
|
+
sharedMemory: processWorker.sharedMemory,
|
|
110
|
+
});
|
|
111
|
+
const submitBuffers = processMemory === undefined
|
|
112
|
+
? (() => {
|
|
113
|
+
const submitCarpet = carpet();
|
|
114
|
+
return toBuffers(submitCarpet.lock, makePayload());
|
|
115
|
+
})()
|
|
116
|
+
: toBuffers(processMemory.workers[0].controlLayout.lock, processMemory.workers[0].lockPayload);
|
|
97
117
|
const hostSubmitLock = lock2({
|
|
98
118
|
headers: submitBuffers.headers,
|
|
99
119
|
headerSlotStrideU32: submitBuffers.headerSlotStrideU32,
|
|
@@ -104,11 +124,14 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
|
|
|
104
124
|
textCompat: submitBuffers.textCompat,
|
|
105
125
|
consumers: threads,
|
|
106
126
|
regionLanes: lanes,
|
|
127
|
+
processBoundary: processMemory !== undefined,
|
|
107
128
|
});
|
|
108
129
|
const returnBuffers = [];
|
|
109
130
|
const hostReturnLocks = [];
|
|
110
131
|
for (let i = 0; i < threads; i++) {
|
|
111
|
-
const buffers =
|
|
132
|
+
const buffers = processMemory === undefined
|
|
133
|
+
? toBuffers(carpet().returnLock, makePayload())
|
|
134
|
+
: toBuffers(processMemory.workers[i].controlLayout.returnLock, processMemory.workers[i].returnPayload);
|
|
112
135
|
returnBuffers.push(buffers);
|
|
113
136
|
hostReturnLocks.push(lock2({
|
|
114
137
|
headers: buffers.headers,
|
|
@@ -118,14 +141,15 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
|
|
|
118
141
|
payloadSector: buffers.payloadSector,
|
|
119
142
|
payloadConfig,
|
|
120
143
|
textCompat: buffers.textCompat,
|
|
144
|
+
processBoundary: processMemory !== undefined,
|
|
121
145
|
}));
|
|
122
146
|
}
|
|
123
147
|
// Abort-aware tasks can execute on any claimant, so all workers and the
|
|
124
148
|
// pool-global queue must observe one bitmap. Per-lane abort buffers would
|
|
125
149
|
// encode an id from one allocator and test it against another lane's bits.
|
|
126
|
-
const resolvedAbortSignalCapacity = resolveAbortSignalCapacity(abortSignalCapacity);
|
|
127
150
|
const abortSignalSAB = usesAbortSignal === true
|
|
128
|
-
?
|
|
151
|
+
? processMemory?.workers[0]?.controlLayout.abortSignals ??
|
|
152
|
+
createSharedArrayBuffer(abortSignalByteLength(resolvedAbortSignalCapacity))
|
|
129
153
|
: undefined;
|
|
130
154
|
const abortSignals = abortSignalSAB === undefined
|
|
131
155
|
? undefined
|
|
@@ -146,10 +170,10 @@ export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSig
|
|
|
146
170
|
hostReturnLocks,
|
|
147
171
|
sharedQueue,
|
|
148
172
|
regionLanes: lanes,
|
|
173
|
+
processMemory,
|
|
149
174
|
abortSignalSAB,
|
|
150
|
-
abortSignalMax:
|
|
151
|
-
? undefined
|
|
152
|
-
: resolvedAbortSignalCapacity,
|
|
175
|
+
abortSignalMax: processMemory?.abortSignalMax ??
|
|
176
|
+
(abortSignalSAB === undefined ? undefined : resolvedAbortSignalCapacity),
|
|
153
177
|
};
|
|
154
178
|
};
|
|
155
179
|
export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, stealPool, }) => {
|
|
@@ -205,15 +229,25 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
205
229
|
const abortBytes = stealPool === undefined && usesAbortSignal === true
|
|
206
230
|
? abortSignalByteLength(resolvedAbortSignalCapacity)
|
|
207
231
|
: 0;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
232
|
+
// A stealing process pool carves every lane out of one mapping, so lane
|
|
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.
|
|
236
|
+
const stealProcessMemory = stealPool?.processMemory;
|
|
237
|
+
const processWorkerMemory = !useProcessWorkerRuntime
|
|
238
|
+
? undefined
|
|
239
|
+
: stealProcessMemory === undefined
|
|
240
|
+
? createProcessWorkerMemoryLayout({
|
|
241
|
+
signalBytes,
|
|
242
|
+
abortBytes,
|
|
243
|
+
payloadBytes: resolvedPayloadConfig.payloadMaxByteLength,
|
|
244
|
+
thread,
|
|
245
|
+
sharedMemory: processSharedMemorySettings,
|
|
246
|
+
})
|
|
247
|
+
: stealProcessMemory.workers[thread] ??
|
|
248
|
+
(() => {
|
|
249
|
+
throw new RangeError(`stealing process pool has no shared-memory slice for worker ${thread}`);
|
|
250
|
+
})();
|
|
217
251
|
const processSharedMemory = processWorkerMemory === undefined
|
|
218
252
|
? createProcessSharedMemoryAllocator(debug)
|
|
219
253
|
: undefined;
|
|
@@ -401,6 +435,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
401
435
|
channelHandler: ownChannel,
|
|
402
436
|
dispatcherOptions: host,
|
|
403
437
|
notifySignal: nativeNotifySignal,
|
|
438
|
+
crossProcess: useProcessWorkerRuntime,
|
|
404
439
|
});
|
|
405
440
|
if (ownsChannel && dispatcherCheck !== undefined) {
|
|
406
441
|
ownChannel.open(dispatcherCheck);
|
|
@@ -442,6 +477,12 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
442
477
|
payloadConfig: resolvedPayloadConfig,
|
|
443
478
|
bufferReferenceReturn,
|
|
444
479
|
permission,
|
|
480
|
+
// Mirrors the dispatcher's `canUseDoorbell`: a cross-process worker cannot
|
|
481
|
+
// ring the host's waiter, so do not make it pay the atomic op.
|
|
482
|
+
notifyOnHostPublish: !useProcessWorkerRuntime &&
|
|
483
|
+
host?.doorbell !== false &&
|
|
484
|
+
(RUNTIME === "bun" || RUNTIME === "node") &&
|
|
485
|
+
typeof Atomics.waitAsync === "function",
|
|
445
486
|
steal: stealPool === undefined ? undefined : {
|
|
446
487
|
consumers: stealPool.consumers,
|
|
447
488
|
consumerId: stealPool.consumerId,
|
|
@@ -601,7 +642,9 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
601
642
|
call,
|
|
602
643
|
kills: async () => {
|
|
603
644
|
markWorkerClosed("Thread closed", true);
|
|
604
|
-
terminateWorkerQuietly(worker);
|
|
645
|
+
const termination = terminateWorkerQuietly(worker);
|
|
646
|
+
if (processWorkerMemory !== undefined)
|
|
647
|
+
await termination;
|
|
605
648
|
cleanupProcessWorkerMemoryQuietly(processWorkerMemory);
|
|
606
649
|
},
|
|
607
650
|
lock,
|
|
@@ -29,6 +29,20 @@ export type ProcessWorkerMemoryLayout = {
|
|
|
29
29
|
returnPayload: SharedBufferSource;
|
|
30
30
|
cleanup: () => void;
|
|
31
31
|
};
|
|
32
|
+
/**
|
|
33
|
+
* Process-worker memory for a stealing pool.
|
|
34
|
+
*
|
|
35
|
+
* The request side is one shared region all child processes may claim from;
|
|
36
|
+
* each worker keeps its own signals and return region. Every per-worker view
|
|
37
|
+
* points into the same OS mapping so the existing fd/name boot protocol can
|
|
38
|
+
* carry all of the slices without adding a second descriptor to WorkerData.
|
|
39
|
+
*/
|
|
40
|
+
export type ProcessStealMemoryLayout = {
|
|
41
|
+
mapping: SharedMemoryMapping<SharedMemoryBuffer>;
|
|
42
|
+
descriptor: FileDescriptor;
|
|
43
|
+
workers: ProcessWorkerMemoryLayout[];
|
|
44
|
+
abortSignalMax?: number;
|
|
45
|
+
};
|
|
32
46
|
export type ProcessWorkerRuntime = "bun" | "deno" | "node";
|
|
33
47
|
export type ProcessWorkerCommandPrefix = NonNullable<WorkerSettings["processCommandPrefix"]>;
|
|
34
48
|
export type ResolvedProcessSharedMemorySettings = {
|
|
@@ -61,6 +75,14 @@ export declare const createProcessWorkerMemoryLayout: ({ signalBytes, abortBytes
|
|
|
61
75
|
thread: number;
|
|
62
76
|
sharedMemory: ResolvedProcessSharedMemorySettings;
|
|
63
77
|
}) => ProcessWorkerMemoryLayout;
|
|
78
|
+
export declare const createProcessStealMemoryLayout: ({ threads, signalBytes, abortBytes, abortSignalMax, payloadBytes, sharedMemory, }: {
|
|
79
|
+
threads: number;
|
|
80
|
+
signalBytes: number;
|
|
81
|
+
abortBytes: number;
|
|
82
|
+
abortSignalMax?: number;
|
|
83
|
+
payloadBytes: number;
|
|
84
|
+
sharedMemory: ResolvedProcessSharedMemorySettings;
|
|
85
|
+
}) => ProcessStealMemoryLayout;
|
|
64
86
|
export declare const toProcessWorkerBootPayload: (workerData: WorkerData, memory: ProcessWorkerMemoryLayout) => ProcessWorkerBootPayload;
|
|
65
87
|
export declare const readProcessWorkerRuntime: (options: WorkerSettings | undefined) => ProcessWorkerRuntime;
|
|
66
88
|
export declare const readProcessWorkerCommandPrefix: (options: WorkerSettings | undefined) => ProcessWorkerCommandPrefix | undefined;
|
|
@@ -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,4 @@ 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
|
-
export declare const terminateWorkerQuietly: (worker: SpawnedWorker) => void
|
|
13
|
+
export declare const terminateWorkerQuietly: (worker: SpawnedWorker) => Promise<void>;
|
|
@@ -95,8 +95,9 @@ export const terminateWorkerQuietly = (worker) => {
|
|
|
95
95
|
// Runaway worker termination can be slow or stuck on some runtimes; once the
|
|
96
96
|
// pool is closing it must not keep the host process alive.
|
|
97
97
|
worker.unref?.();
|
|
98
|
-
|
|
98
|
+
return Promise.resolve(worker.terminate()).then(() => { }, () => { });
|
|
99
99
|
}
|
|
100
100
|
catch {
|
|
101
|
+
return Promise.resolve();
|
|
101
102
|
}
|
|
102
103
|
};
|
package/src/types.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ type WorkerData = {
|
|
|
34
34
|
payloadConfig?: PayloadBufferOptions;
|
|
35
35
|
bufferReferenceReturn?: "copy" | "borrow";
|
|
36
36
|
permission?: ResolvedPermissionProtocol;
|
|
37
|
+
/** Whether this host can arm an async completion waiter on the return lock. */
|
|
38
|
+
notifyOnHostPublish?: boolean;
|
|
37
39
|
/**
|
|
38
40
|
* Work stealing. When present, `lock` is a submit region shared by every
|
|
39
41
|
* worker and this worker claims from it as consumer `consumerId` of
|
|
@@ -341,13 +343,46 @@ type WorkerTimers = {
|
|
|
341
343
|
};
|
|
342
344
|
type DispatcherSettings = {
|
|
343
345
|
/**
|
|
344
|
-
* How many immediate notify loops before
|
|
346
|
+
* How many immediate notify loops before the dispatcher stops re-arming the
|
|
347
|
+
* pump for free.
|
|
348
|
+
*
|
|
349
|
+
* The default depends on what the dispatcher escalates *to*. Polling
|
|
350
|
+
* escalates to a `setTimeout` ladder costing ~1.1ms even at delay 0, so it
|
|
351
|
+
* defaults to 128 — escalating is expensive and the wide window also batches
|
|
352
|
+
* completions. A doorbell escalates to `Atomics.waitAsync` at roughly the
|
|
353
|
+
* price of one hop, so pools that have one default to 1. Pools without a
|
|
354
|
+
* doorbell — Deno, or `doorbell: false` — keep 128.
|
|
355
|
+
*
|
|
356
|
+
* Setting this explicitly opts out of that coupling for every pool shape.
|
|
345
357
|
*/
|
|
346
358
|
stallFreeLoops?: number;
|
|
347
359
|
/**
|
|
348
360
|
* Max backoff delay (milliseconds).
|
|
349
361
|
*/
|
|
350
362
|
maxBackoffMs?: number;
|
|
363
|
+
/**
|
|
364
|
+
* Replace idle completion polling with an `Atomics.waitAsync` doorbell when
|
|
365
|
+
* the host runtime supports it.
|
|
366
|
+
*
|
|
367
|
+
* Defaults to enabled on Node and Bun at any worker count, where it costs
|
|
368
|
+
* 1.6-3.9x less host CPU per completed call. Under HTTP load, where the host
|
|
369
|
+
* has real work of its own, that converts to +13% to +36% throughput.
|
|
370
|
+
*
|
|
371
|
+
* Turn it off for a pool that oversubscribes its machine. The doorbell only
|
|
372
|
+
* progresses when the host gets scheduled, so once workers occupy every core
|
|
373
|
+
* a wake must preempt one: measured +5% to +12.6% rps while workers+host fit
|
|
374
|
+
* within the cores, -22% to -32% once they do not. That is not gated
|
|
375
|
+
* automatically because the core count cannot be probed portably.
|
|
376
|
+
*
|
|
377
|
+
* Forced off, overriding an explicit `true`, where a doorbell cannot work:
|
|
378
|
+
* on Deno, whose `waitAsync` does not wake an idle event loop, and for
|
|
379
|
+
* process workers, which live in another process and so cannot ring a host
|
|
380
|
+
* waiter at all — V8 keeps its Atomics waiter list per isolate, which is why
|
|
381
|
+
* they wake through a native futex addon instead. Compiled (Porffor) workers
|
|
382
|
+
* never reach this path: they reject `host` outright and use pipes rather
|
|
383
|
+
* than shared memory.
|
|
384
|
+
*/
|
|
385
|
+
doorbell?: boolean;
|
|
351
386
|
/**
|
|
352
387
|
* Host dispatcher topology.
|
|
353
388
|
* - `"per-thread"`: each worker owns its dispatcher and macro channel.
|
|
@@ -366,14 +401,12 @@ type DispatcherSettings = {
|
|
|
366
401
|
* claim from, private return lanes, and a pool-global pending registry. The
|
|
367
402
|
* endpoint that claims a task owns its response.
|
|
368
403
|
*
|
|
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.
|
|
404
|
+
* Enabled by default for compatible multi-worker thread and process pools
|
|
405
|
+
* unless a balancer or private-lane dispatcher was explicitly selected.
|
|
406
|
+
* One-worker pools, inliners, compiled/Porffor workers, and pools above the
|
|
407
|
+
* current 31-claimant protocol limit retain their existing transport. Set
|
|
408
|
+
* `false` (or `KNITTING_STEAL=0`) to opt out; `KNITTING_STEAL=1` explicitly
|
|
409
|
+
* opts in and overrides a balancer/dispatcher selection.
|
|
377
410
|
*/
|
|
378
411
|
steal?: boolean;
|
|
379
412
|
/**
|
package/src/worker/loop.js
CHANGED
|
@@ -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,6 +120,9 @@ 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 ??
|
|
@@ -269,6 +272,10 @@ export const workerMainLoop = async (startupData) => {
|
|
|
269
272
|
// a peer withdrawing its intent. Measured to hurt the per-lane path,
|
|
270
273
|
// where a claim is a cheap private decode and picking work up promptly
|
|
271
274
|
// matters more, so the classic order is kept below.
|
|
275
|
+
// Reordering must not make `progressed` sticky: it answers "did this
|
|
276
|
+
// iteration move anything", so it has to start false every pass or the
|
|
277
|
+
// park below is unreachable and the worker spins a core forever.
|
|
278
|
+
progressed = false;
|
|
272
279
|
if (_hasCompleted()) {
|
|
273
280
|
if (_writeBatch(WRITE_MAX) > 0)
|
|
274
281
|
progressed = true;
|