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
package/src/worker/loop.js
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createRuntimeMessageChannel, PROCESS_COMPLETION_DOORBELL, RUNTIME_IS_MAIN_THREAD, RUNTIME_IS_PROCESS_WORKER, RUNTIME_PARENT_PORT, RUNTIME_WORKER_DATA, } from "../common/worker-runtime.js";
|
|
2
2
|
import { isSharedBufferSource } from "../common/shared-buffer-region.js";
|
|
3
3
|
import { isLockBufferTextCompat } from "../common/shared-buffer-text.js";
|
|
4
|
+
import { createDenoCompletionNotifier } from "../runtime/deno-doorbell.js";
|
|
5
|
+
import { createNodeCompletionNotifier } from "../runtime/node-doorbell.js";
|
|
4
6
|
import { createWorkerRxQueue } from "./rx-queue.js";
|
|
7
|
+
import { installSharedReturn } from "./shared-return.js";
|
|
5
8
|
import { createSharedMemoryTransport, WORKER_STOP, } from "../ipc/transport/shared-memory.js";
|
|
6
9
|
import { lock2 } from "../memory/lock.js";
|
|
7
|
-
//
|
|
8
|
-
// see lock.ts). Must run before any lock2() call.
|
|
10
|
+
// Registers the payload codec before any lock is built.
|
|
9
11
|
import "../memory/payloadCodec.js";
|
|
10
12
|
import { getFunctions } from "./task-loader.js";
|
|
11
|
-
import { pauseGeneric, sleepUntilChanged, whilePausing } from "./timers.js";
|
|
13
|
+
import { IS_BUN_WINDOWS, pauseGeneric, sleepUntilChanged, whilePausing, } from "./timers.js";
|
|
12
14
|
import { IS_ANDROMEDA, RUNTIME, SET_IMMEDIATE } from "../common/runtime.js";
|
|
13
15
|
import { getNodeProcess } from "../common/node-compat.js";
|
|
14
|
-
import { assertWorkerImportsResolved, assertWorkerSharedMemoryBootData, installPerformanceNowGuard, installTerminationGuard, installUnhandledRejectionSilencer, scrubWorkerDataSensitiveBuffers, } from "./safety/index.js";
|
|
16
|
+
import { assertWorkerImportsResolved, assertWorkerSharedMemoryBootData, installPerformanceNowGuard, installTerminationGuard, installUnhandledRejectionSilencer, reportSilencedRejections, scrubWorkerDataSensitiveBuffers, } from "./safety/index.js";
|
|
15
17
|
import { signalAbortFactory } from "../shared/abortSignal.js";
|
|
16
18
|
import { runWorkerBootstrap } from "./bootstrap.js";
|
|
17
19
|
import { getProcessWorkerNativeWaitU32, installProcessWorkerBootstrap, } from "./process-worker-bootstrap.js";
|
|
18
|
-
import { readBufferReferenceReturnReleaseMessage, } from "../connections/buffer-reference.js";
|
|
19
20
|
import { resolveDebugNamespaces } from "../debug/gate.js";
|
|
20
21
|
const WORKER_FATAL_MESSAGE_KEY = "__knittingWorkerFatal";
|
|
22
|
+
// Park timeout while finished results wait for return slots (see the loop).
|
|
23
|
+
const BLOCKED_RETURN_PARK_MS = 1;
|
|
21
24
|
const reportWorkerStartupFatal = (error) => {
|
|
22
25
|
const message = String(error?.message ?? error);
|
|
23
26
|
const payload = {
|
|
@@ -53,38 +56,28 @@ const reportWorkerStartupFatal = (error) => {
|
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
};
|
|
56
|
-
const installBufferReferenceReleaseListener = (releaseReturnedBufferReference) => {
|
|
57
|
-
const handleMessage = (message) => {
|
|
58
|
-
const token = readBufferReferenceReturnReleaseMessage(message);
|
|
59
|
-
if (token !== undefined)
|
|
60
|
-
releaseReturnedBufferReference(token);
|
|
61
|
-
};
|
|
62
|
-
if (RUNTIME_PARENT_PORT !== undefined) {
|
|
63
|
-
addRuntimeDataListener(RUNTIME_PARENT_PORT, handleMessage);
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
const scope = globalThis;
|
|
67
|
-
scope.addEventListener?.("message", (event) => {
|
|
68
|
-
handleMessage(event?.data);
|
|
69
|
-
});
|
|
70
|
-
};
|
|
71
59
|
export const workerMainLoop = async (startupData) => {
|
|
72
60
|
// Startup-only safety layer: no per-iteration checks in the hot loop.
|
|
73
61
|
installTerminationGuard();
|
|
74
62
|
installUnhandledRejectionSilencer();
|
|
75
63
|
installPerformanceNowGuard();
|
|
76
|
-
const { debug, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig,
|
|
64
|
+
const { debug, debugEpoch, sab, thread, startAt, workerOptions, lock, returnLock, abortSignalSAB, abortSignalMax, payloadConfig, sharedReturn, permission, notifyOnHostPublish, processCompletionDoorbell, denoCompletionDoorbell, nodeCompletionDoorbell, totalNumberOfThread, list, ids, names, at, steal, } = startupData;
|
|
77
65
|
scrubWorkerDataSensitiveBuffers(startupData);
|
|
78
66
|
assertWorkerSharedMemoryBootData({ sab, lock, returnLock });
|
|
79
67
|
const debugNamespaces = resolveDebugNamespaces(debug);
|
|
80
68
|
const dbg = debugNamespaces.size > 0
|
|
81
|
-
? await import("../debug/handle.
|
|
69
|
+
? await import("../debug/handle.js").then((module) => module.initDebug({
|
|
82
70
|
name: `w${thread}`,
|
|
83
71
|
runtime: RUNTIME,
|
|
84
72
|
namespaces: debugNamespaces,
|
|
73
|
+
epoch: debugEpoch,
|
|
85
74
|
}))
|
|
86
75
|
: undefined;
|
|
87
|
-
|
|
76
|
+
if (dbg?.enabled("lifecycle") === true) {
|
|
77
|
+
// Detached task promises reject silently otherwise (see the silencer).
|
|
78
|
+
reportSilencedRejections((reason) => dbg.log("lifecycle", `unhandled rejection silenced: ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}`));
|
|
79
|
+
}
|
|
80
|
+
// Andromeda cannot parse enums.
|
|
88
81
|
const Comment = {
|
|
89
82
|
thisIsAHint: 0,
|
|
90
83
|
};
|
|
@@ -110,7 +103,28 @@ export const workerMainLoop = async (startupData) => {
|
|
|
110
103
|
consumers: steal?.consumers,
|
|
111
104
|
consumerId: steal?.consumerId,
|
|
112
105
|
regionLanes: steal?.regionLanes,
|
|
106
|
+
stealClaim: steal?.claim,
|
|
107
|
+
// Undefined unless `steal` is traced, so the claim path stays untouched.
|
|
108
|
+
traceClaim: steal !== undefined && dbg?.enabled("steal") === true
|
|
109
|
+
? (message) => dbg.log("steal", message)
|
|
110
|
+
: undefined,
|
|
113
111
|
});
|
|
112
|
+
const notifyDenoHost = createDenoCompletionNotifier(denoCompletionDoorbell);
|
|
113
|
+
const notifyNodeHost = createNodeCompletionNotifier(nodeCompletionDoorbell);
|
|
114
|
+
const processParentPort = processCompletionDoorbell === true
|
|
115
|
+
? RUNTIME_PARENT_PORT
|
|
116
|
+
: undefined;
|
|
117
|
+
// Bun on Windows flushes process messages only after an event-loop turn.
|
|
118
|
+
let processRingPending = false;
|
|
119
|
+
const flushProcessRing = IS_BUN_WINDOWS;
|
|
120
|
+
const notifyProcessHost = processParentPort === undefined
|
|
121
|
+
? undefined
|
|
122
|
+
: flushProcessRing
|
|
123
|
+
? () => {
|
|
124
|
+
processParentPort.postMessage(PROCESS_COMPLETION_DOORBELL);
|
|
125
|
+
processRingPending = true;
|
|
126
|
+
}
|
|
127
|
+
: () => processParentPort.postMessage(PROCESS_COMPLETION_DOORBELL);
|
|
114
128
|
const returnLockState = lock2({
|
|
115
129
|
headers: returnLock.headers,
|
|
116
130
|
headerSlotStrideU32: returnLock.headerSlotStrideU32,
|
|
@@ -120,9 +134,22 @@ export const workerMainLoop = async (startupData) => {
|
|
|
120
134
|
payloadConfig,
|
|
121
135
|
textCompat: returnLock.textCompat,
|
|
122
136
|
processBoundary: RUNTIME_IS_PROCESS_WORKER,
|
|
137
|
+
sharedReturn: sharedReturn === true,
|
|
138
|
+
// A thread return moves its source before publication. Node transfers the
|
|
139
|
+
// backing-store ownership; Deno and Bun make one owned host copy before
|
|
140
|
+
// the worker drops its pin. Both avoid the borrowed-return lifetime.
|
|
141
|
+
moveReturn: !RUNTIME_IS_PROCESS_WORKER,
|
|
123
142
|
// The host parks on this lock's publication word when it has no work to
|
|
124
143
|
// flush. Request locks are host-produced and must not wake that waiter.
|
|
125
|
-
notifyOnHostPublish
|
|
144
|
+
notifyOnHostPublish: notifyOnHostPublish ||
|
|
145
|
+
notifyDenoHost !== undefined ||
|
|
146
|
+
notifyNodeHost !== undefined ||
|
|
147
|
+
notifyProcessHost !== undefined,
|
|
148
|
+
notifyHostPublish: notifyDenoHost !== undefined
|
|
149
|
+
? () => notifyDenoHost(thread)
|
|
150
|
+
: notifyNodeHost !== undefined
|
|
151
|
+
? notifyNodeHost
|
|
152
|
+
: notifyProcessHost,
|
|
126
153
|
});
|
|
127
154
|
const timers = workerOptions?.timers;
|
|
128
155
|
const spinMicroseconds = timers?.spinMicroseconds ??
|
|
@@ -163,16 +190,16 @@ export const workerMainLoop = async (startupData) => {
|
|
|
163
190
|
maxSignals: abortSignalMax,
|
|
164
191
|
})
|
|
165
192
|
: undefined;
|
|
166
|
-
const { enqueueLock, serviceBatchImmediate, hasCompleted, writeBatch,
|
|
193
|
+
const { enqueueLock, serviceBatchImmediate, hasCompleted, writeBatch, getAwaiting, drainReturnReleases, releaseDeferredReturns, } = createWorkerRxQueue({
|
|
167
194
|
listOfFunctions,
|
|
168
195
|
workerOptions,
|
|
169
196
|
lock: lockState,
|
|
170
197
|
returnLock: returnLockState,
|
|
171
|
-
borrowReturnedBufferReferences: bufferReferenceReturn === "borrow",
|
|
172
198
|
hasAborted: abortSignals?.hasAborted,
|
|
173
199
|
stealing: steal !== undefined,
|
|
174
200
|
});
|
|
175
|
-
|
|
201
|
+
if (sharedReturn === true)
|
|
202
|
+
installSharedReturn(returnLock.payload);
|
|
176
203
|
a_store(rxStatus, 0, 1);
|
|
177
204
|
const WRITE_MAX = 64;
|
|
178
205
|
const pauseUntil = sleepUntilChanged({
|
|
@@ -224,9 +251,9 @@ export const workerMainLoop = async (startupData) => {
|
|
|
224
251
|
};
|
|
225
252
|
const traceSignals = dbg?.enabled("signals") === true;
|
|
226
253
|
const _hasCompleted = hasCompleted;
|
|
227
|
-
const _hasPending = hasPending;
|
|
228
254
|
const _getAwaiting = getAwaiting;
|
|
229
255
|
const _drainReturnReleases = drainReturnReleases;
|
|
256
|
+
const _releaseDeferredReturns = releaseDeferredReturns;
|
|
230
257
|
const _pauseSpin = pauseSpin;
|
|
231
258
|
const _enqueueLock = traceSignals
|
|
232
259
|
? () => {
|
|
@@ -259,9 +286,24 @@ export const workerMainLoop = async (startupData) => {
|
|
|
259
286
|
pauseUntil(value, spinMicroseconds, parkMs);
|
|
260
287
|
}
|
|
261
288
|
: pauseUntil;
|
|
262
|
-
|
|
289
|
+
// Stealing flushes completed work before claiming more; private lanes claim
|
|
290
|
+
// first to keep their single-consumer path short.
|
|
291
|
+
const pump = steal !== undefined
|
|
292
|
+
? () => {
|
|
293
|
+
let progressed = false;
|
|
294
|
+
if (_hasCompleted() && _writeBatch(WRITE_MAX) > 0)
|
|
295
|
+
progressed = true;
|
|
296
|
+
return _enqueueLock() || progressed;
|
|
297
|
+
}
|
|
298
|
+
: () => {
|
|
299
|
+
const claimed = _enqueueLock();
|
|
300
|
+
return (_hasCompleted() && _writeBatch(WRITE_MAX) > 0) || claimed;
|
|
301
|
+
};
|
|
263
302
|
/** Leave the dispatch loop and acknowledge shutdown. */
|
|
264
303
|
const stopLoop = () => {
|
|
304
|
+
// A shutdown rejects unconsumed calls, so no return can still be adopted.
|
|
305
|
+
// Do not let a worker-isolate exit strand native BufferReference pins.
|
|
306
|
+
_releaseDeferredReturns();
|
|
265
307
|
a_store(stopView, 0, WORKER_STOP.acknowledged);
|
|
266
308
|
a_store(rxStatus, 0, 0);
|
|
267
309
|
try {
|
|
@@ -272,41 +314,20 @@ export const workerMainLoop = async (startupData) => {
|
|
|
272
314
|
};
|
|
273
315
|
const loop = () => {
|
|
274
316
|
isInMacro = false;
|
|
317
|
+
// Recompute progress on every pass so idle workers can park.
|
|
275
318
|
let progressed = true;
|
|
276
319
|
let awaiting = 0;
|
|
277
320
|
while (true) {
|
|
278
321
|
if (stopView[0] !== WORKER_STOP.running)
|
|
279
322
|
return stopLoop();
|
|
280
|
-
|
|
281
|
-
// Stealing only. Flush finished work before taking more on: a computed
|
|
282
|
-
// response should not wait behind a claim, and that claim can block on
|
|
283
|
-
// a peer withdrawing its intent. Measured to hurt the per-lane path,
|
|
284
|
-
// where a claim is a cheap private decode and picking work up promptly
|
|
285
|
-
// matters more, so the classic order is kept below.
|
|
286
|
-
// Reordering must not make `progressed` sticky: it answers "did this
|
|
287
|
-
// iteration move anything", so it has to start false every pass or the
|
|
288
|
-
// park below is unreachable and the worker spins a core forever.
|
|
289
|
-
progressed = false;
|
|
290
|
-
if (_hasCompleted()) {
|
|
291
|
-
if (_writeBatch(WRITE_MAX) > 0)
|
|
292
|
-
progressed = true;
|
|
293
|
-
}
|
|
294
|
-
progressed = _enqueueLock() || progressed;
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
progressed = _enqueueLock();
|
|
298
|
-
if (_hasCompleted()) {
|
|
299
|
-
if (_writeBatch(WRITE_MAX) > 0)
|
|
300
|
-
progressed = true;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
323
|
+
progressed = pump();
|
|
303
324
|
_drainReturnReleases();
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
progressed = true;
|
|
307
|
-
}
|
|
325
|
+
if (_serviceBatchImmediate() > 0)
|
|
326
|
+
progressed = true;
|
|
308
327
|
if ((awaiting = _getAwaiting()) > 0) {
|
|
309
|
-
|
|
328
|
+
// Promise callbacks need the loop to yield; delay only when no progress
|
|
329
|
+
// was made, otherwise queued work pays an unnecessary timer hop.
|
|
330
|
+
if (progressed || awaiting !== lastAwaiting)
|
|
310
331
|
awaitingSpins = 0;
|
|
311
332
|
lastAwaiting = awaiting;
|
|
312
333
|
awaitingSpins++;
|
|
@@ -315,12 +336,26 @@ export const workerMainLoop = async (startupData) => {
|
|
|
315
336
|
return;
|
|
316
337
|
}
|
|
317
338
|
awaitingSpins = lastAwaiting = 0;
|
|
339
|
+
if (processRingPending) {
|
|
340
|
+
processRingPending = false;
|
|
341
|
+
scheduleTimer(0);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
318
344
|
if (!progressed) {
|
|
319
345
|
if (txStatus[Comment.thisIsAHint] === 1) {
|
|
320
|
-
|
|
346
|
+
// During a hint spin only host-driven state can change.
|
|
347
|
+
do {
|
|
348
|
+
_pauseSpin();
|
|
349
|
+
if (stopView[0] !== WORKER_STOP.running)
|
|
350
|
+
return stopLoop();
|
|
351
|
+
if (pump())
|
|
352
|
+
break;
|
|
353
|
+
} while (txStatus[Comment.thisIsAHint] === 1);
|
|
321
354
|
continue;
|
|
322
355
|
}
|
|
323
|
-
|
|
356
|
+
// Results that could not be written wait on the host freeing return
|
|
357
|
+
// slots, which rings no worker in particular: keep that park short.
|
|
358
|
+
_pauseUntil(wakeToken, spinMicroseconds, _hasCompleted() ? Math.min(parkMs, BLOCKED_RETURN_PARK_MS) : parkMs);
|
|
324
359
|
wakeToken = a_load(opView, 0);
|
|
325
360
|
}
|
|
326
361
|
}
|
package/src/worker/rx-queue.d.ts
CHANGED
|
@@ -6,7 +6,6 @@ type ArgumentsForCreateWorkerQueue = {
|
|
|
6
6
|
workerOptions?: WorkerSettings;
|
|
7
7
|
lock: Lock2;
|
|
8
8
|
returnLock: Lock2;
|
|
9
|
-
borrowReturnedBufferReferences?: boolean;
|
|
10
9
|
hasAborted?: (signal: number) => boolean;
|
|
11
10
|
now?: () => number;
|
|
12
11
|
/**
|
|
@@ -21,14 +20,14 @@ type ArgumentsForCreateWorkerQueue = {
|
|
|
21
20
|
stealing?: boolean;
|
|
22
21
|
};
|
|
23
22
|
export type CreateWorkerRxQueue = ReturnType<typeof createWorkerRxQueue>;
|
|
24
|
-
export declare const createWorkerRxQueue: ({ listOfFunctions, workerOptions, lock, returnLock,
|
|
23
|
+
export declare const createWorkerRxQueue: ({ listOfFunctions, workerOptions, lock, returnLock, hasAborted, now, stealing, }: ArgumentsForCreateWorkerQueue) => {
|
|
25
24
|
hasCompleted: () => boolean;
|
|
26
25
|
hasPending: () => boolean;
|
|
27
26
|
writeBatch: (max: number) => number;
|
|
28
27
|
serviceBatchImmediate: () => number;
|
|
29
28
|
enqueueLock: () => boolean;
|
|
30
29
|
drainReturnReleases: () => void;
|
|
31
|
-
|
|
30
|
+
releaseDeferredReturns: () => void;
|
|
32
31
|
hasAwaiting: () => boolean;
|
|
33
32
|
getAwaiting: () => number;
|
|
34
33
|
};
|
package/src/worker/rx-queue.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import RingQueue from "../ipc/tools/ring-queue.js";
|
|
2
2
|
import { attachPayloadTransportFinalizer, runTaskFinalizers, TaskFlag, TaskIndex, } from "../memory/lock.js";
|
|
3
3
|
import { composeWorkerRunner } from "./composable-runners.js";
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/** Tasks run per `serviceBatchImmediate` call before returning to the loop. */
|
|
5
|
+
const SERVICE_BATCH_MAX = 32;
|
|
6
|
+
export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, returnLock, hasAborted, now, stealing, }) => {
|
|
6
7
|
const PLACE_HOLDER = (_) => {
|
|
7
8
|
throw ("UNREACHABLE FROM PLACE HOLDER (thread)");
|
|
8
9
|
};
|
|
9
|
-
let hasAnythingFinished = 0;
|
|
10
10
|
let awaiting = 0;
|
|
11
11
|
const jobs = listOfFunctions.reduce((acc, fixed) => (acc.push(fixed.run), acc), []);
|
|
12
12
|
const toWork = new RingQueue();
|
|
@@ -26,12 +26,7 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
|
|
|
26
26
|
const returnHostBits = returnLock.hostBits;
|
|
27
27
|
const returnWorkerBits = returnLock.workerBits;
|
|
28
28
|
const deferredReleases = [];
|
|
29
|
-
const
|
|
30
|
-
const drainReturnReleases = () => {
|
|
31
|
-
if (deferredReleases.length === 0)
|
|
32
|
-
return;
|
|
33
|
-
if ((a_load(returnHostBits, 0) ^ a_load(returnWorkerBits, 0)) !== 0)
|
|
34
|
-
return;
|
|
29
|
+
const releaseDeferredReturns = () => {
|
|
35
30
|
for (let i = 0; i < deferredReleases.length; i++) {
|
|
36
31
|
try {
|
|
37
32
|
deferredReleases[i]();
|
|
@@ -42,18 +37,17 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
|
|
|
42
37
|
}
|
|
43
38
|
deferredReleases.length = 0;
|
|
44
39
|
};
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
const release = explicitReturnReleases.get(key);
|
|
48
|
-
if (release === undefined)
|
|
40
|
+
const drainReturnReleases = () => {
|
|
41
|
+
if (deferredReleases.length === 0)
|
|
49
42
|
return;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
43
|
+
// `returnHostBits` is the publication word of the return lock, and only
|
|
44
|
+
// this thread ever writes it (encode() toggles hostBits, the host toggles
|
|
45
|
+
// workerBits). Reading our own word plainly skips an `Atomics.load`, which
|
|
46
|
+
// is a non-inlined call an order of magnitude dearer than a plain read;
|
|
47
|
+
// the peer's word still needs the atomic.
|
|
48
|
+
if ((returnHostBits[0] ^ a_load(returnWorkerBits, 0)) !== 0)
|
|
49
|
+
return;
|
|
50
|
+
releaseDeferredReturns();
|
|
57
51
|
};
|
|
58
52
|
const runByIndex = listOfFunctions.reduce((acc, fixed, idx) => {
|
|
59
53
|
const job = jobs[idx];
|
|
@@ -65,9 +59,13 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
|
|
|
65
59
|
}));
|
|
66
60
|
return acc;
|
|
67
61
|
}, []);
|
|
62
|
+
// A settled task is either written into the return lock straight away or
|
|
63
|
+
// parked in `pendingFrames`, so the queue's own size already answers "is
|
|
64
|
+
// there anything left to flush" — the separate counter this used to keep was
|
|
65
|
+
// an increment and a decrement per task tracking a value it could read.
|
|
68
66
|
const hasCompleted = workerOptions?.resolveAfterFinishingAll === true
|
|
69
|
-
? () =>
|
|
70
|
-
: () =>
|
|
67
|
+
? () => pendingFrames.size !== 0 && toWork.size === 0
|
|
68
|
+
: () => pendingFrames.size !== 0;
|
|
71
69
|
const { decode, resolved } = lock;
|
|
72
70
|
const resolvedShift = () => resolved.shiftNoClear();
|
|
73
71
|
const enqueueLock = () => {
|
|
@@ -86,34 +84,22 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
|
|
|
86
84
|
}
|
|
87
85
|
return true;
|
|
88
86
|
};
|
|
89
|
-
const
|
|
90
|
-
if (!returnLock.encode(slot))
|
|
91
|
-
return false;
|
|
92
|
-
return true;
|
|
93
|
-
};
|
|
87
|
+
const returnEncode = returnLock.encode;
|
|
94
88
|
const sendReturn = (slot, shouldReject) => {
|
|
95
89
|
slot[IDX_FLAGS] = shouldReject ? FLAG_REJECT : 0;
|
|
96
|
-
if (!
|
|
90
|
+
if (!returnEncode(slot))
|
|
97
91
|
return false;
|
|
98
92
|
// Preserve return finalizers past slot recycle; release after drain.
|
|
99
93
|
if (slot.finalize !== undefined) {
|
|
100
|
-
|
|
101
|
-
if (token === undefined || borrowReturnedBufferReferences !== true) {
|
|
102
|
-
deferredReleases.push(slot.finalize);
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
explicitReturnReleases.set(token.toString(), slot.finalize);
|
|
106
|
-
}
|
|
94
|
+
deferredReleases.push(slot.finalize);
|
|
107
95
|
slot.finalize = undefined;
|
|
108
96
|
}
|
|
109
|
-
hasAnythingFinished--;
|
|
110
97
|
recyclePush(slot);
|
|
111
98
|
return true;
|
|
112
99
|
};
|
|
113
100
|
const settleNow = (slot, isError, value, wasAwaited) => {
|
|
114
101
|
runTaskFinalizers(slot);
|
|
115
102
|
slot.value = value;
|
|
116
|
-
hasAnythingFinished++;
|
|
117
103
|
if (wasAwaited && awaiting > 0)
|
|
118
104
|
awaiting--;
|
|
119
105
|
const shouldReject = isError ||
|
|
@@ -145,14 +131,20 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
|
|
|
145
131
|
},
|
|
146
132
|
serviceBatchImmediate: () => {
|
|
147
133
|
let processed = 0;
|
|
148
|
-
|
|
134
|
+
// Every trip back out to the dispatch loop costs a claim attempt and the
|
|
135
|
+
// per-pass bookkeeping, so a low cap makes that scaffolding a per-task
|
|
136
|
+
// tax on cheap tasks. Run a longer run when the return lock is keeping
|
|
137
|
+
// up, and cut it short the moment a settle lands in `pendingFrames` --
|
|
138
|
+
// that means the return region is full and the right move is to go
|
|
139
|
+
// flush it rather than pile up more finished work.
|
|
140
|
+
while (processed < SERVICE_BATCH_MAX && toWork.size !== 0) {
|
|
149
141
|
const slot = toWorkShift();
|
|
150
142
|
try {
|
|
151
143
|
const fnIndex = slot[TaskIndex.FunctionID] & FUNCTION_ID_MASK;
|
|
152
144
|
const result = runByIndex[fnIndex](slot);
|
|
153
145
|
slot[IDX_FLAGS] = 0;
|
|
154
|
-
slot.value = null;
|
|
155
146
|
if (result instanceof Promise) {
|
|
147
|
+
slot.value = null;
|
|
156
148
|
awaiting++;
|
|
157
149
|
result.then((value) => settleNow(slot, false, value, true), (err) => settleNow(slot, true, err, true));
|
|
158
150
|
}
|
|
@@ -164,12 +156,14 @@ export const createWorkerRxQueue = ({ listOfFunctions, workerOptions, lock, retu
|
|
|
164
156
|
settleNow(slot, true, err, false);
|
|
165
157
|
}
|
|
166
158
|
++processed;
|
|
159
|
+
if (pendingFrames.size !== 0)
|
|
160
|
+
break;
|
|
167
161
|
}
|
|
168
162
|
return processed;
|
|
169
163
|
},
|
|
170
164
|
enqueueLock,
|
|
171
165
|
drainReturnReleases,
|
|
172
|
-
|
|
166
|
+
releaseDeferredReturns,
|
|
173
167
|
hasAwaiting: () => awaiting > 0,
|
|
174
168
|
getAwaiting: () => awaiting,
|
|
175
169
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { installTerminationGuard, installUnhandledRejectionSilencer, } from "./process.js";
|
|
1
|
+
export { installTerminationGuard, installUnhandledRejectionSilencer, reportSilencedRejections, } from "./process.js";
|
|
2
2
|
export { installPerformanceNowGuard } from "./performance.js";
|
|
3
3
|
export { scrubWorkerDataSensitiveBuffers } from "./worker-data.js";
|
|
4
4
|
export { assertWorkerSharedMemoryBootData, assertWorkerImportsResolved, } from "./startup.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { installTerminationGuard, installUnhandledRejectionSilencer, } from "./process.js";
|
|
1
|
+
export { installTerminationGuard, installUnhandledRejectionSilencer, reportSilencedRejections, } from "./process.js";
|
|
2
2
|
export { installPerformanceNowGuard } from "./performance.js";
|
|
3
3
|
export { scrubWorkerDataSensitiveBuffers } from "./worker-data.js";
|
|
4
4
|
export { assertWorkerSharedMemoryBootData, assertWorkerImportsResolved, } from "./startup.js";
|
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export declare const installTerminationGuard: () => void;
|
|
2
|
+
/** Debug hook: see what the silencer below swallows. Unset by default. */
|
|
3
|
+
export declare const reportSilencedRejections: (report: ((reason: unknown) => void) | undefined) => void;
|
|
2
4
|
export declare const installUnhandledRejectionSilencer: () => void;
|
|
@@ -66,6 +66,11 @@ export const installTerminationGuard = () => {
|
|
|
66
66
|
}
|
|
67
67
|
proc.__knittingTerminationGuard = true;
|
|
68
68
|
};
|
|
69
|
+
let reportSilencedRejection;
|
|
70
|
+
/** Debug hook: see what the silencer below swallows. Unset by default. */
|
|
71
|
+
export const reportSilencedRejections = (report) => {
|
|
72
|
+
reportSilencedRejection = report;
|
|
73
|
+
};
|
|
69
74
|
export const installUnhandledRejectionSilencer = () => {
|
|
70
75
|
const proc = getNodeProcess();
|
|
71
76
|
if (!proc || typeof proc.on !== "function") {
|
|
@@ -75,5 +80,7 @@ export const installUnhandledRejectionSilencer = () => {
|
|
|
75
80
|
return;
|
|
76
81
|
proc.__knittingUnhandledRejectionSilencer = true;
|
|
77
82
|
// Worker task code may create detached promises; keep workers alive.
|
|
78
|
-
proc.on("unhandledRejection", () => {
|
|
83
|
+
proc.on("unhandledRejection", (reason) => {
|
|
84
|
+
reportSilencedRejection?.(reason);
|
|
85
|
+
});
|
|
79
86
|
};
|
|
@@ -21,10 +21,15 @@ export const assertWorkerImportsResolved = ({ list, ids, names, listOfFunctions
|
|
|
21
21
|
if (listOfFunctions.length > 0 &&
|
|
22
22
|
(names === undefined || listOfFunctions.length === names.length))
|
|
23
23
|
return;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
const found = new Set(listOfFunctions.map((entry) => entry?.name));
|
|
25
|
+
const missing = (names ?? []).filter((name) => !found.has(name));
|
|
26
|
+
const tasks = missing.length > 0
|
|
27
|
+
? ` Missing task export(s): ${missing.join(", ")}.`
|
|
28
|
+
: "";
|
|
29
|
+
const modules = list.length > 0 ? ` Looked in: ${list.join(", ")}.` : "";
|
|
30
|
+
throw new Error(`No imports were found.${tasks}${modules}` +
|
|
31
|
+
" Workers re-import each listed module and look tasks up by export" +
|
|
32
|
+
" name: a bare function must be exported from the module that calls" +
|
|
33
|
+
" createPool, and a bundle that inlines task modules drops their" +
|
|
34
|
+
" exports.");
|
|
30
35
|
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Point `sharedBytes` at the arena of `payload`, this worker's return lane. */
|
|
2
|
+
export declare const installSharedReturn: (payload: object) => void;
|
|
3
|
+
export declare const resetSharedReturn: () => void;
|
|
4
|
+
/**
|
|
5
|
+
* Allocate an uninitialized borrowed return buffer. The caller must write every
|
|
6
|
+
* byte it returns and must not retain the view beyond the borrow window.
|
|
7
|
+
* Ordinary allocation is used when borrowing is unavailable.
|
|
8
|
+
*/
|
|
9
|
+
export declare const sharedBytes: (byteLength: number, zeroFill?: boolean) => Uint8Array;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getSharedReturnAllocator } from "../memory/payloadCodec.js";
|
|
2
|
+
// `sharedBytes` uses the worker's return-lane arena; outside that context it
|
|
3
|
+
// falls back to an ordinary Uint8Array.
|
|
4
|
+
let allocate;
|
|
5
|
+
/** Point `sharedBytes` at the arena of `payload`, this worker's return lane. */
|
|
6
|
+
export const installSharedReturn = (payload) => {
|
|
7
|
+
allocate = getSharedReturnAllocator(payload);
|
|
8
|
+
};
|
|
9
|
+
export const resetSharedReturn = () => {
|
|
10
|
+
allocate = undefined;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Allocate an uninitialized borrowed return buffer. The caller must write every
|
|
14
|
+
* byte it returns and must not retain the view beyond the borrow window.
|
|
15
|
+
* Ordinary allocation is used when borrowing is unavailable.
|
|
16
|
+
*/
|
|
17
|
+
export const sharedBytes = (byteLength, zeroFill = false) => {
|
|
18
|
+
if (!Number.isInteger(byteLength) || byteLength < 0) {
|
|
19
|
+
throw new RangeError("sharedBytes(byteLength) requires a non-negative integer");
|
|
20
|
+
}
|
|
21
|
+
return allocate?.(byteLength, zeroFill) ?? new Uint8Array(byteLength);
|
|
22
|
+
};
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { endpointSymbol } from "../common/task-symbol.js";
|
|
2
2
|
import { toImportSpecifier, toModuleUrl } from "../common/module-url.js";
|
|
3
|
-
//
|
|
4
|
-
// No longer inlined, but only read on cold paths.
|
|
3
|
+
// Use a const object; Andromeda's Nova engine cannot parse `enum`.
|
|
5
4
|
export const TimeoutKind = {
|
|
6
5
|
Reject: 0,
|
|
7
6
|
Resolve: 1,
|
package/src/worker/timers.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ type PauseOptions = {
|
|
|
2
2
|
pauseInNanoseconds?: number;
|
|
3
3
|
};
|
|
4
4
|
export type NativeWaitU32 = (buffer: ArrayBuffer | SharedArrayBuffer, byteOffset: number, expected: number, timeoutMs?: number) => unknown;
|
|
5
|
+
export declare const IS_BUN_WINDOWS: boolean;
|
|
5
6
|
export declare const whilePausing: ({ pauseInNanoseconds }: PauseOptions) => () => void;
|
|
6
7
|
export declare const pauseGeneric: () => void;
|
|
7
8
|
export declare const sleepUntilChanged: ({ at, opView, pauseInNanoseconds, rxStatus, txStatus, enqueueLock, write, nativeWaitU32, useSharedMemoryWait, flushBeforeClaim, }: {
|
|
@@ -14,12 +15,7 @@ export declare const sleepUntilChanged: ({ at, opView, pauseInNanoseconds, rxSta
|
|
|
14
15
|
write?: () => number | boolean;
|
|
15
16
|
nativeWaitU32?: NativeWaitU32;
|
|
16
17
|
useSharedMemoryWait?: boolean;
|
|
17
|
-
/**
|
|
18
|
-
* Stealing only: write results back before attempting to claim more work.
|
|
19
|
-
* A claim can spin waiting for a peer to withdraw its intent, so claiming
|
|
20
|
-
* first puts arbitration latency in front of a finished response. Measured
|
|
21
|
-
* to hurt the per-lane path, so it stays off by default.
|
|
22
|
-
*/
|
|
18
|
+
/** Stealing only: flush results before claiming more work. */
|
|
23
19
|
flushBeforeClaim?: boolean;
|
|
24
20
|
}) => (value: number, spinMicroseconds: number, parkMs?: number) => void;
|
|
25
21
|
export {};
|