knitting 0.1.61 → 0.1.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -9
- package/knitting.browser.js +1 -1
- package/package.json +1 -1
- package/src/api.js +26 -16
- package/src/common/task-source.d.ts +9 -0
- package/src/common/task-source.js +15 -0
- package/src/ipc/transport/shared-memory.d.ts +9 -1
- package/src/ipc/transport/shared-memory.js +13 -1
- package/src/memory/lock.d.ts +12 -1
- package/src/memory/lock.js +82 -41
- package/src/runtime/dispatcher.d.ts +11 -8
- package/src/runtime/dispatcher.js +159 -26
- package/src/runtime/inline-executor.js +6 -1
- package/src/runtime/pool.d.ts +22 -4
- package/src/runtime/pool.js +104 -27
- package/src/runtime/process-worker.d.ts +22 -0
- package/src/runtime/process-worker.js +116 -2
- package/src/runtime/tx-queue.d.ts +2 -0
- package/src/runtime/tx-queue.js +68 -0
- package/src/runtime/worker-common.d.ts +2 -1
- package/src/runtime/worker-common.js +20 -5
- package/src/types.d.ts +44 -9
- package/src/worker/loop.js +24 -6
package/src/api.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getCallerFilePath, getCallerHref, setModuleUrl, } from "./common/task-source.js";
|
|
2
2
|
import { DEBUG_ENABLED, resolveDebugNamespaces } from "./debug/gate.js";
|
|
3
|
-
import { genTaskID } from "./common/task-source.js";
|
|
3
|
+
import { genTaskID, stableTaskID } from "./common/task-source.js";
|
|
4
4
|
import { toModuleUrl } from "./common/module-url.js";
|
|
5
5
|
import { endpointSymbol } from "./common/task-symbol.js";
|
|
6
6
|
import { createStealPoolBuffers, MAX_STEAL_CONSUMERS, spawnWorkerContext, } from "./runtime/pool.js";
|
|
@@ -9,7 +9,8 @@ import { createSharedArrayBuffer, RUNTIME } from "./common/runtime.js";
|
|
|
9
9
|
import { RUNTIME_IS_MAIN_THREAD, RUNTIME_POOL_DEPTH, RUNTIME_WORKER_DATA, } from "./common/worker-runtime.js";
|
|
10
10
|
import { classifyProcessPermissionCompatibility, enforceProcessPermissionCompatibility, resolvePermissionProtocol, toRuntimePermissionFlags, } from "./permission/index.js";
|
|
11
11
|
import { getNodeProcess } from "./common/node-compat.js";
|
|
12
|
-
import { readProcessWorkerNodeMajor, readProcessWorkerRuntime, } from "./runtime/process-worker.js";
|
|
12
|
+
import { readProcessSharedMemorySettings, readProcessWorkerNodeMajor, readProcessWorkerRuntime, } from "./runtime/process-worker.js";
|
|
13
|
+
import { TRANSPORT_SIGNAL_BYTES } from "./ipc/transport/shared-memory.js";
|
|
13
14
|
import { inspectCompiledWorkerArtifact } from "./runtime/compiled-artifact.js";
|
|
14
15
|
import { managerMethod } from "./runtime/balancer.js";
|
|
15
16
|
import { createInlineExecutor } from "./runtime/inline-executor.js";
|
|
@@ -370,17 +371,14 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
370
371
|
: stealEnvRaw === "0" || stealEnvRaw === "false"
|
|
371
372
|
? false
|
|
372
373
|
: undefined;
|
|
373
|
-
// Shared-submit stealing is the default for compatible multi-worker
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
// explicitly requests an unsupported combination.
|
|
374
|
+
// Shared-submit stealing is the default for compatible multi-worker pools.
|
|
375
|
+
// An explicit option wins over the environment; compiled and inline
|
|
376
|
+
// transports retain their existing topology.
|
|
377
377
|
const dispatcherExplicitlySelected = host?.dispatcher !== undefined ||
|
|
378
378
|
dispatcherEnv === "serial-channel" || dispatcherEnv === "per-thread";
|
|
379
379
|
const stealDefaultCompatible = balancer === undefined &&
|
|
380
380
|
!dispatcherExplicitlySelected && (threads ?? 1) <= MAX_STEAL_CONSUMERS;
|
|
381
381
|
const stealRequested = host?.steal ?? stealEnv ?? stealDefaultCompatible;
|
|
382
|
-
const stealExplicitlyEnabled = host?.steal === true ||
|
|
383
|
-
(host?.steal === undefined && stealEnv === true);
|
|
384
382
|
const usingCompiledWorker = resolvedWorker?.runtime === "compiled";
|
|
385
383
|
if (resolvedWorker?.compiled !== undefined && !usingCompiledWorker) {
|
|
386
384
|
throw new Error("worker.compiled requires worker.runtime to be compiled");
|
|
@@ -435,9 +433,6 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
435
433
|
if (usingInliner && resolvedWorker?.bootstrap !== undefined) {
|
|
436
434
|
throw new Error("worker.bootstrap cannot be used with the inliner");
|
|
437
435
|
}
|
|
438
|
-
if (stealExplicitlyEnabled && resolvedWorker?.runtime === "process") {
|
|
439
|
-
throw new Error("host.steal does not support process workers; use worker threads or disable stealing");
|
|
440
|
-
}
|
|
441
436
|
if (resolvedWorker?.runtime === "process") {
|
|
442
437
|
const processRuntime = readProcessWorkerRuntime(resolvedWorker);
|
|
443
438
|
enforceProcessPermissionCompatibility(classifyProcessPermissionCompatibility({
|
|
@@ -479,12 +474,11 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
479
474
|
})();
|
|
480
475
|
const dispatcher = explicitDispatcher ?? autoDispatcher;
|
|
481
476
|
// Work stealing: one shared submit region, private return lanes, one
|
|
482
|
-
// pool-global pending registry. It is the compatible multi-
|
|
477
|
+
// pool-global pending registry. It is the compatible multi-worker default;
|
|
483
478
|
// an explicit balancer/dispatcher keeps the private-lane topology unless
|
|
484
479
|
// stealing itself was explicitly requested.
|
|
485
480
|
const useSteal = stealRequested &&
|
|
486
481
|
!usingCompiledWorker &&
|
|
487
|
-
resolvedWorker?.runtime !== "process" &&
|
|
488
482
|
(threads ?? 1) > 1 &&
|
|
489
483
|
!usingInliner;
|
|
490
484
|
const stealBuffers = useSteal
|
|
@@ -494,12 +488,22 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
494
488
|
regionLanes: host?.stealRegionLanes,
|
|
495
489
|
abortSignalCapacity,
|
|
496
490
|
usesAbortSignal,
|
|
491
|
+
processWorker: resolvedWorker?.runtime === "process"
|
|
492
|
+
? {
|
|
493
|
+
signalBytes: TRANSPORT_SIGNAL_BYTES,
|
|
494
|
+
sharedMemory: readProcessSharedMemorySettings(resolvedWorker),
|
|
495
|
+
}
|
|
496
|
+
: undefined,
|
|
497
497
|
})
|
|
498
498
|
: undefined;
|
|
499
499
|
const serialChannel = !usingCompiledWorker && !useSteal &&
|
|
500
500
|
dispatcher === "serial-channel";
|
|
501
|
+
// One serial-channel hop drives every lane check in turn, and it depends on
|
|
502
|
+
// the channel's delivery to do that: swapping in the cheaper `setImmediate`
|
|
503
|
+
// pump cost 25% throughput and tripled p99 here, where every other topology
|
|
504
|
+
// gained. See ChannelHandler's note.
|
|
501
505
|
const serialDispatcherChannel = serialChannel
|
|
502
|
-
? new ChannelHandler()
|
|
506
|
+
? new ChannelHandler("channel")
|
|
503
507
|
: undefined;
|
|
504
508
|
let workers = Array.from({
|
|
505
509
|
length: threads ?? 1,
|
|
@@ -512,6 +516,9 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
512
516
|
debug,
|
|
513
517
|
hostDebug: hostDebug?.log,
|
|
514
518
|
totalNumberOfThread,
|
|
519
|
+
// Worker count without the inline lane. The inliner runs on the host
|
|
520
|
+
// thread and never spins, so it must not count towards the spin policy.
|
|
521
|
+
workerCount: threads ?? 1,
|
|
515
522
|
source,
|
|
516
523
|
workerOptions: resolvedWorker,
|
|
517
524
|
workerExecArgv: execArgv,
|
|
@@ -533,6 +540,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
533
540
|
regionLanes: stealBuffers.regionLanes,
|
|
534
541
|
abortSignalSAB: stealBuffers.abortSignalSAB,
|
|
535
542
|
abortSignalMax: stealBuffers.abortSignalMax,
|
|
543
|
+
processMemory: stealBuffers.processMemory,
|
|
536
544
|
},
|
|
537
545
|
}));
|
|
538
546
|
const stealChannel = useSteal ? new ChannelHandler() : undefined;
|
|
@@ -565,6 +573,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
565
573
|
channelHandler: channel,
|
|
566
574
|
dispatcherOptions: host,
|
|
567
575
|
notifySignal: wakeOne,
|
|
576
|
+
crossProcess: resolvedWorker?.runtime === "process",
|
|
568
577
|
});
|
|
569
578
|
channel.open(check);
|
|
570
579
|
workers.forEach((context) => {
|
|
@@ -681,7 +690,8 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
681
690
|
if (closePromise)
|
|
682
691
|
return closePromise;
|
|
683
692
|
closing = true;
|
|
684
|
-
closePromise = Promise.allSettled(workers.map((context) => context.
|
|
693
|
+
closePromise = Promise.allSettled(workers.map((context) => context.requestStop?.()))
|
|
694
|
+
.then(() => Promise.allSettled(workers.map((context) => context.kills())))
|
|
685
695
|
.then(() => {
|
|
686
696
|
sharedDispatcherChannel?.close();
|
|
687
697
|
stealChannel?.close();
|
|
@@ -843,7 +853,7 @@ const buildTaskDefinitionFromCaller = (input, callerHref, at, imported = false)
|
|
|
843
853
|
const importedFrom = new URL(callerHref).href;
|
|
844
854
|
const out = ({
|
|
845
855
|
...input,
|
|
846
|
-
id:
|
|
856
|
+
id: stableTaskID(importedFrom, at),
|
|
847
857
|
importedFrom,
|
|
848
858
|
at,
|
|
849
859
|
imported,
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
export declare const genTaskID: () => number;
|
|
2
|
+
/**
|
|
3
|
+
* Identity of a task, stable across processes.
|
|
4
|
+
*
|
|
5
|
+
* `genTaskID` numbers tasks by the order their module happened to be evaluated,
|
|
6
|
+
* which host and worker do not share: the host imports in its own source order
|
|
7
|
+
* and the worker imports in task-name order. `(href, at)` is the same pair in
|
|
8
|
+
* both, because `at` counts `task()` calls within one module.
|
|
9
|
+
*/
|
|
10
|
+
export declare const stableTaskID: (href: string, at: number) => number;
|
|
2
11
|
export declare const setModuleUrl: (url: string | undefined) => void;
|
|
3
12
|
export declare const getModuleUrlOverride: () => string | undefined;
|
|
4
13
|
export declare const getCallerHref: (offset?: number) => string;
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { toModuleUrl } from "./module-url.js";
|
|
2
2
|
export const genTaskID = ((counter) => () => counter++)(0);
|
|
3
|
+
/**
|
|
4
|
+
* Identity of a task, stable across processes.
|
|
5
|
+
*
|
|
6
|
+
* `genTaskID` numbers tasks by the order their module happened to be evaluated,
|
|
7
|
+
* which host and worker do not share: the host imports in its own source order
|
|
8
|
+
* and the worker imports in task-name order. `(href, at)` is the same pair in
|
|
9
|
+
* both, because `at` counts `task()` calls within one module.
|
|
10
|
+
*/
|
|
11
|
+
export const stableTaskID = (href, at) => {
|
|
12
|
+
let hash = 0x811c9dc5;
|
|
13
|
+
for (let index = 0; index < href.length; index++) {
|
|
14
|
+
hash = Math.imul(hash ^ href.charCodeAt(index), 0x01000193);
|
|
15
|
+
}
|
|
16
|
+
return (Math.imul(hash ^ at, 0x01000193) >>> 0);
|
|
17
|
+
};
|
|
3
18
|
const INTERNAL_CALLER_HINTS = [
|
|
4
19
|
"/src/common/task-source.ts",
|
|
5
20
|
"/src/common/task-source.js",
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export type SignalArguments = ReturnType<typeof createSharedMemoryTransport>;
|
|
2
2
|
import { type SharedBufferSource } from "../../common/shared-buffer-region.js";
|
|
3
3
|
export declare const TRANSPORT_SIGNAL_BYTES: number;
|
|
4
|
+
/** States of the worker-stop word. */
|
|
5
|
+
export declare const WORKER_STOP: {
|
|
6
|
+
readonly running: 0;
|
|
7
|
+
readonly requested: 1;
|
|
8
|
+
/** The worker has left its dispatch loop. */
|
|
9
|
+
readonly acknowledged: 2;
|
|
10
|
+
};
|
|
4
11
|
export type Sab = {
|
|
5
12
|
size?: number;
|
|
6
13
|
sharedSab?: SharedBufferSource;
|
|
@@ -18,6 +25,7 @@ export declare const createSharedMemoryTransport: ({ sabObject, isMain, startTim
|
|
|
18
25
|
opView: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
|
|
19
26
|
rxStatus: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
|
|
20
27
|
txStatus: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
|
|
28
|
+
stopView: Int32Array<import("../../common/shared-buffer-region.js").SharedBuffer>;
|
|
21
29
|
};
|
|
22
|
-
export type MainSignal = Pick<SignalArguments, "opView" | "startAt" | "rxStatus" | "txStatus">;
|
|
30
|
+
export type MainSignal = Pick<SignalArguments, "opView" | "startAt" | "rxStatus" | "txStatus" | "stopView">;
|
|
23
31
|
export {};
|
|
@@ -7,8 +7,16 @@ const SIGNAL_OFFSETS = {
|
|
|
7
7
|
op: 0,
|
|
8
8
|
rxStatus: CACHE_LINE_BYTES,
|
|
9
9
|
txStatus: CACHE_LINE_BYTES * 2,
|
|
10
|
+
stop: CACHE_LINE_BYTES * 3,
|
|
11
|
+
};
|
|
12
|
+
export const TRANSPORT_SIGNAL_BYTES = CACHE_LINE_BYTES * 4;
|
|
13
|
+
/** States of the worker-stop word. */
|
|
14
|
+
export const WORKER_STOP = {
|
|
15
|
+
running: 0,
|
|
16
|
+
requested: 1,
|
|
17
|
+
/** The worker has left its dispatch loop. */
|
|
18
|
+
acknowledged: 2,
|
|
10
19
|
};
|
|
11
|
-
export const TRANSPORT_SIGNAL_BYTES = CACHE_LINE_BYTES * 3;
|
|
12
20
|
const a_store = Atomics.store;
|
|
13
21
|
export const createSharedMemoryTransport = ({ sabObject, isMain, startTime }) => {
|
|
14
22
|
const toGrow = sabObject?.size ?? page;
|
|
@@ -24,6 +32,9 @@ export const createSharedMemoryTransport = ({ sabObject, isMain, startTime }) =>
|
|
|
24
32
|
a_store(opView, 0, 0);
|
|
25
33
|
const rxStatus = new Int32Array(sab, baseByteOffset + SIGNAL_OFFSETS.rxStatus, 1);
|
|
26
34
|
a_store(rxStatus, 0, 1);
|
|
35
|
+
const stopView = new Int32Array(sab, baseByteOffset + SIGNAL_OFFSETS.stop, 1);
|
|
36
|
+
if (isMain)
|
|
37
|
+
a_store(stopView, 0, WORKER_STOP.running);
|
|
27
38
|
return {
|
|
28
39
|
sab: signalRegion,
|
|
29
40
|
op: opView,
|
|
@@ -31,5 +42,6 @@ export const createSharedMemoryTransport = ({ sabObject, isMain, startTime }) =>
|
|
|
31
42
|
opView,
|
|
32
43
|
rxStatus,
|
|
33
44
|
txStatus: new Int32Array(sab, baseByteOffset + SIGNAL_OFFSETS.txStatus, 1),
|
|
45
|
+
stopView,
|
|
34
46
|
};
|
|
35
47
|
};
|
package/src/memory/lock.d.ts
CHANGED
|
@@ -173,6 +173,8 @@ export declare const STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32: number;
|
|
|
173
173
|
* clear here. This preserves single-writer ownership of every WANT word.
|
|
174
174
|
*/
|
|
175
175
|
export declare const STEAL_LIVE_SLOT_OFFSET_U32: number;
|
|
176
|
+
/** Host-owned arm word for the return-lock completion doorbell. */
|
|
177
|
+
export declare const DOORBELL_ARMED_SLOT_OFFSET_U32: number;
|
|
176
178
|
export declare const HEADER_U32_LENGTH: number;
|
|
177
179
|
export declare const HEADER_BYTE_LENGTH: number;
|
|
178
180
|
export declare const makeTask: () => Task;
|
|
@@ -191,7 +193,12 @@ type ResolveHostOptions = {
|
|
|
191
193
|
* - encode/decode are not re-entrant; payload codec uses a shared scratch buffer.
|
|
192
194
|
*/
|
|
193
195
|
export type Lock2 = ReturnType<typeof lock2>;
|
|
194
|
-
export
|
|
196
|
+
export type WaitAsyncState = "not-equal" | "ok" | "timed-out";
|
|
197
|
+
export type WaitAsyncResult = {
|
|
198
|
+
async: boolean;
|
|
199
|
+
value: WaitAsyncState | PromiseLike<WaitAsyncState>;
|
|
200
|
+
};
|
|
201
|
+
export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, consumers, consumerId, regionLanes, notifyOnHostPublish, }: {
|
|
195
202
|
headers?: SharedBufferSource;
|
|
196
203
|
headerSlotStrideU32?: number;
|
|
197
204
|
LockBoundSector?: SharedBufferSource;
|
|
@@ -213,6 +220,8 @@ export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, pa
|
|
|
213
220
|
consumerId?: number;
|
|
214
221
|
/** Lanes claimed per Dekker handshake. Paper rule: `slots / regionLanes >= consumers + 1`. */
|
|
215
222
|
regionLanes?: number;
|
|
223
|
+
/** Notify a host-side wait after this endpoint publishes a frame. */
|
|
224
|
+
notifyOnHostPublish?: boolean;
|
|
216
225
|
}) => {
|
|
217
226
|
enlist: (task: Task) => true;
|
|
218
227
|
encode: (task: Task, state?: number) => boolean;
|
|
@@ -227,6 +236,8 @@ export declare const lock2: ({ headers, headerSlotStrideU32, LockBoundSector, pa
|
|
|
227
236
|
workerBits: Int32Array<import("../common/shared-buffer-region.js").SharedBuffer>;
|
|
228
237
|
recyclecList: RingQueue<Task>;
|
|
229
238
|
resolveHost: ({ queue, onResolved, shouldSettle, activeRejectPlaceholder, }: ResolveHostOptions) => () => number;
|
|
239
|
+
waitForHostChange: (timeoutMs?: number) => WaitAsyncResult | undefined;
|
|
240
|
+
setHostWaiterArmed: (armed: boolean) => void;
|
|
230
241
|
hasPendingFrames: () => boolean;
|
|
231
242
|
getPendingFrameCount: () => number;
|
|
232
243
|
getPendingPromiseCount: () => number;
|
package/src/memory/lock.js
CHANGED
|
@@ -228,10 +228,9 @@ export const HEADER_STATIC_PAYLOAD_U32 = TaskIndex.TotalBuff -
|
|
|
228
228
|
export const HEADER_TASK_OFFSET_IN_SLOT_U32 = HEADER_STATIC_PAYLOAD_U32;
|
|
229
229
|
// Work-stealing control words live in the unused tail of each slot's header
|
|
230
230
|
// cache line. Task words occupy 0..7 (`TASK_LOCAL_FLAGS_INDEX = 7`) of the
|
|
231
|
-
// 16-word line, leaving words 8..15 free. Consumer `c` owns slot `c`'s
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
// and a peer reading one pulls the other for free.
|
|
231
|
+
// 16-word line, leaving words 8..15 free. Consumer `c` owns slot `c`'s words,
|
|
232
|
+
// and the slot stride keeps those words on separate cache lines.
|
|
233
|
+
// The ACK word was removed; retirement is folded into `workerBits`.
|
|
235
234
|
export const STEAL_ACK_SLOT_OFFSET_U32 = HEADER_TASK_OFFSET_IN_SLOT_U32 +
|
|
236
235
|
TaskIndex.Size;
|
|
237
236
|
export const STEAL_WANT_SLOT_OFFSET_U32 = STEAL_ACK_SLOT_OFFSET_U32 + 1;
|
|
@@ -247,6 +246,8 @@ export const STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32 = STEAL_ACK_SLOT_OFFSET_U32 + 2;
|
|
|
247
246
|
* clear here. This preserves single-writer ownership of every WANT word.
|
|
248
247
|
*/
|
|
249
248
|
export const STEAL_LIVE_SLOT_OFFSET_U32 = STEAL_PAYLOAD_ACK_SLOT_OFFSET_U32 + 1;
|
|
249
|
+
/** Host-owned arm word for the return-lock completion doorbell. */
|
|
250
|
+
export const DOORBELL_ARMED_SLOT_OFFSET_U32 = STEAL_LIVE_SLOT_OFFSET_U32 + 1;
|
|
250
251
|
export const HEADER_U32_LENGTH = LockBound.header +
|
|
251
252
|
(HEADER_SLOT_STRIDE_U32 * LockBound.slots);
|
|
252
253
|
export const HEADER_BYTE_LENGTH = HEADER_U32_LENGTH *
|
|
@@ -297,7 +298,7 @@ const settleTask = (task) => {
|
|
|
297
298
|
task[TaskIndex["FlagsToHost"]] = 0;
|
|
298
299
|
}
|
|
299
300
|
};
|
|
300
|
-
export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, consumers, consumerId, regionLanes, }) => {
|
|
301
|
+
export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload, payloadConfig, payloadSector, textCompat, resultList, toSentList, recycleList, processBoundary, consumers, consumerId, regionLanes, notifyOnHostPublish, }) => {
|
|
301
302
|
// Layout within `lockSectorRegion`:
|
|
302
303
|
// - hostBits starts at byte 0
|
|
303
304
|
// - workerBits starts at byte 64
|
|
@@ -316,6 +317,11 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
316
317
|
const headersRegion = toSharedBufferRegion(headers ?? createWasmSharedArrayBuffer(HEADER_BYTE_LENGTH));
|
|
317
318
|
const headersBuffer = new Uint32Array(headersRegion.sab, headersRegion.byteOffset, headersRegion.byteLength >>> 2);
|
|
318
319
|
const headersSlotStride = headerSlotStrideU32 ?? HEADER_SLOT_STRIDE_U32;
|
|
320
|
+
// The first task cache line has four unused control words after the task
|
|
321
|
+
// header. Keep the doorbell arm bit in one of those words so the host and
|
|
322
|
+
// worker lock instances can share it without allocating another SAB.
|
|
323
|
+
const doorbellArmed = new Int32Array(headersRegion.sab, headersRegion.byteOffset +
|
|
324
|
+
(DOORBELL_ARMED_SLOT_OFFSET_U32 * Uint32Array.BYTES_PER_ELEMENT), 1);
|
|
319
325
|
const resolvedPayloadConfig = resolvePayloadBufferOptions({
|
|
320
326
|
sab: payload,
|
|
321
327
|
options: payloadConfig,
|
|
@@ -356,12 +362,10 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
356
362
|
// Int32 alias over the headers SAB so the control words get signed bit math
|
|
357
363
|
// consistent with the rest of the protocol.
|
|
358
364
|
const stealView = new Int32Array(headersRegion.sab, headersRegion.byteOffset, headersRegion.byteLength >>> 2);
|
|
359
|
-
const stealAckIndex = new Int32Array(stealConsumers);
|
|
360
365
|
const stealWantIndex = new Int32Array(stealConsumers);
|
|
361
366
|
for (let c = 0; c < stealConsumers; c++) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
stealWantIndex[c] = slotBase + STEAL_WANT_SLOT_OFFSET_U32;
|
|
367
|
+
stealWantIndex[c] = (c * headersSlotStride) + LockBound.header +
|
|
368
|
+
STEAL_WANT_SLOT_OFFSET_U32;
|
|
365
369
|
}
|
|
366
370
|
const stealLiveIndex = LockBound.header + STEAL_LIVE_SLOT_OFFSET_U32;
|
|
367
371
|
const stealAllLiveMask = stealConsumers === 32
|
|
@@ -371,13 +375,6 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
371
375
|
Atomics.store(stealView, stealLiveIndex, stealAllLiveMask);
|
|
372
376
|
}
|
|
373
377
|
const stealIsLive = (mask, consumer) => (mask & (1 << consumer)) !== 0;
|
|
374
|
-
const stealAckXorAll = () => {
|
|
375
|
-
let x = 0 | 0;
|
|
376
|
-
for (let c = 0; c < stealConsumers; c++) {
|
|
377
|
-
x = (x ^ a_load(stealView, stealAckIndex[c])) | 0;
|
|
378
|
-
}
|
|
379
|
-
return x;
|
|
380
|
-
};
|
|
381
378
|
let promiseHandler;
|
|
382
379
|
if (registeredEncodePayload === undefined ||
|
|
383
380
|
registeredDecodePayload === undefined) {
|
|
@@ -428,14 +425,18 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
428
425
|
// Atomics aliases (hot path)
|
|
429
426
|
const a_load = Atomics.load;
|
|
430
427
|
const a_store = Atomics.store;
|
|
428
|
+
const a_notify = Atomics.notify;
|
|
429
|
+
const shouldNotifyHostPublish = notifyOnHostPublish === true;
|
|
430
|
+
const a_waitAsync = typeof Atomics.waitAsync === "function"
|
|
431
|
+
? Atomics.waitAsync.bind(Atomics)
|
|
432
|
+
: undefined;
|
|
431
433
|
// Sender-side cached shadow of the receiver-owned queue word. Under the XSC
|
|
432
434
|
// false-busy-only sender-side staleness property, this may hide newly freed
|
|
433
435
|
// lanes but cannot make a genuinely pending lane appear free. Refresh only
|
|
434
436
|
// when the cached free set is exhausted.
|
|
435
437
|
let workerShadow = 0 | 0;
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
: () => workerShadow = a_load(workerBits, 0) | 0;
|
|
438
|
+
// Under stealing, workerBits also records retired lanes.
|
|
439
|
+
const refreshWorkerShadow = () => workerShadow = a_load(workerBits, 0) | 0;
|
|
439
440
|
refreshWorkerShadow();
|
|
440
441
|
const ensureSenderStateHasFree = (state) => (~state) !== 0 ? state : (LastLocal ^ refreshWorkerShadow()) | 0;
|
|
441
442
|
// RingQueue method aliases (hot path)
|
|
@@ -560,7 +561,12 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
560
561
|
deferredCount = 0 | 0;
|
|
561
562
|
return toBeSent.isEmpty;
|
|
562
563
|
};
|
|
563
|
-
const storeHost = (bit) =>
|
|
564
|
+
const storeHost = (bit) => {
|
|
565
|
+
a_store(hostBits, 0, LastLocal = (LastLocal ^ bit) | 0);
|
|
566
|
+
if (shouldNotifyHostPublish && a_load(doorbellArmed, 0) !== 0) {
|
|
567
|
+
a_notify(hostBits, 0, 1);
|
|
568
|
+
}
|
|
569
|
+
};
|
|
564
570
|
const storeWorker = (bit) => a_store(workerBits, 0, LastWorker = (LastWorker ^ bit) | 0);
|
|
565
571
|
const encode = (task, state = (LastLocal ^ workerShadow) | 0) => {
|
|
566
572
|
state = ensureSenderStateHasFree(state);
|
|
@@ -649,14 +655,6 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
649
655
|
const stealLaneMask = (region) => stealRegionLanes === 32
|
|
650
656
|
? -1
|
|
651
657
|
: ((((1 << stealRegionLanes) - 1) << (region * stealRegionLanes)) | 0);
|
|
652
|
-
const stealPeerAcks = () => {
|
|
653
|
-
let x = 0 | 0;
|
|
654
|
-
for (let c = 0; c < stealConsumers; c++) {
|
|
655
|
-
if (c !== stealId)
|
|
656
|
-
x = (x ^ a_load(stealView, stealAckIndex[c])) | 0;
|
|
657
|
-
}
|
|
658
|
-
return x;
|
|
659
|
-
};
|
|
660
658
|
const stealJuniorWants = (intent) => {
|
|
661
659
|
const live = a_load(stealView, stealLiveIndex) | 0;
|
|
662
660
|
for (let c = stealId + 1; c < stealConsumers; c++) {
|
|
@@ -667,9 +665,11 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
667
665
|
}
|
|
668
666
|
return false;
|
|
669
667
|
};
|
|
670
|
-
|
|
668
|
+
const stealHome = ((stealRegions * stealId) / stealConsumers) | 0;
|
|
669
|
+
let stealCursor = stealHome;
|
|
671
670
|
const decodeSteal = () => {
|
|
672
|
-
|
|
671
|
+
// workerBits includes retired lanes under stealing.
|
|
672
|
+
const pending = (a_load(hostBits, 0) ^ a_load(workerBits, 0)) | 0;
|
|
673
673
|
if (pending === 0)
|
|
674
674
|
return false;
|
|
675
675
|
let pendingRegions = 0 | 0;
|
|
@@ -731,23 +731,32 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
731
731
|
}
|
|
732
732
|
// Last control loads before ownership. After this no control word is read
|
|
733
733
|
// until every slot in `take` has been decoded.
|
|
734
|
-
const take = ((a_load(hostBits, 0) ^
|
|
734
|
+
const take = ((a_load(hostBits, 0) ^ a_load(workerBits, 0)) &
|
|
735
735
|
stealLaneMask(region)) | 0;
|
|
736
736
|
if (take === 0) {
|
|
737
737
|
a_store(stealView, stealWantIndex[stealId], 0);
|
|
738
738
|
return false;
|
|
739
739
|
}
|
|
740
|
+
// Retire decoded lanes and release the region even if decoding throws.
|
|
740
741
|
let lanes = take;
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
742
|
+
let done = 0 | 0;
|
|
743
|
+
try {
|
|
744
|
+
while (lanes !== 0) {
|
|
745
|
+
const bit = (lanes & -lanes) | 0;
|
|
746
|
+
decodeAt(31 - clz32(bit >>> 0));
|
|
747
|
+
lanes = (lanes & (lanes - 1)) | 0;
|
|
748
|
+
done = (done ^ bit) | 0;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
finally {
|
|
752
|
+
// ACK before clearing intent.
|
|
753
|
+
LastWorker = (LastWorker ^ done) | 0;
|
|
754
|
+
if (done !== 0)
|
|
755
|
+
Atomics.xor(workerBits, 0, done);
|
|
756
|
+
a_store(stealView, stealWantIndex[stealId], 0);
|
|
757
|
+
// Restart from this consumer's home region.
|
|
758
|
+
stealCursor = stealHome;
|
|
759
|
+
}
|
|
751
760
|
return true;
|
|
752
761
|
};
|
|
753
762
|
/**
|
|
@@ -878,6 +887,34 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
878
887
|
return modified;
|
|
879
888
|
};
|
|
880
889
|
};
|
|
890
|
+
/**
|
|
891
|
+
* HOST SIDE: wait until the producer changes hostBits.
|
|
892
|
+
*
|
|
893
|
+
* LastWorker is the host's acknowledgement shadow. Passing it as the
|
|
894
|
+
* expected value makes the arm race-free: a publication between the drain
|
|
895
|
+
* and this call returns `not-equal` synchronously instead of being lost.
|
|
896
|
+
*/
|
|
897
|
+
const waitForHostChange = (timeoutMs) => {
|
|
898
|
+
if (a_waitAsync === undefined) {
|
|
899
|
+
a_store(doorbellArmed, 0, 0);
|
|
900
|
+
return undefined;
|
|
901
|
+
}
|
|
902
|
+
a_store(doorbellArmed, 0, 1);
|
|
903
|
+
try {
|
|
904
|
+
const wait = a_waitAsync(hostBits, 0, LastWorker | 0, timeoutMs);
|
|
905
|
+
if (!wait.async)
|
|
906
|
+
a_store(doorbellArmed, 0, 0);
|
|
907
|
+
return wait;
|
|
908
|
+
}
|
|
909
|
+
catch {
|
|
910
|
+
a_store(doorbellArmed, 0, 0);
|
|
911
|
+
// Some runtimes reject particular SharedArrayBuffer implementations
|
|
912
|
+
// (for example a growable or native-backed buffer). Fall back to the
|
|
913
|
+
// existing dispatcher rather than turning a capability issue into a
|
|
914
|
+
// hung pool.
|
|
915
|
+
return undefined;
|
|
916
|
+
}
|
|
917
|
+
};
|
|
881
918
|
const decodeAt = (at) => {
|
|
882
919
|
const off = (at * headersSlotStride) + slotBaseU32;
|
|
883
920
|
const recycled = recycleShift();
|
|
@@ -951,6 +988,10 @@ export const lock2 = ({ headers, headerSlotStrideU32, LockBoundSector, payload,
|
|
|
951
988
|
workerBits,
|
|
952
989
|
recyclecList,
|
|
953
990
|
resolveHost,
|
|
991
|
+
waitForHostChange,
|
|
992
|
+
setHostWaiterArmed: (armed) => {
|
|
993
|
+
a_store(doorbellArmed, 0, armed ? 1 : 0);
|
|
994
|
+
},
|
|
954
995
|
hasPendingFrames: () => toBeSent.size !== 0,
|
|
955
996
|
getPendingFrameCount: () => toBeSent.size | 0,
|
|
956
997
|
getPendingPromiseCount: () => pendingPromiseCount | 0,
|
|
@@ -2,12 +2,14 @@ import { type MultiQueue } from "./tx-queue.js";
|
|
|
2
2
|
import { type MainSignal } from "../ipc/transport/shared-memory.js";
|
|
3
3
|
import { type RuntimeMessageChannelLike, type RuntimeMessagePortLike } from "../common/worker-runtime.js";
|
|
4
4
|
import type { DispatcherSettings } from "../types.js";
|
|
5
|
-
export declare const hostDispatcherLoop: ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, }, channelHandler, dispatcherOptions, notifySignal, }: {
|
|
5
|
+
export declare const hostDispatcherLoop: ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, crossProcess, }: {
|
|
6
6
|
queue: MultiQueue;
|
|
7
7
|
signalBox: MainSignal;
|
|
8
8
|
channelHandler: ChannelHandler;
|
|
9
9
|
dispatcherOptions?: DispatcherSettings;
|
|
10
10
|
notifySignal?: () => void;
|
|
11
|
+
/** Workers live in other processes; disables the doorbell. */
|
|
12
|
+
crossProcess?: boolean;
|
|
11
13
|
}) => {
|
|
12
14
|
check: {
|
|
13
15
|
(): void;
|
|
@@ -20,20 +22,21 @@ type CheckWithState = (() => void) & {
|
|
|
20
22
|
rerun: boolean;
|
|
21
23
|
};
|
|
22
24
|
export type DispatcherCheck = CheckWithState;
|
|
25
|
+
export type ChannelHandlerPump = "auto" | "channel";
|
|
23
26
|
export declare class ChannelHandler {
|
|
24
27
|
#private;
|
|
25
|
-
channel: RuntimeMessageChannelLike;
|
|
26
|
-
port1: RuntimeMessagePortLike;
|
|
27
|
-
port2: RuntimeMessagePortLike;
|
|
28
|
-
constructor();
|
|
28
|
+
channel: RuntimeMessageChannelLike | undefined;
|
|
29
|
+
port1: RuntimeMessagePortLike | undefined;
|
|
30
|
+
port2: RuntimeMessagePortLike | undefined;
|
|
31
|
+
constructor(pump?: ChannelHandlerPump);
|
|
29
32
|
notify(): void;
|
|
30
33
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
34
|
+
* Registers the handler the pump calls back into. On the channel pump this is
|
|
35
|
+
* also where the ports are opened, so `notify` can reach port 1.
|
|
33
36
|
*/
|
|
34
37
|
open(f: () => void): void;
|
|
35
38
|
/**
|
|
36
|
-
*
|
|
39
|
+
* Detaches the handler, and closes the channel if this pump has one.
|
|
37
40
|
*/
|
|
38
41
|
close(): void;
|
|
39
42
|
}
|