knitting 0.1.63 → 0.1.73
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 +623 -342
- package/knitting.browser.d.ts +3 -1
- package/knitting.browser.js +1 -1
- package/knitting.d.ts +3 -1
- package/knitting.js +2 -1
- package/map.md +0 -6
- package/package.json +10 -5
- 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 +158 -71
- 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/node-addons.js +11 -1
- package/src/connections/shared-array-buffer-payload.d.ts +7 -0
- package/src/connections/shared-array-buffer-payload.js +27 -11
- package/src/debug/gate.js +1 -1
- package/src/debug/handle.d.ts +6 -1
- package/src/debug/handle.js +14 -6
- package/src/error.d.ts +9 -0
- package/src/error.js +16 -2
- 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 +80 -15
- package/src/memory/lock.js +473 -139
- package/src/memory/payloadCodec.d.ts +18 -2
- package/src/memory/payloadCodec.js +340 -76
- 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/permission/protocol.d.ts +1 -0
- package/src/permission/protocol.js +8 -3
- package/src/runtime/deno-doorbell.d.ts +26 -0
- package/src/runtime/deno-doorbell.js +117 -0
- package/src/runtime/dispatcher.d.ts +13 -6
- package/src/runtime/dispatcher.js +101 -63
- package/src/runtime/host-arg-arena.d.ts +3 -0
- package/src/runtime/host-arg-arena.js +16 -0
- package/src/runtime/inline-executor.js +2 -1
- package/src/runtime/node-doorbell.d.ts +14 -0
- package/src/runtime/node-doorbell.js +84 -0
- package/src/runtime/pool.d.ts +30 -15
- package/src/runtime/pool.js +199 -151
- package/src/runtime/process-worker.d.ts +9 -0
- package/src/runtime/process-worker.js +32 -3
- package/src/runtime/tx-queue.d.ts +4 -6
- package/src/runtime/tx-queue.js +63 -48
- package/src/runtime/worker-common.d.ts +7 -0
- package/src/runtime/worker-common.js +28 -2
- package/src/types.d.ts +66 -78
- package/src/worker/loop.js +95 -60
- package/src/worker/rx-queue.d.ts +2 -3
- package/src/worker/rx-queue.js +34 -40
- package/src/worker/safety/index.d.ts +1 -1
- package/src/worker/safety/index.js +1 -1
- package/src/worker/safety/process.d.ts +2 -0
- package/src/worker/safety/process.js +8 -1
- package/src/worker/safety/startup.js +11 -6
- 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 +39 -22
- 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,30 @@ 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, wakeAfterPass, crossProcess, nativeCompletionDoorbell, processCompletionDoorbell, }: {
|
|
6
6
|
queue: MultiQueue;
|
|
7
7
|
signalBox: MainSignal;
|
|
8
8
|
channelHandler: ChannelHandler;
|
|
9
9
|
dispatcherOptions?: DispatcherSettings;
|
|
10
10
|
notifySignal?: () => void;
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Runs at the end of every drain pass. Defaults to waking this lane's worker
|
|
13
|
+
* if it is parked; a shared queue supplies its own policy.
|
|
14
|
+
*/
|
|
15
|
+
wakeAfterPass?: () => void;
|
|
16
|
+
/** Workers live in other processes; disables only the atomic doorbell. */
|
|
12
17
|
crossProcess?: boolean;
|
|
18
|
+
/** A runtime-native completion ring that wakes the host event loop. */
|
|
19
|
+
nativeCompletionDoorbell?: boolean;
|
|
20
|
+
/** A process IPC completion doorbell that wakes the host event loop. */
|
|
21
|
+
processCompletionDoorbell?: boolean;
|
|
13
22
|
}) => {
|
|
14
23
|
check: {
|
|
15
24
|
(): void;
|
|
16
25
|
isRunning: boolean;
|
|
17
26
|
rerun: boolean;
|
|
18
27
|
};
|
|
28
|
+
wakeCompletion: () => void;
|
|
19
29
|
};
|
|
20
30
|
type CheckWithState = (() => void) & {
|
|
21
31
|
isRunning: boolean;
|
|
@@ -30,10 +40,7 @@ export declare class ChannelHandler {
|
|
|
30
40
|
port2: RuntimeMessagePortLike | undefined;
|
|
31
41
|
constructor(pump?: ChannelHandlerPump);
|
|
32
42
|
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
|
-
*/
|
|
43
|
+
/** Register the pump handler and start channel ports when present. */
|
|
37
44
|
open(f: () => void): void;
|
|
38
45
|
/**
|
|
39
46
|
* Detaches the handler, and closes the channel if this pump has one.
|
|
@@ -1,36 +1,15 @@
|
|
|
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, wakeAfterPass, crossProcess, nativeCompletionDoorbell, processCompletionDoorbell, }) => {
|
|
11
|
+
const a_add = Atomics.add;
|
|
32
12
|
const a_load = Atomics.load;
|
|
33
|
-
const a_store = Atomics.store;
|
|
34
13
|
const a_notify = Atomics.notify;
|
|
35
14
|
const canNotifySignal = opView.buffer instanceof SharedArrayBuffer;
|
|
36
15
|
const wakeSignal = notifySignal ??
|
|
@@ -38,15 +17,25 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
38
17
|
if (canNotifySignal)
|
|
39
18
|
a_notify(opView, 0, 1);
|
|
40
19
|
});
|
|
20
|
+
// Bump rather than store a constant: the worker parks on the value it last
|
|
21
|
+
// read, so only a changed word makes a ring that lands before its wait count.
|
|
22
|
+
const wakeLane = () => {
|
|
23
|
+
a_add(opView, 0, 1);
|
|
24
|
+
wakeSignal();
|
|
25
|
+
};
|
|
26
|
+
const wakeIfParked = wakeAfterPass ?? (() => {
|
|
27
|
+
if (a_load(rxStatus, 0) === 0)
|
|
28
|
+
wakeLane();
|
|
29
|
+
});
|
|
41
30
|
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") &&
|
|
31
|
+
// Atomics waiters are process-local, so cross-process workers cannot use this
|
|
32
|
+
// doorbell.
|
|
33
|
+
const canUseAtomicDoorbell = (RUNTIME === "bun" || RUNTIME === "node") &&
|
|
49
34
|
typeof Atomics.waitAsync === "function";
|
|
35
|
+
const canUseDoorbell = (dispatcherOptions?.doorbell ?? true) &&
|
|
36
|
+
(processCompletionDoorbell === true ||
|
|
37
|
+
(crossProcess !== true &&
|
|
38
|
+
(canUseAtomicDoorbell || nativeCompletionDoorbell === true)));
|
|
50
39
|
let doorbellEnabled = canUseDoorbell;
|
|
51
40
|
let doorbellArmed = false;
|
|
52
41
|
let doorbellEpoch = 0 | 0;
|
|
@@ -80,24 +69,55 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
80
69
|
doorbellEpoch = token;
|
|
81
70
|
doorbellArmed = true;
|
|
82
71
|
let woke = false;
|
|
83
|
-
const wake = () => {
|
|
72
|
+
const wake = (direct = false) => {
|
|
84
73
|
if (!doorbellArmed || doorbellEpoch !== token || woke)
|
|
85
74
|
return;
|
|
86
75
|
woke = true;
|
|
87
76
|
doorbellArmed = false;
|
|
88
77
|
doorbellEpoch = (doorbellEpoch + 1) | 0;
|
|
89
78
|
setCompletionWaiterArmed(false);
|
|
79
|
+
if (direct) {
|
|
80
|
+
// A native arm that observes an already-published completion has no
|
|
81
|
+
// callback to wait for. Drain it now: routing this known result back
|
|
82
|
+
// through setImmediate recreates the very millisecond-sized hop that
|
|
83
|
+
// the native doorbell exists to remove.
|
|
84
|
+
check.isRunning = true;
|
|
85
|
+
check();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
90
88
|
// Keep the existing macrotask boundary. Calling check directly from a
|
|
91
89
|
// resolved waitAsync promise can chain microtasks under a hot workload
|
|
92
90
|
// and starve host I/O.
|
|
93
91
|
notify();
|
|
94
92
|
};
|
|
95
93
|
let supported = false;
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
if (nativeCompletionDoorbell === true ||
|
|
95
|
+
processCompletionDoorbell === true) {
|
|
96
|
+
// A native arm reports two different things and they must not be
|
|
97
|
+
// conflated: `false` means a completion was already published so the arm
|
|
98
|
+
// deliberately did not ring, which is a normal hot-path outcome. Only a
|
|
99
|
+
// throw means the transport is unusable. Treating the race as
|
|
100
|
+
// "unsupported" disabled the doorbell permanently on its second arm.
|
|
101
|
+
let armed = false;
|
|
102
|
+
let usable = true;
|
|
103
|
+
try {
|
|
104
|
+
armed = armCompletionNotifier();
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
usable = false;
|
|
108
|
+
}
|
|
109
|
+
supported = usable;
|
|
110
|
+
// The arm itself drains the completion it just observed.
|
|
111
|
+
if (usable && !armed)
|
|
112
|
+
wake(true);
|
|
98
113
|
}
|
|
99
|
-
|
|
100
|
-
|
|
114
|
+
else {
|
|
115
|
+
try {
|
|
116
|
+
supported = waitForCompletion(wake, DOORBELL_WATCHDOG_MS);
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
supported = false;
|
|
120
|
+
}
|
|
101
121
|
}
|
|
102
122
|
// Some runtimes reject waitAsync on their SharedArrayBuffer variants; fall
|
|
103
123
|
// back to polling rather than hanging the pool.
|
|
@@ -111,7 +131,9 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
111
131
|
doorbellArmed = false;
|
|
112
132
|
doorbellEpoch = (doorbellEpoch + 1) | 0;
|
|
113
133
|
setCompletionWaiterArmed(false);
|
|
114
|
-
|
|
134
|
+
if (nativeCompletionDoorbell !== true &&
|
|
135
|
+
processCompletionDoorbell !== true)
|
|
136
|
+
notify();
|
|
115
137
|
}
|
|
116
138
|
};
|
|
117
139
|
const check = () => {
|
|
@@ -124,9 +146,7 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
124
146
|
// A waitAsync cannot be cancelled, so bumping its epoch makes the eventual
|
|
125
147
|
// callback inert instead.
|
|
126
148
|
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.
|
|
149
|
+
// Avoid waking an idle lane.
|
|
130
150
|
if (txIdle()) {
|
|
131
151
|
check.isRunning = false;
|
|
132
152
|
return;
|
|
@@ -139,15 +159,12 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
139
159
|
do {
|
|
140
160
|
check.rerun = false;
|
|
141
161
|
txStatus[0] = 1;
|
|
142
|
-
// Wake
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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.
|
|
162
|
+
// Wake a parked worker whenever its receive status is clear, so it is
|
|
163
|
+
// up by the time the frames land. Pending frames are not a reliable gate
|
|
164
|
+
// because the queue may have room.
|
|
165
|
+
if (a_load(rxStatus, 0) === 0)
|
|
166
|
+
wakeLane();
|
|
167
|
+
// Only completed frames count as progress for backoff purposes.
|
|
151
168
|
let completed = false;
|
|
152
169
|
let progressed = true;
|
|
153
170
|
while (progressed) {
|
|
@@ -162,9 +179,18 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
162
179
|
progressed = true;
|
|
163
180
|
}
|
|
164
181
|
}
|
|
182
|
+
// The ring above was decided before the frames flushed here existed, and
|
|
183
|
+
// calls publish straight from enqueue without passing through this loop.
|
|
184
|
+
// A worker that parked in between saw neither the frames nor a ring and
|
|
185
|
+
// slept out its whole parkMs. Publishing ends in a seq-cst store and the
|
|
186
|
+
// worker re-checks after clearing rxStatus, so re-reading rxStatus after
|
|
187
|
+
// the pass means either the worker sees the frames or this sees it parked.
|
|
188
|
+
wakeIfParked();
|
|
165
189
|
txStatus[0] = 0;
|
|
166
190
|
if (!txIdle()) {
|
|
167
|
-
|
|
191
|
+
// Queued frames indicate back-pressure, not progress; only a completion
|
|
192
|
+
// frees a request slot.
|
|
193
|
+
if (completed) {
|
|
168
194
|
stallCount = 0 | 0;
|
|
169
195
|
}
|
|
170
196
|
else {
|
|
@@ -182,6 +208,15 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
182
208
|
};
|
|
183
209
|
check.isRunning = false;
|
|
184
210
|
check.rerun = false;
|
|
211
|
+
/** Enter the drain from a native or IPC completion event. */
|
|
212
|
+
const wakeCompletion = () => {
|
|
213
|
+
if (inFlight || check.isRunning) {
|
|
214
|
+
check.rerun = true;
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
check.isRunning = true;
|
|
218
|
+
check();
|
|
219
|
+
};
|
|
185
220
|
const scheduleNotify = () => {
|
|
186
221
|
if (stallCount <= stallFreeLoops) {
|
|
187
222
|
notify();
|
|
@@ -213,7 +248,7 @@ export const hostDispatcherLoop = ({ signalBox: { opView, txStatus, rxStatus, },
|
|
|
213
248
|
}
|
|
214
249
|
}, delay);
|
|
215
250
|
};
|
|
216
|
-
return { check };
|
|
251
|
+
return { check, wakeCompletion };
|
|
217
252
|
};
|
|
218
253
|
export class ChannelHandler {
|
|
219
254
|
// Set only when the pump is a MessageChannel. The `setImmediate` pump has no
|
|
@@ -223,14 +258,16 @@ export class ChannelHandler {
|
|
|
223
258
|
port2;
|
|
224
259
|
#handler;
|
|
225
260
|
#notify;
|
|
261
|
+
#pending = false;
|
|
262
|
+
#run = () => {
|
|
263
|
+
// Clear before draining so a busy lane can request the next turn.
|
|
264
|
+
this.#pending = false;
|
|
265
|
+
this.#handler?.();
|
|
266
|
+
};
|
|
226
267
|
constructor(pump = "auto") {
|
|
227
268
|
if (pump === "auto" && IMMEDIATE_PUMP !== undefined) {
|
|
228
269
|
const immediate = IMMEDIATE_PUMP;
|
|
229
|
-
|
|
230
|
-
// Routing via `#handler` also makes a callback outliving `close()` inert.
|
|
231
|
-
const run = () => {
|
|
232
|
-
this.#handler?.();
|
|
233
|
-
};
|
|
270
|
+
const run = this.#run;
|
|
234
271
|
this.#notify = () => {
|
|
235
272
|
immediate(run);
|
|
236
273
|
};
|
|
@@ -246,23 +283,24 @@ export class ChannelHandler {
|
|
|
246
283
|
};
|
|
247
284
|
}
|
|
248
285
|
notify() {
|
|
286
|
+
// Coalesce notifications into one pending drain.
|
|
287
|
+
if (this.#pending)
|
|
288
|
+
return;
|
|
289
|
+
this.#pending = true;
|
|
249
290
|
this.#notify();
|
|
250
291
|
}
|
|
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
|
-
*/
|
|
292
|
+
/** Register the pump handler and start channel ports when present. */
|
|
255
293
|
open(f) {
|
|
256
294
|
this.#handler = f;
|
|
257
295
|
const port1 = this.port1;
|
|
258
296
|
if (port1 === undefined)
|
|
259
297
|
return;
|
|
260
298
|
if (typeof port1.on === "function") {
|
|
261
|
-
port1.on("message",
|
|
299
|
+
port1.on("message", this.#run);
|
|
262
300
|
}
|
|
263
301
|
else {
|
|
264
302
|
// @ts-ignore
|
|
265
|
-
port1.onmessage =
|
|
303
|
+
port1.onmessage = this.#run;
|
|
266
304
|
}
|
|
267
305
|
this.port1?.start?.();
|
|
268
306
|
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
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { withResolvers } from "../common/with-resolvers.js";
|
|
2
|
+
import { KnittingError } from "../error.js";
|
|
2
3
|
import RingQueue from "../ipc/tools/ring-queue.js";
|
|
3
4
|
import { createRuntimeMessageChannel } from "../common/worker-runtime.js";
|
|
4
5
|
// const objects, not `enum`s: Andromeda's Nova engine can't parse `enum`. Same
|
|
@@ -270,7 +271,7 @@ export const createInlineExecutor = ({ tasks, genTaskID, batchSize, }) => {
|
|
|
270
271
|
if (stateByIndex[index] !== SlotStateMacro.Pending)
|
|
271
272
|
continue;
|
|
272
273
|
try {
|
|
273
|
-
deferredByIndex[index]?.reject("Thread closed");
|
|
274
|
+
deferredByIndex[index]?.reject(new KnittingError("THREAD_CLOSED", "Thread closed"));
|
|
274
275
|
}
|
|
275
276
|
catch {
|
|
276
277
|
}
|
|
@@ -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
|
+
};
|