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/knitting.js
CHANGED
|
@@ -2,5 +2,6 @@
|
|
|
2
2
|
import { workerMainLoop } from "./src/worker/loop.js";
|
|
3
3
|
import { checkCompiledWorker, createPool, importTask, isMain, setModuleUrl, task, } from "./src/api.js";
|
|
4
4
|
import { Envelope } from "./src/common/envelope.js";
|
|
5
|
+
import { KnittingError } from "./src/error.js";
|
|
5
6
|
import { isNumericArray, NumericArray, } from "./src/connections/numeric-array.js";
|
|
6
|
-
export { checkCompiledWorker as checkCompiledWorker, createPool as createPool, Envelope as Envelope, importTask as importTask, isMain as isMain, isNumericArray as isNumericArray, NumericArray as NumericArray, setModuleUrl as setModuleUrl, task as task, workerMainLoop as workerMainLoop, };
|
|
7
|
+
export { checkCompiledWorker as checkCompiledWorker, createPool as createPool, Envelope as Envelope, importTask as importTask, isMain as isMain, isNumericArray as isNumericArray, KnittingError as KnittingError, NumericArray as NumericArray, setModuleUrl as setModuleUrl, task as task, workerMainLoop as workerMainLoop, };
|
package/map.md
CHANGED
|
@@ -237,12 +237,8 @@ The core flow is:
|
|
|
237
237
|
|
|
238
238
|
- `bench/latency.ts`: Compares basic task-call latency against a direct worker
|
|
239
239
|
postMessage baseline.
|
|
240
|
-
- `bench/types.ts`: Compares payload-type behavior between knitting and worker
|
|
241
|
-
messaging.
|
|
242
240
|
- `bench/types_knitting.ts`: Knitting-only payload-type benchmark with broader
|
|
243
241
|
type coverage.
|
|
244
|
-
- `bench/ipc.ts`: Larger IPC comparison benchmark covering local calls,
|
|
245
|
-
postMessage, HTTP, WebSocket, and knitting.
|
|
246
242
|
- `bench/withload.ts`: Measures behavior under main-thread load.
|
|
247
243
|
- `bench/call-growth.ts`: Measures call cost as payload size grows.
|
|
248
244
|
- `bench/call-growth-batch.ts`: Batch-focused version of call-growth tests.
|
|
@@ -254,8 +250,6 @@ The core flow is:
|
|
|
254
250
|
- `bench/payload-sweep.ts`: Uint8Array payload-size sweep promoted from the old
|
|
255
251
|
scratch file. Supports table output and `--json`.
|
|
256
252
|
- `bench/postmessage/single.ts`: Worker-thread postMessage baseline helper.
|
|
257
|
-
- `bench/postmessage/test.go`: Go comparison/experiment for postMessage-style
|
|
258
|
-
IPC.
|
|
259
253
|
- `bench/util/json-parse.ts`: Mitata JSON output reducer used by benchmark
|
|
260
254
|
scripts.
|
|
261
255
|
- `bench/util/type-payloads.ts`: Shared payload cases and size estimation for
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "knitting",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.73",
|
|
4
4
|
"description": "Shared-memory IPC runtime for Node.js, Deno, and Bun.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -83,11 +83,12 @@
|
|
|
83
83
|
"release:npm": "npm run test:all && npm publish",
|
|
84
84
|
"release:npm:dry-run": "npm publish --dry-run",
|
|
85
85
|
"test": "npm run test:deno",
|
|
86
|
-
"test:deno": "deno test -A --ignore=test/runtime.node.test.ts --ignore=test/runtime.process.test.ts",
|
|
86
|
+
"test:deno": "deno test -A --ignore=test/runtime.node.test.ts --ignore=test/runtime.process.test.ts test",
|
|
87
87
|
"test:node": "node --no-warnings --experimental-transform-types --test \"./test/*.test.ts\"",
|
|
88
88
|
"test:node:26": "node --no-warnings --experimental-ffi test/node26-ffi.integration.mjs",
|
|
89
|
-
"test:bun": "bun test
|
|
89
|
+
"test:bun": "bun test",
|
|
90
90
|
"test:browser": "bun run scripts/test-browser.ts",
|
|
91
|
+
"check:windows-park": "bun run scripts/check-windows-park.ts",
|
|
91
92
|
"test:runtimes": "npm run test:node && npm run test:bun",
|
|
92
93
|
"test:all": "npm run test:deno && npm run test:runtimes && npm run test:browser",
|
|
93
94
|
"coverage:node": "node --no-warnings --experimental-transform-types --experimental-test-coverage --test \"./test/*.test.ts\"",
|
|
@@ -101,13 +102,17 @@
|
|
|
101
102
|
"bun": ">=1.0.0"
|
|
102
103
|
},
|
|
103
104
|
"peerDependencies": {
|
|
104
|
-
"typescript": "
|
|
105
|
+
"typescript": ">=5.2.0"
|
|
106
|
+
},
|
|
107
|
+
"peerDependenciesMeta": {
|
|
108
|
+
"typescript": {
|
|
109
|
+
"optional": true
|
|
110
|
+
}
|
|
105
111
|
},
|
|
106
112
|
"devDependencies": {
|
|
107
113
|
"@types/bun": "latest",
|
|
108
114
|
"@types/deno": "latest",
|
|
109
115
|
"@types/node": "latest",
|
|
110
|
-
"dts-bundle-generator": "^9.5.1",
|
|
111
116
|
"mitata": "^1.0.34",
|
|
112
117
|
"typescript": "^5.2.0"
|
|
113
118
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -237,6 +237,11 @@ const addons = [
|
|
|
237
237
|
source: "src/knitting_buffer_pointer.cc",
|
|
238
238
|
output: "build/Release/knitting_buffer_pointer.node",
|
|
239
239
|
},
|
|
240
|
+
{
|
|
241
|
+
name: "knitting_doorbell",
|
|
242
|
+
source: "src/knitting_doorbell.cc",
|
|
243
|
+
output: "build/Release/knitting_doorbell.node",
|
|
244
|
+
},
|
|
240
245
|
];
|
|
241
246
|
const ffiLibraries = isWindows
|
|
242
247
|
? [
|
package/shared-memory.d.ts
CHANGED
|
@@ -1 +1,4 @@
|
|
|
1
1
|
export { getDefaultProcessSharedBufferPrimitives, parseProcessSharedBufferMetadata, PROCESS_SHARED_BUFFER_BRAND, ProcessSharedBuffer, setDefaultProcessSharedBufferPrimitives, type ProcessSharedBufferCreator, type ProcessSharedBufferMapper, type ProcessSharedBufferMetadata, type ProcessSharedBufferPrimitives, type ProcessSharedBufferRange, type ProcessSharedBufferView, type ProcessSharedBufferViewConstructor, } from "./src/connections/process-shared-buffer.js";
|
|
2
|
+
export { attachKnittingAllocator, createKnittingAllocator, DEFAULT_ARENA_BYTE_LENGTH, detectRegion, type KnittingAllocator, type KnittingAllocatorOptions, type KnittingBufferDescriptor, KnittingSharedBuffer, } from "./src/memory/knitting-buffer.js";
|
|
3
|
+
export { createBodyReader, type KnittingBody, type KnittingBodyWire, type KnittingTransport, } from "./src/memory/knitting-body.js";
|
|
4
|
+
export { HTTP_BODY_STREAM_THRESHOLD_BYTES, HTTP_BODY_REFERENCE_THRESHOLD_BYTES, type ReadBodyIntoBytesOptions, type ReadBodyOptions, type ReadBodyOrReferOptions, type ReadBodyPayload, readBodyIntoBytes, readBodyIntoRegion, readBodyOrRefer, type RegionAllocator, } from "./src/memory/knitting-buffer-http.js";
|
package/shared-memory.js
CHANGED
|
@@ -1 +1,4 @@
|
|
|
1
1
|
export { getDefaultProcessSharedBufferPrimitives, parseProcessSharedBufferMetadata, PROCESS_SHARED_BUFFER_BRAND, ProcessSharedBuffer, setDefaultProcessSharedBufferPrimitives, } from "./src/connections/process-shared-buffer.js";
|
|
2
|
+
export { attachKnittingAllocator, createKnittingAllocator, DEFAULT_ARENA_BYTE_LENGTH, detectRegion, KnittingSharedBuffer, } from "./src/memory/knitting-buffer.js";
|
|
3
|
+
export { createBodyReader, } from "./src/memory/knitting-body.js";
|
|
4
|
+
export { HTTP_BODY_STREAM_THRESHOLD_BYTES, HTTP_BODY_REFERENCE_THRESHOLD_BYTES, readBodyIntoBytes, readBodyIntoRegion, readBodyOrRefer, } from "./src/memory/knitting-buffer-http.js";
|
package/src/api.js
CHANGED
|
@@ -4,22 +4,28 @@ import { genTaskID, stableTaskID } from "./common/task-source.js";
|
|
|
4
4
|
import { toModuleUrl } from "./common/module-url.js";
|
|
5
5
|
import { endpointSymbol } from "./common/task-symbol.js";
|
|
6
6
|
import { createStealPoolBuffers, MAX_STEAL_CONSUMERS, spawnWorkerContext, } from "./runtime/pool.js";
|
|
7
|
+
import { isNodePermissionExecFlag } from "./runtime/worker-common.js";
|
|
7
8
|
import { ChannelHandler, hostDispatcherLoop } from "./runtime/dispatcher.js";
|
|
9
|
+
import { createDenoCompletionDoorbell } from "./runtime/deno-doorbell.js";
|
|
8
10
|
import { createSharedArrayBuffer, RUNTIME } from "./common/runtime.js";
|
|
9
11
|
import { RUNTIME_IS_MAIN_THREAD, RUNTIME_POOL_DEPTH, RUNTIME_WORKER_DATA, } from "./common/worker-runtime.js";
|
|
10
12
|
import { classifyProcessPermissionCompatibility, enforceProcessPermissionCompatibility, resolvePermissionProtocol, toRuntimePermissionFlags, } from "./permission/index.js";
|
|
11
13
|
import { getNodeProcess } from "./common/node-compat.js";
|
|
12
14
|
import { readProcessSharedMemorySettings, readProcessWorkerNodeMajor, readProcessWorkerRuntime, } from "./runtime/process-worker.js";
|
|
15
|
+
import { assertStealClaim, DEFAULT_STEAL_CLAIM, } from "./memory/lock.js";
|
|
13
16
|
import { TRANSPORT_SIGNAL_BYTES } from "./ipc/transport/shared-memory.js";
|
|
14
17
|
import { inspectCompiledWorkerArtifact } from "./runtime/compiled-artifact.js";
|
|
15
18
|
import { managerMethod } from "./runtime/balancer.js";
|
|
16
19
|
import { createInlineExecutor } from "./runtime/inline-executor.js";
|
|
20
|
+
import { createHostArgAllocator } from "./runtime/host-arg-arena.js";
|
|
17
21
|
const hasDebugNamespace = (namespaces, namespace) => namespaces.has("*") || namespaces.has(namespace);
|
|
18
|
-
const createHostDebug = (namespaces) => {
|
|
22
|
+
const createHostDebug = (namespaces, epoch) => {
|
|
19
23
|
const enabled = (namespace) => hasDebugNamespace(namespaces, namespace);
|
|
20
24
|
if (!enabled("host"))
|
|
21
25
|
return undefined;
|
|
22
|
-
|
|
26
|
+
// Same rebase the workers do (see debug/handle.ts), so host and worker lines
|
|
27
|
+
// share one zero.
|
|
28
|
+
const base = epoch - performance.timeOrigin;
|
|
23
29
|
const tag = `host·${RUNTIME}`;
|
|
24
30
|
const log = (message) => {
|
|
25
31
|
const elapsed = (performance.now() - base).toFixed(1);
|
|
@@ -242,13 +248,20 @@ const toPoolTaskEntries = (input, callerHref) => Object.entries(input).map(([nam
|
|
|
242
248
|
* argument; use an object or tuple for multiple values.
|
|
243
249
|
*/
|
|
244
250
|
export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe, abortSignalCapacity, source, worker, workerExecArgv, permission, host, }) => (tasks) => {
|
|
245
|
-
const
|
|
251
|
+
const sharedBytesEnabled = unsafe?.SharedBytes === true;
|
|
252
|
+
const sharedArgsEnabled = unsafe?.SharedArgs === true;
|
|
246
253
|
const debugRequested = DEBUG_ENABLED ||
|
|
247
254
|
(debug !== undefined && debug !== false);
|
|
248
255
|
let debugNamespaces;
|
|
249
256
|
const getDebugNamespaces = () => debugNamespaces ??= resolveDebugNamespaces(debug);
|
|
250
|
-
|
|
251
|
-
|
|
257
|
+
// Absolute (Unix-epoch ms) zero for every debug clock in this pool. Raw
|
|
258
|
+
// `performance.now()` is relative to each thread/process's own time origin;
|
|
259
|
+
// `timeOrigin + now()` is not, so workers can rebase onto this value.
|
|
260
|
+
const debugEpoch = debugRequested
|
|
261
|
+
? performance.timeOrigin + performance.now()
|
|
262
|
+
: undefined;
|
|
263
|
+
const hostDebug = debugEpoch !== undefined
|
|
264
|
+
? createHostDebug(getDebugNamespaces(), debugEpoch)
|
|
252
265
|
: undefined;
|
|
253
266
|
const debugEnabled = (namespace) => debugRequested && hasDebugNamespace(getDebugNamespaces(), namespace);
|
|
254
267
|
/**
|
|
@@ -275,6 +288,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
275
288
|
return {
|
|
276
289
|
shutdown: mainThreadOnlyProxy,
|
|
277
290
|
[Symbol.dispose]: () => { },
|
|
291
|
+
[Symbol.asyncDispose]: async () => { },
|
|
278
292
|
call: mainThreadOnlyProxy,
|
|
279
293
|
};
|
|
280
294
|
}
|
|
@@ -303,20 +317,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
303
317
|
const permissionExecArgv = toRuntimePermissionFlags(permissionProtocol);
|
|
304
318
|
const nodeProcess = getNodeProcess();
|
|
305
319
|
const allowedFlags = nodeProcess?.allowedNodeEnvironmentFlags ?? null;
|
|
306
|
-
const
|
|
307
|
-
const key = flag.split("=", 1)[0];
|
|
308
|
-
return key === "--permission" ||
|
|
309
|
-
key === "--experimental-permission" ||
|
|
310
|
-
key === "--allow-fs-read" ||
|
|
311
|
-
key === "--allow-fs-write" ||
|
|
312
|
-
key === "--allow-worker" ||
|
|
313
|
-
key === "--allow-child-process" ||
|
|
314
|
-
key === "--allow-net" ||
|
|
315
|
-
key === "--allow-addons" ||
|
|
316
|
-
key === "--allow-ffi" ||
|
|
317
|
-
key === "--allow-wasi";
|
|
318
|
-
};
|
|
319
|
-
const stripNodePermissionFlags = (flags) => flags?.filter((flag) => !isNodePermissionFlag(flag));
|
|
320
|
+
const stripNodePermissionFlags = (flags) => flags?.filter((flag) => !isNodePermissionExecFlag(flag));
|
|
320
321
|
const dedupeFlags = (flags) => {
|
|
321
322
|
const out = [];
|
|
322
323
|
const seen = new Set();
|
|
@@ -343,16 +344,11 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
343
344
|
? nodeProcess.execArgv
|
|
344
345
|
: undefined;
|
|
345
346
|
const defaultExecArgvCandidate = workerExecArgv ??
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
: inheritedExecArgv)
|
|
352
|
-
: undefined);
|
|
353
|
-
const defaultExecArgv = permissionProtocol?.unsafe === true
|
|
354
|
-
? stripNodePermissionFlags(defaultExecArgvCandidate)
|
|
355
|
-
: defaultExecArgvCandidate;
|
|
347
|
+
inheritedExecArgv;
|
|
348
|
+
// Knitting resolves the worker's permission policy itself. Do not inherit
|
|
349
|
+
// the host's Node grants (or caller-supplied permission flags), which could
|
|
350
|
+
// broaden that policy when Node combines repeated --allow-* options.
|
|
351
|
+
const defaultExecArgv = stripNodePermissionFlags(defaultExecArgvCandidate);
|
|
356
352
|
const combinedExecArgv = dedupeFlags([
|
|
357
353
|
...permissionExecArgv,
|
|
358
354
|
...(defaultExecArgv ?? []),
|
|
@@ -364,6 +360,16 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
364
360
|
hostDebug?.log(`permission=${permissionProtocol?.mode ?? "off"} execArgv=${formatDebugList(execArgv)}`);
|
|
365
361
|
const usesAbortSignal = listOfFunctions.some((fn) => fn.abortSignal !== undefined);
|
|
366
362
|
const resolvedWorker = resolveWorkerSettings(worker, callerHref);
|
|
363
|
+
if (RUNTIME === "node" &&
|
|
364
|
+
resolvedWorker?.runtime !== "process" &&
|
|
365
|
+
resolvedWorker?.runtime !== "compiled") {
|
|
366
|
+
const missingPermissionFlags = permissionExecArgv.filter((flag) => isNodePermissionExecFlag(flag) && !execArgv?.includes(flag));
|
|
367
|
+
if (missingPermissionFlags.length > 0) {
|
|
368
|
+
throw new Error("Node cannot apply the resolved permission policy to this thread " +
|
|
369
|
+
`worker (unsupported flags: ${missingPermissionFlags.join(", ")}). ` +
|
|
370
|
+
"Refusing to start the worker without those permissions.");
|
|
371
|
+
}
|
|
372
|
+
}
|
|
367
373
|
const dispatcherEnv = nodeProcess?.env?.KNITTING_DISPATCHER;
|
|
368
374
|
const stealEnvRaw = nodeProcess?.env?.KNITTING_STEAL?.trim().toLowerCase();
|
|
369
375
|
const stealEnv = stealEnvRaw === "1" || stealEnvRaw === "true"
|
|
@@ -371,14 +377,24 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
371
377
|
: stealEnvRaw === "0" || stealEnvRaw === "false"
|
|
372
378
|
? false
|
|
373
379
|
: undefined;
|
|
374
|
-
// Shared-submit stealing is the default for compatible multi-worker pools
|
|
375
|
-
//
|
|
376
|
-
// transports retain their existing topology.
|
|
380
|
+
// Shared-submit stealing is the default for compatible multi-worker pools;
|
|
381
|
+
// explicit options take precedence over the environment.
|
|
377
382
|
const dispatcherExplicitlySelected = host?.dispatcher !== undefined ||
|
|
378
383
|
dispatcherEnv === "serial-channel" || dispatcherEnv === "per-thread";
|
|
379
384
|
const stealDefaultCompatible = balancer === undefined &&
|
|
380
385
|
!dispatcherExplicitlySelected && (threads ?? 1) <= MAX_STEAL_CONSUMERS;
|
|
381
386
|
const stealRequested = host?.steal ?? stealEnv ?? stealDefaultCompatible;
|
|
387
|
+
const stealClaimEnvRaw = nodeProcess?.env?.KNITTING_STEAL_CLAIM?.trim()
|
|
388
|
+
.toLowerCase();
|
|
389
|
+
// An unrecognised discipline is an error, not a fallback: `cas-mask` and
|
|
390
|
+
// typos used to select Dekker silently and run under the wrong name.
|
|
391
|
+
const stealClaimEnv = stealClaimEnvRaw === undefined || stealClaimEnvRaw === ""
|
|
392
|
+
? undefined
|
|
393
|
+
: assertStealClaim(stealClaimEnvRaw, "KNITTING_STEAL_CLAIM");
|
|
394
|
+
// Select the stealing claim discipline, preferring the explicit option.
|
|
395
|
+
const stealClaim = host?.stealClaim === undefined
|
|
396
|
+
? stealClaimEnv ?? DEFAULT_STEAL_CLAIM
|
|
397
|
+
: assertStealClaim(host.stealClaim, "host.stealClaim");
|
|
382
398
|
const usingCompiledWorker = resolvedWorker?.runtime === "compiled";
|
|
383
399
|
if (resolvedWorker?.compiled !== undefined && !usingCompiledWorker) {
|
|
384
400
|
throw new Error("worker.compiled requires worker.runtime to be compiled");
|
|
@@ -463,9 +479,8 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
463
479
|
? dispatcherEnv
|
|
464
480
|
: undefined);
|
|
465
481
|
const autoDispatcher = (() => {
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
// macro channel over the per-lane dispatcher checks.
|
|
482
|
+
// Bun and single-worker pools use per-thread dispatch; other pools share a
|
|
483
|
+
// channel across their private lanes.
|
|
469
484
|
if (RUNTIME === "bun")
|
|
470
485
|
return "per-thread";
|
|
471
486
|
if ((threads ?? 1) <= 1)
|
|
@@ -473,10 +488,8 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
473
488
|
return "serial-channel";
|
|
474
489
|
})();
|
|
475
490
|
const dispatcher = explicitDispatcher ?? autoDispatcher;
|
|
476
|
-
//
|
|
477
|
-
//
|
|
478
|
-
// an explicit balancer/dispatcher keeps the private-lane topology unless
|
|
479
|
-
// stealing itself was explicitly requested.
|
|
491
|
+
// Stealing uses one submit region and private return lanes. Explicit balancer
|
|
492
|
+
// or dispatcher settings retain private lanes unless stealing is requested.
|
|
480
493
|
const useSteal = stealRequested &&
|
|
481
494
|
!usingCompiledWorker &&
|
|
482
495
|
(threads ?? 1) > 1 &&
|
|
@@ -485,7 +498,9 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
485
498
|
? createStealPoolBuffers({
|
|
486
499
|
threads: threads ?? 1,
|
|
487
500
|
payload,
|
|
501
|
+
sharedArgs: sharedArgsEnabled,
|
|
488
502
|
regionLanes: host?.stealRegionLanes,
|
|
503
|
+
stealClaim,
|
|
489
504
|
abortSignalCapacity,
|
|
490
505
|
usesAbortSignal,
|
|
491
506
|
processWorker: resolvedWorker?.runtime === "process"
|
|
@@ -498,13 +513,16 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
498
513
|
: undefined;
|
|
499
514
|
const serialChannel = !usingCompiledWorker && !useSteal &&
|
|
500
515
|
dispatcher === "serial-channel";
|
|
501
|
-
//
|
|
502
|
-
// the channel's delivery to do that: swapping in the cheaper `setImmediate`
|
|
503
|
-
// pump cost 25% throughput and tripled p99 here, where every other topology
|
|
504
|
-
// gained. See ChannelHandler's note.
|
|
516
|
+
// The serial topology relies on channel delivery to run each lane check.
|
|
505
517
|
const serialDispatcherChannel = serialChannel
|
|
506
518
|
? new ChannelHandler("channel")
|
|
507
519
|
: undefined;
|
|
520
|
+
// Deno uses a threadSafe FFI callback because waitAsync does not wake an idle
|
|
521
|
+
// event loop; process workers cannot use the process-local pointer.
|
|
522
|
+
const denoCompletionDoorbell = !usingCompiledWorker &&
|
|
523
|
+
resolvedWorker?.runtime !== "process" && host?.doorbell !== false
|
|
524
|
+
? createDenoCompletionDoorbell()
|
|
525
|
+
: undefined;
|
|
508
526
|
let workers = Array.from({
|
|
509
527
|
length: threads ?? 1,
|
|
510
528
|
}).map((_, thread) => spawnWorkerContext({
|
|
@@ -514,6 +532,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
514
532
|
at,
|
|
515
533
|
thread,
|
|
516
534
|
debug,
|
|
535
|
+
debugEpoch,
|
|
517
536
|
hostDebug: hostDebug?.log,
|
|
518
537
|
totalNumberOfThread,
|
|
519
538
|
// Worker count without the inline lane. The inliner runs on the host
|
|
@@ -522,13 +541,15 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
522
541
|
source,
|
|
523
542
|
workerOptions: resolvedWorker,
|
|
524
543
|
workerExecArgv: execArgv,
|
|
544
|
+
requestedExecArgv: workerExecArgv,
|
|
525
545
|
host,
|
|
526
546
|
payload,
|
|
527
|
-
|
|
547
|
+
sharedBytesEnabled,
|
|
528
548
|
abortSignalCapacity,
|
|
529
549
|
usesAbortSignal,
|
|
530
550
|
permission: permissionProtocol,
|
|
531
551
|
sharedChannelHandler: serialDispatcherChannel,
|
|
552
|
+
denoCompletionDoorbell,
|
|
532
553
|
stealPool: stealBuffers === undefined ? undefined : {
|
|
533
554
|
submitBuffers: stealBuffers.submitBuffers,
|
|
534
555
|
returnBuffers: stealBuffers.returnBuffers[thread],
|
|
@@ -538,6 +559,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
538
559
|
consumers: threads ?? 1,
|
|
539
560
|
consumerId: thread,
|
|
540
561
|
regionLanes: stealBuffers.regionLanes,
|
|
562
|
+
stealClaim: stealBuffers.stealClaim,
|
|
541
563
|
abortSignalSAB: stealBuffers.abortSignalSAB,
|
|
542
564
|
abortSignalMax: stealBuffers.abortSignalMax,
|
|
543
565
|
processMemory: stealBuffers.processMemory,
|
|
@@ -547,23 +569,34 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
547
569
|
if (useSteal) {
|
|
548
570
|
const channel = stealChannel;
|
|
549
571
|
const queue = stealBuffers.sharedQueue;
|
|
550
|
-
//
|
|
551
|
-
//
|
|
552
|
-
// idle endpoint claims the region instead. So this costs one notify per
|
|
553
|
-
// publish regardless of pool size, rather than one per lane.
|
|
572
|
+
// Round-robin wake keeps stealing at one notify per publish; any idle worker
|
|
573
|
+
// can claim the region.
|
|
554
574
|
let wakeCursor = 0;
|
|
575
|
+
let lastWoken = 0;
|
|
555
576
|
const wakeOne = () => {
|
|
556
577
|
const lanes = workers.length;
|
|
557
578
|
if (lanes === 0)
|
|
558
579
|
return;
|
|
559
|
-
const lane = wakeCursor;
|
|
580
|
+
const lane = lastWoken = wakeCursor;
|
|
560
581
|
wakeCursor = wakeCursor + 1 < lanes ? wakeCursor + 1 : 0;
|
|
561
582
|
workers[lane].laneWake?.();
|
|
562
583
|
};
|
|
584
|
+
// Any awake worker will claim what was just published, so a lost wake
|
|
585
|
+
// only strands work when every worker is parked. Ring the lane this pass
|
|
586
|
+
// already rang: one still waking up absorbs it, and a second lane would
|
|
587
|
+
// be one more thread woken per call.
|
|
588
|
+
const wakeIfAllParked = () => {
|
|
589
|
+
for (let lane = 0; lane < workers.length; lane++) {
|
|
590
|
+
const context = workers[lane];
|
|
591
|
+
if (context.laneAwake?.() !== false)
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
workers[lastWoken]?.laneWake?.();
|
|
595
|
+
};
|
|
563
596
|
// The shared dispatcher drives one queue, so it needs signal words of its
|
|
564
597
|
// own rather than any single lane's; waking is delegated to `wakeOne`.
|
|
565
598
|
const signalWords = new Int32Array(createSharedArrayBuffer(3 * Int32Array.BYTES_PER_ELEMENT));
|
|
566
|
-
const { check } = hostDispatcherLoop({
|
|
599
|
+
const { check, wakeCompletion } = hostDispatcherLoop({
|
|
567
600
|
signalBox: {
|
|
568
601
|
opView: signalWords.subarray(0, 1),
|
|
569
602
|
txStatus: signalWords.subarray(1, 2),
|
|
@@ -573,19 +606,32 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
573
606
|
channelHandler: channel,
|
|
574
607
|
dispatcherOptions: host,
|
|
575
608
|
notifySignal: wakeOne,
|
|
609
|
+
wakeAfterPass: wakeIfAllParked,
|
|
576
610
|
crossProcess: resolvedWorker?.runtime === "process",
|
|
611
|
+
nativeCompletionDoorbell: denoCompletionDoorbell !== undefined ||
|
|
612
|
+
workers.some((context) => context
|
|
613
|
+
.nodeCompletionDoorbell === true),
|
|
614
|
+
processCompletionDoorbell: workers.some((context) => context.processCompletionDoorbell === true),
|
|
577
615
|
});
|
|
578
616
|
channel.open(check);
|
|
617
|
+
if (denoCompletionDoorbell !== undefined) {
|
|
618
|
+
workers.forEach((_, thread) => {
|
|
619
|
+
denoCompletionDoorbell.listen(thread, wakeCompletion);
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
workers.forEach((context) => {
|
|
623
|
+
context.bindCompletionWake?.(wakeCompletion);
|
|
624
|
+
});
|
|
579
625
|
workers.forEach((context) => {
|
|
580
626
|
context.bindSend(() => {
|
|
581
627
|
if (check.isRunning !== true) {
|
|
582
628
|
check.isRunning = true;
|
|
583
|
-
|
|
629
|
+
queueMicrotask(check);
|
|
584
630
|
}
|
|
585
631
|
wakeOne();
|
|
586
632
|
});
|
|
587
633
|
});
|
|
588
|
-
hostDebug?.log(`dispatcher=steal lanes=${workers.length} g=${stealBuffers.regionLanes}`);
|
|
634
|
+
hostDebug?.log(`dispatcher=steal lanes=${workers.length} g=${stealBuffers.regionLanes} claim=${stealClaim}`);
|
|
589
635
|
}
|
|
590
636
|
const sharedDispatcherChannel = serialDispatcherChannel;
|
|
591
637
|
if (serialChannel) {
|
|
@@ -595,11 +641,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
595
641
|
let serialScheduled = false;
|
|
596
642
|
let serialInFlight = false;
|
|
597
643
|
let serialRerun = false;
|
|
598
|
-
//
|
|
599
|
-
// them. `active` is a dense list of lane indices; `isActive` keeps the
|
|
600
|
-
// membership test O(1). A lane joins on send() and leaves once its queue
|
|
601
|
-
// reports idle, so an N-worker pool with one busy lane costs one check per
|
|
602
|
-
// tick instead of N.
|
|
644
|
+
// Visit only active private lanes; inactive lanes stay out of the tick.
|
|
603
645
|
const active = [];
|
|
604
646
|
const isActive = new Uint8Array(checks.length);
|
|
605
647
|
const markActive = (lane) => {
|
|
@@ -642,7 +684,7 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
642
684
|
if (serialScheduled)
|
|
643
685
|
return;
|
|
644
686
|
serialScheduled = true;
|
|
645
|
-
|
|
687
|
+
queueMicrotask(runSerialChecks);
|
|
646
688
|
};
|
|
647
689
|
channel.open(runSerialChecks);
|
|
648
690
|
workers.forEach((context, lane) => {
|
|
@@ -690,11 +732,29 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
690
732
|
if (closePromise)
|
|
691
733
|
return closePromise;
|
|
692
734
|
closing = true;
|
|
735
|
+
let canCloseNativeDoorbells = denoCompletionDoorbell === undefined;
|
|
693
736
|
closePromise = Promise.allSettled(workers.map((context) => context.requestStop?.()))
|
|
694
|
-
.then(() =>
|
|
737
|
+
.then((stops) => {
|
|
738
|
+
if (denoCompletionDoorbell !== undefined) {
|
|
739
|
+
canCloseNativeDoorbells = stops.every((result, index) => {
|
|
740
|
+
if (workers[index].requestStop === undefined)
|
|
741
|
+
return true;
|
|
742
|
+
return result.status === "fulfilled" && result.value === true;
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
return Promise.allSettled(workers.map((context) => context.kills()));
|
|
746
|
+
})
|
|
695
747
|
.then(() => {
|
|
696
748
|
sharedDispatcherChannel?.close();
|
|
697
749
|
stealChannel?.close();
|
|
750
|
+
})
|
|
751
|
+
.finally(() => {
|
|
752
|
+
if (canCloseNativeDoorbells) {
|
|
753
|
+
denoCompletionDoorbell?.close();
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
// Keep the callback alive if a worker failed to acknowledge shutdown.
|
|
757
|
+
denoCompletionDoorbell?.unref();
|
|
698
758
|
});
|
|
699
759
|
return closePromise;
|
|
700
760
|
};
|
|
@@ -763,23 +823,41 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
763
823
|
for (const { name } of indexedFunctions) {
|
|
764
824
|
callHandlers.set(name, []);
|
|
765
825
|
}
|
|
766
|
-
|
|
826
|
+
if (useSteal) {
|
|
827
|
+
// Every worker context points at the same pool-global submit queue in the
|
|
828
|
+
// stealing topology. There is therefore no worker to assign here: the
|
|
829
|
+
// claimant is selected later by decodeSteal(). Keep one call closure per
|
|
830
|
+
// task and avoid paying N equivalent wrapper allocations at pool setup.
|
|
831
|
+
const sharedWorker = workers[0];
|
|
767
832
|
for (const { name, index, timeout, abortSignal } of indexedFunctions) {
|
|
768
|
-
callHandlers.
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
833
|
+
callHandlers.set(name, [
|
|
834
|
+
wrapGuardedInvoke({
|
|
835
|
+
taskName: name,
|
|
836
|
+
invoke: sharedWorker.call({
|
|
837
|
+
fnNumber: index,
|
|
838
|
+
timeout,
|
|
839
|
+
abortSignal,
|
|
840
|
+
}),
|
|
774
841
|
}),
|
|
775
|
-
|
|
842
|
+
]);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
for (const worker of workers) {
|
|
847
|
+
for (const { name, index, timeout, abortSignal } of indexedFunctions) {
|
|
848
|
+
callHandlers.get(name).push(wrapGuardedInvoke({
|
|
849
|
+
taskName: name,
|
|
850
|
+
invoke: worker.call({
|
|
851
|
+
fnNumber: index,
|
|
852
|
+
timeout,
|
|
853
|
+
abortSignal,
|
|
854
|
+
}),
|
|
855
|
+
}));
|
|
856
|
+
}
|
|
776
857
|
}
|
|
777
858
|
}
|
|
778
859
|
const useDirectHandler = (threads ?? 1) === 1 && !usingInliner;
|
|
779
|
-
// Imported tasks must
|
|
780
|
-
// import is meant to happen inside the worker so worker permission policies
|
|
781
|
-
// apply. When the inliner is active we strip the inline lane from their
|
|
782
|
-
// handler set so they only ever reach real worker lanes.
|
|
860
|
+
// Imported tasks must run on a worker so worker-side permission policies apply.
|
|
783
861
|
const buildImportedInvoker = (handlers) => {
|
|
784
862
|
const workerHandlers = [];
|
|
785
863
|
const workerContexts = [];
|
|
@@ -804,6 +882,11 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
804
882
|
});
|
|
805
883
|
};
|
|
806
884
|
const buildInvoker = (handlers, imported) => {
|
|
885
|
+
// Stealing owns one shared submit queue. The worker that runs a call is
|
|
886
|
+
// deliberately chosen by the worker-side claim protocol, not by the host
|
|
887
|
+
// balancer, so round-robin/first-idle selection would only add overhead.
|
|
888
|
+
if (useSteal)
|
|
889
|
+
return handlers[0];
|
|
807
890
|
if (imported && usingInliner) {
|
|
808
891
|
return buildImportedInvoker(handlers);
|
|
809
892
|
}
|
|
@@ -835,7 +918,10 @@ export const createPool = ({ threads, debug, inliner, balancer, payload, unsafe,
|
|
|
835
918
|
return {
|
|
836
919
|
shutdown: shutdownWithDelay,
|
|
837
920
|
[Symbol.dispose]: disposePool,
|
|
921
|
+
[Symbol.asyncDispose]: () => shutdownWithDelay(),
|
|
838
922
|
call: Object.fromEntries(callEntries),
|
|
923
|
+
// Only the shared submit queue has a single arena to build arguments in.
|
|
924
|
+
sharedArgBytes: createHostArgAllocator(sharedArgsEnabled ? stealBuffers?.submitBuffers.payload : undefined),
|
|
839
925
|
};
|
|
840
926
|
};
|
|
841
927
|
const SINGLE_TASK_KEY = "__task__";
|
|
@@ -847,6 +933,7 @@ const createSingleTaskPool = (single, options) => {
|
|
|
847
933
|
call: pool.call[SINGLE_TASK_KEY],
|
|
848
934
|
shutdown: pool.shutdown,
|
|
849
935
|
[Symbol.dispose]: pool[Symbol.dispose],
|
|
936
|
+
[Symbol.asyncDispose]: pool[Symbol.asyncDispose],
|
|
850
937
|
};
|
|
851
938
|
};
|
|
852
939
|
const buildTaskDefinitionFromCaller = (input, callerHref, at, imported = false) => {
|
|
@@ -7,11 +7,8 @@ export const withResolvers = () => {
|
|
|
7
7
|
const native = Promise.withResolvers;
|
|
8
8
|
if (typeof native === "function") {
|
|
9
9
|
const deferred = native.call(Promise);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
resolve: deferred.resolve,
|
|
13
|
-
reject: deferred.reject,
|
|
14
|
-
};
|
|
10
|
+
attachReject(deferred.promise, deferred.reject);
|
|
11
|
+
return deferred;
|
|
15
12
|
}
|
|
16
13
|
let resolve;
|
|
17
14
|
let reject;
|
|
@@ -29,6 +29,13 @@ export declare const RUNTIME_PROCESS_WORKER_ENV = "KNITTING_PROCESS_WORKER";
|
|
|
29
29
|
export declare const RUNTIME_PROCESS_WORKER_BOOT_ENV = "KNITTING_PROCESS_WORKER_BOOT";
|
|
30
30
|
export declare const RUNTIME_PROCESS_WORKER_BOOT_VERSION = 1;
|
|
31
31
|
export declare const RUNTIME_POOL_DEPTH_ENV = "KNITTING_POOL_DEPTH";
|
|
32
|
+
/**
|
|
33
|
+
* A coalesced completion wake sent over a process worker's existing IPC
|
|
34
|
+
* channel. The result itself remains in shared memory; this is only the
|
|
35
|
+
* doorbell byte.
|
|
36
|
+
*/
|
|
37
|
+
export declare const PROCESS_COMPLETION_DOORBELL = "__knittingCompletionDoorbell";
|
|
38
|
+
export declare const isProcessCompletionDoorbell: (value: unknown) => boolean;
|
|
32
39
|
export declare const RUNTIME_IS_PROCESS_WORKER: boolean;
|
|
33
40
|
export declare const RUNTIME_POOL_DEPTH: number;
|
|
34
41
|
export declare const RUNTIME_WORKER: new (specifier: string | URL, options?: Record<string, unknown>) => RuntimeWorkerLike;
|