knitting 0.1.63 → 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 +105 -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/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 +205 -79
- 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 +21 -15
- package/src/runtime/pool.js +104 -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/types.d.ts +35 -71
- package/src/worker/loop.js +79 -57
- 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/tx-queue.js
CHANGED
|
@@ -4,7 +4,6 @@ import "../memory/payloadCodec.js";
|
|
|
4
4
|
import { makeTask, resetTaskLocalFlags, runTaskFinalizers, TaskIndex, } from "../memory/lock.js";
|
|
5
5
|
import { withResolvers } from "../common/with-resolvers.js";
|
|
6
6
|
import { AbortSignalPoolExhausted, OneShotDeferred, } from "../shared/abortSignal.js";
|
|
7
|
-
import { withBufferReferenceReturnReleaser, } from "../connections/buffer-reference.js";
|
|
8
7
|
const SLOT_INDEX_MASK = 31;
|
|
9
8
|
const SLOT_META_MASK = 0x07ffffff;
|
|
10
9
|
const SLOT_META_SHIFT = 5;
|
|
@@ -14,7 +13,7 @@ const FUNCTION_META_SHIFT = 16;
|
|
|
14
13
|
const ABORT_SIGNAL_META_OFFSET = 1;
|
|
15
14
|
const NO_ABORT_SIGNAL = -1;
|
|
16
15
|
const p_now = performance.now.bind(performance);
|
|
17
|
-
export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks,
|
|
16
|
+
export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, abortSignals, now, }) {
|
|
18
17
|
const PLACE_HOLDER = (_) => {
|
|
19
18
|
throw ("UNREACHABLE FROM PLACE HOLDER (main)");
|
|
20
19
|
};
|
|
@@ -66,33 +65,18 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
|
|
|
66
65
|
].map((each) => typeof each.setHostWaiterArmed === "function"
|
|
67
66
|
? each.setHostWaiterArmed
|
|
68
67
|
: (_armed) => { });
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (releaseBufferReferenceReturn !== undefined) {
|
|
76
|
-
returnHooks[0] = releaseBufferReferenceReturn;
|
|
77
|
-
}
|
|
78
|
-
const resolveReturnAt = (index) => {
|
|
79
|
-
const resolve = returnResolvers[index];
|
|
80
|
-
const hooks = returnHooks[index];
|
|
81
|
-
return hooks === undefined
|
|
82
|
-
? resolve()
|
|
83
|
-
: withBufferReferenceReturnReleaser(hooks, resolve);
|
|
84
|
-
};
|
|
85
|
-
// Preserve the original one-lane fast path exactly: no wrapper, array lookup,
|
|
86
|
-
// or hook branch is paid by the default one-worker transport. Mutable
|
|
87
|
-
// per-return-lane hooks are needed only by a multi-worker stealing queue.
|
|
68
|
+
const returnNativeArmers = [
|
|
69
|
+
returnLock,
|
|
70
|
+
...(extraReturnLocks ?? []),
|
|
71
|
+
].map((each) => typeof each.armHostNotifier === "function"
|
|
72
|
+
? each.armHostNotifier
|
|
73
|
+
: () => false);
|
|
88
74
|
const completeFrame = returnResolvers.length === 1
|
|
89
|
-
?
|
|
90
|
-
? returnResolvers[0]
|
|
91
|
-
: () => withBufferReferenceReturnReleaser(releaseBufferReferenceReturn, returnResolvers[0])
|
|
75
|
+
? returnResolvers[0]
|
|
92
76
|
: () => {
|
|
93
77
|
let resolved = 0 | 0;
|
|
94
78
|
for (let i = 0; i < returnResolvers.length; i++) {
|
|
95
|
-
resolved = (resolved +
|
|
79
|
+
resolved = (resolved + returnResolvers[i]()) | 0;
|
|
96
80
|
}
|
|
97
81
|
return resolved;
|
|
98
82
|
};
|
|
@@ -103,19 +87,35 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
|
|
|
103
87
|
const completionArmed = new Uint8Array(returnWaiters.length);
|
|
104
88
|
let completionWake;
|
|
105
89
|
let completionGeneration = 0 | 0;
|
|
90
|
+
const completionGenerations = new Int32Array(returnWaiters.length);
|
|
91
|
+
// Reuse one callback per lane and capture the generation before re-arming.
|
|
92
|
+
const completionCallbacks = returnWaiters.map((_, index) => () => {
|
|
93
|
+
if (completionArmed[index] === 0)
|
|
94
|
+
return;
|
|
95
|
+
const generation = completionGenerations[index];
|
|
96
|
+
completionArmed[index] = 0;
|
|
97
|
+
returnArmers[index](false);
|
|
98
|
+
if (generation !== completionGeneration)
|
|
99
|
+
return;
|
|
100
|
+
completionWake?.();
|
|
101
|
+
});
|
|
106
102
|
const setCompletionWaiterArmed = (armed) => {
|
|
107
103
|
for (const setArmed of returnArmers)
|
|
108
104
|
setArmed(armed);
|
|
109
105
|
};
|
|
110
106
|
const waitForCompletion = (onWake, timeoutMs) => {
|
|
111
107
|
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
108
|
let supported = true;
|
|
116
109
|
for (let index = 0; index < returnWaiters.length; index++) {
|
|
117
|
-
if (completionArmed[index] !== 0)
|
|
110
|
+
if (completionArmed[index] !== 0) {
|
|
111
|
+
// Re-arm persistent waiters with an atomic check; a result may have
|
|
112
|
+
// arrived while the gate was off, and setting ARMED alone can strand it.
|
|
113
|
+
if (!returnNativeArmers[index]()) {
|
|
114
|
+
onWake();
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
118
117
|
continue;
|
|
118
|
+
}
|
|
119
119
|
let wait;
|
|
120
120
|
try {
|
|
121
121
|
wait = returnWaiters[index](timeoutMs);
|
|
@@ -128,20 +128,14 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
|
|
|
128
128
|
break;
|
|
129
129
|
}
|
|
130
130
|
completionArmed[index] = 1;
|
|
131
|
-
|
|
132
|
-
const wakeLane =
|
|
133
|
-
|
|
134
|
-
return;
|
|
135
|
-
completionArmed[index] = 0;
|
|
136
|
-
returnArmers[index](false);
|
|
137
|
-
if (generation !== completionGeneration)
|
|
138
|
-
return;
|
|
139
|
-
completionWake?.();
|
|
140
|
-
};
|
|
141
|
-
if (!wait.async)
|
|
131
|
+
completionGenerations[index] = completionGeneration;
|
|
132
|
+
const wakeLane = completionCallbacks[index];
|
|
133
|
+
if (!wait.async) {
|
|
142
134
|
wakeLane();
|
|
143
|
-
|
|
144
|
-
|
|
135
|
+
// A synchronous wake may disarm the whole queue, so stop here.
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
Promise.resolve(wait.value).then(wakeLane, wakeLane);
|
|
145
139
|
}
|
|
146
140
|
if (!supported) {
|
|
147
141
|
completionWake = undefined;
|
|
@@ -150,6 +144,21 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
|
|
|
150
144
|
}
|
|
151
145
|
return supported;
|
|
152
146
|
};
|
|
147
|
+
/**
|
|
148
|
+
* Native callbacks (Deno's threadSafe UnsafeCallback) cannot use waitAsync,
|
|
149
|
+
* but they use the same shared arm word. Each lane is armed and checked for
|
|
150
|
+
* a publication in one operation; a false return means the dispatcher must
|
|
151
|
+
* drain again rather than sleep waiting for a ring that already happened.
|
|
152
|
+
*/
|
|
153
|
+
const armCompletionNotifier = () => {
|
|
154
|
+
for (const arm of returnNativeArmers) {
|
|
155
|
+
if (arm())
|
|
156
|
+
continue;
|
|
157
|
+
setCompletionWaiterArmed(false);
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
};
|
|
153
162
|
const hasActiveTasks = () => {
|
|
154
163
|
const count = (inUsed - getPendingPromiseCount()) | 0;
|
|
155
164
|
return count > 0;
|
|
@@ -183,13 +192,8 @@ export function createHostTxQueue({ max, lock, returnLock, extraReturnLocks, rel
|
|
|
183
192
|
txIdle,
|
|
184
193
|
completeFrame,
|
|
185
194
|
waitForCompletion,
|
|
195
|
+
armCompletionNotifier,
|
|
186
196
|
setCompletionWaiterArmed,
|
|
187
|
-
setReturnHooks: (lane, hooks) => {
|
|
188
|
-
if (!Number.isInteger(lane) || lane < 0 || lane >= returnHooks.length) {
|
|
189
|
-
throw new RangeError(`return lane ${lane} out of range`);
|
|
190
|
-
}
|
|
191
|
-
returnHooks[lane] = hooks;
|
|
192
|
-
},
|
|
193
197
|
enqueue: (functionID, timeout, abortSignal) => {
|
|
194
198
|
const HAS_TIMER = timeout !== undefined;
|
|
195
199
|
const functionIDMasked = functionID & FUNCTION_ID_MASK;
|
package/src/types.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ interface WorkerContext {
|
|
|
14
14
|
txIdle(): boolean;
|
|
15
15
|
call(descriptor: WorkerCall): WorkerInvoke;
|
|
16
16
|
/** Ask the worker to leave its dispatch loop before termination. */
|
|
17
|
-
requestStop?(): Promise<
|
|
17
|
+
requestStop?(): Promise<boolean>;
|
|
18
18
|
kills(): Promise<void>;
|
|
19
19
|
}
|
|
20
20
|
type CreateContext = WorkerContext;
|
|
@@ -34,30 +34,31 @@ type WorkerData = {
|
|
|
34
34
|
lock: LockBuffers;
|
|
35
35
|
returnLock: LockBuffers;
|
|
36
36
|
payloadConfig?: PayloadBufferOptions;
|
|
37
|
-
|
|
37
|
+
/** Enable borrowed returns for this worker's return lane. */
|
|
38
|
+
sharedReturn?: boolean;
|
|
38
39
|
permission?: ResolvedPermissionProtocol;
|
|
39
40
|
/** Whether this host can arm an async completion waiter on the return lock. */
|
|
40
41
|
notifyOnHostPublish?: boolean;
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
/** Process-local IPC channel can carry coalesced completion doorbells. */
|
|
43
|
+
processCompletionDoorbell?: boolean;
|
|
44
|
+
/** Process-local Deno callback pointer for waking the host completion pump. */
|
|
45
|
+
denoCompletionDoorbell?: bigint;
|
|
46
|
+
/** Process-local Node uv_async handle for waking the host completion pump. */
|
|
47
|
+
nodeCompletionDoorbell?: bigint;
|
|
48
|
+
/** Shared submit lock and private return lock for a stealing worker. */
|
|
47
49
|
steal?: {
|
|
48
50
|
consumers: number;
|
|
49
51
|
consumerId: number;
|
|
50
52
|
regionLanes: number;
|
|
53
|
+
/** Region mutual-exclusion discipline; see `DispatcherSettings.stealClaim`. */
|
|
54
|
+
claim?: "dekker" | "cas-mask";
|
|
51
55
|
};
|
|
52
56
|
};
|
|
53
57
|
type UnsafeOptions = {
|
|
54
|
-
/**
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
* but must be released before producer shutdown and must not outlive its ref.
|
|
59
|
-
*/
|
|
60
|
-
BufferReferenceReturn?: "copy" | "borrow";
|
|
58
|
+
/** Enable borrowed large returns and the zero-copy `sharedBytes()` path. */
|
|
59
|
+
SharedBytes?: boolean;
|
|
60
|
+
/** Enable borrowed large arguments and `pool.sharedArgBytes()`. */
|
|
61
|
+
SharedArgs?: boolean;
|
|
61
62
|
};
|
|
62
63
|
type LockBuffers = {
|
|
63
64
|
headers: SharedBufferSource;
|
|
@@ -183,6 +184,11 @@ type Pool<T extends Record<string, TaskLike<any> | TaskFunctionLike>> = {
|
|
|
183
184
|
* Thrown errors/rejections reject here as Error objects with cause chains.
|
|
184
185
|
*/
|
|
185
186
|
call: FunctionMapType<T>;
|
|
187
|
+
/**
|
|
188
|
+
* Allocate an argument buffer. Borrowed buffers are recycled after 32 later
|
|
189
|
+
* large arguments, so the receiving task must consume them before awaiting.
|
|
190
|
+
*/
|
|
191
|
+
sharedArgBytes: (byteLength: number) => Uint8Array;
|
|
186
192
|
};
|
|
187
193
|
type ReturnFixed<A extends TaskInput = undefined, B extends Args = undefined, AS extends AbortSignalOption = undefined> = FixPoint<A, B, AS> & SecondPart & {
|
|
188
194
|
createPool: (options?: CreatePool) => SingleTaskPool<A, B, AS>;
|
|
@@ -309,10 +315,10 @@ type WorkerSettings = {
|
|
|
309
315
|
/**
|
|
310
316
|
* How process workers discover their shared-memory control channel.
|
|
311
317
|
*
|
|
312
|
-
* "inherit" keeps the POSIX fd-inheritance path
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
318
|
+
* "inherit" keeps the POSIX fd-inheritance path. "named" creates an
|
|
319
|
+
* OS-named shared-memory object that wrappers such as containers can reopen
|
|
320
|
+
* by name when they share the same IPC namespace. Deno-hosted pools and
|
|
321
|
+
* Windows use a named mapping automatically.
|
|
316
322
|
*/
|
|
317
323
|
processSharedMemory?: ProcessSharedMemoryMode | ProcessSharedMemorySettings;
|
|
318
324
|
timers?: WorkerTimers;
|
|
@@ -344,47 +350,19 @@ type WorkerTimers = {
|
|
|
344
350
|
pauseNanoseconds?: number;
|
|
345
351
|
};
|
|
346
352
|
type DispatcherSettings = {
|
|
347
|
-
/**
|
|
348
|
-
* How many immediate notify loops before the dispatcher stops re-arming the
|
|
349
|
-
* pump for free.
|
|
350
|
-
*
|
|
351
|
-
* The default depends on what the dispatcher escalates *to*. Polling
|
|
352
|
-
* escalates to a `setTimeout` ladder costing ~1.1ms even at delay 0, so it
|
|
353
|
-
* defaults to 128 — escalating is expensive and the wide window also batches
|
|
354
|
-
* completions. A doorbell escalates to `Atomics.waitAsync` at roughly the
|
|
355
|
-
* price of one hop, so pools that have one default to 1. Pools without a
|
|
356
|
-
* doorbell — Deno, or `doorbell: false` — keep 128.
|
|
357
|
-
*
|
|
358
|
-
* Setting this explicitly opts out of that coupling for every pool shape.
|
|
359
|
-
*/
|
|
353
|
+
/** Number of immediate notify loops before backoff starts. */
|
|
360
354
|
stallFreeLoops?: number;
|
|
361
355
|
/**
|
|
362
356
|
* Max backoff delay (milliseconds).
|
|
363
357
|
*/
|
|
364
358
|
maxBackoffMs?: number;
|
|
365
359
|
/**
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* Defaults to enabled on Node and Bun at any worker count, where it costs
|
|
370
|
-
* 1.6-3.9x less host CPU per completed call. Under HTTP load, where the host
|
|
371
|
-
* has real work of its own, that converts to +13% to +36% throughput.
|
|
372
|
-
*
|
|
373
|
-
* Turn it off for a pool that oversubscribes its machine. The doorbell only
|
|
374
|
-
* progresses when the host gets scheduled, so once workers occupy every core
|
|
375
|
-
* a wake must preempt one: measured +5% to +12.6% rps while workers+host fit
|
|
376
|
-
* within the cores, -22% to -32% once they do not. That is not gated
|
|
377
|
-
* automatically because the core count cannot be probed portably.
|
|
378
|
-
*
|
|
379
|
-
* Forced off, overriding an explicit `true`, where a doorbell cannot work:
|
|
380
|
-
* on Deno, whose `waitAsync` does not wake an idle event loop, and for
|
|
381
|
-
* process workers, which live in another process and so cannot ring a host
|
|
382
|
-
* waiter at all — V8 keeps its Atomics waiter list per isolate, which is why
|
|
383
|
-
* they wake through a native futex addon instead. Compiled (Porffor) workers
|
|
384
|
-
* never reach this path: they reject `host` outright and use pipes rather
|
|
385
|
-
* than shared memory.
|
|
360
|
+
* Use a completion doorbell when supported. Deno uses a thread-safe FFI
|
|
361
|
+
* callback; process and compiled workers use their own completion transport.
|
|
386
362
|
*/
|
|
387
363
|
doorbell?: boolean;
|
|
364
|
+
/** Use Node's native `uv_async_t` completion bridge when available. */
|
|
365
|
+
nativeDoorbell?: boolean;
|
|
388
366
|
/**
|
|
389
367
|
* Host dispatcher topology.
|
|
390
368
|
* - `"per-thread"`: each worker owns its dispatcher and macro channel.
|
|
@@ -398,29 +376,15 @@ type DispatcherSettings = {
|
|
|
398
376
|
* `KNITTING_DISPATCHER` env var (`serial-channel` or `per-thread`).
|
|
399
377
|
*/
|
|
400
378
|
dispatcher?: "per-thread" | "serial-channel";
|
|
401
|
-
/**
|
|
402
|
-
* Work stealing: one shared submit region that any worker may
|
|
403
|
-
* claim from, private return lanes, and a pool-global pending registry. The
|
|
404
|
-
* endpoint that claims a task owns its response.
|
|
405
|
-
*
|
|
406
|
-
* Enabled by default for compatible multi-worker thread and process pools
|
|
407
|
-
* unless a balancer or private-lane dispatcher was explicitly selected.
|
|
408
|
-
* One-worker pools, inliners, compiled/Porffor workers, and pools above the
|
|
409
|
-
* current 31-claimant protocol limit retain their existing transport. Set
|
|
410
|
-
* `false` (or `KNITTING_STEAL=0`) to opt out; `KNITTING_STEAL=1` explicitly
|
|
411
|
-
* opts in and overrides a balancer/dispatcher selection.
|
|
412
|
-
*/
|
|
379
|
+
/** Use a shared submit region with private return lanes. */
|
|
413
380
|
steal?: boolean;
|
|
414
381
|
/**
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
* amortises arbitration best for cheap tasks.
|
|
418
|
-
*
|
|
419
|
-
* **A region is a batch.** For expensive tasks, a wide region lets one worker
|
|
420
|
-
* claim work the others could have run in parallel; set this to `1` (or a
|
|
421
|
-
* small value) when per-task cost dominates arbitration cost.
|
|
382
|
+
* Number of lanes claimed per handshake. Use smaller regions for expensive
|
|
383
|
+
* tasks; Dekker requires at least one spare region per live consumer.
|
|
422
384
|
*/
|
|
423
385
|
stealRegionLanes?: number;
|
|
386
|
+
/** Region-claim discipline: per-consumer Dekker intents or a shared CAS mask. */
|
|
387
|
+
stealClaim?: "dekker" | "cas-mask";
|
|
424
388
|
};
|
|
425
389
|
type CreatePool = {
|
|
426
390
|
/** Number of workers. Default: 1. */
|
package/src/worker/loop.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRuntimeMessageChannel, PROCESS_COMPLETION_DOORBELL, RUNTIME_IS_MAIN_THREAD, RUNTIME_IS_PROCESS_WORKER, RUNTIME_PARENT_PORT, RUNTIME_WORKER_DATA, } from "../common/worker-runtime.js";
|
|
2
2
|
import { isSharedBufferSource } from "../common/shared-buffer-region.js";
|
|
3
3
|
import { isLockBufferTextCompat } from "../common/shared-buffer-text.js";
|
|
4
|
+
import { createDenoCompletionNotifier } from "../runtime/deno-doorbell.js";
|
|
5
|
+
import { createNodeCompletionNotifier } from "../runtime/node-doorbell.js";
|
|
4
6
|
import { createWorkerRxQueue } from "./rx-queue.js";
|
|
7
|
+
import { installSharedReturn } from "./shared-return.js";
|
|
5
8
|
import { createSharedMemoryTransport, WORKER_STOP, } from "../ipc/transport/shared-memory.js";
|
|
6
9
|
import { lock2 } from "../memory/lock.js";
|
|
7
|
-
//
|
|
8
|
-
// see lock.ts). Must run before any lock2() call.
|
|
10
|
+
// Registers the payload codec before any lock is built.
|
|
9
11
|
import "../memory/payloadCodec.js";
|
|
10
12
|
import { getFunctions } from "./task-loader.js";
|
|
11
|
-
import { pauseGeneric, sleepUntilChanged, whilePausing } from "./timers.js";
|
|
13
|
+
import { IS_BUN_WINDOWS, pauseGeneric, sleepUntilChanged, whilePausing, } from "./timers.js";
|
|
12
14
|
import { IS_ANDROMEDA, RUNTIME, SET_IMMEDIATE } from "../common/runtime.js";
|
|
13
15
|
import { getNodeProcess } from "../common/node-compat.js";
|
|
14
16
|
import { assertWorkerImportsResolved, assertWorkerSharedMemoryBootData, installPerformanceNowGuard, installTerminationGuard, installUnhandledRejectionSilencer, scrubWorkerDataSensitiveBuffers, } from "./safety/index.js";
|
|
15
17
|
import { signalAbortFactory } from "../shared/abortSignal.js";
|
|
16
18
|
import { runWorkerBootstrap } from "./bootstrap.js";
|
|
17
19
|
import { getProcessWorkerNativeWaitU32, installProcessWorkerBootstrap, } from "./process-worker-bootstrap.js";
|
|
18
|
-
import { readBufferReferenceReturnReleaseMessage, } from "../connections/buffer-reference.js";
|
|
19
20
|
import { resolveDebugNamespaces } from "../debug/gate.js";
|
|
20
21
|
const WORKER_FATAL_MESSAGE_KEY = "__knittingWorkerFatal";
|
|
21
22
|
const reportWorkerStartupFatal = (error) => {
|
|
@@ -53,27 +54,12 @@ const reportWorkerStartupFatal = (error) => {
|
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
};
|
|
56
|
-
const installBufferReferenceReleaseListener = (releaseReturnedBufferReference) => {
|
|
57
|
-
const handleMessage = (message) => {
|
|
58
|
-
const token = readBufferReferenceReturnReleaseMessage(message);
|
|
59
|
-
if (token !== undefined)
|
|
60
|
-
releaseReturnedBufferReference(token);
|
|
61
|
-
};
|
|
62
|
-
if (RUNTIME_PARENT_PORT !== undefined) {
|
|
63
|
-
addRuntimeDataListener(RUNTIME_PARENT_PORT, handleMessage);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const scope = globalThis;
|
|
67
|
-
scope.addEventListener?.("message", (event) => {
|
|
68
|
-
handleMessage(event?.data);
|
|
69
|
-
});
|
|
70
|
-
};
|
|
71
57
|
export const workerMainLoop = async (startupData) => {
|
|
72
58
|
// Startup-only safety layer: no per-iteration checks in the hot loop.
|
|
73
59
|
installTerminationGuard();
|
|
74
60
|
installUnhandledRejectionSilencer();
|
|
75
61
|
installPerformanceNowGuard();
|
|
76
|
-
const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig,
|
|
62
|
+
const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, sharedReturn, permission, notifyOnHostPublish, processCompletionDoorbell, denoCompletionDoorbell, nodeCompletionDoorbell, totalNumberOfThread, list, ids, names, at, steal, } = startupData;
|
|
77
63
|
scrubWorkerDataSensitiveBuffers(startupData);
|
|
78
64
|
assertWorkerSharedMemoryBootData({ sab, lock, returnLock });
|
|
79
65
|
const debugNamespaces = resolveDebugNamespaces(debug);
|
|
@@ -84,7 +70,7 @@ export const workerMainLoop = async (startupData) => {
|
|
|
84
70
|
namespaces: debugNamespaces,
|
|
85
71
|
}))
|
|
86
72
|
: undefined;
|
|
87
|
-
//
|
|
73
|
+
// Andromeda cannot parse enums.
|
|
88
74
|
const Comment = {
|
|
89
75
|
thisIsAHint: 0,
|
|
90
76
|
};
|
|
@@ -110,7 +96,24 @@ export const workerMainLoop = async (startupData) => {
|
|
|
110
96
|
consumers: steal?.consumers,
|
|
111
97
|
consumerId: steal?.consumerId,
|
|
112
98
|
regionLanes: steal?.regionLanes,
|
|
99
|
+
stealClaim: steal?.claim,
|
|
113
100
|
});
|
|
101
|
+
const notifyDenoHost = createDenoCompletionNotifier(denoCompletionDoorbell);
|
|
102
|
+
const notifyNodeHost = createNodeCompletionNotifier(nodeCompletionDoorbell);
|
|
103
|
+
const processParentPort = processCompletionDoorbell === true
|
|
104
|
+
? RUNTIME_PARENT_PORT
|
|
105
|
+
: undefined;
|
|
106
|
+
// Bun on Windows flushes process messages only after an event-loop turn.
|
|
107
|
+
let processRingPending = false;
|
|
108
|
+
const flushProcessRing = IS_BUN_WINDOWS;
|
|
109
|
+
const notifyProcessHost = processParentPort === undefined
|
|
110
|
+
? undefined
|
|
111
|
+
: flushProcessRing
|
|
112
|
+
? () => {
|
|
113
|
+
processParentPort.postMessage(PROCESS_COMPLETION_DOORBELL);
|
|
114
|
+
processRingPending = true;
|
|
115
|
+
}
|
|
116
|
+
: () => processParentPort.postMessage(PROCESS_COMPLETION_DOORBELL);
|
|
114
117
|
const returnLockState = lock2({
|
|
115
118
|
headers: returnLock.headers,
|
|
116
119
|
headerSlotStrideU32: returnLock.headerSlotStrideU32,
|
|
@@ -120,9 +123,22 @@ export const workerMainLoop = async (startupData) => {
|
|
|
120
123
|
payloadConfig,
|
|
121
124
|
textCompat: returnLock.textCompat,
|
|
122
125
|
processBoundary: RUNTIME_IS_PROCESS_WORKER,
|
|
126
|
+
sharedReturn: sharedReturn === true,
|
|
127
|
+
// A thread return moves its source before publication. Node transfers the
|
|
128
|
+
// backing-store ownership; Deno and Bun make one owned host copy before
|
|
129
|
+
// the worker drops its pin. Both avoid the borrowed-return lifetime.
|
|
130
|
+
moveReturn: !RUNTIME_IS_PROCESS_WORKER,
|
|
123
131
|
// The host parks on this lock's publication word when it has no work to
|
|
124
132
|
// flush. Request locks are host-produced and must not wake that waiter.
|
|
125
|
-
notifyOnHostPublish
|
|
133
|
+
notifyOnHostPublish: notifyOnHostPublish ||
|
|
134
|
+
notifyDenoHost !== undefined ||
|
|
135
|
+
notifyNodeHost !== undefined ||
|
|
136
|
+
notifyProcessHost !== undefined,
|
|
137
|
+
notifyHostPublish: notifyDenoHost !== undefined
|
|
138
|
+
? () => notifyDenoHost(thread)
|
|
139
|
+
: notifyNodeHost !== undefined
|
|
140
|
+
? notifyNodeHost
|
|
141
|
+
: notifyProcessHost,
|
|
126
142
|
});
|
|
127
143
|
const timers = workerOptions?.timers;
|
|
128
144
|
const spinMicroseconds = timers?.spinMicroseconds ??
|
|
@@ -163,16 +179,16 @@ export const workerMainLoop = async (startupData) => {
|
|
|
163
179
|
maxSignals: abortSignalMax,
|
|
164
180
|
})
|
|
165
181
|
: undefined;
|
|
166
|
-
const { enqueueLock, serviceBatchImmediate, hasCompleted, writeBatch,
|
|
182
|
+
const { enqueueLock, serviceBatchImmediate, hasCompleted, writeBatch, getAwaiting, drainReturnReleases, releaseDeferredReturns, } = createWorkerRxQueue({
|
|
167
183
|
listOfFunctions,
|
|
168
184
|
workerOptions,
|
|
169
185
|
lock: lockState,
|
|
170
186
|
returnLock: returnLockState,
|
|
171
|
-
borrowReturnedBufferReferences: bufferReferenceReturn === "borrow",
|
|
172
187
|
hasAborted: abortSignals?.hasAborted,
|
|
173
188
|
stealing: steal !== undefined,
|
|
174
189
|
});
|
|
175
|
-
|
|
190
|
+
if (sharedReturn === true)
|
|
191
|
+
installSharedReturn(returnLock.payload);
|
|
176
192
|
a_store(rxStatus, 0, 1);
|
|
177
193
|
const WRITE_MAX = 64;
|
|
178
194
|
const pauseUntil = sleepUntilChanged({
|
|
@@ -224,9 +240,9 @@ export const workerMainLoop = async (startupData) => {
|
|
|
224
240
|
};
|
|
225
241
|
const traceSignals = dbg?.enabled("signals") === true;
|
|
226
242
|
const _hasCompleted = hasCompleted;
|
|
227
|
-
const _hasPending = hasPending;
|
|
228
243
|
const _getAwaiting = getAwaiting;
|
|
229
244
|
const _drainReturnReleases = drainReturnReleases;
|
|
245
|
+
const _releaseDeferredReturns = releaseDeferredReturns;
|
|
230
246
|
const _pauseSpin = pauseSpin;
|
|
231
247
|
const _enqueueLock = traceSignals
|
|
232
248
|
? () => {
|
|
@@ -259,9 +275,24 @@ export const workerMainLoop = async (startupData) => {
|
|
|
259
275
|
pauseUntil(value, spinMicroseconds, parkMs);
|
|
260
276
|
}
|
|
261
277
|
: pauseUntil;
|
|
262
|
-
|
|
278
|
+
// Stealing flushes completed work before claiming more; private lanes claim
|
|
279
|
+
// first to keep their single-consumer path short.
|
|
280
|
+
const pump = steal !== undefined
|
|
281
|
+
? () => {
|
|
282
|
+
let progressed = false;
|
|
283
|
+
if (_hasCompleted() && _writeBatch(WRITE_MAX) > 0)
|
|
284
|
+
progressed = true;
|
|
285
|
+
return _enqueueLock() || progressed;
|
|
286
|
+
}
|
|
287
|
+
: () => {
|
|
288
|
+
const claimed = _enqueueLock();
|
|
289
|
+
return (_hasCompleted() && _writeBatch(WRITE_MAX) > 0) || claimed;
|
|
290
|
+
};
|
|
263
291
|
/** Leave the dispatch loop and acknowledge shutdown. */
|
|
264
292
|
const stopLoop = () => {
|
|
293
|
+
// A shutdown rejects unconsumed calls, so no return can still be adopted.
|
|
294
|
+
// Do not let a worker-isolate exit strand native BufferReference pins.
|
|
295
|
+
_releaseDeferredReturns();
|
|
265
296
|
a_store(stopView, 0, WORKER_STOP.acknowledged);
|
|
266
297
|
a_store(rxStatus, 0, 0);
|
|
267
298
|
try {
|
|
@@ -272,41 +303,20 @@ export const workerMainLoop = async (startupData) => {
|
|
|
272
303
|
};
|
|
273
304
|
const loop = () => {
|
|
274
305
|
isInMacro = false;
|
|
306
|
+
// Recompute progress on every pass so idle workers can park.
|
|
275
307
|
let progressed = true;
|
|
276
308
|
let awaiting = 0;
|
|
277
309
|
while (true) {
|
|
278
310
|
if (stopView[0] !== WORKER_STOP.running)
|
|
279
311
|
return stopLoop();
|
|
280
|
-
|
|
281
|
-
// Stealing only. Flush finished work before taking more on: a computed
|
|
282
|
-
// response should not wait behind a claim, and that claim can block on
|
|
283
|
-
// a peer withdrawing its intent. Measured to hurt the per-lane path,
|
|
284
|
-
// where a claim is a cheap private decode and picking work up promptly
|
|
285
|
-
// matters more, so the classic order is kept below.
|
|
286
|
-
// Reordering must not make `progressed` sticky: it answers "did this
|
|
287
|
-
// iteration move anything", so it has to start false every pass or the
|
|
288
|
-
// park below is unreachable and the worker spins a core forever.
|
|
289
|
-
progressed = false;
|
|
290
|
-
if (_hasCompleted()) {
|
|
291
|
-
if (_writeBatch(WRITE_MAX) > 0)
|
|
292
|
-
progressed = true;
|
|
293
|
-
}
|
|
294
|
-
progressed = _enqueueLock() || progressed;
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
progressed = _enqueueLock();
|
|
298
|
-
if (_hasCompleted()) {
|
|
299
|
-
if (_writeBatch(WRITE_MAX) > 0)
|
|
300
|
-
progressed = true;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
312
|
+
progressed = pump();
|
|
303
313
|
_drainReturnReleases();
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
progressed = true;
|
|
307
|
-
}
|
|
314
|
+
if (_serviceBatchImmediate() > 0)
|
|
315
|
+
progressed = true;
|
|
308
316
|
if ((awaiting = _getAwaiting()) > 0) {
|
|
309
|
-
|
|
317
|
+
// Promise callbacks need the loop to yield; delay only when no progress
|
|
318
|
+
// was made, otherwise queued work pays an unnecessary timer hop.
|
|
319
|
+
if (progressed || awaiting !== lastAwaiting)
|
|
310
320
|
awaitingSpins = 0;
|
|
311
321
|
lastAwaiting = awaiting;
|
|
312
322
|
awaitingSpins++;
|
|
@@ -315,9 +325,21 @@ export const workerMainLoop = async (startupData) => {
|
|
|
315
325
|
return;
|
|
316
326
|
}
|
|
317
327
|
awaitingSpins = lastAwaiting = 0;
|
|
328
|
+
if (processRingPending) {
|
|
329
|
+
processRingPending = false;
|
|
330
|
+
scheduleTimer(0);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
318
333
|
if (!progressed) {
|
|
319
334
|
if (txStatus[Comment.thisIsAHint] === 1) {
|
|
320
|
-
|
|
335
|
+
// During a hint spin only host-driven state can change.
|
|
336
|
+
do {
|
|
337
|
+
_pauseSpin();
|
|
338
|
+
if (stopView[0] !== WORKER_STOP.running)
|
|
339
|
+
return stopLoop();
|
|
340
|
+
if (pump())
|
|
341
|
+
break;
|
|
342
|
+
} while (txStatus[Comment.thisIsAHint] === 1);
|
|
321
343
|
continue;
|
|
322
344
|
}
|
|
323
345
|
_pauseUntil(wakeToken, spinMicroseconds, parkMs);
|
package/src/worker/rx-queue.d.ts
CHANGED
|
@@ -6,7 +6,6 @@ type ArgumentsForCreateWorkerQueue = {
|
|
|
6
6
|
workerOptions?: WorkerSettings;
|
|
7
7
|
lock: Lock2;
|
|
8
8
|
returnLock: Lock2;
|
|
9
|
-
borrowReturnedBufferReferences?: boolean;
|
|
10
9
|
hasAborted?: (signal: number) => boolean;
|
|
11
10
|
now?: () => number;
|
|
12
11
|
/**
|
|
@@ -21,14 +20,14 @@ type ArgumentsForCreateWorkerQueue = {
|
|
|
21
20
|
stealing?: boolean;
|
|
22
21
|
};
|
|
23
22
|
export type CreateWorkerRxQueue = ReturnType<typeof createWorkerRxQueue>;
|
|
24
|
-
export declare const createWorkerRxQueue: ({ listOfFunctions, workerOptions, lock, returnLock,
|
|
23
|
+
export declare const createWorkerRxQueue: ({ listOfFunctions, workerOptions, lock, returnLock, hasAborted, now, stealing, }: ArgumentsForCreateWorkerQueue) => {
|
|
25
24
|
hasCompleted: () => boolean;
|
|
26
25
|
hasPending: () => boolean;
|
|
27
26
|
writeBatch: (max: number) => number;
|
|
28
27
|
serviceBatchImmediate: () => number;
|
|
29
28
|
enqueueLock: () => boolean;
|
|
30
29
|
drainReturnReleases: () => void;
|
|
31
|
-
|
|
30
|
+
releaseDeferredReturns: () => void;
|
|
32
31
|
hasAwaiting: () => boolean;
|
|
33
32
|
getAwaiting: () => number;
|
|
34
33
|
};
|