knitting 0.1.60 → 0.1.61
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/BROWSER.md +134 -0
- package/README.md +93 -6
- package/knitting.browser.d.ts +6 -0
- package/knitting.browser.js +1 -0
- package/package.json +11 -2
- package/prebuilds/darwin-arm64-node-127/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-127/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-arm64-node-127/knitting_shm.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_buffer_pointer.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_shared_memory.node +0 -0
- package/prebuilds/darwin-arm64-node-137/knitting_shm.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_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_shared_memory.node +0 -0
- package/prebuilds/win32-x64-node-137/knitting_shm.node +0 -0
- package/src/api.js +127 -22
- package/src/common/runtime.d.ts +2 -1
- package/src/common/runtime.js +8 -1
- package/src/memory/lock.d.ts +26 -1
- package/src/memory/lock.js +212 -4
- package/src/memory/payloadCodec.js +2 -6
- package/src/memory/regionRegistry.d.ts +1 -3
- package/src/memory/regionRegistry.js +16 -14
- package/src/permission/protocol.js +5 -1
- package/src/runtime/dispatcher.js +9 -0
- package/src/runtime/pool.d.ts +114 -3
- package/src/runtime/pool.js +209 -63
- package/src/runtime/process-worker.d.ts +2 -12
- package/src/runtime/process-worker.js +69 -113
- package/src/runtime/tx-queue.d.ts +15 -1
- package/src/runtime/tx-queue.js +52 -15
- package/src/runtime/worker-common.d.ts +13 -0
- package/src/runtime/worker-common.js +102 -0
- package/src/types.d.ts +41 -3
- package/src/worker/loop.js +27 -5
- package/src/worker/rx-queue.d.ts +11 -1
- package/src/worker/rx-queue.js +4 -1
- package/src/worker/timers.d.ts +8 -1
- package/src/worker/timers.js +20 -17
|
@@ -4,7 +4,7 @@ import { toSharedBufferRegion, } from "../common/shared-buffer-region.js";
|
|
|
4
4
|
// Low 5 bits = slot index, high 27 bits = caller meta.
|
|
5
5
|
// Inlined from setTaskSlotIndex to avoid cross-closure call on hot path.
|
|
6
6
|
const SLOT_META_PACKED_MASK = 0xFFFFFFE0; // (~0x1F) >>> 0
|
|
7
|
-
export const register = ({ lockSector,
|
|
7
|
+
export const register = ({ lockSector, }) => {
|
|
8
8
|
const lockRegion = toSharedBufferRegion(lockSector ?? createWasmSharedArrayBuffer(LOCK_SECTOR_BYTE_LENGTH));
|
|
9
9
|
const lockSAB = lockRegion.sab;
|
|
10
10
|
const hostBits = new Int32Array(lockSAB, lockRegion.byteOffset + PAYLOAD_LOCK_HOST_BITS_OFFSET_BYTES, 1);
|
|
@@ -14,7 +14,7 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
|
|
|
14
14
|
const clz32 = Math.clz32;
|
|
15
15
|
const a_load = Atomics.load;
|
|
16
16
|
const a_store = Atomics.store;
|
|
17
|
-
const
|
|
17
|
+
const a_xor = Atomics.xor;
|
|
18
18
|
const EMPTY = 0xFFFFFFFF >>> 0;
|
|
19
19
|
const SLOT_MASK = TASK_SLOT_INDEX_MASK;
|
|
20
20
|
const START_MASK = (~SLOT_MASK) >>> 0;
|
|
@@ -23,7 +23,6 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
|
|
|
23
23
|
let usedBits = 0 | 0;
|
|
24
24
|
// scalar state (faster than Uint32Array(1))
|
|
25
25
|
let hostLast = 0 | 0;
|
|
26
|
-
let workerLast = 0 | 0;
|
|
27
26
|
const startAndIndexToArray = (length) => startAndIndex.slice(0, length);
|
|
28
27
|
const compactFreeBitsStable = (b, freeBits) => {
|
|
29
28
|
const sai = startAndIndex;
|
|
@@ -92,10 +91,14 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
|
|
|
92
91
|
let freeBits = (~(hostLast ^ w)) >>> 0;
|
|
93
92
|
if (freeBits !== 0)
|
|
94
93
|
freeBits &= usedBits;
|
|
95
|
-
|
|
94
|
+
// Every tracked slot is free, so restart the arena from offset 0. A free
|
|
95
|
+
// racing after this snapshot can only delay reclamation until the next
|
|
96
|
+
// allocation; it cannot make a live slot appear free.
|
|
97
|
+
if (freeBits === (usedBits >>> 0)) {
|
|
96
98
|
tableLength = 0;
|
|
97
99
|
usedBits = 0 | 0;
|
|
98
100
|
tl = 0;
|
|
101
|
+
freeBits = 0 >>> 0;
|
|
99
102
|
}
|
|
100
103
|
else if (freeBits !== 0) {
|
|
101
104
|
for (let i = 0; i < tl; i++) {
|
|
@@ -247,10 +250,7 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
|
|
|
247
250
|
const slotIndex = findAndInsert(task, size);
|
|
248
251
|
if (slotIndex === -1)
|
|
249
252
|
return -1;
|
|
250
|
-
|
|
251
|
-
a_store(hostBits, 0, hostLast);
|
|
252
|
-
else
|
|
253
|
-
hostBits[0] = hostLast;
|
|
253
|
+
a_store(hostBits, 0, hostLast);
|
|
254
254
|
return slotIndex;
|
|
255
255
|
};
|
|
256
256
|
const setSlotLength = (slotIndex, payloadLen) => {
|
|
@@ -264,13 +264,15 @@ export const register = ({ lockSector, publishMode = "plain", }) => {
|
|
|
264
264
|
size64bit[slotIndex] = aligned >>> 0;
|
|
265
265
|
return true;
|
|
266
266
|
};
|
|
267
|
+
// Always a read-modify-write. `free()` genuinely has two callers even without
|
|
268
|
+
// work stealing: the encode-side rollback (`failDynamicWriteAfterReserve`)
|
|
269
|
+
// and the decode-side release (`freeTaskSlot`). Each held a private
|
|
270
|
+
// `workerLast` shadow and blind-stored the whole word, so one overwrote the
|
|
271
|
+
// other's toggles — the allocator then handed out a region that was still
|
|
272
|
+
// live, aliasing payloads. XOR of distinct bits commutes, so one atomic
|
|
273
|
+
// toggle per free is correct for any number of writers.
|
|
267
274
|
const free = (index) => {
|
|
268
|
-
|
|
269
|
-
workerLast ^= 1 << index;
|
|
270
|
-
if (useAtomicPublish)
|
|
271
|
-
a_store(workerBits, 0, workerLast);
|
|
272
|
-
else
|
|
273
|
-
workerBits[0] = workerLast;
|
|
275
|
+
a_xor(workerBits, 0, 1 << (index & TASK_SLOT_INDEX_MASK));
|
|
274
276
|
};
|
|
275
277
|
return {
|
|
276
278
|
allocTask,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RUNTIME } from "../common/runtime.js";
|
|
1
|
+
import { IS_BROWSER, RUNTIME } from "../common/runtime.js";
|
|
2
2
|
import { toCanonicalPath as toSharedCanonicalPath } from "../common/path-canonical.js";
|
|
3
3
|
import { getNodeBuiltinModule, getNodeProcess } from "../common/node-compat.js";
|
|
4
4
|
const requireNodeModule = (specifier) => {
|
|
@@ -436,6 +436,10 @@ export const resolvePermissionProtocol = ({ permission, modules, }) => {
|
|
|
436
436
|
const input = normalizeProtocolInput(permission);
|
|
437
437
|
if (!input)
|
|
438
438
|
return undefined;
|
|
439
|
+
// Browsers have no filesystem, process, or runtime flags to police, and the
|
|
440
|
+
// strict default would otherwise reach for `node:path` and throw.
|
|
441
|
+
if (IS_BROWSER)
|
|
442
|
+
return undefined;
|
|
439
443
|
const rawMode = input.mode;
|
|
440
444
|
const mode = (rawMode === "unsafe" || rawMode === "off")
|
|
441
445
|
? "unsafe"
|
|
@@ -24,6 +24,15 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
24
24
|
check.rerun = true;
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
|
+
// Nothing enqueued and nothing outstanding at the worker: skip the drain.
|
|
28
|
+
// Without this an idle lane still pays a txStatus write, an rxStatus load
|
|
29
|
+
// and — worst of all — an Atomics.notify that wakes a parked worker for no
|
|
30
|
+
// reason. Under the serial-channel dispatcher every lane is checked on
|
|
31
|
+
// every tick, so that waste is what makes latency grow with thread count.
|
|
32
|
+
if (txIdle()) {
|
|
33
|
+
check.isRunning = false;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
27
36
|
inFlight = true;
|
|
28
37
|
if (backoffTimer !== undefined) {
|
|
29
38
|
clearTimeout(backoffTimer);
|
package/src/runtime/pool.d.ts
CHANGED
|
@@ -1,12 +1,104 @@
|
|
|
1
|
+
import { createHostTxQueue } from "./tx-queue.js";
|
|
1
2
|
import { type ProcessSharedMemoryBacking } from "./process-worker.js";
|
|
2
3
|
import { type Sab } from "../ipc/transport/shared-memory.js";
|
|
3
4
|
import { ChannelHandler, type DispatcherCheck } from "./dispatcher.js";
|
|
4
|
-
import { lock2 } from "../memory/lock.js";
|
|
5
|
+
import { lock2, type Task } from "../memory/lock.js";
|
|
5
6
|
import "../memory/payloadCodec.js";
|
|
6
|
-
import type { DebugOptions, DispatcherSettings, WorkerContext, WorkerData, WorkerSettings } from "../types.js";
|
|
7
|
+
import type { DebugOptions, DispatcherSettings, LockBuffers, WorkerContext, WorkerData, WorkerSettings } from "../types.js";
|
|
7
8
|
import "../worker/loop.js";
|
|
8
9
|
import { type PayloadBufferOptions } from "../memory/payload-config.js";
|
|
9
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Build the buffers, host-side locks and pending registry that a stealing pool
|
|
12
|
+
* shares. One submit region for every worker to claim from, one private return
|
|
13
|
+
* region per worker, and a single pool-global queue so a response arriving on
|
|
14
|
+
* any lane settles the right promise.
|
|
15
|
+
*
|
|
16
|
+
* Region width follows paper §6.1: the largest power-of-two `g` leaving a spare
|
|
17
|
+
* region for a delayed claimant (`slots / g >= consumers + 1`). At 32 slots that
|
|
18
|
+
* caps how wide a region can be well before it caps the worker count.
|
|
19
|
+
*/
|
|
20
|
+
export declare const resolveStealRegionLanes: (consumers: number) => number;
|
|
21
|
+
/** Maximum claimants that leave the protocol's required spare region. */
|
|
22
|
+
export declare const MAX_STEAL_CONSUMERS: number;
|
|
23
|
+
export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, abortSignalCapacity, usesAbortSignal, }: {
|
|
24
|
+
threads: number;
|
|
25
|
+
payload?: PayloadBufferOptions;
|
|
26
|
+
regionLanes?: number;
|
|
27
|
+
abortSignalCapacity?: number;
|
|
28
|
+
usesAbortSignal?: boolean;
|
|
29
|
+
}) => {
|
|
30
|
+
submitBuffers: LockBuffers;
|
|
31
|
+
returnBuffers: LockBuffers[];
|
|
32
|
+
hostSubmitLock: {
|
|
33
|
+
enlist: (task: Task) => true;
|
|
34
|
+
encode: (task: Task, state?: number) => boolean;
|
|
35
|
+
encodeManyFrom: (list: import("../ipc/tools/ring-queue.js").default<Task>) => number;
|
|
36
|
+
encodeAll: () => boolean;
|
|
37
|
+
publish: (task: Task) => boolean;
|
|
38
|
+
flushPending: () => boolean;
|
|
39
|
+
decode: () => boolean;
|
|
40
|
+
hasSpace: () => boolean;
|
|
41
|
+
resolved: import("../ipc/tools/ring-queue.js").default<Task>;
|
|
42
|
+
hostBits: Int32Array<import("../common/shared-buffer-region.js").SharedBuffer>;
|
|
43
|
+
workerBits: Int32Array<import("../common/shared-buffer-region.js").SharedBuffer>;
|
|
44
|
+
recyclecList: import("../ipc/tools/ring-queue.js").default<Task>;
|
|
45
|
+
resolveHost: ({ queue, onResolved, shouldSettle, activeRejectPlaceholder, }: {
|
|
46
|
+
queue: Task[];
|
|
47
|
+
onResolved?: (task: Task) => void;
|
|
48
|
+
shouldSettle?: (task: Task) => boolean;
|
|
49
|
+
activeRejectPlaceholder?: Task["reject"];
|
|
50
|
+
}) => () => number;
|
|
51
|
+
hasPendingFrames: () => boolean;
|
|
52
|
+
getPendingFrameCount: () => number;
|
|
53
|
+
getPendingPromiseCount: () => number;
|
|
54
|
+
resetPendingState: () => void;
|
|
55
|
+
deactivateStealConsumer: (id: number) => boolean;
|
|
56
|
+
takeDeferredCount: () => number;
|
|
57
|
+
setPromiseHandler: (handler?: import("../memory/lock.js").PromisePayloadHandler) => void;
|
|
58
|
+
};
|
|
59
|
+
hostReturnLocks: {
|
|
60
|
+
enlist: (task: Task) => true;
|
|
61
|
+
encode: (task: Task, state?: number) => boolean;
|
|
62
|
+
encodeManyFrom: (list: import("../ipc/tools/ring-queue.js").default<Task>) => number;
|
|
63
|
+
encodeAll: () => boolean;
|
|
64
|
+
publish: (task: Task) => boolean;
|
|
65
|
+
flushPending: () => boolean;
|
|
66
|
+
decode: () => boolean;
|
|
67
|
+
hasSpace: () => boolean;
|
|
68
|
+
resolved: import("../ipc/tools/ring-queue.js").default<Task>;
|
|
69
|
+
hostBits: Int32Array<import("../common/shared-buffer-region.js").SharedBuffer>;
|
|
70
|
+
workerBits: Int32Array<import("../common/shared-buffer-region.js").SharedBuffer>;
|
|
71
|
+
recyclecList: import("../ipc/tools/ring-queue.js").default<Task>;
|
|
72
|
+
resolveHost: ({ queue, onResolved, shouldSettle, activeRejectPlaceholder, }: {
|
|
73
|
+
queue: Task[];
|
|
74
|
+
onResolved?: (task: Task) => void;
|
|
75
|
+
shouldSettle?: (task: Task) => boolean;
|
|
76
|
+
activeRejectPlaceholder?: Task["reject"];
|
|
77
|
+
}) => () => number;
|
|
78
|
+
hasPendingFrames: () => boolean;
|
|
79
|
+
getPendingFrameCount: () => number;
|
|
80
|
+
getPendingPromiseCount: () => number;
|
|
81
|
+
resetPendingState: () => void;
|
|
82
|
+
deactivateStealConsumer: (id: number) => boolean;
|
|
83
|
+
takeDeferredCount: () => number;
|
|
84
|
+
setPromiseHandler: (handler?: import("../memory/lock.js").PromisePayloadHandler) => void;
|
|
85
|
+
}[];
|
|
86
|
+
sharedQueue: {
|
|
87
|
+
rejectAll: (reason: string) => void;
|
|
88
|
+
hasPendingFrames: () => boolean;
|
|
89
|
+
txIdle: () => boolean;
|
|
90
|
+
completeFrame: () => number;
|
|
91
|
+
setReturnHooks: (lane: number, hooks: (import("../connections/buffer-reference.js").BufferReferenceReturnHooks | ((token: bigint) => void)) | undefined) => void;
|
|
92
|
+
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
|
+
flushToWorker: () => boolean;
|
|
94
|
+
enqueueKnown: (task: Task) => boolean;
|
|
95
|
+
settlePromisePayload: (task: Task, isRejected: boolean, value: unknown) => boolean;
|
|
96
|
+
};
|
|
97
|
+
regionLanes: number;
|
|
98
|
+
abortSignalSAB: SharedArrayBuffer;
|
|
99
|
+
abortSignalMax: number;
|
|
100
|
+
};
|
|
101
|
+
export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, stealPool, }: {
|
|
10
102
|
list: string[];
|
|
11
103
|
ids: number[];
|
|
12
104
|
names: string[];
|
|
@@ -30,6 +122,25 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
|
|
|
30
122
|
* macro channel that runs all lane checks.
|
|
31
123
|
*/
|
|
32
124
|
sharedChannelHandler?: ChannelHandler;
|
|
125
|
+
/**
|
|
126
|
+
* Work-stealing wiring. When present the submit region, both host-side locks
|
|
127
|
+
* and the pending registry are owned by `createPool` and shared: every worker
|
|
128
|
+
* claims from one request region, and the host demultiplexes responses by
|
|
129
|
+
* task id. This lane only contributes its private return region and its
|
|
130
|
+
* worker. The dispatcher is owned by the pool too, so none is built here.
|
|
131
|
+
*/
|
|
132
|
+
stealPool?: {
|
|
133
|
+
submitBuffers: LockBuffers;
|
|
134
|
+
returnBuffers: LockBuffers;
|
|
135
|
+
hostSubmitLock: ReturnType<typeof lock2>;
|
|
136
|
+
hostReturnLock: ReturnType<typeof lock2>;
|
|
137
|
+
sharedQueue: ReturnType<typeof createHostTxQueue>;
|
|
138
|
+
consumers: number;
|
|
139
|
+
consumerId: number;
|
|
140
|
+
regionLanes: number;
|
|
141
|
+
abortSignalSAB?: LockBuffers["headers"];
|
|
142
|
+
abortSignalMax?: number;
|
|
143
|
+
};
|
|
33
144
|
}) => (WorkerContext & {
|
|
34
145
|
dispatcherCheck?: never;
|
|
35
146
|
laneWake?: never;
|
package/src/runtime/pool.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHostTxQueue } from "./tx-queue.js";
|
|
2
|
-
import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime,
|
|
2
|
+
import { cleanupProcessWorkerMemoryQuietly, createProcessSharedMemoryAllocator, createProcessWorkerMemoryLayout, createProcessWorkerNativeSignalNotifier, readProcessSharedMemorySettings, readProcessWorkerCommandPrefix, readProcessWorkerRuntime, spawnProcessWorker, toProcessWorkerBootPayload, } from "./process-worker.js";
|
|
3
|
+
import { serializeWorkerBootstrapData, terminateWorkerQuietly, toWorkerCompatExecArgv, toWorkerSafeExecArgv, } from "./worker-common.js";
|
|
3
4
|
import { createSharedMemoryTransport, TRANSPORT_SIGNAL_BYTES, } from "../ipc/transport/shared-memory.js";
|
|
4
5
|
import { ChannelHandler, hostDispatcherLoop, } from "./dispatcher.js";
|
|
5
6
|
import { HEADER_SLOT_STRIDE_U32, lock2, LOCK_SECTOR_BYTE_LENGTH, LockBound, } from "../memory/lock.js";
|
|
@@ -7,7 +8,7 @@ import { HEADER_SLOT_STRIDE_U32, lock2, LOCK_SECTOR_BYTE_LENGTH, LockBound, } fr
|
|
|
7
8
|
// see lock.ts). Must run before any lock2() call.
|
|
8
9
|
import "../memory/payloadCodec.js";
|
|
9
10
|
import "../worker/loop.js";
|
|
10
|
-
import { createSharedArrayBuffer, createWasmSharedArrayBuffer, } from "../common/runtime.js";
|
|
11
|
+
import { createSharedArrayBuffer, createWasmSharedArrayBuffer, IS_BROWSER, } from "../common/runtime.js";
|
|
11
12
|
import { HAS_NODE_WORKER_THREADS, RUNTIME_WORKER, } from "../common/worker-runtime.js";
|
|
12
13
|
import { probeLockBufferTextCompat } from "../common/shared-buffer-text.js";
|
|
13
14
|
import { signalAbortFactory } from "../shared/abortSignal.js";
|
|
@@ -21,6 +22,15 @@ const isWorkerFatalMessage = (value) => !!value &&
|
|
|
21
22
|
typeof value[WORKER_FATAL_MESSAGE_KEY] === "string";
|
|
22
23
|
// Keep idle workers self-healing if an Atomics.notify wake is missed.
|
|
23
24
|
const DEFAULT_WORKER_PARK_MS = 1;
|
|
25
|
+
const DEFAULT_ABORT_SIGNAL_CAPACITY = 258;
|
|
26
|
+
const sanitizePositiveInteger = (value) => {
|
|
27
|
+
if (!Number.isFinite(value))
|
|
28
|
+
return undefined;
|
|
29
|
+
const parsed = Math.floor(value);
|
|
30
|
+
return parsed > 0 ? parsed : undefined;
|
|
31
|
+
};
|
|
32
|
+
const resolveAbortSignalCapacity = (value) => sanitizePositiveInteger(value) ?? DEFAULT_ABORT_SIGNAL_CAPACITY;
|
|
33
|
+
const abortSignalByteLength = (capacity) => Math.max(1, Math.ceil(capacity / 32)) * Uint32Array.BYTES_PER_ELEMENT;
|
|
24
34
|
const withDefaultWorkerTimers = (options) => {
|
|
25
35
|
const parkMs = options?.timers?.parkMs ?? DEFAULT_WORKER_PARK_MS;
|
|
26
36
|
if (options === undefined)
|
|
@@ -38,7 +48,111 @@ const withFixedPayloadConfig = (config) => ({
|
|
|
38
48
|
mode: "fixed",
|
|
39
49
|
payloadInitialBytes: config.payloadMaxByteLength,
|
|
40
50
|
});
|
|
41
|
-
|
|
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
|
+
*/
|
|
61
|
+
export const resolveStealRegionLanes = (consumers) => {
|
|
62
|
+
for (let lanes = LockBound.slots; lanes >= 1; lanes >>= 1) {
|
|
63
|
+
if (LockBound.slots / lanes >= consumers + 1)
|
|
64
|
+
return lanes;
|
|
65
|
+
}
|
|
66
|
+
throw new RangeError(`${consumers} stealing workers need more than ${LockBound.slots} lanes`);
|
|
67
|
+
};
|
|
68
|
+
/** Maximum claimants that leave the protocol's required spare region. */
|
|
69
|
+
export const MAX_STEAL_CONSUMERS = LockBound.slots - 1;
|
|
70
|
+
export const createStealPoolBuffers = ({ threads, payload, regionLanes, abortSignalCapacity, usesAbortSignal, }) => {
|
|
71
|
+
const payloadConfig = resolvePayloadBufferOptions({ options: payload });
|
|
72
|
+
const makePayload = () => payloadConfig.mode === "growable"
|
|
73
|
+
? createSharedArrayBuffer(payloadConfig.payloadInitialBytes, payloadConfig.payloadMaxByteLength)
|
|
74
|
+
: createSharedArrayBuffer(payloadConfig.payloadInitialBytes);
|
|
75
|
+
const carpet = () => createLockControlCarpet({
|
|
76
|
+
signalBytes: 0,
|
|
77
|
+
abortBytes: 0,
|
|
78
|
+
lockSectorBytes: LOCK_SECTOR_BYTE_LENGTH,
|
|
79
|
+
headerSlotStrideU32: HEADER_SLOT_STRIDE_U32,
|
|
80
|
+
slotCount: LockBound.slots,
|
|
81
|
+
headerLayout: "split",
|
|
82
|
+
});
|
|
83
|
+
const toBuffers = (half, payloadSab) => ({
|
|
84
|
+
...half,
|
|
85
|
+
payload: payloadSab,
|
|
86
|
+
textCompat: probeLockBufferTextCompat({
|
|
87
|
+
headers: half.headers,
|
|
88
|
+
payload: payloadSab,
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
const maxLanes = resolveStealRegionLanes(threads);
|
|
92
|
+
const lanes = regionLanes === undefined
|
|
93
|
+
? maxLanes
|
|
94
|
+
: Math.min(Math.max(1, regionLanes | 0), maxLanes);
|
|
95
|
+
const submitCarpet = carpet();
|
|
96
|
+
const submitBuffers = toBuffers(submitCarpet.lock, makePayload());
|
|
97
|
+
const hostSubmitLock = lock2({
|
|
98
|
+
headers: submitBuffers.headers,
|
|
99
|
+
headerSlotStrideU32: submitBuffers.headerSlotStrideU32,
|
|
100
|
+
LockBoundSector: submitBuffers.lockSector,
|
|
101
|
+
payload: submitBuffers.payload,
|
|
102
|
+
payloadSector: submitBuffers.payloadSector,
|
|
103
|
+
payloadConfig,
|
|
104
|
+
textCompat: submitBuffers.textCompat,
|
|
105
|
+
consumers: threads,
|
|
106
|
+
regionLanes: lanes,
|
|
107
|
+
});
|
|
108
|
+
const returnBuffers = [];
|
|
109
|
+
const hostReturnLocks = [];
|
|
110
|
+
for (let i = 0; i < threads; i++) {
|
|
111
|
+
const buffers = toBuffers(carpet().returnLock, makePayload());
|
|
112
|
+
returnBuffers.push(buffers);
|
|
113
|
+
hostReturnLocks.push(lock2({
|
|
114
|
+
headers: buffers.headers,
|
|
115
|
+
headerSlotStrideU32: buffers.headerSlotStrideU32,
|
|
116
|
+
LockBoundSector: buffers.lockSector,
|
|
117
|
+
payload: buffers.payload,
|
|
118
|
+
payloadSector: buffers.payloadSector,
|
|
119
|
+
payloadConfig,
|
|
120
|
+
textCompat: buffers.textCompat,
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
// Abort-aware tasks can execute on any claimant, so all workers and the
|
|
124
|
+
// pool-global queue must observe one bitmap. Per-lane abort buffers would
|
|
125
|
+
// encode an id from one allocator and test it against another lane's bits.
|
|
126
|
+
const resolvedAbortSignalCapacity = resolveAbortSignalCapacity(abortSignalCapacity);
|
|
127
|
+
const abortSignalSAB = usesAbortSignal === true
|
|
128
|
+
? createSharedArrayBuffer(abortSignalByteLength(resolvedAbortSignalCapacity))
|
|
129
|
+
: undefined;
|
|
130
|
+
const abortSignals = abortSignalSAB === undefined
|
|
131
|
+
? undefined
|
|
132
|
+
: signalAbortFactory({
|
|
133
|
+
sab: abortSignalSAB,
|
|
134
|
+
maxSignals: resolvedAbortSignalCapacity,
|
|
135
|
+
});
|
|
136
|
+
const sharedQueue = createHostTxQueue({
|
|
137
|
+
lock: hostSubmitLock,
|
|
138
|
+
returnLock: hostReturnLocks[0],
|
|
139
|
+
extraReturnLocks: hostReturnLocks.slice(1),
|
|
140
|
+
abortSignals,
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
submitBuffers,
|
|
144
|
+
returnBuffers,
|
|
145
|
+
hostSubmitLock,
|
|
146
|
+
hostReturnLocks,
|
|
147
|
+
sharedQueue,
|
|
148
|
+
regionLanes: lanes,
|
|
149
|
+
abortSignalSAB,
|
|
150
|
+
abortSignalMax: abortSignalSAB === undefined
|
|
151
|
+
? undefined
|
|
152
|
+
: resolvedAbortSignalCapacity,
|
|
153
|
+
};
|
|
154
|
+
};
|
|
155
|
+
export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, source, at, workerOptions, workerExecArgv, permission, host, payload, bufferReferenceReturn, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, stealPool, }) => {
|
|
42
156
|
if (workerOptions?.runtime === "compiled") {
|
|
43
157
|
return spawnCompiledWorkerContext({
|
|
44
158
|
list,
|
|
@@ -65,32 +179,32 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
65
179
|
if (!useProcessWorkerRuntime && typeof poliWorker !== "function") {
|
|
66
180
|
throw new Error("Worker is not available in this runtime");
|
|
67
181
|
}
|
|
182
|
+
// Without this the failure is a `ReferenceError` from whichever module
|
|
183
|
+
// happens to allocate first.
|
|
184
|
+
if (IS_BROWSER && typeof SharedArrayBuffer !== "function") {
|
|
185
|
+
throw new Error("SharedArrayBuffer is unavailable: serve the page cross-origin isolated " +
|
|
186
|
+
"(Cross-Origin-Opener-Policy: same-origin, " +
|
|
187
|
+
"Cross-Origin-Embedder-Policy: require-corp).");
|
|
188
|
+
}
|
|
68
189
|
const WorkerCtor = poliWorker;
|
|
69
190
|
// Lock buffers must be shared between host and worker.
|
|
70
|
-
const sanitizeBytes =
|
|
71
|
-
if (!Number.isFinite(value))
|
|
72
|
-
return undefined;
|
|
73
|
-
const bytes = Math.floor(value);
|
|
74
|
-
return bytes > 0 ? bytes : undefined;
|
|
75
|
-
};
|
|
191
|
+
const sanitizeBytes = sanitizePositiveInteger;
|
|
76
192
|
const basePayloadConfig = resolvePayloadBufferOptions({
|
|
77
193
|
options: payload,
|
|
78
194
|
});
|
|
79
195
|
const resolvedPayloadConfig = useProcessWorkerRuntime
|
|
80
196
|
? withFixedPayloadConfig(basePayloadConfig)
|
|
81
197
|
: basePayloadConfig;
|
|
82
|
-
const
|
|
83
|
-
const requestedAbortSignalCapacity = sanitizeBytes(abortSignalCapacity);
|
|
84
|
-
const resolvedAbortSignalCapacity = requestedAbortSignalCapacity ??
|
|
85
|
-
defaultAbortSignalCapacity;
|
|
86
|
-
const abortSignalWords = Math.max(1, Math.ceil(resolvedAbortSignalCapacity / 32));
|
|
198
|
+
const resolvedAbortSignalCapacity = resolveAbortSignalCapacity(abortSignalCapacity);
|
|
87
199
|
const requestedSignalBytes = sanitizeBytes(sab?.size);
|
|
88
200
|
const externalSignalSab = sab?.sharedSab;
|
|
89
201
|
if (useProcessWorkerRuntime && externalSignalSab != null) {
|
|
90
202
|
throw new Error("process worker runtime cannot use an external signal buffer");
|
|
91
203
|
}
|
|
92
204
|
const signalBytes = Math.max(TRANSPORT_SIGNAL_BYTES, requestedSignalBytes ?? TRANSPORT_SIGNAL_BYTES);
|
|
93
|
-
const abortBytes =
|
|
205
|
+
const abortBytes = stealPool === undefined && usesAbortSignal === true
|
|
206
|
+
? abortSignalByteLength(resolvedAbortSignalCapacity)
|
|
207
|
+
: 0;
|
|
94
208
|
const processWorkerMemory = useProcessWorkerRuntime
|
|
95
209
|
? createProcessWorkerMemoryLayout({
|
|
96
210
|
signalBytes,
|
|
@@ -132,7 +246,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
132
246
|
const controlLayout = processWorkerMemory?.controlLayout ??
|
|
133
247
|
makeLockControlLayout();
|
|
134
248
|
const lockPayload = processWorkerMemory?.lockPayload ?? makePayloadBuffer();
|
|
135
|
-
const lockBuffers = {
|
|
249
|
+
const lockBuffers = stealPool?.submitBuffers ?? {
|
|
136
250
|
...controlLayout.lock,
|
|
137
251
|
payload: lockPayload,
|
|
138
252
|
textCompat: probeLockBufferTextCompat({
|
|
@@ -142,7 +256,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
142
256
|
};
|
|
143
257
|
const returnPayload = processWorkerMemory?.returnPayload ??
|
|
144
258
|
makePayloadBuffer();
|
|
145
|
-
const returnLockBuffers = {
|
|
259
|
+
const returnLockBuffers = stealPool?.returnBuffers ?? {
|
|
146
260
|
...controlLayout.returnLock,
|
|
147
261
|
payload: returnPayload,
|
|
148
262
|
textCompat: probeLockBufferTextCompat({
|
|
@@ -150,7 +264,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
150
264
|
payload: returnPayload,
|
|
151
265
|
}),
|
|
152
266
|
};
|
|
153
|
-
const lock = lock2({
|
|
267
|
+
const lock = stealPool?.hostSubmitLock ?? lock2({
|
|
154
268
|
headers: lockBuffers.headers,
|
|
155
269
|
headerSlotStrideU32: lockBuffers.headerSlotStrideU32,
|
|
156
270
|
LockBoundSector: lockBuffers.lockSector,
|
|
@@ -160,7 +274,7 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
160
274
|
textCompat: lockBuffers.textCompat,
|
|
161
275
|
processBoundary: useProcessWorkerRuntime,
|
|
162
276
|
});
|
|
163
|
-
const returnLock = lock2({
|
|
277
|
+
const returnLock = stealPool?.hostReturnLock ?? lock2({
|
|
164
278
|
headers: returnLockBuffers.headers,
|
|
165
279
|
headerSlotStrideU32: returnLockBuffers.headerSlotStrideU32,
|
|
166
280
|
LockBoundSector: returnLockBuffers.lockSector,
|
|
@@ -170,10 +284,9 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
170
284
|
textCompat: returnLockBuffers.textCompat,
|
|
171
285
|
processBoundary: useProcessWorkerRuntime,
|
|
172
286
|
});
|
|
173
|
-
const abortSignalSAB =
|
|
174
|
-
? controlLayout.abortSignals
|
|
175
|
-
|
|
176
|
-
const abortSignals = abortSignalSAB
|
|
287
|
+
const abortSignalSAB = stealPool?.abortSignalSAB ??
|
|
288
|
+
(usesAbortSignal === true ? controlLayout.abortSignals : undefined);
|
|
289
|
+
const abortSignals = abortSignalSAB && stealPool === undefined
|
|
177
290
|
? signalAbortFactory({
|
|
178
291
|
sab: abortSignalSAB,
|
|
179
292
|
maxSignals: resolvedAbortSignalCapacity,
|
|
@@ -224,34 +337,38 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
224
337
|
}
|
|
225
338
|
outstandingBorrowedReturns.clear();
|
|
226
339
|
};
|
|
227
|
-
const
|
|
340
|
+
const returnHooks = bufferReferenceReturn === "borrow"
|
|
341
|
+
? {
|
|
342
|
+
release: releaseBorrowedReturnToken,
|
|
343
|
+
track: (ref, token, aliasBuffer) => {
|
|
344
|
+
if (outstandingBorrowedReturns === undefined)
|
|
345
|
+
return;
|
|
346
|
+
const record = outstandingBorrowedReturns.get(token);
|
|
347
|
+
if (record !== undefined) {
|
|
348
|
+
if (aliasBuffer !== undefined) {
|
|
349
|
+
record.aliasBuffer = new WeakRef(aliasBuffer);
|
|
350
|
+
}
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
outstandingBorrowedReturns.set(token, {
|
|
354
|
+
ref: new WeakRef(ref),
|
|
355
|
+
aliasBuffer: aliasBuffer === undefined
|
|
356
|
+
? undefined
|
|
357
|
+
: new WeakRef(aliasBuffer),
|
|
358
|
+
runtime: ref.runtime,
|
|
359
|
+
});
|
|
360
|
+
},
|
|
361
|
+
}
|
|
362
|
+
: undefined;
|
|
363
|
+
const queue = stealPool?.sharedQueue ?? createHostTxQueue({
|
|
228
364
|
lock,
|
|
229
365
|
returnLock,
|
|
230
366
|
abortSignals,
|
|
231
|
-
releaseBufferReferenceReturn:
|
|
232
|
-
? {
|
|
233
|
-
release: releaseBorrowedReturnToken,
|
|
234
|
-
track: (ref, token, aliasBuffer) => {
|
|
235
|
-
if (outstandingBorrowedReturns === undefined)
|
|
236
|
-
return;
|
|
237
|
-
const record = outstandingBorrowedReturns.get(token);
|
|
238
|
-
if (record !== undefined) {
|
|
239
|
-
if (aliasBuffer !== undefined) {
|
|
240
|
-
record.aliasBuffer = new WeakRef(aliasBuffer);
|
|
241
|
-
}
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
outstandingBorrowedReturns.set(token, {
|
|
245
|
-
ref: new WeakRef(ref),
|
|
246
|
-
aliasBuffer: aliasBuffer === undefined
|
|
247
|
-
? undefined
|
|
248
|
-
: new WeakRef(aliasBuffer),
|
|
249
|
-
runtime: ref.runtime,
|
|
250
|
-
});
|
|
251
|
-
},
|
|
252
|
-
}
|
|
253
|
-
: undefined,
|
|
367
|
+
releaseBufferReferenceReturn: returnHooks,
|
|
254
368
|
});
|
|
369
|
+
if (stealPool !== undefined) {
|
|
370
|
+
stealPool.sharedQueue.setReturnHooks(stealPool.consumerId, returnHooks);
|
|
371
|
+
}
|
|
255
372
|
const { enqueue, rejectAll, txIdle, } = queue;
|
|
256
373
|
const thisSignal = signalBox.opView;
|
|
257
374
|
const a_add = Atomics.add;
|
|
@@ -271,16 +388,21 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
271
388
|
let dispatchSend = () => { };
|
|
272
389
|
const send = () => dispatchSend();
|
|
273
390
|
let channelHandler;
|
|
274
|
-
const ownsChannel = sharedChannelHandler === undefined
|
|
391
|
+
const ownsChannel = sharedChannelHandler === undefined &&
|
|
392
|
+
stealPool === undefined;
|
|
275
393
|
const ownChannel = sharedChannelHandler ?? new ChannelHandler();
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
394
|
+
// Under stealing there is one queue, so one dispatcher drives it from the
|
|
395
|
+
// pool; a per-lane dispatcher would double-drain the shared registry.
|
|
396
|
+
const { check: dispatcherCheck } = stealPool !== undefined
|
|
397
|
+
? { check: undefined }
|
|
398
|
+
: hostDispatcherLoop({
|
|
399
|
+
signalBox,
|
|
400
|
+
queue,
|
|
401
|
+
channelHandler: ownChannel,
|
|
402
|
+
dispatcherOptions: host,
|
|
403
|
+
notifySignal: nativeNotifySignal,
|
|
404
|
+
});
|
|
405
|
+
if (ownsChannel && dispatcherCheck !== undefined) {
|
|
284
406
|
ownChannel.open(dispatcherCheck);
|
|
285
407
|
channelHandler = ownChannel;
|
|
286
408
|
dispatchSend = () => {
|
|
@@ -304,9 +426,8 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
304
426
|
const workerDataPayload = {
|
|
305
427
|
sab: signals.sab,
|
|
306
428
|
abortSignalSAB,
|
|
307
|
-
abortSignalMax:
|
|
308
|
-
? resolvedAbortSignalCapacity
|
|
309
|
-
: undefined,
|
|
429
|
+
abortSignalMax: stealPool?.abortSignalMax ??
|
|
430
|
+
(usesAbortSignal === true ? resolvedAbortSignalCapacity : undefined),
|
|
310
431
|
list,
|
|
311
432
|
ids,
|
|
312
433
|
names,
|
|
@@ -321,6 +442,11 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
321
442
|
payloadConfig: resolvedPayloadConfig,
|
|
322
443
|
bufferReferenceReturn,
|
|
323
444
|
permission,
|
|
445
|
+
steal: stealPool === undefined ? undefined : {
|
|
446
|
+
consumers: stealPool.consumers,
|
|
447
|
+
consumerId: stealPool.consumerId,
|
|
448
|
+
regionLanes: stealPool.regionLanes,
|
|
449
|
+
},
|
|
324
450
|
};
|
|
325
451
|
const baseWorkerOptions = {
|
|
326
452
|
//@ts-ignore Reason
|
|
@@ -389,6 +515,20 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
389
515
|
worker.postMessage?.(workerDataPayload);
|
|
390
516
|
}
|
|
391
517
|
let closedReason;
|
|
518
|
+
const deactivateStealConsumer = () => {
|
|
519
|
+
stealPool?.hostSubmitLock.deactivateStealConsumer(stealPool.consumerId);
|
|
520
|
+
};
|
|
521
|
+
const terminateFailedWorker = () => {
|
|
522
|
+
try {
|
|
523
|
+
worker.unref?.();
|
|
524
|
+
void Promise.resolve(worker.terminate())
|
|
525
|
+
.catch(() => { })
|
|
526
|
+
.finally(deactivateStealConsumer);
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
deactivateStealConsumer();
|
|
530
|
+
}
|
|
531
|
+
};
|
|
392
532
|
// `workerBytesReadable`: while the worker is still alive its pinned bytes
|
|
393
533
|
// can be copied out; after a crash/exit they are gone and borrows are
|
|
394
534
|
// detached so stale reads fail loud instead of aliasing freed memory.
|
|
@@ -404,18 +544,22 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
404
544
|
if (!isWorkerFatalMessage(message))
|
|
405
545
|
return;
|
|
406
546
|
markWorkerClosed(`Worker startup failed: ${message[WORKER_FATAL_MESSAGE_KEY]}`, true);
|
|
407
|
-
|
|
547
|
+
terminateFailedWorker();
|
|
408
548
|
};
|
|
409
549
|
const onWorkerError = (error) => {
|
|
410
550
|
const message = String(error?.message ?? error);
|
|
411
551
|
markWorkerClosed(`Worker crashed: ${message}`);
|
|
552
|
+
terminateFailedWorker();
|
|
412
553
|
};
|
|
413
554
|
const nodeWorker = worker;
|
|
414
555
|
if (typeof nodeWorker.on === "function") {
|
|
415
556
|
nodeWorker.on("message", onWorkerMessage);
|
|
416
557
|
nodeWorker.on("error", onWorkerError);
|
|
417
558
|
nodeWorker.on("exit", (code) => {
|
|
418
|
-
|
|
559
|
+
// Exit is the point at which this endpoint can no longer write WANT, so
|
|
560
|
+
// survivors may safely ignore any intent it left behind.
|
|
561
|
+
deactivateStealConsumer();
|
|
562
|
+
if (closedReason !== undefined)
|
|
419
563
|
return;
|
|
420
564
|
const normalized = typeof code === "number" ? code : -1;
|
|
421
565
|
markWorkerClosed(`Worker exited with code ${normalized}`);
|
|
@@ -463,8 +607,10 @@ export const spawnWorkerContext = ({ list, ids, names, sab, thread, debug, hostD
|
|
|
463
607
|
lock,
|
|
464
608
|
processSharedMemoryBackings: processSharedMemory?.backings,
|
|
465
609
|
dispatcherCheck,
|
|
466
|
-
laneWake: sharedChannelHandler !== undefined
|
|
467
|
-
|
|
610
|
+
laneWake: sharedChannelHandler !== undefined || stealPool !== undefined
|
|
611
|
+
? laneWake
|
|
612
|
+
: undefined,
|
|
613
|
+
bindSend: sharedChannelHandler !== undefined || stealPool !== undefined
|
|
468
614
|
? ((fn) => void (dispatchSend = fn))
|
|
469
615
|
: undefined,
|
|
470
616
|
};
|