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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { RUNTIME } from "../common/runtime.js";
|
|
2
|
+
const getDeno = () => globalThis.Deno;
|
|
3
|
+
/**
|
|
4
|
+
* Creates Deno's native thread-to-event-loop bridge without loading a custom
|
|
5
|
+
* library. `threadSafe()` is essential: ordinary UnsafeCallbacks may be
|
|
6
|
+
* called cross-thread, but do not wake an idle Deno event loop.
|
|
7
|
+
*
|
|
8
|
+
* The default pool configuration asks Deno for FFI permission once, so the
|
|
9
|
+
* native wake path is used whenever the caller approves it. `--allow-ffi`
|
|
10
|
+
* skips the prompt; `--deny-ffi`, `--no-prompt`, or a rejection preserves the
|
|
11
|
+
* portable polling dispatcher. Set `host.doorbell` to `false` to avoid asking.
|
|
12
|
+
*/
|
|
13
|
+
export const createDenoCompletionDoorbell = () => {
|
|
14
|
+
if (RUNTIME !== "deno")
|
|
15
|
+
return undefined;
|
|
16
|
+
const deno = getDeno();
|
|
17
|
+
const queryPermission = deno?.permissions?.querySync;
|
|
18
|
+
const requestPermission = deno?.permissions?.requestSync;
|
|
19
|
+
const Callback = deno?.UnsafeCallback;
|
|
20
|
+
const pointerValue = deno?.UnsafePointer?.value;
|
|
21
|
+
if (typeof queryPermission !== "function" ||
|
|
22
|
+
typeof Callback?.threadSafe !== "function" ||
|
|
23
|
+
typeof pointerValue !== "function") {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
// `querySync` avoids a redundant prompt for an already granted or explicitly
|
|
27
|
+
// denied capability. Only the normal `prompt` state asks, and a refusal
|
|
28
|
+
// leaves the portable polling path intact.
|
|
29
|
+
try {
|
|
30
|
+
let state = queryPermission({ name: "ffi" }).state;
|
|
31
|
+
if (state === "prompt" && typeof requestPermission === "function") {
|
|
32
|
+
state = requestPermission({ name: "ffi" }).state;
|
|
33
|
+
}
|
|
34
|
+
if (state !== "granted")
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const listeners = new Map();
|
|
41
|
+
let closed = false;
|
|
42
|
+
let callback;
|
|
43
|
+
try {
|
|
44
|
+
callback = Callback.threadSafe({ parameters: ["i32"], result: "void" }, (lane) => {
|
|
45
|
+
if (closed)
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
listeners.get(lane)?.();
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// FFI callbacks must never propagate an exception into the worker
|
|
52
|
+
// thread that rang them. The dispatcher watchdog remains a final
|
|
53
|
+
// liveness backstop if a host pump has already been closed.
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
const pointer = pointerValue(callback.pointer);
|
|
57
|
+
if (typeof pointer !== "bigint" || pointer === 0n) {
|
|
58
|
+
callback.close();
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
pointer,
|
|
63
|
+
listen: (lane, notify) => {
|
|
64
|
+
if (!closed)
|
|
65
|
+
listeners.set(lane, notify);
|
|
66
|
+
},
|
|
67
|
+
unref: () => {
|
|
68
|
+
try {
|
|
69
|
+
callback.unref?.();
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// best effort; a runtime may already be tearing down its callback
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
close: () => {
|
|
76
|
+
if (closed)
|
|
77
|
+
return;
|
|
78
|
+
closed = true;
|
|
79
|
+
listeners.clear();
|
|
80
|
+
callback.close();
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// Creating a callback is native code. Treat an unsupported Deno build just
|
|
86
|
+
// like a denied FFI capability and keep the portable poll path.
|
|
87
|
+
try {
|
|
88
|
+
callback?.close();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// best effort; callback may not have been initialized
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* Reconstruct a host-owned thread-safe callback in a Deno worker. The pointer
|
|
98
|
+
* stays process-local and is never included in process-worker boot payloads.
|
|
99
|
+
*/
|
|
100
|
+
export const createDenoCompletionNotifier = (pointer) => {
|
|
101
|
+
if (RUNTIME !== "deno" || pointer === undefined || pointer === 0n) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const deno = getDeno();
|
|
105
|
+
const FnPointer = deno?.UnsafeFnPointer;
|
|
106
|
+
const pointerCreate = deno?.UnsafePointer?.create;
|
|
107
|
+
if (typeof FnPointer !== "function" || typeof pointerCreate !== "function") {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const callback = new FnPointer(pointerCreate(pointer), { parameters: ["i32"], result: "void" });
|
|
112
|
+
return (lane) => callback.call(lane);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
@@ -2,20 +2,25 @@ 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, waitForCompletion, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, crossProcess, }: {
|
|
5
|
+
export declare const hostDispatcherLoop: ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, armCompletionNotifier, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, crossProcess, nativeCompletionDoorbell, processCompletionDoorbell, }: {
|
|
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. */
|
|
11
|
+
/** Workers live in other processes; disables only the atomic doorbell. */
|
|
12
12
|
crossProcess?: boolean;
|
|
13
|
+
/** A runtime-native completion ring that wakes the host event loop. */
|
|
14
|
+
nativeCompletionDoorbell?: boolean;
|
|
15
|
+
/** A process IPC completion doorbell that wakes the host event loop. */
|
|
16
|
+
processCompletionDoorbell?: boolean;
|
|
13
17
|
}) => {
|
|
14
18
|
check: {
|
|
15
19
|
(): void;
|
|
16
20
|
isRunning: boolean;
|
|
17
21
|
rerun: boolean;
|
|
18
22
|
};
|
|
23
|
+
wakeCompletion: () => void;
|
|
19
24
|
};
|
|
20
25
|
type CheckWithState = (() => void) & {
|
|
21
26
|
isRunning: boolean;
|
|
@@ -30,10 +35,7 @@ export declare class ChannelHandler {
|
|
|
30
35
|
port2: RuntimeMessagePortLike | undefined;
|
|
31
36
|
constructor(pump?: ChannelHandlerPump);
|
|
32
37
|
notify(): void;
|
|
33
|
-
/**
|
|
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.
|
|
36
|
-
*/
|
|
38
|
+
/** Register the pump handler and start channel ports when present. */
|
|
37
39
|
open(f: () => void): void;
|
|
38
40
|
/**
|
|
39
41
|
* Detaches the handler, and closes the channel if this pump has one.
|
|
@@ -1,34 +1,13 @@
|
|
|
1
1
|
import { createRuntimeMessageChannel, } from "../common/worker-runtime.js";
|
|
2
2
|
import { RUNTIME, SET_IMMEDIATE } from "../common/runtime.js";
|
|
3
|
-
/**
|
|
4
|
-
* Macrotask primitive for the host pump, picked per runtime: a round trip costs
|
|
5
|
-
* 757ns on Bun via MessageChannel but 4662ns on Deno, where `setImmediate` is
|
|
6
|
-
* 1110ns. Bun keeps the channel; browser and Andromeda have no choice.
|
|
7
|
-
*
|
|
8
|
-
* `serial-channel` opts back into the channel explicitly (see `src/api.ts`): one
|
|
9
|
-
* hop drives every lane's check in turn and relies on the channel's delivery to
|
|
10
|
-
* do it, so a merely cheaper pump starves it -- it cost 25% throughput there.
|
|
11
|
-
*/
|
|
3
|
+
/** Runtime-specific macrotask primitive used by the host dispatcher. */
|
|
12
4
|
const IMMEDIATE_PUMP = RUNTIME === "deno" || RUNTIME === "node"
|
|
13
5
|
? SET_IMMEDIATE
|
|
14
6
|
: undefined;
|
|
15
|
-
|
|
16
|
-
* Free drain hops before `scheduleNotify` stops re-arming the pump for free.
|
|
17
|
-
*
|
|
18
|
-
* The window has to match what the dispatcher escalates *to*, so these are
|
|
19
|
-
* chosen together and `canUseDoorbell` picks between them. Polling escalates to
|
|
20
|
-
* the `setTimeout` ladder, whose finest rung is ~1.1ms on every runtime, so it
|
|
21
|
-
* wants a wide window; that sleep is also load-bearing, since it batches
|
|
22
|
-
* completions. A doorbell escalates to `Atomics.waitAsync` at about the price of
|
|
23
|
-
* one hop, so it wants a narrow one.
|
|
24
|
-
*
|
|
25
|
-
* Mixing them is the trap: a doorbell behind the wide window keeps every poll
|
|
26
|
-
* hop *and* adds the arm, and loses to plain polling at every thread count.
|
|
27
|
-
* Measurements behind both values: `docs/host-doorbell-proposal.md`.
|
|
28
|
-
*/
|
|
7
|
+
// Polling tolerates a wider free window; a doorbell should arm promptly.
|
|
29
8
|
const POLL_STALL_FREE_LOOPS = 128;
|
|
30
9
|
const DOORBELL_STALL_FREE_LOOPS = 1;
|
|
31
|
-
export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, crossProcess, }) => {
|
|
10
|
+
export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, }, queue: { completeFrame, hasPendingFrames, flushToWorker, txIdle, waitForCompletion, armCompletionNotifier, setCompletionWaiterArmed, }, channelHandler, dispatcherOptions, notifySignal, crossProcess, nativeCompletionDoorbell, processCompletionDoorbell, }) => {
|
|
32
11
|
const a_load = Atomics.load;
|
|
33
12
|
const a_store = Atomics.store;
|
|
34
13
|
const a_notify = Atomics.notify;
|
|
@@ -39,14 +18,14 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
39
18
|
a_notify(opView, 0, 1);
|
|
40
19
|
});
|
|
41
20
|
const notify = () => channelHandler.notify();
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
// doorbell would just sleep to the watchdog.
|
|
46
|
-
const canUseDoorbell = crossProcess !== true &&
|
|
47
|
-
(dispatcherOptions?.doorbell ?? true) &&
|
|
48
|
-
(RUNTIME === "bun" || RUNTIME === "node") &&
|
|
21
|
+
// Atomics waiters are process-local, so cross-process workers cannot use this
|
|
22
|
+
// doorbell.
|
|
23
|
+
const canUseAtomicDoorbell = (RUNTIME === "bun" || RUNTIME === "node") &&
|
|
49
24
|
typeof Atomics.waitAsync === "function";
|
|
25
|
+
const canUseDoorbell = (dispatcherOptions?.doorbell ?? true) &&
|
|
26
|
+
(processCompletionDoorbell === true ||
|
|
27
|
+
(crossProcess !== true &&
|
|
28
|
+
(canUseAtomicDoorbell || nativeCompletionDoorbell === true)));
|
|
50
29
|
let doorbellEnabled = canUseDoorbell;
|
|
51
30
|
let doorbellArmed = false;
|
|
52
31
|
let doorbellEpoch = 0 | 0;
|
|
@@ -80,24 +59,55 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
80
59
|
doorbellEpoch = token;
|
|
81
60
|
doorbellArmed = true;
|
|
82
61
|
let woke = false;
|
|
83
|
-
const wake = () => {
|
|
62
|
+
const wake = (direct = false) => {
|
|
84
63
|
if (!doorbellArmed || doorbellEpoch !== token || woke)
|
|
85
64
|
return;
|
|
86
65
|
woke = true;
|
|
87
66
|
doorbellArmed = false;
|
|
88
67
|
doorbellEpoch = (doorbellEpoch + 1) | 0;
|
|
89
68
|
setCompletionWaiterArmed(false);
|
|
69
|
+
if (direct) {
|
|
70
|
+
// A native arm that observes an already-published completion has no
|
|
71
|
+
// callback to wait for. Drain it now: routing this known result back
|
|
72
|
+
// through setImmediate recreates the very millisecond-sized hop that
|
|
73
|
+
// the native doorbell exists to remove.
|
|
74
|
+
check.isRunning = true;
|
|
75
|
+
check();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
90
78
|
// Keep the existing macrotask boundary. Calling check directly from a
|
|
91
79
|
// resolved waitAsync promise can chain microtasks under a hot workload
|
|
92
80
|
// and starve host I/O.
|
|
93
81
|
notify();
|
|
94
82
|
};
|
|
95
83
|
let supported = false;
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
if (nativeCompletionDoorbell === true ||
|
|
85
|
+
processCompletionDoorbell === true) {
|
|
86
|
+
// A native arm reports two different things and they must not be
|
|
87
|
+
// conflated: `false` means a completion was already published so the arm
|
|
88
|
+
// deliberately did not ring, which is a normal hot-path outcome. Only a
|
|
89
|
+
// throw means the transport is unusable. Treating the race as
|
|
90
|
+
// "unsupported" disabled the doorbell permanently on its second arm.
|
|
91
|
+
let armed = false;
|
|
92
|
+
let usable = true;
|
|
93
|
+
try {
|
|
94
|
+
armed = armCompletionNotifier();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
usable = false;
|
|
98
|
+
}
|
|
99
|
+
supported = usable;
|
|
100
|
+
// The arm itself drains the completion it just observed.
|
|
101
|
+
if (usable && !armed)
|
|
102
|
+
wake(true);
|
|
98
103
|
}
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
else {
|
|
105
|
+
try {
|
|
106
|
+
supported = waitForCompletion(wake, DOORBELL_WATCHDOG_MS);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
supported = false;
|
|
110
|
+
}
|
|
101
111
|
}
|
|
102
112
|
// Some runtimes reject waitAsync on their SharedArrayBuffer variants; fall
|
|
103
113
|
// back to polling rather than hanging the pool.
|
|
@@ -111,7 +121,9 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
111
121
|
doorbellArmed = false;
|
|
112
122
|
doorbellEpoch = (doorbellEpoch + 1) | 0;
|
|
113
123
|
setCompletionWaiterArmed(false);
|
|
114
|
-
|
|
124
|
+
if (nativeCompletionDoorbell !== true &&
|
|
125
|
+
processCompletionDoorbell !== true)
|
|
126
|
+
notify();
|
|
115
127
|
}
|
|
116
128
|
};
|
|
117
129
|
const check = () => {
|
|
@@ -124,9 +136,7 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
124
136
|
// A waitAsync cannot be cancelled, so bumping its epoch makes the eventual
|
|
125
137
|
// callback inert instead.
|
|
126
138
|
cancelDoorbell();
|
|
127
|
-
//
|
|
128
|
-
// load, and an Atomics.notify that wakes a parked worker for nothing — the
|
|
129
|
-
// waste that makes serial-channel latency grow with thread count.
|
|
139
|
+
// Avoid waking an idle lane.
|
|
130
140
|
if (txIdle()) {
|
|
131
141
|
check.isRunning = false;
|
|
132
142
|
return;
|
|
@@ -139,15 +149,13 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
139
149
|
do {
|
|
140
150
|
check.rerun = false;
|
|
141
151
|
txStatus[0] = 1;
|
|
142
|
-
// Wake
|
|
152
|
+
// Wake a parked worker whenever its receive status is clear. Pending
|
|
153
|
+
// frames are not a reliable gate because the queue may have room.
|
|
143
154
|
if (a_load(rxStatus, 0) === 0) {
|
|
144
155
|
a_store(opView, 0, 1);
|
|
145
156
|
wakeSignal();
|
|
146
157
|
}
|
|
147
|
-
//
|
|
148
|
-
// counts as progress for `stallCount`: the pump exists to notice work a
|
|
149
|
-
// worker cannot announce, and letting a flush reset the counter would
|
|
150
|
-
// leave the escalation unreachable for a pool that never runs dry.
|
|
158
|
+
// Only completed frames count as progress for backoff purposes.
|
|
151
159
|
let completed = false;
|
|
152
160
|
let progressed = true;
|
|
153
161
|
while (progressed) {
|
|
@@ -164,7 +172,9 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
164
172
|
}
|
|
165
173
|
txStatus[0] = 0;
|
|
166
174
|
if (!txIdle()) {
|
|
167
|
-
|
|
175
|
+
// Queued frames indicate back-pressure, not progress; only a completion
|
|
176
|
+
// frees a request slot.
|
|
177
|
+
if (completed) {
|
|
168
178
|
stallCount = 0 | 0;
|
|
169
179
|
}
|
|
170
180
|
else {
|
|
@@ -182,6 +192,15 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
182
192
|
};
|
|
183
193
|
check.isRunning = false;
|
|
184
194
|
check.rerun = false;
|
|
195
|
+
/** Enter the drain from a native or IPC completion event. */
|
|
196
|
+
const wakeCompletion = () => {
|
|
197
|
+
if (inFlight || check.isRunning) {
|
|
198
|
+
check.rerun = true;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
check.isRunning = true;
|
|
202
|
+
check();
|
|
203
|
+
};
|
|
185
204
|
const scheduleNotify = () => {
|
|
186
205
|
if (stallCount <= stallFreeLoops) {
|
|
187
206
|
notify();
|
|
@@ -213,7 +232,7 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
213
232
|
}
|
|
214
233
|
}, delay);
|
|
215
234
|
};
|
|
216
|
-
return { check };
|
|
235
|
+
return { check, wakeCompletion };
|
|
217
236
|
};
|
|
218
237
|
export class ChannelHandler {
|
|
219
238
|
// Set only when the pump is a MessageChannel. The `setImmediate` pump has no
|
|
@@ -223,14 +242,16 @@ export class ChannelHandler {
|
|
|
223
242
|
port2;
|
|
224
243
|
#handler;
|
|
225
244
|
#notify;
|
|
245
|
+
#pending = false;
|
|
246
|
+
#run = () => {
|
|
247
|
+
// Clear before draining so a busy lane can request the next turn.
|
|
248
|
+
this.#pending = false;
|
|
249
|
+
this.#handler?.();
|
|
250
|
+
};
|
|
226
251
|
constructor(pump = "auto") {
|
|
227
252
|
if (pump === "auto" && IMMEDIATE_PUMP !== undefined) {
|
|
228
253
|
const immediate = IMMEDIATE_PUMP;
|
|
229
|
-
|
|
230
|
-
// Routing via `#handler` also makes a callback outliving `close()` inert.
|
|
231
|
-
const run = () => {
|
|
232
|
-
this.#handler?.();
|
|
233
|
-
};
|
|
254
|
+
const run = this.#run;
|
|
234
255
|
this.#notify = () => {
|
|
235
256
|
immediate(run);
|
|
236
257
|
};
|
|
@@ -246,23 +267,24 @@ export class ChannelHandler {
|
|
|
246
267
|
};
|
|
247
268
|
}
|
|
248
269
|
notify() {
|
|
270
|
+
// Coalesce notifications into one pending drain.
|
|
271
|
+
if (this.#pending)
|
|
272
|
+
return;
|
|
273
|
+
this.#pending = true;
|
|
249
274
|
this.#notify();
|
|
250
275
|
}
|
|
251
|
-
/**
|
|
252
|
-
* Registers the handler the pump calls back into. On the channel pump this is
|
|
253
|
-
* also where the ports are opened, so `notify` can reach port 1.
|
|
254
|
-
*/
|
|
276
|
+
/** Register the pump handler and start channel ports when present. */
|
|
255
277
|
open(f) {
|
|
256
278
|
this.#handler = f;
|
|
257
279
|
const port1 = this.port1;
|
|
258
280
|
if (port1 === undefined)
|
|
259
281
|
return;
|
|
260
282
|
if (typeof port1.on === "function") {
|
|
261
|
-
port1.on("message",
|
|
283
|
+
port1.on("message", this.#run);
|
|
262
284
|
}
|
|
263
285
|
else {
|
|
264
286
|
// @ts-ignore
|
|
265
|
-
port1.onmessage =
|
|
287
|
+
port1.onmessage = this.#run;
|
|
266
288
|
}
|
|
267
289
|
this.port1?.start?.();
|
|
268
290
|
this.port2?.start?.();
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getSharedReturnAllocator } from "../memory/payloadCodec.js";
|
|
2
|
+
/** Create an argument allocator with a plain-allocation fallback. */
|
|
3
|
+
export const createHostArgAllocator = (payload) => {
|
|
4
|
+
const allocate = payload === undefined
|
|
5
|
+
? undefined
|
|
6
|
+
: getSharedReturnAllocator(payload);
|
|
7
|
+
if (allocate === undefined) {
|
|
8
|
+
return (byteLength) => new Uint8Array(byteLength);
|
|
9
|
+
}
|
|
10
|
+
return (byteLength) => {
|
|
11
|
+
if (!Number.isInteger(byteLength) || byteLength < 0) {
|
|
12
|
+
throw new RangeError("sharedArgBytes(byteLength) requires a non-negative integer");
|
|
13
|
+
}
|
|
14
|
+
return allocate(byteLength, false) ?? new Uint8Array(byteLength);
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type NodeCompletionDoorbell = {
|
|
2
|
+
/** Process-local handle a Node thread worker may ring through the addon. */
|
|
3
|
+
pointer: bigint;
|
|
4
|
+
/** Invalidates the native handle after every worker that holds it has exited. */
|
|
5
|
+
close: () => void;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Bridge a Node worker thread into the host's libuv I/O phase. The addon owns
|
|
9
|
+
* the `uv_async_t`; this wrapper owns the JS callback and makes missing native
|
|
10
|
+
* prebuilds a normal capability fallback.
|
|
11
|
+
*/
|
|
12
|
+
export declare const createNodeCompletionDoorbell: (notify: () => void) => NodeCompletionDoorbell | undefined;
|
|
13
|
+
/** Rebuild the process-local native ring in a Node thread worker. */
|
|
14
|
+
export declare const createNodeCompletionNotifier: (pointer: bigint | undefined) => (() => void) | undefined;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { RUNTIME } from "../common/runtime.js";
|
|
2
|
+
import { getNodeBuiltinModule } from "../common/node-compat.js";
|
|
3
|
+
import { loadNodeNativeAddon } from "../connections/node-addons.js";
|
|
4
|
+
const loadDoorbellAddon = () => {
|
|
5
|
+
if (RUNTIME !== "node")
|
|
6
|
+
return undefined;
|
|
7
|
+
const nodeModule = getNodeBuiltinModule("node:module");
|
|
8
|
+
if (nodeModule === undefined)
|
|
9
|
+
return undefined;
|
|
10
|
+
try {
|
|
11
|
+
return loadNodeNativeAddon(nodeModule.createRequire(import.meta.url), "knitting_doorbell");
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Native wakeups are an optional fast path. A missing prebuild must retain
|
|
15
|
+
// the portable Atomics.waitAsync dispatcher rather than preventing plain
|
|
16
|
+
// thread workers from starting.
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Bridge a Node worker thread into the host's libuv I/O phase. The addon owns
|
|
22
|
+
* the `uv_async_t`; this wrapper owns the JS callback and makes missing native
|
|
23
|
+
* prebuilds a normal capability fallback.
|
|
24
|
+
*/
|
|
25
|
+
export const createNodeCompletionDoorbell = (notify) => {
|
|
26
|
+
const addon = loadDoorbellAddon();
|
|
27
|
+
if (addon === undefined)
|
|
28
|
+
return undefined;
|
|
29
|
+
let pointer;
|
|
30
|
+
let closed = false;
|
|
31
|
+
try {
|
|
32
|
+
pointer = addon.createCompletionDoorbell(() => {
|
|
33
|
+
if (closed)
|
|
34
|
+
return;
|
|
35
|
+
try {
|
|
36
|
+
notify();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// An exception must not escape a libuv callback. A scheduled host
|
|
40
|
+
// drain or the dispatcher watchdog still provides a liveness backstop.
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
if (typeof pointer !== "bigint" || pointer === 0n)
|
|
44
|
+
return undefined;
|
|
45
|
+
// A dormant pool must not be kept alive solely by an idle uv_async_t.
|
|
46
|
+
addon.unrefCompletionDoorbell(pointer);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
pointer,
|
|
53
|
+
close: () => {
|
|
54
|
+
if (closed)
|
|
55
|
+
return;
|
|
56
|
+
closed = true;
|
|
57
|
+
try {
|
|
58
|
+
addon.closeCompletionDoorbell(pointer);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Shutdown can race Node environment teardown; the addon's environment
|
|
62
|
+
// cleanup hook owns the remaining lifetime in that case.
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/** Rebuild the process-local native ring in a Node thread worker. */
|
|
68
|
+
export const createNodeCompletionNotifier = (pointer) => {
|
|
69
|
+
if (RUNTIME !== "node" || pointer === undefined || pointer === 0n) {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
const addon = loadDoorbellAddon();
|
|
73
|
+
if (addon === undefined)
|
|
74
|
+
return undefined;
|
|
75
|
+
return () => {
|
|
76
|
+
try {
|
|
77
|
+
addon.ringCompletionDoorbell(pointer);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// The host can close while a forced worker teardown is in flight. Its
|
|
81
|
+
// return-lock watchdog covers that final in-flight publication.
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
};
|
package/src/runtime/pool.d.ts
CHANGED
|
@@ -2,28 +2,25 @@ import { createHostTxQueue } from "./tx-queue.js";
|
|
|
2
2
|
import { type ProcessSharedMemoryBacking, type ProcessStealMemoryLayout, type ResolvedProcessSharedMemorySettings } from "./process-worker.js";
|
|
3
3
|
import { type Sab } from "../ipc/transport/shared-memory.js";
|
|
4
4
|
import { ChannelHandler, type DispatcherCheck } from "./dispatcher.js";
|
|
5
|
-
import {
|
|
5
|
+
import type { DenoCompletionDoorbell } from "./deno-doorbell.js";
|
|
6
|
+
import { lock2, type StealClaimDiscipline, type Task } from "../memory/lock.js";
|
|
6
7
|
import "../memory/payloadCodec.js";
|
|
7
8
|
import type { DebugOptions, DispatcherSettings, LockBuffers, WorkerContext, WorkerData, WorkerSettings } from "../types.js";
|
|
8
9
|
import "../worker/loop.js";
|
|
9
10
|
import { type PayloadBufferOptions } from "../memory/payload-config.js";
|
|
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
|
-
*/
|
|
11
|
+
/** Build the shared submit and private return buffers for a stealing pool. */
|
|
20
12
|
export declare const resolveStealRegionLanes: (consumers: number) => number;
|
|
13
|
+
/** Return the widest region allowed by the selected claim discipline. */
|
|
14
|
+
export declare const resolveMaxStealRegionLanes: (consumers: number, stealClaim?: StealClaimDiscipline) => number;
|
|
21
15
|
/** Maximum claimants that leave the protocol's required spare region. */
|
|
22
16
|
export declare const MAX_STEAL_CONSUMERS: number;
|
|
23
|
-
export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, abortSignalCapacity, usesAbortSignal, processWorker, }: {
|
|
17
|
+
export declare const createStealPoolBuffers: ({ threads, payload, sharedArgs, regionLanes, stealClaim, abortSignalCapacity, usesAbortSignal, processWorker, }: {
|
|
24
18
|
threads: number;
|
|
25
19
|
payload?: PayloadBufferOptions;
|
|
20
|
+
/** Hand large arguments to workers as borrowed regions. See `unsafe.SharedArgs`. */
|
|
21
|
+
sharedArgs?: boolean;
|
|
26
22
|
regionLanes?: number;
|
|
23
|
+
stealClaim?: StealClaimDiscipline;
|
|
27
24
|
abortSignalCapacity?: number;
|
|
28
25
|
usesAbortSignal?: boolean;
|
|
29
26
|
processWorker?: {
|
|
@@ -53,6 +50,7 @@ export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, a
|
|
|
53
50
|
activeRejectPlaceholder?: Task["reject"];
|
|
54
51
|
}) => () => number;
|
|
55
52
|
waitForHostChange: (timeoutMs?: number) => import("../memory/lock.js").WaitAsyncResult | undefined;
|
|
53
|
+
armHostNotifier: () => boolean;
|
|
56
54
|
setHostWaiterArmed: (armed: boolean) => void;
|
|
57
55
|
hasPendingFrames: () => boolean;
|
|
58
56
|
getPendingFrameCount: () => number;
|
|
@@ -82,6 +80,7 @@ export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, a
|
|
|
82
80
|
activeRejectPlaceholder?: Task["reject"];
|
|
83
81
|
}) => () => number;
|
|
84
82
|
waitForHostChange: (timeoutMs?: number) => import("../memory/lock.js").WaitAsyncResult | undefined;
|
|
83
|
+
armHostNotifier: () => boolean;
|
|
85
84
|
setHostWaiterArmed: (armed: boolean) => void;
|
|
86
85
|
hasPendingFrames: () => boolean;
|
|
87
86
|
getPendingFrameCount: () => number;
|
|
@@ -97,19 +96,20 @@ export declare const createStealPoolBuffers: ({ threads, payload, regionLanes, a
|
|
|
97
96
|
txIdle: () => boolean;
|
|
98
97
|
completeFrame: () => number;
|
|
99
98
|
waitForCompletion: (onWake: () => void, timeoutMs?: number) => boolean;
|
|
99
|
+
armCompletionNotifier: () => boolean;
|
|
100
100
|
setCompletionWaiterArmed: (armed: boolean) => void;
|
|
101
|
-
setReturnHooks: (lane: number, hooks: (import("../connections/buffer-reference.js").BufferReferenceReturnHooks | ((token: bigint) => void)) | undefined) => void;
|
|
102
101
|
enqueue: (functionID: number, timeout?: import("../types.js").TaskTimeout, abortSignal?: import("../types.js").AbortSignalOption) => (rawArgs: unknown) => Promise<never> | import("../common/with-resolvers.js").PromiseWithMaybeReject<unknown>;
|
|
103
102
|
flushToWorker: () => boolean;
|
|
104
103
|
enqueueKnown: (task: Task) => boolean;
|
|
105
104
|
settlePromisePayload: (task: Task, isRejected: boolean, value: unknown) => boolean;
|
|
106
105
|
};
|
|
107
106
|
regionLanes: number;
|
|
107
|
+
stealClaim: StealClaimDiscipline;
|
|
108
108
|
processMemory: ProcessStealMemoryLayout;
|
|
109
109
|
abortSignalSAB: SharedArrayBuffer | import("../types.js").SharedBufferRegion;
|
|
110
110
|
abortSignalMax: number;
|
|
111
111
|
};
|
|
112
|
-
export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, permission, host, payload,
|
|
112
|
+
export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug, hostDebug, totalNumberOfThread, workerCount, source, at, workerOptions, workerExecArgv, permission, host, payload, sharedBytesEnabled, abortSignalCapacity, usesAbortSignal, sharedChannelHandler, denoCompletionDoorbell, stealPool, }: {
|
|
113
113
|
list: string[];
|
|
114
114
|
ids: number[];
|
|
115
115
|
names: string[];
|
|
@@ -131,7 +131,7 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
|
|
|
131
131
|
permission?: WorkerData["permission"];
|
|
132
132
|
host?: DispatcherSettings;
|
|
133
133
|
payload?: PayloadBufferOptions;
|
|
134
|
-
|
|
134
|
+
sharedBytesEnabled?: boolean;
|
|
135
135
|
abortSignalCapacity?: number;
|
|
136
136
|
usesAbortSignal?: boolean;
|
|
137
137
|
/**
|
|
@@ -139,6 +139,8 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
|
|
|
139
139
|
* macro channel that runs all lane checks.
|
|
140
140
|
*/
|
|
141
141
|
sharedChannelHandler?: ChannelHandler;
|
|
142
|
+
/** Deno-only native callback shared by every thread worker in this pool. */
|
|
143
|
+
denoCompletionDoorbell?: DenoCompletionDoorbell;
|
|
142
144
|
/**
|
|
143
145
|
* Work-stealing wiring. When present the submit region, both host-side locks
|
|
144
146
|
* and the pending registry are owned by `createPool` and shared: every worker
|
|
@@ -155,6 +157,7 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
|
|
|
155
157
|
consumers: number;
|
|
156
158
|
consumerId: number;
|
|
157
159
|
regionLanes: number;
|
|
160
|
+
stealClaim?: StealClaimDiscipline;
|
|
158
161
|
abortSignalSAB?: LockBuffers["headers"];
|
|
159
162
|
abortSignalMax?: number;
|
|
160
163
|
processMemory?: ProcessStealMemoryLayout;
|
|
@@ -169,5 +172,8 @@ export declare const spawnWorkerContext: ({ list, ids, names, sab, thread, debug
|
|
|
169
172
|
dispatcherCheck?: DispatcherCheck;
|
|
170
173
|
laneWake?: () => void;
|
|
171
174
|
bindSend?: (fn: () => void) => void;
|
|
175
|
+
bindCompletionWake?: (fn: () => void) => void;
|
|
176
|
+
processCompletionDoorbell?: boolean;
|
|
177
|
+
nodeCompletionDoorbell?: boolean;
|
|
172
178
|
});
|
|
173
179
|
export type CreateContext = WorkerContext;
|